Migrate to Mantine UI framework

This commit is contained in:
Philipinho
2023-09-26 03:31:20 +01:00
parent 2de9f6d60b
commit d733b9a7f6
83 changed files with 1296 additions and 2841 deletions

View File

@ -12,7 +12,7 @@ pnpm dev
Open [http://localhost:3001](http://localhost:3000) with your browser to see the result. Open [http://localhost:3001](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. You can start editing the page by modifying `app/workspace-members.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.

View File

@ -1,16 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/app/globals.css",
"baseColor": "zinc",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

View File

@ -11,17 +11,9 @@
}, },
"dependencies": { "dependencies": {
"@hocuspocus/provider": "^2.5.0", "@hocuspocus/provider": "^2.5.0",
"@hookform/resolvers": "^3.3.1", "@mantine/core": "^7.0.0",
"@radix-ui/react-dialog": "^1.0.4", "@mantine/form": "^7.0.0",
"@radix-ui/react-icons": "^1.3.0", "@mantine/hooks": "^7.0.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-scroll-area": "^1.0.4",
"@radix-ui/react-select": "^1.2.2",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.4",
"@radix-ui/react-tooltip": "^1.0.6",
"@tabler/icons-react": "^2.32.0", "@tabler/icons-react": "^2.32.0",
"@tanstack/react-query": "^4.33.0", "@tanstack/react-query": "^4.33.0",
"@tanstack/react-table": "^8.9.3", "@tanstack/react-table": "^8.9.3",
@ -33,35 +25,30 @@
"@tiptap/pm": "^2.1.8", "@tiptap/pm": "^2.1.8",
"@tiptap/react": "^2.1.8", "@tiptap/react": "^2.1.8",
"@tiptap/starter-kit": "^2.1.8", "@tiptap/starter-kit": "^2.1.8",
"@types/node": "20.4.8",
"@types/react": "18.2.18",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.14",
"axios": "^1.4.0", "axios": "^1.4.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"eslint": "8.46.0",
"eslint-config-next": "13.4.13",
"jotai": "^2.3.1", "jotai": "^2.3.1",
"jotai-optics": "^0.3.1", "jotai-optics": "^0.3.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"next": "13.4.13", "next": "13.5.3",
"next-themes": "^0.2.1",
"postcss": "8.4.27",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.45.4",
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"tailwind-merge": "^1.14.0", "typescript": "5.2.2",
"tailwindcss": "3.3.3",
"tailwindcss-animate": "^1.0.6",
"typescript": "5.1.6",
"yjs": "^13.6.7", "yjs": "^13.6.7",
"zod": "^3.22.2" "zod": "^3.22.2"
}, },
"devDependencies": { "devDependencies": {
"@types/js-cookie": "^3.0.3", "@types/js-cookie": "^3.0.3",
"optics-ts": "^2.4.1" "@types/node": "20.4.8",
"@types/react": "18.2.18",
"@types/react-dom": "18.2.7",
"autoprefixer": "^10.4.16",
"eslint": "8.46.0",
"eslint-config-next": "13.4.13",
"optics-ts": "^2.4.1",
"postcss": "^8.4.30",
"postcss-preset-env": "^9.1.4",
"postcss-preset-mantine": "^1.7.0",
"postcss-simple-vars": "^7.0.1"
} }
} }

View File

@ -1,6 +1,16 @@
module.exports = { module.exports = {
plugins: { plugins: {
tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
"postcss-preset-env": {},
"postcss-preset-mantine": {},
"postcss-simple-vars": {
variables: {
"mantine-breakpoint-xs": "36em",
"mantine-breakpoint-sm": "48em",
"mantine-breakpoint-md": "62em",
"mantine-breakpoint-lg": "75em",
"mantine-breakpoint-xl": "88em",
}, },
} },
},
};

View File

@ -3,5 +3,5 @@ interface AuthLayoutProps {
} }
export default function AuthLayout({ children }: AuthLayoutProps) { export default function AuthLayout({ children }: AuthLayoutProps) {
return <div className="min-h-screen">{children}</div> return <div>{children}</div>
} }

View File

@ -1,49 +1,7 @@
"use client" 'use client';
import Link from "next/link"; import { LoginForm } from '@/features/auth/components/login-form';
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
import { Icons } from "@/components/icons";
import { ChevronLeftIcon } from "@radix-ui/react-icons";
import { LoginForm } from "@/features/auth/components/login-form";
import LegalTerms from "@/features/auth/components/legal-terms";
export default function LoginPage() { export default function LoginPage() {
return ( return <LoginForm />;
<div className="container flex h-screen w-screen flex-col items-center justify-center">
<Link
href="/"
className={cn(
buttonVariants({ variant: "ghost" }),
"absolute left-4 top-4 md:left-8 md:top-8")}>
<>
<ChevronLeftIcon className="mr-2 h-4 w-4" />Back
</>
</Link>
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<div className="flex flex-col space-y-2 text-center">
<Icons.logo className="mx-auto h-6 w-6" />
<h1 className="text-2xl font-semibold tracking-tight">
Welcome back
</h1>
<p className="text-sm text-muted-foreground">
Enter your email and password to continue
</p>
</div>
<LoginForm />
<p className="px-8 text-center text-sm text-muted-foreground">
<Link
href="/signup"
className="hover:text-brand underline underline-offset-4">
Don&apos;t have an account? Sign Up
</Link>
</p>
<LegalTerms />
</div>
</div>
);
} }

View File

@ -1,52 +1,7 @@
"use client" 'use client';
import Link from "next/link"; import { SignUpForm } from '@/features/auth/components/sign-up-form';
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
import { Icons } from "@/components/icons";
import { ChevronLeftIcon } from "@radix-ui/react-icons";
import { SignUpForm } from "@/features/auth/components/sign-up-form";
import LegalTerms from "@/features/auth/components/legal-terms";
export default function SignUpPage() { export default function SignUpPage() {
return <SignUpForm />;
return (
<div className="container flex h-screen w-screen flex-col items-center justify-center">
<Link
href="/"
className={cn(
buttonVariants({ variant: "ghost" }),
"absolute left-4 top-4 md:left-8 md:top-8")}>
<>
<ChevronLeftIcon className="mr-2 h-4 w-4" />Back
</>
</Link>
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<div className="flex flex-col space-y-2 text-center">
<Icons.logo className="mx-auto h-6 w-6" />
<h1 className="text-2xl font-semibold tracking-tight">
Create an account
</h1>
<p className="text-sm text-muted-foreground">
Enter your name, email and password to signup
</p>
</div>
<SignUpForm />
<p className="px-8 text-center text-sm text-muted-foreground">
<Link
href="/login"
className="hover:text-brand underline underline-offset-4">
Already have an account? Sign In
</Link>
</p>
<LegalTerms />
</div>
</div>
);
} }

View File

@ -12,7 +12,7 @@ export default function Page() {
return ( return (
<div className="w-full h-[500px]"> <div className="w-full h-[500px]">
<Editor pageId={pageId} /> <Editor pageId={pageId as string} />
</div> </div>
); );
} }

View File

@ -1,9 +1,9 @@
"use client"; 'use client';
import { useAtom } from "jotai"; import { useAtom } from 'jotai';
import { currentUserAtom } from "@/features/user/atoms/current-user-atom"; import { currentUserAtom } from '@/features/user/atoms/current-user-atom';
export default function Home() { export default function HomeB() {
const [currentUser] = useAtom(currentUserAtom); const [currentUser] = useAtom(currentUserAtom);
return ( return (

View File

@ -1,9 +1,9 @@
"use client" 'use client';
import dynamic from "next/dynamic"; import dynamic from 'next/dynamic';
import { UserProvider } from "@/features/user/user-provider"; import { UserProvider } from '@/features/user/user-provider';
const Shell = dynamic(() => import("./shell"), { const Shell = dynamic(() => import('./shell'), {
ssr: false, ssr: false,
}); });
@ -14,11 +14,7 @@ export default function DashboardLayout({ children }: {
return ( return (
<UserProvider> <UserProvider>
<Shell> <Shell>
<div className="w-full flex justify-center z-10 flex-shrink-0">
<div className={`w-[900px]`}>
{children} {children}
</div>
</div>
</Shell> </Shell>
</UserProvider> </UserProvider>
); );

View File

@ -1,23 +0,0 @@
'use client';
import AccountNameForm from '@/features/user/components/account-name-form';
import ChangePassword from "@/features/user/components/change-password";
import ChangeEmail from "@/features/user/components/change-email";
import { Separator } from "@/components/ui/separator";
import React from "react";
export default function Home() {
return (
<>
<AccountNameForm />
<Separator className="my-4" />
<ChangeEmail />
<Separator className="my-4" />
<ChangePassword />
</>);
}

View File

@ -1,9 +0,0 @@
import { ReactNode } from 'react';
export default function SettingsLayout({ children }: { children: ReactNode }) {
return (
<>
{children}
</>
);
}

View File

@ -1,14 +0,0 @@
'use client';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
export default function Home() {
const router = useRouter();
useEffect(() => {
router.push('/settings/account');
}, [router]);
return <></>;
}

View File

@ -1,27 +0,0 @@
"use client";
import { Separator } from "@/components/ui/separator";
import WorkspaceInviteSection from "@/features/workspace/components/workspace-invite-section";
import React from "react";
import WorkspaceInviteDialog from "@/features/workspace/components/workspace-invite-dialog";
const WorkspaceMembersTable = React.lazy(() => import('@/features/workspace/components/workspace-members-table'));
export default function WorkspaceMembers() {
return (
<>
<WorkspaceInviteSection />
<Separator className="my-8" />
<div className="space-y-4">
<h4 className="font-semibold">Members</h4>
<WorkspaceInviteDialog />
<WorkspaceMembersTable />
</div>
</>
);
}

View File

@ -1,9 +0,0 @@
'use client';
import WorkspaceNameForm from "@/features/workspace/components/workspace-name-form";
export default function Home() {
return (<WorkspaceNameForm />);
}

View File

@ -1,23 +1,54 @@
'use client'; 'use client';
import Sidebar from '@/components/sidebar/sidebar'; import { desktopSidebarAtom } from '@/components/navbar/atoms/sidebar-atom';
import TopBar from '@/components/sidebar/topbar'; import { useToggleSidebar } from '@/components/navbar/hooks/use-toggle-sidebar';
import { Navbar } from '@/components/navbar/navbar';
import { AppShell, Burger, Group } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useAtom } from 'jotai';
export default function Shell({ children }: { children: React.ReactNode }) { export default function Shell({ children }: { children: React.ReactNode }) {
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
const [desktopOpened] = useAtom(desktopSidebarAtom);
const toggleDesktop = useToggleSidebar(desktopSidebarAtom);
return ( return (
<div className="flex justify-start min-h-screen"> <AppShell
<Sidebar /> layout="alt"
header={{ height: 45 }}
<div className="flex flex-col w-full overflow-hidden"> navbar={{
<TopBar /> width: 300,
breakpoint: 'sm',
<main collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
className="overflow-y-auto overscroll-none w-full p-8" }}
style={{ height: 'calc(100vh - 50px)' }} padding="md"
> >
{children} <AppShell.Header>
</main> <Group h="100%" px="md">
</div> <Burger
</div> opened={mobileOpened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Burger
opened={desktopOpened}
onClick={toggleDesktop}
visibleFrom="sm"
size="sm"
/>
Header
</Group>
</AppShell.Header>
<AppShell.Navbar>
<Navbar />
</AppShell.Navbar>
<AppShell.Main>{children}</AppShell.Main>
</AppShell>
); );
} }

View File

@ -1,76 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 62.8% 50.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 10%;
--foreground: 0 0% 85%;
--card: 0 0% 10%;
--card-foreground: 0 0% 85%;
--popover: 0 0% 10%;
--popover-foreground: 0 0% 85%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 85%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 85%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -1,12 +1,9 @@
import './globals.css' import '@mantine/core/styles.css';
import type { Metadata } from 'next' import type { Metadata } from 'next';
import { Inter } from 'next/font/google' import { TanstackProvider } from '@/components/providers/tanstack-provider';
import { cn } from "@/lib/utils"; import CustomToaster from '@/components/ui/custom-toaster';
import { ThemeProvider } from "@/components/providers/theme-provider"; import { theme } from '@/app/theme';
import { TanstackProvider } from "@/components/providers/tanstack-provider"; import { ColorSchemeScript, MantineProvider } from '@mantine/core';
import CustomToaster from "@/components/ui/custom-toaster";
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Create Next App', title: 'Create Next App',
@ -16,25 +13,26 @@ export const metadata: Metadata = {
initialScale: 1, initialScale: 1,
maximumScale: 1, maximumScale: 1,
}, },
} };
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
children: React.ReactNode children: React.ReactNode;
}) { }) {
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body className={cn("min-h-screen bg-background antialiased", inter.className)}> <head>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem> <ColorSchemeScript />
</head>
<body>
<MantineProvider theme={theme}>
<TanstackProvider> <TanstackProvider>
{children} {children}
<CustomToaster /> <CustomToaster />
</TanstackProvider> </TanstackProvider>
</ThemeProvider> </MantineProvider>
</body> </body>
</html> </html>
) );
} }

View File

@ -1,18 +1,5 @@
import {ThemeToggle} from "@/components/theme-toggle"; import { Welcome } from '@/components/welcome/welcome';
export default function Home() { export default function Home() {
return ( return <Welcome />;
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
Get started by editing&nbsp;
<code className="font-mono font-bold">src/app/page.tsx</code>
</p>
<div className="fixed bottom-0 left-z0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
<ThemeToggle />
</div>
</div>
</main>
)
} }

View File

@ -0,0 +1,7 @@
'use client';
import { createTheme } from '@mantine/core';
export const theme = createTheme({
});

View File

@ -1,5 +1,3 @@
import { atomWithWebStorage } from "@/lib/jotai-helper"; import { atomWithWebStorage } from "@/lib/jotai-helper";
import { atom } from "jotai";
export const desktopSidebarAtom = atomWithWebStorage('showSidebar',true); export const desktopSidebarAtom = atomWithWebStorage('showSidebar',true);
export const mobileSidebarAtom = atom(false);

View File

@ -1,6 +1,6 @@
import { useAtom } from "jotai"; import { useAtom } from "jotai";
export function useToggleSidebar(sidebarAtom) { export function useToggleSidebar(sidebarAtom: any) {
const [sidebarState, setSidebarState] = useAtom(sidebarAtom); const [sidebarState, setSidebarState] = useAtom(sidebarAtom);
return () => { return () => {
setSidebarState(!sidebarState); setSidebarState(!sidebarState);

View File

@ -0,0 +1,89 @@
.navbar {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
height: 100%;
width: rem(300px);
padding: var(--mantine-spacing-md);
padding-top: 0;
display: flex;
flex-direction: column;
border-right: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
.section {
margin-left: calc(var(--mantine-spacing-md) * -1);
margin-right: calc(var(--mantine-spacing-md) * -1);
margin-bottom: var(--mantine-spacing-md);
&:not(:last-of-type) {
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
}
.searchCode {
font-weight: 700;
font-size: rem(10px);
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
border: rem(1px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-7));
}
.menuItems {
padding-left: calc(var(--mantine-spacing-md) - var(--mantine-spacing-xs));
padding-right: calc(var(--mantine-spacing-md) - var(--mantine-spacing-xs));
padding-bottom: var(--mantine-spacing-md);
}
.menu {
display: flex;
align-items: center;
width: 100%;
font-size: var(--mantine-font-size-sm);
padding: rem(8px) var(--mantine-spacing-xs);
border-radius: var(--mantine-radius-sm);
font-weight: 500;
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
&
:hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
}
}
.menuItemInner {
display: flex;
align-items: center;
flex: 1;
}
.menuItemIcon {
margin-right: var(--mantine-spacing-sm);
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
}
.pages {
padding-left: calc(var(--mantine-spacing-md) - rem(6px));
padding-right: calc(var(--mantine-spacing-md) - rem(6px));
padding-bottom: var(--mantine-spacing-md);
}
.pagesHeader {
padding-left: calc(var(--mantine-spacing-md) + rem(2px));
padding-right: var(--mantine-spacing-md);
margin-bottom: rem(5px);
}
.pageLink {
display: block;
padding: rem(8px) var(--mantine-spacing-xs);
text-decoration: none;
border-radius: var(--mantine-radius-sm);
font-size: var(--mantine-font-size-xs);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
line-height: 1;
font-weight: 500;
&:hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
}
}

View File

@ -0,0 +1,118 @@
import {
UnstyledButton,
Text,
Group,
ActionIcon,
Tooltip,
rem,
} from '@mantine/core';
import {
IconSearch,
IconPlus,
IconSettings,
IconFilePlus,
} from '@tabler/icons-react';
import classes from './navbar.module.css';
import { UserButton } from './user-button';
import React from 'react';
import { useAtom } from 'jotai';
import { settingsModalAtom } from '@/features/settings/modal/atoms/settings-modal-atom';
import SettingsModal from '@/features/settings/modal/settings-modal';
interface PrimaryMenuItem {
icon: React.ElementType;
label: string;
onClick?: () => void;
}
interface PageItem {
emoji: string;
label: string;
}
const primaryMenu: PrimaryMenuItem[] = [
{ icon: IconSearch, label: 'Search' },
{ icon: IconSettings, label: 'Settings' },
{ icon: IconFilePlus, label: 'New Page' },
];
const pages: PageItem[] = [
{ emoji: '👍', label: 'Sales' },
{ emoji: '🚚', label: 'Deliveries' },
{ emoji: '💸', label: 'Discounts' },
{ emoji: '💰', label: 'Profits' },
{ emoji: '✨', label: 'Reports' },
{ emoji: '🛒', label: 'Orders' },
{ emoji: '📅', label: 'Events' },
{ emoji: '🙈', label: 'Debts' },
{ emoji: '💁‍♀️', label: 'Customers' },
];
export function Navbar() {
const [, setSettingsModalOpen] = useAtom(settingsModalAtom);
const handleMenuItemClick = (label: string) => {
if (label === 'Settings') {
setSettingsModalOpen(true);
}
};
const primaryMenuItems = primaryMenu.map((menuItem) => (
<UnstyledButton key={menuItem.label} className={classes.menu}
onClick={() => handleMenuItemClick(menuItem.label)}
>
<div className={classes.menuItemInner}>
<menuItem.icon size={20} className={classes.menuItemIcon} stroke={1.5} />
<span>{menuItem.label}</span>
</div>
</UnstyledButton>
));
const pageLinks = pages.map((page) => (
<a
href="#"
onClick={(event) => event.preventDefault()}
key={page.label}
className={classes.pageLink}
>
<span style={{ marginRight: rem(9), fontSize: rem(16) }}>
{page.emoji}
</span>{' '}
{page.label}
</a>
));
return (
<>
<nav className={classes.navbar}>
<div className={classes.section}>
<UserButton />
</div>
<div className={classes.section}>
<div className={classes.menuItems}>{primaryMenuItems}</div>
</div>
<div className={classes.section}>
<Group className={classes.pagesHeader} justify="space-between">
<Text size="xs" fw={500} c="dimmed">
Pages
</Text>
<Tooltip label="Create page" withArrow position="right">
<ActionIcon variant="default" size={18}>
<IconPlus
style={{ width: rem(12), height: rem(12) }}
stroke={1.5}
/>
</ActionIcon>
</Tooltip>
</Group>
<div className={classes.pages}>{pageLinks}</div>
</div>
</nav>
<SettingsModal />
</>
);
}

View File

@ -0,0 +1,10 @@
.user {
display: block;
width: 100%;
padding: var(--mantine-spacing-md);
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
@mixin hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
}
}

View File

@ -0,0 +1,28 @@
import { UnstyledButton, Group, Avatar, Text, rem } from '@mantine/core';
import { IconChevronRight } from '@tabler/icons-react';
import classes from './user-button.module.css';
export function UserButton() {
return (
<UnstyledButton className={classes.user}>
<Group>
<Avatar
src="https://images.unsplash.com/photo-1508214751196-bcfd4ca60f91?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=255&q=80"
radius="xl"
/>
<div style={{ flex: 1 }}>
<Text size="sm" fw={500}>
Harriette Spoonlicker
</Text>
<Text c="dimmed" size="xs">
hspoonlicker@outlook.com
</Text>
</div>
<IconChevronRight style={{ width: rem(14), height: rem(14) }} stroke={1.5} />
</Group>
</UnstyledButton>
);
}

View File

@ -1,4 +1,4 @@
'use client' 'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
@ -7,9 +7,9 @@ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
refetchOnMount: false, refetchOnMount: false,
refetchOnWindowFocus: false refetchOnWindowFocus: false,
} },
} },
}); });
export function TanstackProvider({ children }: React.PropsWithChildren) { export function TanstackProvider({ children }: React.PropsWithChildren) {

View File

@ -1,15 +0,0 @@
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { ThemeProviderProps } from "next-themes/dist/types"
import { TooltipProvider } from "@/components/ui/tooltip"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return (
<NextThemesProvider {...props}>
<TooltipProvider>{children}</TooltipProvider>
</NextThemesProvider>
)
}

View File

@ -1,69 +0,0 @@
import React, { ReactNode } from 'react';
import {
IconHome,
IconSearch,
IconSettings,
IconFilePlus,
} from '@tabler/icons-react';
import NavigationLink from "@/components/sidebar/navigation/navigation-link";
import ButtonWithIcon from "@/components/ui/button-with-icon";
export type NavigationMenuType = {
label: string;
path: string;
icon: ReactNode;
target?: string,
onClick?: React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement>;
};
export const navigationMenu: NavigationMenuType[] = [
{
label: 'Home',
path: '',
icon: <IconHome size={16} />,
target: '/home',
},
{
label: 'Search',
path: '',
icon: <IconSearch size={16} />,
},
{
label: 'Settings',
path: '',
icon: <IconSettings size={16} />,
target: '/settings/account'
},
{
label: 'New Page',
path: '',
icon: <IconFilePlus size={16} />,
},
];
export const renderMenuItem = (menu, index) => {
if (menu.target) {
return (
<NavigationLink
key={index}
href={menu.target}
icon={menu.icon}
className="w-full flex flex-1 justify-start items-center"
>
{menu.label}
</NavigationLink>
);
}
return (
<ButtonWithIcon
key={index}
icon={menu.icon}
variant="ghost"
className="w-full flex flex-1 justify-start items-center"
// onClick={}
>
<span className="text-ellipsis overflow-hidden">{menu.label}</span>
</ButtonWithIcon>
);
};

View File

@ -1,25 +0,0 @@
import { ReactNode } from "react";
import { buttonVariants } from "@/components/ui/button";
import Link from "next/link";
import { cn } from "@/lib/utils";
interface NavigationLinkProps {
children: ReactNode,
href: string;
icon?: ReactNode;
variant?: "default" | "ghost" | "outline";
className?: string;
}
export default function NavigationLink({ children, href, icon, variant = "ghost", className }: NavigationLinkProps) {
return (
<Link href={href} className={cn(buttonVariants({ variant: variant }), className)}>
{icon && <span className="mr-[8px]">
{icon}
</span>}
<span className="text-ellipsis overflow-hidden">
{children}
</span>
</Link>
);
}

View File

@ -1,40 +0,0 @@
import { SidebarSection } from "@/components/sidebar/sidebar-section";
import { navigationMenu, renderMenuItem } from "@/components/sidebar/navigation/navigation-items";
import { ScrollArea } from "@/components/ui/scroll-area";
import NavigationLink from "@/components/sidebar/navigation/navigation-link";
import { IconFileText } from "@tabler/icons-react";
import React from "react";
export default function Navigation() {
return (
<div className="pt-8">
<PrimaryNavigation />
<SecondaryNavigationArea />
</div>
);
}
function PrimaryNavigation() {
return (
<SidebarSection className="pb-2 mb-4 select-none border-b">
{navigationMenu.map(renderMenuItem)}
</SidebarSection>
);
}
function SecondaryNavigationArea() {
return (
<ScrollArea className="h-[70vh]">
<div className="space-y-1">
<NavigationLink
href="#"
className="w-full justify-start"
icon={<IconFileText size={16} />}
>
Welcome page
</NavigationLink>
</div>
</ScrollArea>
);
}

View File

@ -1,15 +0,0 @@
import React, { ReactNode } from "react";
import { cn } from "@/lib/utils";
interface SidebarSectionProps {
className?: string;
children: ReactNode
}
export function SidebarSection({className, children}: SidebarSectionProps) {
return (
<div className={cn('flex-shrink-0 flex-grow-0 pb-0.5', className)}>
{children}
</div>
)
}

View File

@ -1,47 +0,0 @@
import React from 'react';
import { useIsMobile } from '@/hooks/use-is-mobile';
import {
desktopSidebarAtom,
mobileSidebarAtom,
} from '@/components/sidebar/atoms/sidebar-atom';
import { useToggleSidebar } from './hooks/use-toggle-sidebar';
import ButtonWithIcon from '../ui/button-with-icon';
import {
IconLayoutSidebarLeftCollapse,
IconLayoutSidebarRightCollapse,
} from '@tabler/icons-react';
import { useAtom } from 'jotai';
import { cn } from '@/lib/utils';
interface SidebarToggleButtonProps {
className?: string;
}
export default function SidebarToggleButton({
className,
}: SidebarToggleButtonProps) {
const isMobile = useIsMobile();
const sidebarStateAtom = isMobile ? mobileSidebarAtom : desktopSidebarAtom;
const [isSidebarOpen] = useAtom(sidebarStateAtom);
const toggleSidebar = useToggleSidebar(sidebarStateAtom);
const SidebarIcon = isSidebarOpen
? IconLayoutSidebarLeftCollapse
: IconLayoutSidebarRightCollapse;
return (
<ButtonWithIcon
className={cn(className, 'z-50')}
icon={<SidebarIcon size={20} />}
variant={'ghost'}
onClick={toggleSidebar}
/>
);
}
export function MobileSidebarToggle({ isSidebarOpen }) {
return (
<SidebarToggleButton className={`absolute top-0 ${isSidebarOpen ? "right-0" : "left-0"} right-0 m-4`} />
);
}

View File

@ -1,56 +0,0 @@
import { useIsMobile } from "@/hooks/use-is-mobile";
import { useAtom } from "jotai";
import {
desktopSidebarAtom,
mobileSidebarAtom,
} from "@/components/sidebar/atoms/sidebar-atom";
import { MobileSidebarToggle } from "./sidebar-toggle-button";
import SettingsNav from "@/features/settings/nav/settings-nav";
import { usePathname } from "next/navigation";
import React, { useEffect } from "react";
import Navigation from "@/components/sidebar/navigation/navigation";
export default function Sidebar() {
const isMobile = useIsMobile();
const pathname = usePathname();
const [isSidebarOpen, setIsSidebarOpen] = useAtom(isMobile ? mobileSidebarAtom : desktopSidebarAtom);
const isSettings = pathname.startsWith("/settings");
const mobileClass = "fixed top-0 left-0 h-screen z-50 bg-background";
const sidebarWidth = isSidebarOpen ? "w-[270px]" : "w-[0px]";
const closeSidebar = () => {
setIsSidebarOpen(false);
};
useEffect(() => {
if (isMobile) {
setIsSidebarOpen(false);
}
}, [pathname, isMobile, setIsSidebarOpen]);
return (
<>
{isMobile && isSidebarOpen && (
<div className="fixed top-0 left-0 w-full h-screen z-[50] bg-black/60"
onClick={closeSidebar}>
</div>
)}
<nav
className={`${sidebarWidth} ${isMobile && isSidebarOpen ? mobileClass : ""}
flex-grow-0 flex-shrink-0 overflow-hidden border-r duration-300 z-49`}>
{isMobile && (
<MobileSidebarToggle isSidebarOpen={isSidebarOpen} />
)}
<div className="flex flex-col flex-shrink-0 gap-0.5 p-[10px]">
<div className="h-full mt-[8px]">
{isSettings ? <SettingsNav /> : <Navigation />}
</div>
</div>
</nav>
</>
);
}

View File

@ -1,21 +0,0 @@
'use client';
import { useIsMobile } from '@/hooks/use-is-mobile';
import SidebarToggleButton from './sidebar-toggle-button';
export default function TopBar() {
const isMobile = useIsMobile();
return (
<header className="max-w-full z-10 select-none">
<div className="w-full max-w-full h-[50px] relative">
<div className="flex justify-between items-center h-full overflow-hidden py-0 px-1 gap-2.5 border-b">
<div className="flex items-center h-full flex-grow-0 mr-[8px] min-w-0">
{!isMobile && <SidebarToggleButton />}
</div>
</div>
</div>
</header>
);
}

View File

@ -1,23 +1,15 @@
"use client" 'use client';
import * as React from "react" import { Button, Group, useMantineColorScheme } from '@mantine/core';
import { MoonIcon, SunIcon } from "@radix-ui/react-icons"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
export function ThemeToggle() { export function ThemeToggle() {
const { setTheme, theme } = useTheme() const { setColorScheme } = useMantineColorScheme();
return ( return (
<Button <Group justify="center" mt="xl">
variant="ghost" <Button onClick={() => setColorScheme('light')}>Light</Button>
size="icon" <Button onClick={() => setColorScheme('dark')}>Dark</Button>
onClick={() => setTheme(theme === "light" ? "dark" : "light")} <Button onClick={() => setColorScheme('auto')}>Auto</Button>
> </Group>
<SunIcon className="h-[1.5rem] w-[1.3rem] dark:hidden" /> );
<MoonIcon className="hidden h-5 w-5 dark:block" />
<span className="sr-only">Toggle theme</span>
</Button>
)
} }

View File

@ -1,36 +0,0 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -1,24 +0,0 @@
import React, { forwardRef, ReactNode } from 'react';
import { Button } from '@/components/ui/button';
interface ButtonIconProps {
icon: ReactNode;
children?: ReactNode;
}
type Props = ButtonIconProps & React.ComponentPropsWithoutRef<typeof Button>;
const ButtonWithIcon = forwardRef<HTMLButtonElement, Props>(
({ icon, children, ...rest }, ref) => {
return (
<Button ref={ref} {...rest} {...(children ? {} : { size: 'icon' })}>
<div className={`${children ? 'mr-[8px]' : ''}`}>{icon}</div>
{children}
</Button>
);
}
);
ButtonWithIcon.displayName = 'ButtonWithIcon';
export default ButtonWithIcon;

View File

@ -1,57 +0,0 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"whitespace-nowrap inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-7 w-7",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -1,76 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -1,155 +0,0 @@
"use client"
import * as React from "react"
import { DialogProps } from "@radix-ui/react-dialog"
import { MagnifyingGlassIcon } from "@radix-ui/react-icons"
import { Command as CommandPrimitive } from "cmdk"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<MagnifyingGlassIcon className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@ -1,123 +0,0 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = ({
className,
...props
}: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal className={cn(className)} {...props} />
)
DialogPortal.displayName = DialogPrimitive.Portal.displayName
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/60 backdrop-blur-none dark:backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -1,176 +0,0 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@ -1,25 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -1,26 +0,0 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -1,31 +0,0 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View File

@ -1,48 +0,0 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@ -1,120 +0,0 @@
"use client"
import * as React from "react"
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
import * as SelectPrimitive from "@radix-ui/react-select"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
}

View File

@ -1,31 +0,0 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

View File

@ -1,117 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn("bg-primary font-medium text-primary-foreground", className)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@ -1,30 +0,0 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -0,0 +1,23 @@
import { Title, Text } from '@mantine/core';
import { ThemeToggle } from '../theme-toggle';
export function Welcome() {
return (
<>
<Title ta="center" mt={100}>
<Text
inherit
variant="gradient"
component="span"
gradient={{ from: 'pink', to: 'yellow' }}
>
Welcome
</Text>
</Title>
<Text ta="center" size="lg" maw={580} mx="auto" mt="xl">
Welcome to something new and interesting.
</Text>
<ThemeToggle />
</>
);
}

View File

@ -1,12 +0,0 @@
import Link from "next/link";
export default function LegalTerms(){
return (
<p className="px-8 text-center text-sm text-muted-foreground">
By clicking continue, you agree to our{" "}
<Link href="#" className="underline underline-offset-4 hover:text-primary">
Terms of Service</Link>{" "} and{" "}
<Link href="#" className="underline underline-offset-4 hover:text-primary">Privacy Policy</Link>.
</p>
)
}

View File

@ -1,92 +1,80 @@
"use client"; 'use client';
import * as React from "react"; import * as React from 'react';
import * as z from "zod"; import * as z from 'zod';
import { cn } from "@/lib/utils"; import { useForm, zodResolver } from '@mantine/form';
import { Button, buttonVariants } from "@/components/ui/button"; import useAuth from '@/features/auth/hooks/use-auth';
import { Icons } from "@/components/icons"; import { ILogin } from '@/features/auth/types/auth.types';
import { useForm } from "react-hook-form"; import {
import { zodResolver } from "@hookform/resolvers/zod"; Container,
import { Label } from "@/components/ui/label"; Title,
import { Input } from "@/components/ui/input"; Anchor,
import useAuth from "@/features/auth/hooks/use-auth"; Paper,
import { ILogin } from "@/features/auth/types/auth.types"; TextInput,
Button,
Text,
PasswordInput,
} from '@mantine/core';
import Link from 'next/link';
const formSchema = z.object({ const formSchema = z.object({
email: z.string({ required_error: "email is required" }).email({ message: "Invalid email address" }), email: z
password: z.string({ required_error: "password is required" }), .string({ required_error: 'email is required' })
.email({ message: 'Invalid email address' }),
password: z.string({ required_error: 'password is required' }),
}); });
interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> { export function LoginForm() {
}
export function LoginForm({ className, ...props }: UserAuthFormProps) {
const { register, handleSubmit, formState: { errors } }
= useForm<ILogin>({ resolver: zodResolver(formSchema) });
const { signIn, isLoading } = useAuth(); const { signIn, isLoading } = useAuth();
const form = useForm<ILogin>({
validate: zodResolver(formSchema),
initialValues: {
email: '',
password: '',
},
});
async function onSubmit(data: ILogin) { async function onSubmit(data: ILogin) {
await signIn(data); await signIn(data);
} }
return ( return (
<> <Container size={420} my={40}>
<div className={cn("grid gap-6 space-y-5", className)} {...props}> <Title ta="center" fw={800}>
<form onSubmit={handleSubmit(onSubmit)}> Login
<div className="grid gap-2"> </Title>
<Text c="dimmed" size="sm" ta="center" mt={5}>
Do not have an account yet?{' '}
<Anchor size="sm" component={Link} href="/signup">
Create account
</Anchor>
</Text>
<div className="space-y-2"> <Paper withBorder shadow="md" p={30} mt={30} radius="md">
<Label className="" htmlFor="email"> <form onSubmit={form.onSubmit(onSubmit)}>
Email <TextInput
</Label>
<Input
id="email" id="email"
placeholder="name@example.com"
type="email" type="email"
autoCapitalize="none" label="Email"
autoComplete="email" placeholder="email@example.com"
autoCorrect="off"
required required
disabled={isLoading} {...form.getInputProps('email')}
{...register("email")} /> />
{errors?.email && (
<p className="px-1 text-xs text-red-600">
{errors.email.message}
</p>
)}
</div>
<div className="space-y-2"> <PasswordInput
<Label htmlFor="password"> label="Password"
Password placeholder="Your password"
</Label>
<Input
id="password"
placeholder="Enter your password"
type="password"
autoComplete="off"
required required
disabled={isLoading} mt="md"
{...register("password")} /> {...form.getInputProps('password')}
{errors?.password && ( />
<p className="px-1 text-xs text-red-600"> <Button type="submit" fullWidth mt="xl" loading={isLoading}>
{errors.password.message}
</p>
)}
</div>
<Button className={cn(buttonVariants(), "mt-2")} disabled={isLoading}>
{isLoading && (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
)}
Sign In Sign In
</Button> </Button>
</div>
</form> </form>
</Paper>
</div> </Container>
</>
); );
} }

View File

@ -1,91 +1,80 @@
"use client"; 'use client';
import * as React from "react"; import * as React from 'react';
import * as z from "zod"; import * as z from 'zod';
import { cn } from "@/lib/utils"; import { useForm, zodResolver } from '@mantine/form';
import { Button, buttonVariants } from "@/components/ui/button"; import useAuth from '@/features/auth/hooks/use-auth';
import { Icons } from "@/components/icons"; import { IRegister } from '@/features/auth/types/auth.types';
import { useForm } from "react-hook-form"; import {
import { zodResolver } from "@hookform/resolvers/zod"; Container,
import { Label } from "@/components/ui/label"; Title,
import { Input } from "@/components/ui/input"; Anchor,
import useAuth from "@/features/auth/hooks/use-auth"; Paper,
import { IRegister } from "@/features/auth/types/auth.types"; TextInput,
Button,
Text,
PasswordInput,
} from '@mantine/core';
import Link from 'next/link';
const formSchema = z.object({ const formSchema = z.object({
email: z.string({ required_error: "email is required" }).email({ message: "Invalid email address" }), email: z
password: z.string({ required_error: "password is required" }).min(8), .string({ required_error: 'email is required' })
.email({ message: 'Invalid email address' }),
password: z.string({ required_error: 'password is required' }),
}); });
interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> {} export function SignUpForm() {
export function SignUpForm({ className, ...props }: UserAuthFormProps) {
const { register, handleSubmit, formState: { errors } }
= useForm<IRegister>({ resolver: zodResolver(formSchema) });
const { signUp, isLoading } = useAuth(); const { signUp, isLoading } = useAuth();
const form = useForm<IRegister>({
validate: zodResolver(formSchema),
initialValues: {
email: '',
password: '',
},
});
async function onSubmit(data: IRegister) { async function onSubmit(data: IRegister) {
await signUp(data); await signUp(data);
} }
return ( return (
<> <Container size={420} my={40}>
<div className={cn("grid gap-6 space-y-5", className)} {...props}> <Title ta="center" fw={800}>
<form onSubmit={handleSubmit(onSubmit)}> Create an account
<div className="grid gap-2"> </Title>
<Text c="dimmed" size="sm" ta="center" mt={5}>
Already have an account?{' '}
<Anchor size="sm" component={Link} href="/login">
Login
</Anchor>
</Text>
<div className="space-y-2"> <Paper withBorder shadow="md" p={30} mt={30} radius="md">
<Label className="" htmlFor="email"> <form onSubmit={form.onSubmit(onSubmit)}>
Email <TextInput
</Label>
<Input
id="email" id="email"
placeholder="name@example.com"
type="email" type="email"
autoCapitalize="none" label="Email"
autoComplete="email" placeholder="email@example.com"
autoCorrect="off"
required required
disabled={isLoading} {...form.getInputProps('email')}
{...register("email")} /> />
{errors?.email && (
<p className="px-1 text-xs text-red-600">
{errors.email.message}
</p>
)}
</div>
<div className="space-y-2"> <PasswordInput
<Label htmlFor="password"> label="Password"
Password placeholder="Your password"
</Label>
<Input
id="password"
placeholder="Enter your password"
type="password"
autoComplete="off"
required required
disabled={isLoading} mt="md"
{...register("password")} /> {...form.getInputProps('password')}
{errors?.password && ( />
<p className="px-1 text-xs text-red-600"> <Button type="submit" fullWidth mt="xl" loading={isLoading}>
{errors.password.message}
</p>
)}
</div>
<Button className={cn(buttonVariants(), "mt-2")} disabled={isLoading}>
{isLoading && (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
)}
Sign Up Sign Up
</Button> </Button>
</div>
</form> </form>
</Paper>
</div> </Container>
</>
); );
} }

View File

@ -16,7 +16,6 @@ import '@/features/editor/css/editor.css';
interface EditorProps{ interface EditorProps{
pageId: string, pageId: string,
token: string,
} }
const colors = ['#958DF1', '#F98181', '#FBBC88', '#FAF594', '#70CFF8', '#94FADB', '#B9F18D'] const colors = ['#958DF1', '#F98181', '#FBBC88', '#FAF594', '#70CFF8', '#94FADB', '#B9F18D']

View File

@ -0,0 +1,23 @@
'use client';
import React from "react";
import AccountNameForm from '@/features/settings/account/settings/components/account-name-form';
import ChangeEmail from '@/features/settings/account/settings/components/change-email';
import ChangePassword from '@/features/settings/account/settings/components/change-password';
import { Divider } from '@mantine/core';
export default function AccountSettings() {
return (
<>
<AccountNameForm />
<Divider my="lg" />
<ChangeEmail />
<Divider my="lg" />
<ChangePassword />
</>);
}

View File

@ -0,0 +1,66 @@
'use client';
import { useAtom } from 'jotai';
import { focusAtom } from 'jotai-optics';
import * as z from 'zod';
import { useForm, zodResolver } from '@mantine/form';
import { currentUserAtom } from '@/features/user/atoms/current-user-atom';
import { updateUser } from '@/features/user/services/user-service';
import { IUser } from '@/features/user/types/user.types';
import { useState } from 'react';
import toast from 'react-hot-toast';
import { TextInput, Button } from '@mantine/core';
const formSchema = z.object({
name: z.string().min(2).max(40).nonempty('Your name cannot be blank'),
});
type FormValues = z.infer<typeof formSchema>;
const userAtom = focusAtom(currentUserAtom, (optic) => optic.prop('user'));
export default function AccountNameForm() {
const [isLoading, setIsLoading] = useState(false);
const [currentUser] = useAtom(currentUserAtom);
const [, setUser] = useAtom(userAtom);
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
initialValues: {
name: currentUser?.user?.name,
},
});
async function handleSubmit(data: Partial<IUser>) {
setIsLoading(true);
try {
const updatedUser = await updateUser(data);
setUser(updatedUser);
toast.success('Updated successfully');
} catch (err) {
console.log(err);
toast.error('Failed to update data.');
}
setIsLoading(false);
}
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<TextInput
id="name"
label="Name"
placeholder="Your name"
variant="filled"
{...form.getInputProps('name')}
rightSection={
<Button type="submit" disabled={isLoading} loading={isLoading}>
Save
</Button>
}
/>
</form>
);
}

View File

@ -0,0 +1,85 @@
'use client';
import { Modal, TextInput, Button, Text, Group, PasswordInput } from '@mantine/core';
import * as z from 'zod';
import { useState } from 'react';
import { useAtom } from 'jotai';
import { currentUserAtom } from '@/features/user/atoms/current-user-atom';
import { useDisclosure } from '@mantine/hooks';
import * as React from 'react';
import { useForm, zodResolver } from '@mantine/form';
export default function ChangeEmail() {
const [currentUser] = useAtom(currentUserAtom);
const [opened, { open, close }] = useDisclosure(false);
return (
<Group justify="space-between" wrap="nowrap" gap="xl">
<div>
<Text size="md">Email</Text>
<Text size="sm" c="dimmed">
{currentUser.user.email}
</Text>
</div>
<Button onClick={open} variant="default">Change email</Button>
<Modal opened={opened} onClose={close} title="Change email" centered>
<Text mb="md">To change your email, you have to enter your password and new email.</Text>
<ChangePasswordForm />
</Modal>
</Group>
);
}
const formSchema = z.object({
email: z.string({ required_error: 'New email is required' }).email(),
password: z.string({ required_error: 'your current password is required' }).min(8),
});
type FormValues = z.infer<typeof formSchema>
function ChangePasswordForm() {
const [isLoading, setIsLoading] = useState(false);
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
initialValues: {
password: '',
email: '',
},
});
function handleSubmit(data: FormValues) {
setIsLoading(true);
console.log(data);
}
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<PasswordInput
label="Password"
placeholder="Enter your password"
variant="filled"
mb="md"
{...form.getInputProps('password')}
/>
<TextInput
id="email"
label="Email"
description="Enter your new preferred email"
placeholder="New email"
variant="filled"
mb="md"
{...form.getInputProps('email')}
/>
<Button type="submit" disabled={isLoading} loading={isLoading}>
Change email
</Button>
</form>
);
}

View File

@ -0,0 +1,87 @@
'use client';
import { Button, Group, Text, Modal, PasswordInput } from '@mantine/core';
import * as z from 'zod';
import { useState } from 'react';
import { useDisclosure } from '@mantine/hooks';
import * as React from 'react';
import { useForm, zodResolver } from '@mantine/form';
export default function ChangePassword() {
const [opened, { open, close }] = useDisclosure(false);
return (
<Group justify="space-between" wrap="nowrap" gap="xl">
<div>
<Text size="md">Password</Text>
<Text size="sm" c="dimmed">
You can change your password here.
</Text>
</div>
<Button onClick={open} variant="default">Change password</Button>
<Modal opened={opened} onClose={close} title="Change password" centered>
<Text mb="md">Your password must be a minimum of 8 characters.</Text>
<ChangePasswordForm />
</Modal>
</Group>
);
}
const formSchema = z.object({
current: z.string({ required_error: 'your current password is required' }).min(1),
password: z.string({ required_error: 'New password is required' }).min(8),
confirm_password: z.string({ required_error: 'Password confirmation is required' }).min(8),
}).refine(data => data.password === data.confirm_password, {
message: 'Your new password and confirmation does not match.',
path: ['confirm_password'],
});
type FormValues = z.infer<typeof formSchema>
function ChangePasswordForm() {
const [isLoading, setIsLoading] = useState(false);
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
initialValues: {
current: '',
password: '',
confirm_password: '',
},
});
function handleSubmit(data: FormValues) {
setIsLoading(true);
console.log(data);
}
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<PasswordInput
label="Current password"
name="current"
placeholder="Enter your password"
variant="filled"
mb="md"
{...form.getInputProps('password')}
/>
<PasswordInput
label="New password"
placeholder="Enter your password"
variant="filled"
mb="md"
{...form.getInputProps('password')}
/>
<Button type="submit" disabled={isLoading} loading={isLoading}>
Change password
</Button>
</form>
);
}

View File

@ -0,0 +1,3 @@
import { atom } from "jotai";
export const settingsModalAtom = atom<boolean>(false);

View File

@ -0,0 +1,75 @@
.sidebar {
max-height: rem(700px);
width: rem(180px);
padding: var(--mantine-spacing-sm);
display: flex;
flex-direction: column;
border-right: rem(1px) solid
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
.sidebarFlex {
display: flex;
}
.sidebarMain {
flex: 1;
}
.sidebarRightSection {
flex: 1;
padding: rem(16px) rem(40px);
}
.sidebarItemHeader {
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
}
.sidebarItem {
cursor: pointer;
display: flex;
align-items: center;
text-decoration: none;
font-size: var(--mantine-font-size-sm);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
border-radius: var(--mantine-radius-sm);
font-weight: 500;
@mixin hover {
background-color: light-dark(
var(--mantine-color-gray-0),
var(--mantine-color-dark-6)
);
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
.sidebarItemIcon {
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
}
}
& [data-active] {
&,
& :hover {
background-color: var(--mantine-color-blue-light);
color: var(--mantine-color-blue-light-color);
.sidebarItemIcon {
color: var(--mantine-color-blue-light-color);
}
}
}
}
.sidebarItemIcon {
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
margin-right: var(--mantine-spacing-sm);
width: rem(20px);
height: rem(20px);
}

View File

@ -0,0 +1,32 @@
'use client';
import { Modal, Text } from '@mantine/core';
import React from 'react';
import SettingsSidebar from '@/features/settings/modal/settings-sidebar';
import { useAtom } from 'jotai';
import { settingsModalAtom } from '@/features/settings/modal/atoms/settings-modal-atom';
export default function SettingsModal() {
const [isModalOpen, setModalOpen] = useAtom(settingsModalAtom);
return (
<>
<Modal.Root size={1000} opened={isModalOpen} onClose={() => setModalOpen(false)}>
<Modal.Overlay />
<Modal.Content>
<Modal.Header>
<Modal.Title>
<Text size="xl" fw={500}>Settings</Text>
</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body>
<SettingsSidebar />
</Modal.Body>
</Modal.Content>
</Modal.Root>
</>
);
}

View File

@ -0,0 +1,103 @@
'use client';
import React, { useState } from 'react';
import classes from '@/features/settings/modal/modal.module.css';
import { IconBell, IconFingerprint, IconReceipt, IconSettingsCog, IconUser, IconUsers } from '@tabler/icons-react';
import { Loader, ScrollArea, Text } from '@mantine/core';
const AccountSettings = React.lazy(() => import('@/features/settings/account/settings/account-settings'));
const WorkspaceSettings = React.lazy(() => import('@/features/settings/workspace/settings/workspace-settings'));
const WorkspaceMembers = React.lazy(() => import('@/features/settings/workspace/members/workspace-members'));
interface DataItem {
label: string;
icon: React.ElementType;
}
interface DataGroup {
heading: string;
items: DataItem[];
}
const groupedData: DataGroup[] = [
{
heading: 'Account',
items: [
{ label: 'Account', icon: IconUser },
{ label: 'Notifications', icon: IconBell },
],
},
{
heading: 'Workspace',
items: [
{ label: 'General', icon: IconSettingsCog },
{ label: 'Members', icon: IconUsers },
{ label: 'Security', icon: IconFingerprint },
{ label: 'Billing', icon: IconReceipt },
],
},
];
export default function SettingsSidebar() {
const [active, setActive] = useState('Account');
const menu = groupedData.map((group) => (
<div key={group.heading}>
<Text c="dimmed" className={classes.sidebarItemHeader}>{group.heading}</Text>
{group.items.map((item) => (
<div
className={classes.sidebarItem}
data-active={item.label === active || undefined}
key={item.label}
onClick={(event) => {
event.preventDefault();
setActive(item.label);
}}
>
<item.icon className={classes.sidebarItemIcon} stroke={1.5} />
<span>{item.label}</span>
</div>
))}
</div>
));
let ActiveComponent;
switch (active) {
case 'Account':
ActiveComponent = AccountSettings;
break;
case 'General':
ActiveComponent = WorkspaceSettings;
break;
case 'Members':
ActiveComponent = WorkspaceMembers;
break;
default:
ActiveComponent = null;
}
return (
<div className={classes.sidebarFlex}>
<nav className={classes.sidebar}>
<div className={classes.sidebarMain}>
{menu}
</div>
</nav>
<ScrollArea h="650" w="100%" scrollbarSize={4}>
<div className={classes.sidebarRightSection}>
<React.Suspense fallback={<Loader size="sm" color="gray" />}>
{ActiveComponent && <ActiveComponent />}
</React.Suspense>
</div>
</ScrollArea>
</div>
);
}

View File

@ -1,50 +0,0 @@
'use client'
import { ReactNode } from 'react';
import { IconUserCircle, IconUser, IconUsers,
IconBuilding, IconSettingsCog } from '@tabler/icons-react';
export interface SettingsNavMenuSection {
heading: string;
icon: ReactNode;
items: SettingsNavMenuItem[];
}
export interface SettingsNavMenuItem {
label: string;
icon: ReactNode;
target?: string;
}
export type SettingsNavItem = SettingsNavMenuSection[];
export const settingsNavItems: SettingsNavItem = [
{
heading: 'Account',
icon: <IconUserCircle size={20}/>,
items: [
{
label: 'My account',
icon: <IconUser size={16}/>,
target: '/settings/account',
},
],
},
{
heading: 'Workspace',
icon: <IconBuilding size={20}/>,
items: [
{
label: 'General',
icon: <IconSettingsCog size={16}/>,
target: '/settings/workspace',
},
{
label: 'Members',
icon: <IconUsers size={16}/>,
target: '/settings/workspace/members',
},
],
},
];

View File

@ -1,67 +0,0 @@
"use client";
import {
SettingsNavItem,
SettingsNavMenuItem, SettingsNavMenuSection,
settingsNavItems
} from "@/features/settings/nav/settings-nav-items";
import { usePathname } from "next/navigation";
import Link from "next/link";
import React from "react";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
import { ChevronLeftIcon } from "@radix-ui/react-icons";
interface SettingsNavProps {
menu: SettingsNavItem;
}
function RenderNavItem({ label, icon, target }: SettingsNavMenuItem): React.ReactNode {
const pathname = usePathname();
const isActive = pathname === target;
return (
<div className="ml-2">
<Link href={target} className={` ${isActive ? "bg-foreground/10 rounded-md" : ""}
w-full flex flex-1 justify-start items-center text-sm font-medium px-3 py-2`}>
<span className="mr-1">{icon}</span>
<span className="text-ellipsis overflow-hidden">
{label}
</span>
</Link>
</div>
);
}
function SettingsNavItems({ menu }: SettingsNavProps): React.ReactNode {
return (
<>
<div>
<Link
href="/home"
className={cn(
buttonVariants({ variant: "ghost" }),
"relative")} style={{marginLeft: '-5px', top:'-5px'}}>
<ChevronLeftIcon className="mr-2 h-4 w-4" /> Back
</Link>
</div>
<div className="p-5 pt-0">
{menu.map((section: SettingsNavMenuSection, index: number) => (
<div key={index}>
<h3 className="flex items-center py-2 text-sm font-semibold text-muted-foreground">
<span className="mr-1">{section.icon}</span> {section.heading}
</h3>
{section.items.map((item: SettingsNavMenuItem, itemIndex: number) => (
<RenderNavItem key={itemIndex} {...item} />
))}
</div>
))}
</div>
</>
);
}
export default function SettingsNav() {
return <SettingsNavItems menu={settingsNavItems} />
}

View File

@ -0,0 +1,67 @@
'use client';
import {
Group,
Box,
Text,
Button,
TagsInput,
Space, Select,
} from '@mantine/core';
import WorkspaceInviteSection from '@/features/settings/workspace/members/components/workspace-invite-section';
import React from 'react';
enum UserRole {
GUEST = 'Guest',
MEMBER = 'Member',
OWNER = 'Owner',
}
export function WorkspaceInviteForm() {
function handleSubmit(data) {
console.log(data);
}
return (
<>
<Box maw="500" mx="auto">
<WorkspaceInviteSection />
<Space h="md" />
<TagsInput
description="Enter valid email addresses separated by comma or space"
label="Invite from email"
placeholder="enter valid emails addresses"
variant="filled"
splitChars={[',', ' ']}
maxDropdownHeight={200}
maxTags={50}
/>
<Space h="md" />
<Select
description="Select role to assign to all invited members"
label="Select role"
placeholder="Pick a role"
variant="filled"
data={Object.values(UserRole)}
defaultValue={UserRole.MEMBER}
allowDeselect={false}
checkIconPosition="right"
/>
<Group justify="center" mt="md">
<Button>Send invitation
</Button>
</Group>
</Box>
</>
);
}

View File

@ -0,0 +1,30 @@
'use client';
import { IconUserPlus } from '@tabler/icons-react';
import { WorkspaceInviteForm } from '@/features/settings/workspace/members/components/workspace-invite-form';
import { Button, Divider, Modal, ScrollArea, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
export default function WorkspaceInviteModal() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Button onClick={open} leftSection={<IconUserPlus size={18} />}>
Invite Members
</Button>
<Modal size="600" opened={opened} onClose={close} title="Invite new members" centered>
<Divider size="xs" mb="xs"/>
<ScrollArea h="80%">
<WorkspaceInviteForm />
</ScrollArea>
</Modal>
</>
);
}

View File

@ -0,0 +1,43 @@
'use client';
import { useAtom } from 'jotai';
import { currentUserAtom } from '@/features/user/atoms/current-user-atom';
import React, { useEffect, useState } from 'react';
import { Button, CopyButton, Text, TextInput } from '@mantine/core';
export default function WorkspaceInviteSection() {
const [currentUser] = useAtom(currentUserAtom);
const [inviteLink, setInviteLink] = useState<string>('');
useEffect(() => {
setInviteLink(`${window.location.origin}/invite/${currentUser.workspace.inviteCode}`);
}, [currentUser.workspace.inviteCode]);
return (
<>
<div>
<Text fw={500} mb="sm">Invite link</Text>
<Text c="dimmed" mb="sm">
Anyone with this link can join this workspace.
</Text>
</div>
<TextInput
variant="filled"
value={inviteLink}
readOnly
rightSection={
<CopyButton value={inviteLink}>
{({ copied, copy }) => (
<Button color={copied ? 'teal' : ''} onClick={copy}>
{copied ? 'Copied' : 'Copy'}
</Button>
)}
</CopyButton>
}
/>
</>
);
}

View File

@ -0,0 +1,51 @@
'use client';
import { useAtom } from 'jotai';
import { currentUserAtom } from '@/features/user/atoms/current-user-atom';
import { useQuery } from '@tanstack/react-query';
import { getWorkspaceUsers } from '@/features/workspace/services/workspace-service';
import { Table } from '@mantine/core';
export default function WorkspaceMembersTable() {
const [currentUser] = useAtom(currentUserAtom);
const workspaceUsers = useQuery({
queryKey: ['workspaceUsers', currentUser.workspace.id],
queryFn: async () => {
return await getWorkspaceUsers();
},
});
const { data, isLoading, isSuccess } = workspaceUsers;
return (
<>
{isSuccess &&
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>Email</Table.Th>
<Table.Th>Role</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{
data['users']?.map((user, index) => (
<Table.Tr key={index}>
<Table.Td>{user.name}</Table.Td>
<Table.Td>{user.email}</Table.Td>
<Table.Td>{user.workspaceRole}</Table.Td>
</Table.Tr>
))
}
</Table.Tbody>
</Table>
}
</>
);
}

View File

@ -0,0 +1,30 @@
'use client';
import WorkspaceInviteSection from '@/features/settings/workspace/members/components/workspace-invite-section';
import React from 'react';
import WorkspaceInviteModal from '@/features/settings/workspace/members/components/workspace-invite-modal';
import { Divider, Group, Space, Text } from '@mantine/core';
const WorkspaceMembersTable = React.lazy(() => import('@/features/settings/workspace/members/components/workspace-members-table'));
export default function WorkspaceMembers() {
return (
<>
<WorkspaceInviteSection />
<Divider my="lg" />
<Group justify="space-between">
<Text fw={500}>Members</Text>
<WorkspaceInviteModal />
</Group>
<Space h="lg" />
<WorkspaceMembersTable />
</>
);
}

View File

@ -0,0 +1,66 @@
'use client';
import { currentUserAtom } from '@/features/user/atoms/current-user-atom';
import { useAtom } from 'jotai';
import * as z from 'zod';
import toast from 'react-hot-toast';
import { useState } from 'react';
import { focusAtom } from 'jotai-optics';
import { updateWorkspace } from '@/features/workspace/services/workspace-service';
import { IWorkspace } from '@/features/workspace/types/workspace.types';
import { TextInput, Button } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form';
const formSchema = z.object({
name: z.string().nonempty('Workspace name cannot be blank'),
});
type FormValues = z.infer<typeof formSchema>;
const workspaceAtom = focusAtom(currentUserAtom, (optic) => optic.prop('workspace'));
export default function WorkspaceNameForm() {
const [isLoading, setIsLoading] = useState(false);
const [currentUser] = useAtom(currentUserAtom);
const [, setWorkspace] = useAtom(workspaceAtom);
const form = useForm<FormValues>({
validate: zodResolver(formSchema),
initialValues: {
name: currentUser?.workspace?.name,
},
});
async function handleSubmit(data: Partial<IWorkspace>) {
setIsLoading(true);
try {
const updatedWorkspace = await updateWorkspace(data);
setWorkspace(updatedWorkspace);
toast.success('Updated successfully');
} catch (err) {
console.log(err);
toast.error('Failed to update data.');
}
setIsLoading(false);
}
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<TextInput
id="name"
label="Name"
placeholder="e.g ACME"
variant="filled"
{...form.getInputProps('name')}
rightSection={
<Button type="submit" disabled={isLoading} loading={isLoading}>
Save
</Button>
}
/>
</form>
);
}

View File

@ -0,0 +1,8 @@
'use client';
import WorkspaceNameForm from '@/features/settings/workspace/settings/components/workspace-name-form';
export default function WorkspaceSettings() {
return (<WorkspaceNameForm />);
}

View File

@ -1,91 +0,0 @@
'use client';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { useAtom } from 'jotai';
import { focusAtom } from 'jotai-optics';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
import { currentUserAtom } from '../atoms/current-user-atom';
import { updateUser } from '../services/user-service';
import { IUser } from '../types/user.types';
import { useState } from 'react';
import { Icons } from '@/components/icons';
import toast from "react-hot-toast";
const profileFormSchema = z.object({
name: z.string().min(2).max(40),
});
type ProfileFormValues = z.infer<typeof profileFormSchema>;
const userAtom = focusAtom(currentUserAtom, (optic) => optic.prop('user'));
export default function AccountNameForm() {
const [isLoading, setIsLoading] = useState(false);
const [currentUser] = useAtom(currentUserAtom);
const [, setUser] = useAtom(userAtom);
const defaultValues: Partial<ProfileFormValues> = {
name: currentUser?.user?.name,
};
const form = useForm<ProfileFormValues>({
resolver: zodResolver(profileFormSchema),
defaultValues,
});
async function onSubmit(data: Partial<IUser>) {
setIsLoading(true);
try {
const updatedUser = await updateUser(data);
setUser(updatedUser);
toast.success('Updated successfully');
} catch (err) {
console.log(err);
toast.error('Failed to update data.')
}
setIsLoading(false);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Your name" {...field} />
</FormControl>
<FormDescription>
This is the name that will be displayed on your account and in
emails.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={isLoading}>
{isLoading && <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />}
Save
</Button>
</form>
</Form>
);
}

View File

@ -1,120 +0,0 @@
"use client";
import { Dialog, DialogTrigger } from "@radix-ui/react-dialog";
import { Button } from "@/components/ui/button";
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import * as z from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Icons } from "@/components/icons";
import { useState } from "react";
import { useAtom } from "jotai";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
export default function ChangeEmail() {
const [currentUser] = useAtom(currentUserAtom);
return (
<div className="flex items-center justify-between space-x-4 mt-5">
<div>
<h4 className="text-xl font-medium">Email</h4>
<p className="text-sm text-muted-foreground">{currentUser.user.email}</p>
</div>
<ChangeEmailDialog />
</div>
);
}
function ChangeEmailDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Change email</Button>
</DialogTrigger>
<DialogContent className="w-[350px]">
<DialogHeader>
<DialogTitle>
Change email
</DialogTitle>
<DialogDescription>
To change your email, you have to enter your password and new email.
</DialogDescription>
</DialogHeader>
<ChangePasswordForm />
</DialogContent>
</Dialog>
);
}
const changeEmailSchema = z.object({
password: z.string({ required_error: "your current password is required" }).min(8),
email: z.string({ required_error: "New email is required" }).email()
});
type ChangeEmailFormValues = z.infer<typeof changeEmailSchema>
function ChangePasswordForm() {
const [isLoading, setIsLoading] = useState(false);
const form = useForm<ChangeEmailFormValues>({
resolver: zodResolver(changeEmailSchema),
defaultValues: {
password: "",
email: "",
},
});
function onSubmit(data: ChangeEmailFormValues) {
setIsLoading(true);
console.log(data);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel className="font-semibold">Password</FormLabel>
<FormControl>
<Input type="password" autoComplete="password" placeholder="Enter your password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel className="font-semibold">New email</FormLabel>
<FormControl>
<Input type="email" autoComplete="email" placeholder="Enter your new email" {...field} />
</FormControl>
<FormDescription>Enter your new preferred email</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit" disabled={isLoading}>
{isLoading && <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />}
Change email
</Button>
</div>
</form>
</Form>
);
}

View File

@ -1,133 +0,0 @@
"use client";
import { Dialog, DialogTrigger } from "@radix-ui/react-dialog";
import { Button } from "@/components/ui/button";
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import * as z from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Icons } from "@/components/icons";
import { useState } from "react";
export default function ChangePassword() {
return (
<div className="flex items-center justify-between space-x-4 mt-5">
<div>
<h4 className="text-xl font-medium">Password</h4>
<p className="text-sm text-muted-foreground">You can change your password here.</p>
</div>
<ChangePasswordDialog />
</div>
);
}
function ChangePasswordDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Change password</Button>
</DialogTrigger>
<DialogContent className="w-[350px]">
<DialogHeader>
<DialogTitle>
Change password
</DialogTitle>
<DialogDescription>
Your password must be at least a minimum of 8 characters.
</DialogDescription>
</DialogHeader>
<ChangePasswordForm />
</DialogContent>
</Dialog>
);
}
const changePasswordSchema = z.object({
current: z.string({ required_error: "your current password is required" }).min(1),
password: z.string({ required_error: "New password is required" }).min(8),
confirm_password: z.string({ required_error: "Password confirmation is required" }).min(8),
}).refine(data => data.password === data.confirm_password, {
message: "Your new password and confirmation does not match.",
path: ["confirm_password"],
});
type ChangePasswordFormValues = z.infer<typeof changePasswordSchema>
function ChangePasswordForm() {
const [isLoading, setIsLoading] = useState(false);
const form = useForm<ChangePasswordFormValues>({
resolver: zodResolver(changePasswordSchema),
defaultValues: {
current: "",
password: "",
confirm_password: "",
},
});
function onSubmit(data: ChangePasswordFormValues) {
setIsLoading(true);
console.log(data);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="current"
render={({ field }) => (
<FormItem>
<FormLabel className="font-semibold">Current password</FormLabel>
<FormControl>
<Input type="password" autoComplete="current-password" placeholder="Your current password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel className="font-semibold">New password</FormLabel>
<FormControl>
<Input type="password" autoComplete="password" placeholder="Your new password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirm_password"
render={({ field }) => (
<FormItem>
<FormLabel className="font-semibold">Repeat new password</FormLabel>
<FormControl>
<Input type="password" autoComplete="password" placeholder="Confirm your new password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit" disabled={isLoading}>
{isLoading && <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />}
Change password
</Button>
</div>
</form>
</Form>
);
}

View File

@ -1,46 +0,0 @@
"use client";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import ButtonWithIcon from "@/components/ui/button-with-icon";
import { IconUserPlus } from "@tabler/icons-react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { WorkspaceInviteForm } from "@/features/workspace/components/workspace-invite-form";
export default function WorkspaceInviteDialog() {
return (
<>
<Dialog>
<DialogTrigger asChild>
<ButtonWithIcon
icon={<IconUserPlus size="20" />}
className="font-medium">
Invite Members
</ButtonWithIcon>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
Invite new members
</DialogTitle>
<DialogDescription>
Here you can invite new members.
</DialogDescription>
</DialogHeader>
<ScrollArea className=" max-h-[60vh]">
<WorkspaceInviteForm />
</ScrollArea>
</DialogContent>
</Dialog>
</>
);
}

View File

@ -1,154 +0,0 @@
"use client";
import * as z from "zod";
import { useFieldArray, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { IconTrashX } from "@tabler/icons-react";
import ButtonWithIcon from "@/components/ui/button-with-icon";
import { Button } from "@/components/ui/button";
enum UserRole {
GUEST = "guest",
MEMBER = "member",
OWNER = "owner",
}
const inviteFormSchema = z.object({
members: z
.array(
z.object({
email: z.string({
required_error: "Email is required",
}).email({ message: "Please enter a valid email" }),
role: z
.string({
required_error: "Please select a role",
}),
}),
),
});
type InviteFormValues = z.infer<typeof inviteFormSchema>
const defaultValues: Partial<InviteFormValues> = {
members: [
{ email: "user@example.com", role: "member" },
],
};
export function WorkspaceInviteForm() {
const form = useForm<InviteFormValues>({
resolver: zodResolver(inviteFormSchema),
defaultValues,
mode: "onChange",
});
const { fields, append, remove } = useFieldArray({
name: "members",
control: form.control,
});
function onSubmit(data: InviteFormValues) {
console.log(data);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<div>
{
fields.map((field, index) => {
const key = index.toString();
return (
<div key={key} className="flex justify-between items-center py-2 gap-2">
<div className="flex-grow">
{index === 0 && <FormLabel>Email</FormLabel>}
<FormField
control={form.control}
key={field.id}
name={`members.${index}.email`}
render={({ field }) => (
<FormItem>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex-grow">
{index === 0 && <FormLabel>Role</FormLabel>}
<FormField
control={form.control}
key={field.id}
name={`members.${index}.role`}
render={({ field }) => (
<FormItem>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a role for this member" />
</SelectTrigger>
</FormControl>
<SelectContent>
{
Object.keys(UserRole).map((key) => {
const value = UserRole[key as keyof typeof UserRole];
return (
<SelectItem key={key} value={value}>
{key.charAt(0).toUpperCase() + key.slice(1).toLowerCase()}
</SelectItem>
);
})
}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)} />
</div>
<div className="flex items-center">
{index != 0 &&
<ButtonWithIcon
icon={<IconTrashX size={16} />}
variant="secondary"
onClick={() => remove(index)}
/>
}
</div>
</div>
);
})
}
<Button
variant="outline"
size="sm"
className="mt-2"
onClick={() => append({ email: "", role: UserRole.MEMBER })}
>
Add
</Button>
</div>
<div className="flex justify-end">
<Button type="submit">Send Invitation</Button>
</div>
</form>
</Form>
);
}

View File

@ -1,45 +0,0 @@
"use client";
import { useAtom } from "jotai/index";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
import { useEffect, useState } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import toast from "react-hot-toast";
export default function WorkspaceInviteSection() {
const [currentUser] = useAtom(currentUserAtom);
const [inviteLink, setInviteLink] = useState<string>("");
useEffect(() => {
setInviteLink(`${window.location.origin}/invite/${currentUser.workspace.inviteCode}`);
}, [currentUser.workspace.inviteCode]);
function handleCopy(): void {
try {
navigator.clipboard?.writeText(inviteLink);
toast.success("Link copied successfully");
} catch (err) {
toast.error("Failed to copy to clipboard");
}
}
return (
<>
<div>
<h2 className="font-semibold py-5">Invite members</h2>
<p className="text-muted-foreground">
Anyone with the link can join this workspace.
</p>
</div>
<div className="flex space-x-2">
<Input value={inviteLink} readOnly />
<Button variant="secondary" className="shrink-0" onClick={handleCopy}>
Copy link
</Button>
</div>
</>
);
}

View File

@ -1,52 +0,0 @@
"use client";
import { useAtom } from "jotai/index";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
import { useQuery } from "@tanstack/react-query";
import { getWorkspaceUsers } from "@/features/workspace/services/workspace-service";
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
export default function WorkspaceMembersTable() {
const [currentUser] = useAtom(currentUserAtom);
const workspaceUsers = useQuery({
queryKey: ["workspaceUsers", currentUser.workspace.id],
queryFn: async () => {
return await getWorkspaceUsers();
},
});
const { data, isLoading, isSuccess } = workspaceUsers;
return (
<>
{isSuccess &&
<Table>
<TableCaption>Your workspace members will appear here.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{
data['users']?.map((user, index) => (
<TableRow key={index}>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell> <Badge variant="secondary">{user.workspaceRole}</Badge></TableCell>
</TableRow>
))
}
</TableBody>
</Table>
}
</>
);
}

View File

@ -1,88 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
import { zodResolver } from "@hookform/resolvers/zod";
import { useAtom } from "jotai";
import { useForm } from "react-hook-form";
import * as z from "zod";
import toast from "react-hot-toast";
import { updateUser } from "@/features/user/services/user-service";
import { useState } from "react";
import { focusAtom } from "jotai-optics";
import { updateWorkspace } from "@/features/workspace/services/workspace-service";
import { IWorkspace } from "@/features/workspace/types/workspace.types";
const profileFormSchema = z.object({
name: z.string(),
});
type ProfileFormValues = z.infer<typeof profileFormSchema>;
const workspaceAtom = focusAtom(currentUserAtom, (optic) => optic.prop("workspace"));
export default function WorkspaceNameForm() {
const [isLoading, setIsLoading] = useState(false);
const [currentUser] = useAtom(currentUserAtom);
const [, setWorkspace] = useAtom(workspaceAtom);
const defaultValues: Partial<ProfileFormValues> = {
name: currentUser?.workspace?.name,
};
const form = useForm<ProfileFormValues>({
resolver: zodResolver(profileFormSchema),
defaultValues,
});
async function onSubmit(data: Partial<IWorkspace>) {
setIsLoading(true);
try {
const updatedWorkspace = await updateWorkspace(data);
setWorkspace(updatedWorkspace);
toast.success("Updated successfully");
} catch (err) {
console.log(err);
toast.error("Failed to update data.");
}
setIsLoading(false);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input className="max-w-md" placeholder="e.g ACME" {...field} />
</FormControl>
<FormDescription>
Your workspace name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Save</Button>
</form>
</Form>
);
}

View File

@ -1,6 +0,0 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -1,76 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}