build ok,

This commit is contained in:
louiscklaw
2025-04-14 09:26:24 +08:00
commit 6c931c1fe8
770 changed files with 63959 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
import * as React from 'react';
import TimelineConnector from '@mui/lab/TimelineConnector';
import TimelineContent from '@mui/lab/TimelineContent';
import TimelineDot from '@mui/lab/TimelineDot';
import TimelineItem from '@mui/lab/TimelineItem';
import TimelineSeparator from '@mui/lab/TimelineSeparator';
import Avatar from '@mui/material/Avatar';
import Link from '@mui/material/Link';
import Typography from '@mui/material/Typography';
import { dayjs } from '@/lib/dayjs';
export type Event = { id: string; createdAt: Date } & (
| { type: 'new_company'; author: { name: string; avatar?: string }; company: { name: string } }
| { type: 'new_member'; author: { name: string; avatar?: string }; member: { name: string } }
| { type: 'new_job'; author: { name: string; avatar?: string }; job: { title: string } }
);
interface ActivityItemProps {
connector: boolean;
event: Event;
}
export function ActivityItem({ event, connector }: ActivityItemProps): React.JSX.Element {
return (
<TimelineItem>
<TimelineSeparator>
<TimelineDot>{event.author ? <Avatar src={event.author.avatar} /> : null}</TimelineDot>
{connector ? <TimelineConnector /> : null}
</TimelineSeparator>
<TimelineContent>
<ActivityContent event={event} />
<Typography color="text.secondary" variant="caption">
{dayjs(event.createdAt).format('MMM D, hh:mm A')}
</Typography>
</TimelineContent>
</TimelineItem>
);
}
interface ActivityContentProps {
event: Event;
}
function ActivityContent({ event }: ActivityContentProps): React.JSX.Element {
if (event.type === 'new_company') {
return (
<Typography variant="body2">
<Typography component="span" variant="subtitle2">
{event.author.name}
</Typography>{' '}
<Typography component="span" variant="inherit">
created
</Typography>{' '}
<Typography component="span" variant="subtitle2">
{event.company.name}
</Typography>{' '}
company
</Typography>
);
}
if (event.type === 'new_member') {
return (
<Typography variant="body2">
<Typography component="span" variant="subtitle2">
{event.author.name}
</Typography>{' '}
<Typography component="span" variant="inherit">
added
</Typography>{' '}
<Typography component="span" variant="subtitle2">
{event.member.name}
</Typography>{' '}
<Typography component="span" variant="inherit">
as a team member
</Typography>
</Typography>
);
}
if (event.type === 'new_job') {
return (
<Typography variant="body2">
<Typography component="span" variant="subtitle2">
{event.author.name}
</Typography>{' '}
<Typography component="span" variant="inherit">
added a new job
</Typography>{' '}
<Link variant="subtitle2">{event.job.title}</Link>
</Typography>
);
}
return <div />;
}

View File

@@ -0,0 +1,66 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import { DotsThree as DotsThreeIcon } from '@phosphor-icons/react/dist/ssr/DotsThree';
import { File as FileIcon } from '@phosphor-icons/react/dist/ssr/File';
import type { Asset } from './types';
export interface AssetCardProps {
asset: Asset;
}
export function AssetCard({ asset }: AssetCardProps): React.JSX.Element {
const isImage = asset.mimeType.includes('image/');
return (
<Card sx={{ borderRadius: 1 }} variant="outlined">
{isImage ? (
<CardMedia image={asset.url} sx={{ height: '140px' }} />
) : (
<Box
sx={{
alignItems: 'center',
bgcolor: 'var(--mui-palette-background-level2)',
display: 'flex',
height: '140px',
justifyContent: 'center',
}}
>
<FileIcon fontSize="var(--icon-fontSize-lg)" />
</Box>
)}
<CardContent sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Box sx={{ minWidth: 0 }}>
<Typography noWrap variant="subtitle2">
{asset.name}
</Typography>
<Typography color="text.secondary" sx={{ whiteSpace: 'nowrap' }} variant="caption">
{asset.size}
</Typography>
</Box>
<div>
<Tooltip title="More options">
<IconButton edge="end">
<DotsThreeIcon weight="bold" />
</IconButton>
</Tooltip>
</div>
</CardContent>
<Divider />
<CardActions sx={{ justifyContent: 'center' }}>
<Button color="secondary" size="small">
Download
</Button>
</CardActions>
</Card>
);
}

View File

@@ -0,0 +1,91 @@
import * as React from 'react';
import RouterLink from 'next/link';
import Avatar from '@mui/material/Avatar';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { SealCheck as SealCheckIcon } from '@phosphor-icons/react/dist/ssr/SealCheck';
import { Star as StarIcon } from '@phosphor-icons/react/dist/ssr/Star';
import { Users as UsersIcon } from '@phosphor-icons/react/dist/ssr/Users';
import { paths } from '@/paths';
import { JobsCard } from './jobs-card';
import type { Job } from './types';
export interface Company {
id: string;
name: string;
logo?: string;
description?: string;
employees: string;
rating: number;
isVerified: boolean;
jobs: Job[];
}
export interface CompanyCardProps {
company: Company;
}
export function CompanyCard({ company }: CompanyCardProps): React.JSX.Element {
return (
<Card>
<CardContent>
<Stack spacing={2}>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ alignItems: 'flex-start' }}>
<Avatar
component={RouterLink}
href={paths.dashboard.jobs.companies.overview('1')}
src={company.logo}
variant="rounded"
/>
<Stack spacing={1}>
<div>
<Link
color="text.primary"
component={RouterLink}
href={paths.dashboard.jobs.companies.overview('1')}
variant="h6"
>
{company.name}
</Link>
<Typography variant="body2">{company.description}</Typography>
</div>
<Stack direction="row" spacing={3} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<UsersIcon fontSize="var(--icon-fontSize-md)" />
<Typography color="text.secondary" noWrap variant="overline">
{company.employees}
</Typography>
</Stack>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<StarIcon color="var(--mui-palette-warning-main)" fontSize="var(--icon-fontSize-md)" weight="fill" />
<Typography color="text.secondary" noWrap variant="overline">
{company.rating}
/5
</Typography>
</Stack>
{company.isVerified ? (
<Stack direction="row" spacing={0.5} sx={{ alignItems: 'center' }}>
<SealCheckIcon
color="var(--mui-palette-success-main)"
fontSize="var(--icon-fontSize-md)"
weight="fill"
/>
<Typography color="success" noWrap variant="overline">
Verified
</Typography>
</Stack>
) : null}
</Stack>
</Stack>
</Stack>
{company.jobs ? <JobsCard jobs={company.jobs} /> : null}
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,36 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { usePathname } from 'next/navigation';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import { paths } from '@/paths';
const tabs = [
{ label: 'Overview', value: 'overview', href: paths.dashboard.jobs.companies.overview('1') },
{ label: 'Reviews', value: 'reviews', href: paths.dashboard.jobs.companies.reviews('1') },
{ label: 'Activity', value: 'activity', href: paths.dashboard.jobs.companies.activity('1') },
{ label: 'Team', value: 'team', href: paths.dashboard.jobs.companies.team('1') },
{ label: 'Assets', value: 'assets', href: paths.dashboard.jobs.companies.assets('1') },
] as const;
function useSegment(): string {
const pathname = usePathname();
const segments = pathname.split('/').filter(Boolean);
return segments[4] ?? 'overview';
}
export function CompanyTabs(): React.JSX.Element {
const segment = useSegment();
return (
<Tabs sx={{ px: 3 }} value={segment} variant="scrollable">
{tabs.map((tab) => (
<Tab {...tab} component={RouterLink} key={tab.value} tabIndex={0} />
))}
</Tabs>
);
}

View File

@@ -0,0 +1,21 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Markdown from 'react-markdown';
export interface DescriptionProps {
content: string;
}
export function Description({ content }: DescriptionProps): React.JSX.Element {
return (
<Box
sx={{
'& h2': { fontWeight: 500, fontSize: '1.5rem', lineHeight: 1.2, mb: 3 },
'& h3': { fontWeight: 500, fontSize: '1.25rem', lineHeight: 1.2, mb: 3 },
'& p': { fontWeight: 400, fontSize: '1rem', lineHeight: 1.5, mb: 2, mt: 0 },
}}
>
<Markdown>{content}</Markdown>
</Box>
);
}

View File

@@ -0,0 +1,107 @@
'use client';
import * as React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import FormControlLabel from '@mui/material/FormControlLabel';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { ArrowRight as ArrowRightIcon } from '@phosphor-icons/react/dist/ssr/ArrowRight';
const categoryOptions = [
{ title: 'Freelancers', description: 'Best for small, friendly-pocket projects', value: 'freelancers' },
{
title: 'Contractors',
description: 'Limited-time projects with highly experienced individuals',
value: 'contractors',
},
{ title: 'Employees', description: 'Unlimited term contracts', value: 'employees', disabled: true },
] satisfies { title: string; description: string; value: string; disabled?: boolean }[];
export interface JobCategoryStepProps {
onNext?: () => void;
onBack?: () => void;
}
export function JobCategoryStep({ onNext }: JobCategoryStepProps): React.JSX.Element {
const [category, setCategory] = React.useState<string>(categoryOptions[1].value);
const handleCategoryChange = React.useCallback((newCategory: string) => {
setCategory(newCategory);
}, []);
return (
<Stack spacing={3}>
<div>
<Typography variant="h6">I&apos;m looking for...</Typography>
</div>
<RadioGroup
onChange={(event) => {
handleCategoryChange(event.target.value);
}}
sx={{
'& .MuiFormControlLabel-root': {
border: '1px solid var(--mui-palette-divider)',
borderRadius: 1,
gap: 2,
p: 2,
position: 'relative',
'&::before': {
borderRadius: 'inherit',
bottom: 0,
content: '" "',
left: 0,
pointerEvents: 'none',
position: 'absolute',
right: 0,
top: 0,
},
'&.Mui-disabled': { bgcolor: 'var(--mui-palette-background-level1)' },
},
}}
value={category}
>
{categoryOptions.map((option) => (
<FormControlLabel
control={<Radio />}
disabled={option.disabled}
key={option.value}
label={
<div>
<Typography
sx={{
color: option.disabled ? 'var(--mui-palette-action-disabled)' : 'var(--mui-palette-text-primary)',
}}
variant="inherit"
>
{option.title}
</Typography>
<Typography
sx={{
color: option.disabled ? 'var(--mui-palette-action-disabled)' : 'var(--mui-palette-text-secondary)',
}}
variant="body2"
>
{option.description}
</Typography>
</div>
}
sx={{
...(option.value === category && {
'&::before': { boxShadow: '0 0 0 2px var(--mui-palette-primary-main)' },
}),
}}
value={option.value}
/>
))}
</RadioGroup>
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button endIcon={<ArrowRightIcon />} onClick={onNext} variant="contained">
Continue
</Button>
</Box>
</Stack>
);
}

View File

@@ -0,0 +1,91 @@
'use client';
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Step from '@mui/material/Step';
import StepContent from '@mui/material/StepContent';
import type { StepIconProps } from '@mui/material/StepIcon';
import StepLabel from '@mui/material/StepLabel';
import Stepper from '@mui/material/Stepper';
import Typography from '@mui/material/Typography';
import { Check as CheckIcon } from '@phosphor-icons/react/dist/ssr/Check';
import { JobCategoryStep } from './job-category-step';
import { JobDescriptionStep } from './job-description-step';
import { JobDetailsStep } from './job-details-step';
import { JobPreview } from './job-preview';
function StepIcon({ active, completed, icon }: StepIconProps): React.JSX.Element {
const highlight = active || completed;
return (
<Avatar
sx={{
...(highlight && {
bgcolor: 'var(--mui-palette-primary-main)',
color: 'var(--mui-palette-primary-contrastText)',
}),
}}
variant="rounded"
>
{completed ? <CheckIcon /> : icon}
</Avatar>
);
}
export function JobCreateForm(): React.JSX.Element {
const [activeStep, setActiveStep] = React.useState<number>(0);
const [isComplete, setIsComplete] = React.useState<boolean>(false);
const handleNext = React.useCallback(() => {
setActiveStep((prevState) => prevState + 1);
}, []);
const handleBack = React.useCallback(() => {
setActiveStep((prevState) => prevState - 1);
}, []);
const handleComplete = React.useCallback(() => {
setIsComplete(true);
}, []);
const steps = React.useMemo(() => {
return [
{ label: 'Category', content: <JobCategoryStep onBack={handleBack} onNext={handleNext} /> },
{ label: 'Job Details', content: <JobDetailsStep onBack={handleBack} onNext={handleNext} /> },
{ label: 'Description', content: <JobDescriptionStep onBack={handleBack} onNext={handleComplete} /> },
];
}, [handleBack, handleNext, handleComplete]);
if (isComplete) {
return <JobPreview />;
}
return (
<Stepper
activeStep={activeStep}
orientation="vertical"
sx={{
'& .MuiStepConnector-root': { ml: '19px' },
'& .MuiStepConnector-line': { borderLeft: '2px solid var(--mui-palette-divider)' },
'& .MuiStepLabel-iconContainer': { paddingRight: '16px' },
'& .MuiStepContent-root': { borderLeft: '2px solid var(--mui-palette-divider)', ml: '19px' },
'& .MuiStep-root:last-of-type .MuiStepContent-root': { borderColor: 'transparent' },
}}
>
{steps.map((step) => {
return (
<Step key={step.label}>
<StepLabel StepIconComponent={StepIcon}>
<Typography variant="overline">{step.label}</Typography>
</StepLabel>
<StepContent>
<Box sx={{ px: 2, py: 3 }}>{step.content}</Box>
</StepContent>
</Step>
);
})}
</Stepper>
);
}

View File

@@ -0,0 +1,45 @@
'use client';
import * as React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import type { EditorEvents } from '@tiptap/react';
import { TextEditor } from '@/components/core/text-editor/text-editor';
export interface JobDescriptionStepProps {
onBack?: () => void;
onNext?: () => void;
}
export function JobDescriptionStep({ onBack, onNext }: JobDescriptionStepProps): React.JSX.Element {
const [content, setContent] = React.useState<string>('');
const handleContentChange = React.useCallback(({ editor }: EditorEvents['update']) => {
setContent(editor.getText());
}, []);
return (
<Stack spacing={4}>
<Stack spacing={3}>
<div>
<Typography variant="h6">How would you describe the job post?</Typography>
</div>
<Box sx={{ '& .tiptap-container': { height: '400px' } }}>
<TextEditor content={content} onUpdate={handleContentChange} placeholder="Write something" />
</Box>
</Stack>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', justifyContent: 'flex-end' }}>
<Button color="secondary" onClick={onBack} startIcon={<ArrowLeftIcon />}>
Back
</Button>
<Button onClick={onNext} variant="contained">
Create job
</Button>
</Stack>
</Stack>
);
}

View File

@@ -0,0 +1,160 @@
'use client';
import * as React from 'react';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import FormControl from '@mui/material/FormControl';
import InputAdornment from '@mui/material/InputAdornment';
import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
import { ArrowRight as ArrowRightIcon } from '@phosphor-icons/react/dist/ssr/ArrowRight';
import type { Dayjs } from 'dayjs';
import { dayjs } from '@/lib/dayjs';
export interface JobDetailsStepProps {
onNext?: () => void;
onBack?: () => void;
}
export function JobDetailsStep({ onBack, onNext }: JobDetailsStepProps): React.JSX.Element {
const [tagValue, setTagValue] = React.useState<string>('');
const [tags, setTags] = React.useState<Set<string>>(new Set());
const [startDate, setStartDate] = React.useState<Date | null>(dayjs().toDate());
const [endDate, setEndDate] = React.useState<Date | null>(dayjs().add(1, 'month').toDate());
const handleStartDateChange = React.useCallback((newDate: Dayjs | null) => {
setStartDate(newDate?.toDate() ?? null);
}, []);
const handleEndDateChange = React.useCallback((newDate: Dayjs | null) => {
setEndDate(newDate?.toDate() ?? null);
}, []);
const handleTagAdd = React.useCallback(() => {
if (!tagValue) {
return;
}
setTags((prevState) => {
const copy = new Set(prevState);
copy.add(tagValue);
return copy;
});
setTagValue('');
}, [tagValue]);
const handleTagDelete = React.useCallback((deletedTag: string) => {
setTags((prevState) => {
const copy = new Set(prevState);
copy.delete(deletedTag);
return copy;
});
}, []);
return (
<Stack spacing={4}>
<Stack spacing={3}>
<div>
<Typography variant="h6">What is the job about?</Typography>
</div>
<Stack spacing={3}>
<FormControl>
<InputLabel>Job title</InputLabel>
<OutlinedInput name="jobTitle" placeholder="e.g Salesforce Analyst" />
</FormControl>
<FormControl>
<InputLabel>Tags</InputLabel>
<OutlinedInput
endAdornment={
<InputAdornment position="end">
<Button color="secondary" onClick={handleTagAdd} size="small">
Add
</Button>
</InputAdornment>
}
name="tags"
onChange={(event) => {
setTagValue(event.target.value);
}}
value={tagValue}
/>
</FormControl>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
{Array.from(tags.values()).map((tag) => (
<Chip
key={tag}
label={tag}
onDelete={() => {
handleTagDelete(tag);
}}
variant="outlined"
/>
))}
</Stack>
</Stack>
</Stack>
<Stack spacing={3}>
<div>
<Typography variant="h6">When is the project starting?</Typography>
</div>
<Stack direction="row" spacing={3} sx={{ alignItems: 'center' }}>
<DatePicker
format="MMM D, YYYY"
label="Start date"
onChange={handleStartDateChange}
sx={{ flex: '1 1 auto' }}
value={dayjs(startDate)}
/>
<DatePicker
format="MMM D, YYYY"
label="End date"
onChange={handleEndDateChange}
sx={{ flex: '1 1 auto' }}
value={dayjs(endDate)}
/>
</Stack>
</Stack>
<Stack spacing={3}>
<div>
<Typography variant="h6">What is the budget?</Typography>
</div>
<Stack direction="row" spacing={3}>
<FormControl sx={{ flex: '1 1 auto' }}>
<InputLabel>Minimum</InputLabel>
<OutlinedInput
fullWidth
inputProps={{ inputMode: 'numeric' }}
name="minBudget"
placeholder="e.g 1000"
type="number"
/>
</FormControl>
<FormControl sx={{ flex: '1 1 auto' }}>
<InputLabel>Maximum</InputLabel>
<OutlinedInput
fullWidth
inputProps={{ inputMode: 'numeric' }}
name="maxBudget"
placeholder="e.g 5000"
type="number"
/>
</FormControl>
</Stack>
</Stack>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', justifyContent: 'flex-end' }}>
<Button color="secondary" onClick={onBack} startIcon={<ArrowLeftIcon />}>
Back
</Button>
<Button endIcon={<ArrowRightIcon />} onClick={onNext} variant="contained">
Continue
</Button>
</Stack>
</Stack>
);
}

View File

@@ -0,0 +1,55 @@
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Check as CheckIcon } from '@phosphor-icons/react/dist/ssr/Check';
export function JobPreview(): React.JSX.Element {
return (
<Stack spacing={2}>
<Avatar
sx={{
'--Icon-fontSize': 'var(--icon-fontSize-lg)',
bgcolor: 'var(--mui-palette-success-main)',
color: 'var(--mui-palette-success-contrastText)',
}}
>
<CheckIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
<div>
<Typography variant="h6">All done!</Typography>
<Typography color="text.secondary" variant="body2">
Here&apos;s a preview of your newly created job
</Typography>
</div>
<Card variant="outlined">
<Stack
direction="row"
sx={{ alignItems: 'center', flexWrap: 'wrap', justifyContent: 'space-between', px: 2, py: 1.5 }}
>
<div>
<Typography variant="subtitle1">Senior Backend Engineer</Typography>
<Typography color="text.secondary" variant="caption">
Remote possible {' '}
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', notation: 'compact' }).format(
150000
)}{' '}
-{' '}
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', notation: 'compact' }).format(
210000
)}
</Typography>
</div>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<Typography color="text.secondary" variant="caption">
1 minute ago
</Typography>
<Button size="small">Apply</Button>
</Stack>
</Stack>
</Card>
</Stack>
);
}

View File

@@ -0,0 +1,56 @@
import * as React from 'react';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import Divider from '@mui/material/Divider';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { dayjs } from '@/lib/dayjs';
import type { Job } from './types';
export interface JobsCardProps {
jobs?: Job[];
}
export function JobsCard({ jobs = [] }: JobsCardProps): React.JSX.Element {
return (
<Card variant="outlined">
<Stack divider={<Divider />}>
{jobs.map((job) => {
const location = job.isRemote ? 'Remote possible' : `(${job.country}, ${job.state}, ${job.city})`;
const salary = `${job.currency} ${new Intl.NumberFormat('en-US', {
style: 'currency',
currency: job.currency,
notation: 'compact',
}).format(job.budgetMin)} - ${job.currency} ${new Intl.NumberFormat('en-US', {
style: 'currency',
currency: job.currency,
notation: 'compact',
}).format(job.budgetMax)}`;
return (
<Stack
direction="row"
key={job.id}
sx={{ alignItems: 'center', flexWrap: 'wrap', justifyContent: 'space-between', px: 2, py: 1.5 }}
>
<div>
<Typography variant="subtitle1">{job.title}</Typography>
<Typography color="text.secondary" variant="caption">
{location} {salary}
</Typography>
</div>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<Typography color="text.secondary" variant="caption">
{dayjs(job.publishedAt).fromNow()}
</Typography>
<Button size="small">Apply</Button>
</Stack>
</Stack>
);
})}
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,124 @@
'use client';
import * as React from 'react';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import Checkbox from '@mui/material/Checkbox';
import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider';
import FormControlLabel from '@mui/material/FormControlLabel';
import Input from '@mui/material/Input';
import InputAdornment from '@mui/material/InputAdornment';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { MagnifyingGlass as MagnifyingGlassIcon } from '@phosphor-icons/react/dist/ssr/MagnifyingGlass';
import { MultiSelect } from '@/components/core/multi-select';
interface FilterChip {
label: string;
field: 'type' | 'level' | 'location' | 'role';
value: unknown;
displayValue?: string;
}
const typeOptions = [
{ label: 'Freelance', value: 'freelance' },
{ label: 'Full Time', value: 'full_time' },
{ label: 'Part Time', value: 'part_time' },
{ label: 'Internship', value: 'internship' },
] as const;
const levelOptions = [
{ label: 'Novice', value: 'novice' },
{ label: 'Expert', value: 'expert' },
] as const;
const locationOptions = [
{ label: 'Africa', value: 'africa' },
{ label: 'Asia', value: 'asia' },
{ label: 'Europe', value: 'europe' },
{ label: 'North America', value: 'north_america' },
{ label: 'South America', value: 'south_america' },
] as const;
const roleOptions = [
{ label: 'Frontend Developer', value: 'frontend_developer' },
{ label: 'Backend Engineer', value: 'backend_engineer' },
{ label: 'iOS Developer', value: 'ios_developer' },
] as const;
export function JobsFilters(): React.JSX.Element {
const chips: FilterChip[] = [
{ label: 'Type', field: 'type', value: 'freelance', displayValue: 'Freelance' },
{ label: 'Type', field: 'type', value: 'internship', displayValue: 'Internship' },
{ label: 'Level', field: 'level', value: 'novice', displayValue: 'Novice' },
{ label: 'Location', field: 'location', value: 'asia', displayValue: 'Asia' },
{ label: 'Role', field: 'role', value: 'frontend_developer', displayValue: 'Frontend Developer' },
];
// You should memoize the values to avoid unnecessary filtering
const typeValues = chips.filter((chip) => chip.field === 'type').map((chip) => chip.value);
const levelValues = chips.filter((chip) => chip.field === 'level').map((chip) => chip.value);
const locationValues = chips.filter((chip) => chip.field === 'location').map((chip) => chip.value);
const roleValues = chips.filter((chip) => chip.field === 'role').map((chip) => chip.value);
return (
<Card>
<Input
fullWidth
placeholder="Enter a keyword"
startAdornment={
<InputAdornment position="start">
<MagnifyingGlassIcon />
</InputAdornment>
}
sx={{ px: 3, py: 2 }}
/>
<Divider />
{chips.length ? (
<Stack direction="row" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap', p: 2 }}>
{chips.map((chip) => (
<Chip
key={`${chip.field}-${chip.displayValue}`}
label={
<Typography variant="inherit">
<Typography component="span" sx={{ fontWeight: 600 }} variant="inherit">
{chip.label}:
</Typography>{' '}
{chip.displayValue}
</Typography>
}
onDelete={() => {
// noop
}}
variant="outlined"
/>
))}
</Stack>
) : (
<Box sx={{ p: 3 }}>
<Typography color="text.secondary" variant="subtitle2">
No filters applied
</Typography>
</Box>
)}
<Divider />
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center', flexWrap: 'wrap', justifyContent: 'space-between', p: 1 }}
>
<Stack direction="row" spacing={2} sx={{ flexWrap: 'wrap' }}>
<MultiSelect label="Type" options={typeOptions} value={typeValues} />
<MultiSelect label="Level" options={levelOptions} value={levelValues} />
<MultiSelect label="Location" options={locationOptions} value={locationValues} />
<MultiSelect label="Role" options={roleOptions} value={roleValues} />
</Stack>
<div>
<FormControlLabel control={<Checkbox defaultChecked />} label="In network" />
</div>
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,38 @@
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Chip from '@mui/material/Chip';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import type { Member } from './types';
export interface MemberCardProps {
member: Member;
}
export function MemberCard({ member }: MemberCardProps): React.JSX.Element {
return (
<Card variant="outlined">
<CardContent>
<Stack spacing={2}>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<Avatar src={member.avatar} />
<div>
<Typography variant="subtitle2">{member.name}</Typography>
<Typography color="text.secondary" variant="body2">
{member.role}
</Typography>
</div>
</Stack>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
{(member.skills ?? []).map((skill) => (
<Chip key={skill} label={skill} size="small" variant="soft" />
))}
</Stack>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,34 @@
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import OutlinedInput from '@mui/material/OutlinedInput';
import Rating from '@mui/material/Rating';
import Stack from '@mui/material/Stack';
import type { User } from '@/types/user';
const user = {
id: 'USR-000',
name: 'Sofia Rivers',
avatar: '/assets/avatar.png',
email: 'sofia@devias.io',
} satisfies User;
export function CompanyReviewAdd(): React.JSX.Element {
return (
<Stack direction="row" spacing={2} sx={{ alignItems: 'flex-start' }}>
<Avatar src={user.avatar} />
<Stack spacing={3} sx={{ flex: '1 1 auto' }}>
<OutlinedInput multiline placeholder="Send your review" rows={3} />
<Stack
direction="row"
spacing={1}
sx={{ alignItems: 'center', flexWrap: 'wrap', justifyContent: 'space-between' }}
>
<Rating value={5} />
<Button variant="contained">Send review</Button>
</Stack>
</Stack>
</Stack>
);
}

View File

@@ -0,0 +1,56 @@
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Rating from '@mui/material/Rating';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { dayjs } from '@/lib/dayjs';
import type { Review } from './types';
export interface ReviewCardProps {
review: Review;
}
export function ReviewCard({ review }: ReviewCardProps): React.JSX.Element {
return (
<Card variant="outlined">
<CardContent>
<Stack spacing={2}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={2}
sx={{ alignItems: { xs: 'flex-start', sm: 'center' } }}
>
<Avatar src={review.author.avatar} />
<Stack spacing={1}>
<Typography variant="subtitle1">{review.title}</Typography>
<Stack
direction="row"
divider={<span></span>}
spacing={2}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<Rating max={1} precision={0.1} readOnly value={review.rating / 5} />
<Typography noWrap variant="subtitle2">
{review.rating}/5
</Typography>
</Stack>
<Typography noWrap variant="subtitle2">
{review.author.name}
</Typography>
<Typography color="text.secondary" noWrap variant="body2">
{dayjs(review.createdAt).fromNow()}
</Typography>
</Stack>
</Stack>
</Stack>
<Typography variant="body1">{review.comment}</Typography>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,35 @@
import * as React from 'react';
import Card from '@mui/material/Card';
import Rating from '@mui/material/Rating';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
export interface ReviewsSummaryProps {
averageRating: number;
totalReviews: number;
}
export function ReviewsSummary({ averageRating, totalReviews }: ReviewsSummaryProps): React.JSX.Element {
return (
<Card variant="outlined">
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={2}
sx={{ alignItems: { xs: 'flex-start', sm: 'center' }, flexWrap: 'wrap', p: 3 }}
>
<Typography variant="subtitle2">Overall reviews</Typography>
<Stack direction="row" divider={<span></span>} spacing={2} sx={{ alignItems: 'center' }}>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<Rating max={1} precision={0.1} readOnly value={averageRating / 5} />
<Typography noWrap variant="subtitle2">
{averageRating}/5
</Typography>
</Stack>
<Typography color="text.secondary" variant="body2">
{totalReviews} reviews in total
</Typography>
</Stack>
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,37 @@
export interface Job {
id: string;
title: string;
currency: string;
budgetMin: number;
budgetMax: number;
isRemote?: boolean;
city?: string;
state?: string;
country?: string;
publishedAt: Date;
}
export interface Member {
id: string;
name: string;
avatar?: string;
role: string;
skills?: string[];
}
export interface Review {
id: string;
title: string;
comment?: string;
rating: number;
author: { name: string; avatar?: string };
createdAt: Date;
}
export interface Asset {
id: string;
mimeType: string;
name: string;
size: string;
url: string;
}