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,138 @@
'use client';
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
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 CardHeader from '@mui/material/CardHeader';
import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText';
import InputAdornment from '@mui/material/InputAdornment';
import InputLabel from '@mui/material/InputLabel';
import Link from '@mui/material/Link';
import OutlinedInput from '@mui/material/OutlinedInput';
import Select from '@mui/material/Select';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera';
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
import { Option } from '@/components/core/option';
export function AccountDetails(): React.JSX.Element {
return (
<Card>
<CardHeader
avatar={
<Avatar>
<UserIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Basic details"
/>
<CardContent>
<Stack spacing={3}>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<Box
sx={{
border: '1px dashed var(--mui-palette-divider)',
borderRadius: '50%',
display: 'inline-flex',
p: '4px',
}}
>
<Box sx={{ borderRadius: 'inherit', position: 'relative' }}>
<Box
sx={{
alignItems: 'center',
bgcolor: 'rgba(0, 0, 0, 0.5)',
borderRadius: 'inherit',
bottom: 0,
color: 'var(--mui-palette-common-white)',
cursor: 'pointer',
display: 'flex',
justifyContent: 'center',
left: 0,
opacity: 0,
position: 'absolute',
right: 0,
top: 0,
zIndex: 1,
'&:hover': { opacity: 1 },
}}
>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<CameraIcon fontSize="var(--icon-fontSize-md)" />
<Typography color="inherit" variant="subtitle2">
Select
</Typography>
</Stack>
</Box>
<Avatar src="/assets/avatar.png" sx={{ '--Avatar-size': '100px' }} />
</Box>
</Box>
<Button color="secondary" size="small">
Remove
</Button>
</Stack>
<Stack spacing={2}>
<FormControl>
<InputLabel>Full name</InputLabel>
<OutlinedInput defaultValue="Sofia Rivers" name="fullName" />
</FormControl>
<FormControl disabled>
<InputLabel>Email address</InputLabel>
<OutlinedInput name="email" type="email" value="sofia@devias.io" />
<FormHelperText>
Please <Link variant="inherit">contact us</Link> to change your email
</FormHelperText>
</FormControl>
<Stack direction="row" spacing={2}>
<FormControl sx={{ width: '160px' }}>
<InputLabel>Dial code</InputLabel>
<Select
name="countryCode"
startAdornment={
<InputAdornment position="start">
<Box
alt="Spain"
component="img"
src="/assets/flag-es.svg"
sx={{ display: 'block', height: '20px', width: 'auto' }}
/>
</InputAdornment>
}
value="+34"
>
<Option value="+1">United States</Option>
<Option value="+49">Germany</Option>
<Option value="+34">Spain</Option>
</Select>
</FormControl>
<FormControl sx={{ flex: '1 1 auto' }}>
<InputLabel>Phone number</InputLabel>
<OutlinedInput defaultValue="965 245 7623" name="phone" />
</FormControl>
</Stack>
<FormControl>
<InputLabel>Title</InputLabel>
<OutlinedInput name="title" placeholder="e.g Golang Developer" />
</FormControl>
<FormControl>
<InputLabel>Biography (optional)</InputLabel>
<OutlinedInput name="bio" placeholder="Describe yourself..." />
<FormHelperText>0/200 characters</FormHelperText>
</FormControl>
</Stack>
</Stack>
</CardContent>
<CardActions sx={{ justifyContent: 'flex-end' }}>
<Button color="secondary">Cancel</Button>
<Button variant="contained">Save changes</Button>
</CardActions>
</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 Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { Warning as WarningIcon } from '@phosphor-icons/react/dist/ssr/Warning';
export function DeleteAccount(): React.JSX.Element {
return (
<Card>
<CardHeader
avatar={
<Avatar>
<WarningIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Delete account"
/>
<CardContent>
<Stack spacing={3} sx={{ alignItems: 'flex-start' }}>
<Typography variant="subtitle1">
Delete your account and all of your source data. This is irreversible.
</Typography>
<Button color="error" variant="outlined">
Delete account
</Button>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,47 @@
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Divider from '@mui/material/Divider';
import Stack from '@mui/material/Stack';
import Switch from '@mui/material/Switch';
import Typography from '@mui/material/Typography';
import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple';
export function EmailNotifications(): React.JSX.Element {
return (
<Card>
<CardHeader
avatar={
<Avatar>
<EnvelopeSimpleIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Email"
/>
<CardContent>
<Stack divider={<Divider />} spacing={3}>
<Stack direction="row" spacing={3} sx={{ alignItems: 'flex-start', justifyContent: 'space-between' }}>
<Stack spacing={1}>
<Typography variant="subtitle1">Product updates</Typography>
<Typography color="text.secondary" variant="body2">
News, announcements, and product updates.
</Typography>
</Stack>
<Switch defaultChecked />
</Stack>
<Stack direction="row" spacing={3} sx={{ alignItems: 'flex-start', justifyContent: 'space-between' }}>
<Stack spacing={1}>
<Typography variant="subtitle1">Security updates</Typography>
<Typography color="text.secondary" variant="body2">
Important notifications about your account security.
</Typography>
</Stack>
<Switch />
</Stack>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,72 @@
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Divider from '@mui/material/Divider';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { PlugsConnected as PlugsConnectedIcon } from '@phosphor-icons/react/dist/ssr/PlugsConnected';
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
export interface Integration {
id: string;
name: string;
icon: string;
description: string;
installed: boolean;
}
export interface IntegrationsProps {
integrations: Integration[];
}
export function Integrations({ integrations }: IntegrationsProps): React.JSX.Element {
return (
<Card>
<CardHeader
avatar={
<Avatar>
<PlugsConnectedIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Integrations"
/>
<CardContent>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<Stack divider={<Divider />}>
{integrations.map((integration) => (
<Stack direction="row" key={integration.id} spacing={2} sx={{ alignItems: 'center', px: 2, py: 1 }}>
<Avatar
src={integration.icon}
sx={{
bgcolor: 'var(--mui-palette-background-paper)',
boxShadow: 'var(--mui-shadows-8)',
color: 'var(--mui-palette-text-primary)',
}}
/>
<Box sx={{ flex: '1 1 auto' }}>
<Typography variant="subtitle2">{integration.name}</Typography>
<Typography color="text.secondary" variant="caption">
{integration.description}
</Typography>
</Box>
<Button
color="secondary"
disabled={integration.installed}
endIcon={<PlusIcon />}
size="small"
variant="outlined"
>
Install
</Button>
</Stack>
))}
</Stack>
</Card>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,52 @@
'use client';
import * as React from 'react';
import Link from '@mui/material/Link';
import { dayjs } from '@/lib/dayjs';
import { DataTable } from '@/components/core/data-table';
import type { ColumnDef } from '@/components/core/data-table';
export interface Invoice {
id: string;
currency: string;
totalAmount: number;
issueDate: Date;
}
const columns = [
{ field: 'id', name: 'ID', width: '200px' },
{
formatter: (row): string => {
return dayjs(row.issueDate).format('MMM D, YYYY');
},
name: 'Issue Date',
width: '250px',
},
{
formatter: (row): string => {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.totalAmount);
},
name: 'Total (incl. tax)',
width: '150px',
},
{
formatter: (): React.JSX.Element => (
<Link color="inherit" underline="always">
View
</Link>
),
name: 'Actions',
hideName: true,
width: '100px',
align: 'right',
},
] satisfies ColumnDef<Invoice>[];
export interface InvoicesTableProps {
rows: Invoice[];
}
export function InvoicesTable({ rows }: InvoicesTableProps): React.JSX.Element {
return <DataTable<Invoice> columns={columns} rows={rows} />;
}

View File

@@ -0,0 +1,37 @@
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import { Receipt as ReceiptIcon } from '@phosphor-icons/react/dist/ssr/Receipt';
import { InvoicesTable } from './invoices-table';
import type { Invoice } from './invoices-table';
export interface InvoicesProps {
invoices: Invoice[];
}
export function Invoices({ invoices }: InvoicesProps): React.JSX.Element {
return (
<Card>
<CardHeader
avatar={
<Avatar>
<ReceiptIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
subheader="If you've just made a payment, it may take a few hours for it to appear in the table below."
title="Invoice history"
/>
<CardContent>
<Card>
<Box sx={{ overflowX: 'auto' }}>
<InvoicesTable rows={invoices} />
</Box>
</Card>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,64 @@
'use client';
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Typography from '@mui/material/Typography';
import { Timer as TimerIcon } from '@phosphor-icons/react/dist/ssr/Timer';
import { dayjs } from '@/lib/dayjs';
import { DataTable } from '@/components/core/data-table';
import type { ColumnDef } from '@/components/core/data-table';
export interface Event {
id: string;
type: string;
ip: string;
userAgent: string;
createdAt: Date;
}
const columns = [
{
formatter: (row): React.JSX.Element => {
return (
<div>
<Typography variant="subtitle2">{row.type}</Typography>
<Typography color="text.secondary" variant="inherit">
on {dayjs(row.createdAt).format('hh:mm A MMM D, YYYY')}
</Typography>
</div>
);
},
name: 'Login type',
width: '250px',
},
{ field: 'ip', name: 'IP address', width: '150px' },
{ field: 'userAgent', name: 'User agent', width: '200px' },
] satisfies ColumnDef<Event>[];
export interface LoginHistoryProps {
events: Event[];
}
export function LoginHistory({ events }: LoginHistoryProps): React.JSX.Element {
return (
<Card>
<CardHeader
avatar={
<Avatar>
<TimerIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Login history"
/>
<CardContent>
<Card sx={{ overflowX: 'auto' }} variant="outlined">
<DataTable<Event> columns={columns} rows={events} />
</Card>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,68 @@
'use client';
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Chip from '@mui/material/Chip';
import IconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { DotsThree as DotsThreeIcon } from '@phosphor-icons/react/dist/ssr/DotsThree';
import { DataTable } from '@/components/core/data-table';
import type { ColumnDef } from '@/components/core/data-table';
export interface Member {
id: string;
name: string;
avatar?: string;
email: string;
role: string;
}
const columns = [
{
formatter: (row): React.JSX.Element => (
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<Avatar src={row.avatar} />
<div>
<Typography variant="subtitle2">{row.name}</Typography>
<Typography color="text.secondary" variant="body2">
{row.email}
</Typography>
</div>
</Stack>
),
name: 'Name',
width: '350px',
},
{
formatter: (row): React.JSX.Element => {
return row.role === 'Owner' ? (
<Chip color="primary" label="Owner" size="small" variant="soft" />
) : (
<Chip label={row.role} size="small" variant="soft" />
);
},
name: 'Role',
width: '100px',
},
{
formatter: (): React.JSX.Element => (
<IconButton>
<DotsThreeIcon weight="bold" />
</IconButton>
),
name: 'Actions',
hideName: true,
width: '100px',
align: 'right',
},
] satisfies ColumnDef<Member>[];
export interface MembersTableProps {
rows: Member[];
}
export function MembersTable({ rows }: MembersTableProps): React.JSX.Element {
return <DataTable<Member> columns={columns} rows={rows} />;
}

View File

@@ -0,0 +1,61 @@
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Divider from '@mui/material/Divider';
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 { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple';
import { Users as UsersIcon } from '@phosphor-icons/react/dist/ssr/Users';
import type { Member } from './members-table';
import { MembersTable } from './members-table';
export interface MembersProps {
members: Member[];
}
export function Members({ members }: MembersProps): React.JSX.Element {
return (
<Card>
<CardHeader
avatar={
<Avatar>
<UsersIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
subheader="You currently pay for 2 Editor Seats."
title="Invite members"
/>
<CardContent>
<Stack spacing={2}>
<FormControl>
<InputLabel>Email address</InputLabel>
<OutlinedInput
name="email"
startAdornment={
<InputAdornment position="start">
<EnvelopeSimpleIcon />
</InputAdornment>
}
type="email"
/>
</FormControl>
<div>
<Button variant="contained">Send invite</Button>
</div>
</Stack>
</CardContent>
<Divider />
<Box sx={{ overflowX: 'auto' }}>
<MembersTable rows={members} />
</Box>
</Card>
);
}

View File

@@ -0,0 +1,104 @@
import * as React from 'react';
import Alert from '@mui/material/Alert';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2';
import { ArrowRight as ArrowRightIcon } from '@phosphor-icons/react/dist/ssr/ArrowRight';
import { Key as KeyIcon } from '@phosphor-icons/react/dist/ssr/Key';
export function MultiFactor(): React.JSX.Element {
return (
<Card>
<CardHeader
avatar={
<Avatar>
<KeyIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Multi factor authentication"
/>
<CardContent>
<Stack spacing={3}>
<Grid container spacing={3}>
<Grid xl={6} xs={12}>
<Card sx={{ height: '100%' }} variant="outlined">
<CardContent>
<Stack spacing={4}>
<Stack spacing={1}>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<Box
sx={{
bgcolor: 'var(--mui-palette-error-main)',
borderRadius: '50%',
display: 'block',
height: '8px',
width: '8px',
}}
/>
<Typography color="error" variant="body2">
Off
</Typography>
</Stack>
<Typography variant="subtitle2">Authenticator app</Typography>
<Typography color="text.secondary" variant="body2">
Use an authenticator app to generate one time security codes.
</Typography>
</Stack>
<div>
<Button endIcon={<ArrowRightIcon />} variant="contained">
Set up authenticator
</Button>
</div>
</Stack>
</CardContent>
</Card>
</Grid>
<Grid xl={6} xs={12}>
<Card sx={{ height: '100%' }} variant="outlined">
<CardContent>
<Stack spacing={4}>
<Stack spacing={1}>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<Box
sx={{
bgcolor: 'var(--mui-palette-error-main)',
borderRadius: '50%',
display: 'block',
height: '8px',
width: '8px',
}}
/>
<Typography color="error" variant="body2">
Off
</Typography>
</Stack>
<Typography variant="subtitle2">Text message</Typography>
<Typography color="text.secondary" variant="body2">
Use your mobile phone to receive security codes via SMS.
</Typography>
</Stack>
<div>
<Button endIcon={<ArrowRightIcon />} variant="contained">
Set up phone
</Button>
</div>
</Stack>
</CardContent>
</Card>
</Grid>
</Grid>
<Alert color="success">
87% of the technology industry has already implemented MFA and it is the top sector with the highest MFA
adoption rate.
</Alert>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,48 @@
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import OutlinedInput from '@mui/material/OutlinedInput';
import Stack from '@mui/material/Stack';
import { Password as PasswordIcon } from '@phosphor-icons/react/dist/ssr/Password';
export function PasswordForm(): React.JSX.Element {
return (
<Card>
<CardHeader
avatar={
<Avatar>
<PasswordIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Change password"
/>
<CardContent>
<Stack spacing={3}>
<Stack spacing={3}>
<FormControl>
<InputLabel>Old password</InputLabel>
<OutlinedInput name="oldPassword" type="password" />
</FormControl>
<FormControl>
<InputLabel>New password</InputLabel>
<OutlinedInput name="password" type="password" />
</FormControl>
<FormControl>
<InputLabel>Re-type new password</InputLabel>
<OutlinedInput name="confirmPassword" type="password" />
</FormControl>
</Stack>
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button variant="contained">Update</Button>
</Box>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,35 @@
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Stack from '@mui/material/Stack';
import Switch from '@mui/material/Switch';
import Typography from '@mui/material/Typography';
import { Phone as PhoneIcon } from '@phosphor-icons/react/dist/ssr/Phone';
export function PhoneNotifications(): React.JSX.Element {
return (
<Card>
<CardHeader
avatar={
<Avatar>
<PhoneIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Phone"
/>
<CardContent>
<Stack direction="row" spacing={3} sx={{ alignItems: 'flex-start', justifyContent: 'space-between' }}>
<Stack spacing={1}>
<Typography variant="subtitle1">Security updates</Typography>
<Typography color="text.secondary" variant="body2">
Important notifications about your account security.
</Typography>
</Stack>
<Switch />
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,100 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import Chip from '@mui/material/Chip';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
export type PlanId = 'startup' | 'standard' | 'business';
export interface Plan {
id: PlanId;
name: string;
currency: string;
price: number;
}
export interface PlanCardProps {
plan: Plan;
isCurrent?: boolean;
}
export function PlanCard({ plan, isCurrent }: PlanCardProps): React.JSX.Element {
return (
<Card
sx={{ cursor: 'pointer', ...(isCurrent && { border: '2px solid var(--mui-palette-primary-main)' }) }}
variant="outlined"
>
<Stack spacing={1} sx={{ p: 3 }}>
<PlanIcon name={plan.id} />
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
<Typography sx={{ flex: '1 1 auto' }} variant="overline">
{plan.name}
</Typography>
{isCurrent ? <Chip color="success" label="Current" size="small" variant="soft" /> : null}
</Stack>
<Box sx={{ alignItems: 'flex-end', display: 'flex', gap: 1, whiteSpace: 'nowrap' }}>
<Typography variant="h5">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: plan.currency }).format(plan.price)}
</Typography>
<Typography color="text.secondary" variant="body2">
/mo
</Typography>
</Box>
</Stack>
</Card>
);
}
interface PlanIconProps {
name: PlanId;
}
function PlanIcon({ name }: PlanIconProps): React.JSX.Element | null {
switch (name) {
case 'startup':
return (
<svg fill="none" height="33" viewBox="0 0 24 33" width="24" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.7898 0.492981L23.0191 5.55771C23.2324 5.67468 23.4097 5.84498 23.5332 6.05102C23.6567 6.25713 23.7218 6.49154 23.7218 6.73031C23.7218 6.96907 23.6567 7.20348 23.5332 7.4096C23.4097 7.61564 23.2324 7.78594 23.0191 7.9029L13.7899 12.9679C13.2008 13.2911 12.5366 13.4609 11.861 13.4609C11.1852 13.4609 10.521 13.2911 9.93202 12.9679L0.702682 7.9029C0.489532 7.78594 0.312084 7.61564 0.188587 7.4096C0.0650921 7.20348 -9.53674e-06 6.96907 -9.53674e-06 6.73031C-9.53674e-06 6.49154 0.0650921 6.25713 0.188587 6.05102C0.312084 5.84498 0.489532 5.67468 0.702682 5.55771L9.93202 0.492981C10.521 0.169739 11.1852 -5.72205e-06 11.861 -5.72205e-06C12.5366 -5.72205e-06 13.2008 0.169739 13.7898 0.492981Z"
fill="var(--mui-palette-primary-main)"
/>
</svg>
);
case 'standard':
return (
<svg fill="none" height="33" viewBox="0 0 33 33" width="33" xmlns="http://www.w3.org/2000/svg">
<path
d="M18.4946 0.492981L27.7239 5.55771C27.9372 5.67468 28.1145 5.84498 28.238 6.05102C28.3615 6.25713 28.4266 6.49154 28.4266 6.73031C28.4266 6.96907 28.3615 7.20348 28.238 7.4096C28.1145 7.61564 27.9372 7.78594 27.7239 7.9029L18.4947 12.9679C17.9056 13.2911 17.2414 13.4609 16.5658 13.4609C15.89 13.4609 15.2258 13.2911 14.6368 12.9679L5.40749 7.9029C5.19434 7.78594 5.01689 7.61564 4.89339 7.4096C4.7699 7.20348 4.70479 6.96907 4.70479 6.73031C4.70479 6.49154 4.7699 6.25713 4.89339 6.05102C5.01689 5.84498 5.19434 5.67468 5.40749 5.55771L14.6368 0.492981C15.2258 0.169739 15.89 -5.72205e-06 16.5658 -5.72205e-06C17.2414 -5.72205e-06 17.9056 0.169739 18.4946 0.492981Z"
fill="var(--mui-palette-primary-main)"
/>
<path
d="M18.4448 5.2478L31.6626 12.5013C31.8758 12.6183 32.0532 12.7886 32.1767 12.9946C32.3002 13.2007 32.3653 13.4351 32.3653 13.6739C32.3653 13.9127 32.3002 14.1471 32.1767 14.3532C32.0532 14.5593 31.8758 14.7295 31.6626 14.8466L18.4448 22.1C17.8558 22.4231 17.1916 22.593 16.516 22.593C15.8403 22.593 15.1761 22.4231 14.5871 22.1L1.3693 14.8466C1.15615 14.7295 0.978699 14.5593 0.855202 14.3532C0.731705 14.1471 0.666607 13.9127 0.666607 13.6739C0.666607 13.4351 0.731705 13.2007 0.855202 12.9946C0.978699 12.7886 1.15615 12.6183 1.3693 12.5013L14.5871 5.2478C15.1761 4.92464 15.8403 4.75489 16.516 4.75489C17.1916 4.75489 17.8558 4.92464 18.4448 5.2478Z"
fill="var(--mui-palette-primary-main)"
opacity="0.7"
/>
</svg>
);
case 'business':
return (
<svg fill="none" height="33" viewBox="0 0 43 33" width="43" xmlns="http://www.w3.org/2000/svg">
<path
d="M23.0075 0.49292L32.2369 5.55765C32.4501 5.67462 32.6275 5.84492 32.751 6.05096C32.8745 6.25707 32.9396 6.49148 32.9396 6.73025C32.9396 6.96901 32.8745 7.20342 32.751 7.40954C32.6275 7.61558 32.4501 7.78587 32.2369 7.90284L23.0076 12.9678C22.4186 13.2911 21.7543 13.4609 21.0787 13.4609C20.403 13.4609 19.7387 13.2911 19.1498 12.9678L9.92043 7.90284C9.70728 7.78587 9.52983 7.61558 9.40633 7.40954C9.28284 7.20342 9.21773 6.96901 9.21773 6.73025C9.21773 6.49148 9.28284 6.25707 9.40633 6.05096C9.52983 5.84492 9.70728 5.67462 9.92043 5.55765L19.1498 0.49292C19.7387 0.169678 20.403 -6.67572e-05 21.0787 -6.67572e-05C21.7543 -6.67572e-05 22.4186 0.169678 23.0075 0.49292Z"
fill="var(--mui-palette-primary-main)"
/>
<path
d="M22.9577 5.24774L36.1755 12.5012C36.3886 12.6182 36.5661 12.7885 36.6896 12.9945C36.8131 13.2007 36.8782 13.4351 36.8782 13.6738C36.8782 13.9126 36.8131 14.1471 36.6896 14.3531C36.5661 14.5592 36.3886 14.7295 36.1755 14.8465L22.9577 22.0999C22.3687 22.4231 21.7045 22.5929 21.0288 22.5929C20.3532 22.5929 19.6889 22.4231 19.1 22.0999L5.88218 14.8465C5.66903 14.7295 5.49158 14.5592 5.36808 14.3531C5.24458 14.1471 5.17949 13.9126 5.17949 13.6738C5.17949 13.4351 5.24458 13.2007 5.36808 12.9945C5.49158 12.7885 5.66903 12.6182 5.88218 12.5012L19.1 5.24774C19.6889 4.92458 20.3532 4.75483 21.0288 4.75483C21.7045 4.75483 22.3687 4.92458 22.9577 5.24774Z"
fill="var(--mui-palette-primary-main)"
opacity="0.7"
/>
<path
d="M23.259 10.0018L41.6317 20.0843C41.8445 20.2012 42.0217 20.3711 42.145 20.5769C42.2683 20.7826 42.3333 21.0167 42.3333 21.2551C42.3333 21.4935 42.2683 21.7275 42.145 21.9333C42.0217 22.139 41.8445 22.309 41.6317 22.4258L23.2589 32.5081C22.6709 32.8307 22.0078 33.0002 21.3332 33.0002C20.6587 33.0002 19.9955 32.8307 19.4075 32.5081L1.03479 22.4258C0.821995 22.309 0.644833 22.139 0.521538 21.9333C0.398247 21.7275 0.333252 21.4935 0.333252 21.2551C0.333252 21.0167 0.398247 20.7826 0.521538 20.5769C0.644833 20.3711 0.821995 20.2012 1.03479 20.0843L19.4075 10.0018C19.9955 9.67921 20.6587 9.50966 21.3332 9.50966C22.0078 9.50966 22.6709 9.67921 23.259 10.0018Z"
fill="var(--mui-palette-primary-main)"
opacity="0.4"
/>
</svg>
);
default:
return null;
}
}

View File

@@ -0,0 +1,90 @@
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Divider from '@mui/material/Divider';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Grid from '@mui/material/Unstable_Grid2';
import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard';
import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple';
import { PropertyItem } from '@/components/core/property-item';
import { PropertyList } from '@/components/core/property-list';
import { PlanCard } from './plan-card';
import type { Plan, PlanId } from './plan-card';
const plans = [
{ id: 'startup', name: 'Startup', currency: 'USD', price: 0 },
{ id: 'standard', name: 'Standard', currency: 'USD', price: 14.99 },
{ id: 'business', name: 'Business', currency: 'USD', price: 29.99 },
] satisfies Plan[];
export function Plans(): React.JSX.Element {
const currentPlanId = 'standard' satisfies PlanId;
return (
<Card>
<CardHeader
avatar={
<Avatar>
<CreditCardIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
subheader="You can upgrade and downgrade whenever you want."
title="Change plan"
/>
<CardContent>
<Stack divider={<Divider />} spacing={3}>
<Stack spacing={3}>
<Grid container spacing={3}>
{plans.map((plan) => (
<Grid key={plan.id} md={4} xs={12}>
<PlanCard isCurrent={plan.id === currentPlanId} plan={plan} />
</Grid>
))}
</Grid>
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button variant="contained">Upgrade plan</Button>
</Box>
</Stack>
<Stack spacing={3}>
<Stack direction="row" spacing={3} sx={{ alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="h6">Billing details</Typography>
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
Edit
</Button>
</Stack>
<Card sx={{ borderRadius: 1 }} variant="outlined">
<PropertyList divider={<Divider />} sx={{ '--PropertyItem-padding': '12px 24px' }}>
{(
[
{ key: 'Name', value: 'Sofia Rivers' },
{ key: 'Country', value: 'Germany' },
{ key: 'State', value: 'Brandenburg' },
{ key: 'City', value: 'Berlin' },
{ key: 'Zip Code', value: '667123' },
{ key: 'Card Number', value: '**** 1111' },
] satisfies { key: string; value: React.ReactNode }[]
).map(
(item): React.JSX.Element => (
<PropertyItem key={item.key} name={item.key} value={item.value} />
)
)}
</PropertyList>
</Card>
<Typography color="text.secondary" variant="body2">
We cannot refund once you purchased a subscription, but you can always{' '}
<Link variant="inherit">cancel</Link>
</Typography>
</Stack>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,47 @@
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
import Divider from '@mui/material/Divider';
import Stack from '@mui/material/Stack';
import Switch from '@mui/material/Switch';
import Typography from '@mui/material/Typography';
import { UserCircle as UserCircleIcon } from '@phosphor-icons/react/dist/ssr/UserCircle';
export function Privacy(): React.JSX.Element {
return (
<Card>
<CardHeader
avatar={
<Avatar>
<UserCircleIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Privacy"
/>
<CardContent>
<Stack divider={<Divider />} spacing={3}>
<Stack direction="row" spacing={3} sx={{ alignItems: 'flex-start', justifyContent: 'space-between' }}>
<Stack spacing={1}>
<Typography variant="subtitle1">Make contact info public</Typography>
<Typography color="text.secondary" variant="body2">
Means that anyone viewing your profile will be able to see your contacts details.
</Typography>
</Stack>
<Switch />
</Stack>
<Stack direction="row" spacing={3} sx={{ alignItems: 'flex-start', justifyContent: 'space-between' }}>
<Stack spacing={1}>
<Typography variant="subtitle1">Available to hire</Typography>
<Typography color="text.secondary" variant="body2">
Toggling this will let your teammates know that you are available for acquiring new projects.
</Typography>
</Stack>
<Switch defaultChecked />
</Stack>
</Stack>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,165 @@
'use client';
import * as React from 'react';
import RouterLink from 'next/link';
import { usePathname } from 'next/navigation';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import type { Icon } from '@phosphor-icons/react/dist/lib/types';
import { Bell as BellIcon } from '@phosphor-icons/react/dist/ssr/Bell';
import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard';
import { LockKey as LockKeyIcon } from '@phosphor-icons/react/dist/ssr/LockKey';
import { PlugsConnected as PlugsConnectedIcon } from '@phosphor-icons/react/dist/ssr/PlugsConnected';
import { UserCircle as UserCircleIcon } from '@phosphor-icons/react/dist/ssr/UserCircle';
import { UsersThree as UsersThreeIcon } from '@phosphor-icons/react/dist/ssr/UsersThree';
import type { NavItemConfig } from '@/types/nav';
import { paths } from '@/paths';
import { isNavItemActive } from '@/lib/is-nav-item-active';
// NOTE: First level elements are groups.
const navItems = [
{
key: 'personal',
title: 'Personal',
items: [
{ key: 'account', title: 'Account', href: paths.dashboard.settings.account, icon: 'user-circle' },
{ key: 'notifications', title: 'Notifications', href: paths.dashboard.settings.notifications, icon: 'bell' },
{ key: 'security', title: 'Security', href: paths.dashboard.settings.security, icon: 'lock-key' },
],
},
{
key: 'organization',
title: 'Organization',
items: [
{ key: 'billing', title: 'Billing & plans', href: paths.dashboard.settings.billing, icon: 'credit-card' },
{ key: 'team', title: 'Team', href: paths.dashboard.settings.team, icon: 'users-three' },
{
key: 'integrations',
title: 'Integrations',
href: paths.dashboard.settings.integrations,
icon: 'plugs-connected',
},
],
},
] satisfies NavItemConfig[];
const icons = {
'credit-card': CreditCardIcon,
'lock-key': LockKeyIcon,
'plugs-connected': PlugsConnectedIcon,
'user-circle': UserCircleIcon,
'users-three': UsersThreeIcon,
bell: BellIcon,
} as Record<string, Icon>;
export function SideNav(): React.JSX.Element {
const pathname = usePathname();
return (
<div>
<Stack
spacing={3}
sx={{
flex: '0 0 auto',
flexDirection: { xs: 'column-reverse', md: 'column' },
position: { md: 'sticky' },
top: '64px',
width: { xs: '100%', md: '240px' },
}}
>
<Stack component="ul" spacing={3} sx={{ listStyle: 'none', m: 0, p: 0 }}>
{navItems.map((group) => (
<Stack component="li" key={group.key} spacing={2}>
{group.title ? (
<div>
<Typography color="text.secondary" variant="caption">
{group.title}
</Typography>
</div>
) : null}
<Stack component="ul" spacing={1} sx={{ listStyle: 'none', m: 0, p: 0 }}>
{group.items.map((item) => (
<NavItem {...item} key={item.key} pathname={pathname} />
))}
</Stack>
</Stack>
))}
</Stack>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<Avatar src="/assets/avatar.png">AV</Avatar>
<div>
<Typography variant="subtitle1">Sofia Rivers</Typography>
<Typography color="text.secondary" variant="caption">
sofia@devias.io
</Typography>
</div>
</Stack>
</Stack>
</div>
);
}
interface NavItemProps extends NavItemConfig {
pathname: string;
}
function NavItem({ disabled, external, href, icon, pathname, title }: NavItemProps): React.JSX.Element {
const active = isNavItemActive({ disabled, external, href, pathname });
const Icon = icon ? icons[icon] : null;
return (
<Box component="li" sx={{ userSelect: 'none' }}>
<Box
{...(href
? {
component: external ? 'a' : RouterLink,
href,
target: external ? '_blank' : undefined,
rel: external ? 'noreferrer' : undefined,
}
: { role: 'button' })}
sx={{
alignItems: 'center',
borderRadius: 1,
color: 'var(--mui-palette-text-secondary)',
cursor: 'pointer',
display: 'flex',
flex: '0 0 auto',
gap: 1,
p: '6px 16px',
textDecoration: 'none',
whiteSpace: 'nowrap',
...(disabled && { color: 'var(--mui-palette-text-disabled)', cursor: 'not-allowed' }),
...(active && { bgcolor: 'var(--mui-palette-action-selected)', color: 'var(--mui-palette-text-primary)' }),
'&:hover': {
...(!active &&
!disabled && { bgcolor: 'var(--mui-palette-action-hover)', color: 'var(---mui-palette-text-primary)' }),
},
}}
tabIndex={0}
>
{Icon ? (
<Box sx={{ alignItems: 'center', display: 'flex', justifyContent: 'center', flex: '0 0 auto' }}>
<Icon
fill={active ? 'var(--mui-palette-text-primary)' : 'var(--mui-palette-text-secondary)'}
fontSize="var(--icon-fontSize-md)"
weight={active ? 'fill' : undefined}
/>
</Box>
) : null}
<Box sx={{ flex: '1 1 auto' }}>
<Typography
component="span"
sx={{ color: 'inherit', fontSize: '0.875rem', fontWeight: 500, lineHeight: '28px' }}
>
{title}
</Typography>
</Box>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,69 @@
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardHeader from '@mui/material/CardHeader';
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 { Gear as GearIcon } from '@phosphor-icons/react/dist/ssr/Gear';
import { Moon as MoonIcon } from '@phosphor-icons/react/dist/ssr/Moon';
import { Sun as SunIcon } from '@phosphor-icons/react/dist/ssr/Sun';
export function ThemeSwitch(): React.JSX.Element {
return (
<Card>
<CardHeader
avatar={
<Avatar>
<GearIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
}
title="Theme options"
/>
<CardContent>
<Card variant="outlined">
<RadioGroup
defaultValue="light"
sx={{
gap: 0,
'& .MuiFormControlLabel-root': {
justifyContent: 'space-between',
p: '8px 12px',
'&:not(:last-of-type)': { borderBottom: '1px solid var(--mui-palette-divider)' },
},
}}
>
{[
{ title: 'Light mode', description: 'Best for bright environments', value: 'light', icon: SunIcon },
{ title: 'Dark mode', description: 'Recommended for dark rooms', value: 'dark', icon: MoonIcon },
{ title: 'System', description: "Adapts to your device's theme", value: 'system', icon: SunIcon },
].map((option) => (
<FormControlLabel
control={<Radio />}
key={option.value}
label={
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<Avatar>
<option.icon fontSize="var(--Icon-fontSize)" />
</Avatar>
<div>
<Typography variant="inherit">{option.title}</Typography>
<Typography color="text.secondary" variant="caption">
{option.description}
</Typography>
</div>
</Stack>
}
labelPlacement="start"
value={option.value}
/>
))}
</RadioGroup>
</Card>
</CardContent>
</Card>
);
}