->(({ className, ...props }, ref) => (
- | [role=checkbox]]:translate-y-[2px]",
- className
- )}
- {...props}
- />
-))
-TableCell.displayName = "TableCell"
-
-const TableCaption = React.forwardRef<
- HTMLTableCaptionElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-TableCaption.displayName = "TableCaption"
-
-export {
- Table,
- TableHeader,
- TableBody,
- TableFooter,
- TableHead,
- TableRow,
- TableCell,
- TableCaption,
-}
diff --git a/frontend/src/components/ui/tooltip.tsx b/frontend/src/components/ui/tooltip.tsx
deleted file mode 100644
index 9e74821..0000000
--- a/frontend/src/components/ui/tooltip.tsx
+++ /dev/null
@@ -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,
- React.ComponentPropsWithoutRef
->(({ className, sideOffset = 4, ...props }, ref) => (
-
-))
-TooltipContent.displayName = TooltipPrimitive.Content.displayName
-
-export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/frontend/src/components/welcome/welcome.tsx b/frontend/src/components/welcome/welcome.tsx
new file mode 100644
index 0000000..60082ff
--- /dev/null
+++ b/frontend/src/components/welcome/welcome.tsx
@@ -0,0 +1,23 @@
+import { Title, Text } from '@mantine/core';
+import { ThemeToggle } from '../theme-toggle';
+
+export function Welcome() {
+ return (
+ <>
+
+
+ Welcome
+
+
+
+ Welcome to something new and interesting.
+
+
+ >
+ );
+}
diff --git a/frontend/src/features/auth/components/legal-terms.tsx b/frontend/src/features/auth/components/legal-terms.tsx
deleted file mode 100644
index 9bb0db5..0000000
--- a/frontend/src/features/auth/components/legal-terms.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import Link from "next/link";
-
-export default function LegalTerms(){
- return (
-
- By clicking continue, you agree to our{" "}
-
- Terms of Service{" "} and{" "}
- Privacy Policy.
-
- )
-}
diff --git a/frontend/src/features/auth/components/login-form.tsx b/frontend/src/features/auth/components/login-form.tsx
index ed8f9be..52dec31 100644
--- a/frontend/src/features/auth/components/login-form.tsx
+++ b/frontend/src/features/auth/components/login-form.tsx
@@ -1,92 +1,80 @@
-"use client";
+'use client';
-import * as React from "react";
-import * as z from "zod";
+import * as React from 'react';
+import * as z from 'zod';
-import { cn } from "@/lib/utils";
-import { Button, buttonVariants } from "@/components/ui/button";
-import { Icons } from "@/components/icons";
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { Label } from "@/components/ui/label";
-import { Input } from "@/components/ui/input";
-import useAuth from "@/features/auth/hooks/use-auth";
-import { ILogin } from "@/features/auth/types/auth.types";
+import { useForm, zodResolver } from '@mantine/form';
+import useAuth from '@/features/auth/hooks/use-auth';
+import { ILogin } from '@/features/auth/types/auth.types';
+import {
+ Container,
+ Title,
+ Anchor,
+ Paper,
+ TextInput,
+ Button,
+ Text,
+ PasswordInput,
+} from '@mantine/core';
+import Link from 'next/link';
const formSchema = z.object({
- email: z.string({ required_error: "email is required" }).email({ message: "Invalid email address" }),
- password: z.string({ required_error: "password is required" }),
+ email: z
+ .string({ required_error: 'email is required' })
+ .email({ message: 'Invalid email address' }),
+ password: z.string({ required_error: 'password is required' }),
});
-interface UserAuthFormProps extends React.HTMLAttributes {
-}
-
-export function LoginForm({ className, ...props }: UserAuthFormProps) {
- const { register, handleSubmit, formState: { errors } }
- = useForm({ resolver: zodResolver(formSchema) });
-
+export function LoginForm() {
const { signIn, isLoading } = useAuth();
+ const form = useForm({
+ validate: zodResolver(formSchema),
+ initialValues: {
+ email: '',
+ password: '',
+ },
+ });
+
async function onSubmit(data: ILogin) {
await signIn(data);
}
return (
- <>
-
- >
+
+
);
}
diff --git a/frontend/src/features/auth/components/sign-up-form.tsx b/frontend/src/features/auth/components/sign-up-form.tsx
index 93cf8eb..cb0718f 100644
--- a/frontend/src/features/auth/components/sign-up-form.tsx
+++ b/frontend/src/features/auth/components/sign-up-form.tsx
@@ -1,91 +1,80 @@
-"use client";
+'use client';
-import * as React from "react";
-import * as z from "zod";
+import * as React from 'react';
+import * as z from 'zod';
-import { cn } from "@/lib/utils";
-import { Button, buttonVariants } from "@/components/ui/button";
-import { Icons } from "@/components/icons";
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { Label } from "@/components/ui/label";
-import { Input } from "@/components/ui/input";
-import useAuth from "@/features/auth/hooks/use-auth";
-import { IRegister } from "@/features/auth/types/auth.types";
+import { useForm, zodResolver } from '@mantine/form';
+import useAuth from '@/features/auth/hooks/use-auth';
+import { IRegister } from '@/features/auth/types/auth.types';
+import {
+ Container,
+ Title,
+ Anchor,
+ Paper,
+ TextInput,
+ Button,
+ Text,
+ PasswordInput,
+} from '@mantine/core';
+import Link from 'next/link';
const formSchema = z.object({
- email: z.string({ required_error: "email is required" }).email({ message: "Invalid email address" }),
- password: z.string({ required_error: "password is required" }).min(8),
+ email: z
+ .string({ required_error: 'email is required' })
+ .email({ message: 'Invalid email address' }),
+ password: z.string({ required_error: 'password is required' }),
});
-interface UserAuthFormProps extends React.HTMLAttributes {}
-
-export function SignUpForm({ className, ...props }: UserAuthFormProps) {
- const { register, handleSubmit, formState: { errors } }
- = useForm({ resolver: zodResolver(formSchema) });
-
+export function SignUpForm() {
const { signUp, isLoading } = useAuth();
+ const form = useForm({
+ validate: zodResolver(formSchema),
+ initialValues: {
+ email: '',
+ password: '',
+ },
+ });
+
async function onSubmit(data: IRegister) {
await signUp(data);
}
return (
- <>
-
-
-
-
- >
+
+
);
}
diff --git a/frontend/src/features/editor/Editor.tsx b/frontend/src/features/editor/Editor.tsx
index 04de084..b9b5d7b 100644
--- a/frontend/src/features/editor/Editor.tsx
+++ b/frontend/src/features/editor/Editor.tsx
@@ -16,7 +16,6 @@ import '@/features/editor/css/editor.css';
interface EditorProps{
pageId: string,
- token: string,
}
const colors = ['#958DF1', '#F98181', '#FBBC88', '#FAF594', '#70CFF8', '#94FADB', '#B9F18D']
diff --git a/frontend/src/features/settings/account/settings/account-settings.tsx b/frontend/src/features/settings/account/settings/account-settings.tsx
new file mode 100644
index 0000000..a40899a
--- /dev/null
+++ b/frontend/src/features/settings/account/settings/account-settings.tsx
@@ -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 (
+ <>
+
+
+
+
+
+
+
+
+
+ >);
+}
diff --git a/frontend/src/features/settings/account/settings/components/account-name-form.tsx b/frontend/src/features/settings/account/settings/components/account-name-form.tsx
new file mode 100644
index 0000000..bce4b09
--- /dev/null
+++ b/frontend/src/features/settings/account/settings/components/account-name-form.tsx
@@ -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;
+
+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({
+ validate: zodResolver(formSchema),
+ initialValues: {
+ name: currentUser?.user?.name,
+ },
+ });
+
+ async function handleSubmit(data: Partial) {
+ 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 (
+
+
+ Save
+
+ }
+ />
+
+ );
+}
+
diff --git a/frontend/src/features/settings/account/settings/components/change-email.tsx b/frontend/src/features/settings/account/settings/components/change-email.tsx
new file mode 100644
index 0000000..3be78c3
--- /dev/null
+++ b/frontend/src/features/settings/account/settings/components/change-email.tsx
@@ -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 (
+
+
+ Email
+
+ {currentUser.user.email}
+
+
+
+
+
+
+ To change your email, you have to enter your password and new email.
+
+
+
+ );
+}
+
+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
+
+function ChangePasswordForm() {
+ const [isLoading, setIsLoading] = useState(false);
+
+ const form = useForm({
+ validate: zodResolver(formSchema),
+ initialValues: {
+ password: '',
+ email: '',
+ },
+ });
+
+ function handleSubmit(data: FormValues) {
+ setIsLoading(true);
+ console.log(data);
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/features/settings/account/settings/components/change-password.tsx b/frontend/src/features/settings/account/settings/components/change-password.tsx
new file mode 100644
index 0000000..2d83282
--- /dev/null
+++ b/frontend/src/features/settings/account/settings/components/change-password.tsx
@@ -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 (
+
+
+ Password
+
+ You can change your password here.
+
+
+
+
+
+
+ Your password must be a minimum of 8 characters.
+
+
+
+
+ );
+}
+
+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
+
+function ChangePasswordForm() {
+ const [isLoading, setIsLoading] = useState(false);
+
+ const form = useForm({
+ validate: zodResolver(formSchema),
+ initialValues: {
+ current: '',
+ password: '',
+ confirm_password: '',
+ },
+ });
+
+ function handleSubmit(data: FormValues) {
+ setIsLoading(true);
+ console.log(data);
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/features/settings/modal/atoms/settings-modal-atom.ts b/frontend/src/features/settings/modal/atoms/settings-modal-atom.ts
new file mode 100644
index 0000000..b052160
--- /dev/null
+++ b/frontend/src/features/settings/modal/atoms/settings-modal-atom.ts
@@ -0,0 +1,3 @@
+import { atom } from "jotai";
+
+export const settingsModalAtom = atom(false);
diff --git a/frontend/src/features/settings/modal/modal.module.css b/frontend/src/features/settings/modal/modal.module.css
new file mode 100644
index 0000000..639782f
--- /dev/null
+++ b/frontend/src/features/settings/modal/modal.module.css
@@ -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);
+}
diff --git a/frontend/src/features/settings/modal/settings-modal.tsx b/frontend/src/features/settings/modal/settings-modal.tsx
new file mode 100644
index 0000000..4428868
--- /dev/null
+++ b/frontend/src/features/settings/modal/settings-modal.tsx
@@ -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 (
+ <>
+ setModalOpen(false)}>
+
+
+
+
+ Settings
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/features/settings/modal/settings-sidebar.tsx b/frontend/src/features/settings/modal/settings-sidebar.tsx
new file mode 100644
index 0000000..1c18a7a
--- /dev/null
+++ b/frontend/src/features/settings/modal/settings-sidebar.tsx
@@ -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) => (
+
+ {group.heading}
+ {group.items.map((item) => (
+ {
+ event.preventDefault();
+ setActive(item.label);
+ }}
+ >
+
+ {item.label}
+
+ ))}
+
+ ));
+
+ let ActiveComponent;
+
+ switch (active) {
+ case 'Account':
+ ActiveComponent = AccountSettings;
+ break;
+ case 'General':
+ ActiveComponent = WorkspaceSettings;
+ break;
+ case 'Members':
+ ActiveComponent = WorkspaceMembers;
+ break;
+ default:
+ ActiveComponent = null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ }>
+ {ActiveComponent && }
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/features/settings/nav/settings-nav-items.tsx b/frontend/src/features/settings/nav/settings-nav-items.tsx
deleted file mode 100644
index 4423f86..0000000
--- a/frontend/src/features/settings/nav/settings-nav-items.tsx
+++ /dev/null
@@ -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: ,
- items: [
- {
- label: 'My account',
- icon: ,
- target: '/settings/account',
- },
- ],
- },
- {
- heading: 'Workspace',
- icon: ,
- items: [
- {
- label: 'General',
- icon: ,
- target: '/settings/workspace',
- },
- {
- label: 'Members',
- icon: ,
- target: '/settings/workspace/members',
- },
- ],
- },
-
-];
diff --git a/frontend/src/features/settings/nav/settings-nav.tsx b/frontend/src/features/settings/nav/settings-nav.tsx
deleted file mode 100644
index 9e123e9..0000000
--- a/frontend/src/features/settings/nav/settings-nav.tsx
+++ /dev/null
@@ -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 (
-
-
- {icon}
-
- {label}
-
-
-
- );
-}
-
-function SettingsNavItems({ menu }: SettingsNavProps): React.ReactNode {
- return (
- <>
-
-
- Back
-
-
-
-
- {menu.map((section: SettingsNavMenuSection, index: number) => (
-
-
- {section.icon} {section.heading}
-
- {section.items.map((item: SettingsNavMenuItem, itemIndex: number) => (
-
- ))}
-
- ))}
-
- >
- );
-}
-
-export default function SettingsNav() {
- return
-}
diff --git a/frontend/src/features/settings/workspace/members/components/workspace-invite-form.tsx b/frontend/src/features/settings/workspace/members/components/workspace-invite-form.tsx
new file mode 100644
index 0000000..add22bc
--- /dev/null
+++ b/frontend/src/features/settings/workspace/members/components/workspace-invite-form.tsx
@@ -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 (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+
+}
diff --git a/frontend/src/features/settings/workspace/members/components/workspace-invite-modal.tsx b/frontend/src/features/settings/workspace/members/components/workspace-invite-modal.tsx
new file mode 100644
index 0000000..9deed56
--- /dev/null
+++ b/frontend/src/features/settings/workspace/members/components/workspace-invite-modal.tsx
@@ -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 (
+ <>
+ }>
+ Invite Members
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/features/settings/workspace/members/components/workspace-invite-section.tsx b/frontend/src/features/settings/workspace/members/components/workspace-invite-section.tsx
new file mode 100644
index 0000000..bae66d3
--- /dev/null
+++ b/frontend/src/features/settings/workspace/members/components/workspace-invite-section.tsx
@@ -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('');
+
+ useEffect(() => {
+ setInviteLink(`${window.location.origin}/invite/${currentUser.workspace.inviteCode}`);
+ }, [currentUser.workspace.inviteCode]);
+
+ return (
+ <>
+
+ Invite link
+
+ Anyone with this link can join this workspace.
+
+
+
+
+ {({ copied, copy }) => (
+
+ )}
+
+ }
+ />
+
+
+ >
+ );
+}
diff --git a/frontend/src/features/settings/workspace/members/components/workspace-members-table.tsx b/frontend/src/features/settings/workspace/members/components/workspace-members-table.tsx
new file mode 100644
index 0000000..c0c3beb
--- /dev/null
+++ b/frontend/src/features/settings/workspace/members/components/workspace-members-table.tsx
@@ -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 &&
+
+
+
+
+ Name
+ Email
+ Role
+
+
+
+
+
+ {
+ data['users']?.map((user, index) => (
+
+ {user.name}
+ {user.email}
+ {user.workspaceRole}
+
+ ))
+ }
+
+
+
+ }
+ >
+ );
+}
diff --git a/frontend/src/features/settings/workspace/members/workspace-members.tsx b/frontend/src/features/settings/workspace/members/workspace-members.tsx
new file mode 100644
index 0000000..36140e8
--- /dev/null
+++ b/frontend/src/features/settings/workspace/members/workspace-members.tsx
@@ -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 (
+ <>
+
+
+
+
+
+ Members
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/features/settings/workspace/settings/components/workspace-name-form.tsx b/frontend/src/features/settings/workspace/settings/components/workspace-name-form.tsx
new file mode 100644
index 0000000..2ba8797
--- /dev/null
+++ b/frontend/src/features/settings/workspace/settings/components/workspace-name-form.tsx
@@ -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;
+
+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({
+ validate: zodResolver(formSchema),
+ initialValues: {
+ name: currentUser?.workspace?.name,
+ },
+ });
+
+ async function handleSubmit(data: Partial) {
+ 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 (
+
+
+ Save
+
+ }
+ />
+
+ );
+}
diff --git a/frontend/src/features/settings/workspace/settings/workspace-settings.tsx b/frontend/src/features/settings/workspace/settings/workspace-settings.tsx
new file mode 100644
index 0000000..357787b
--- /dev/null
+++ b/frontend/src/features/settings/workspace/settings/workspace-settings.tsx
@@ -0,0 +1,8 @@
+'use client';
+
+import WorkspaceNameForm from '@/features/settings/workspace/settings/components/workspace-name-form';
+
+export default function WorkspaceSettings() {
+
+ return ();
+}
diff --git a/frontend/src/features/user/components/account-name-form.tsx b/frontend/src/features/user/components/account-name-form.tsx
deleted file mode 100644
index 5f9cf43..0000000
--- a/frontend/src/features/user/components/account-name-form.tsx
+++ /dev/null
@@ -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;
-
-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 = {
- name: currentUser?.user?.name,
- };
-
- const form = useForm({
- resolver: zodResolver(profileFormSchema),
- defaultValues,
- });
-
- async function onSubmit(data: Partial) {
- 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 (
-
-
- (
-
- Name
-
-
-
-
- This is the name that will be displayed on your account and in
- emails.
-
-
-
- )}
- />
-
-
-
-
- );
-}
diff --git a/frontend/src/features/user/components/change-email.tsx b/frontend/src/features/user/components/change-email.tsx
deleted file mode 100644
index 2d04834..0000000
--- a/frontend/src/features/user/components/change-email.tsx
+++ /dev/null
@@ -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 (
-
-
- Email
- {currentUser.user.email}
-
-
-
- );
-}
-
-function ChangeEmailDialog() {
- return (
-
-
- );
-}
-
-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
-
-function ChangePasswordForm() {
- const [isLoading, setIsLoading] = useState(false);
-
- const form = useForm({
- resolver: zodResolver(changeEmailSchema),
- defaultValues: {
- password: "",
- email: "",
- },
- });
-
- function onSubmit(data: ChangeEmailFormValues) {
- setIsLoading(true);
- console.log(data);
- }
-
- return (
-
-
- (
-
- Password
-
-
-
-
-
- )}
- />
-
- (
-
- New email
-
-
-
- Enter your new preferred email
-
-
- )}
- />
-
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/features/user/components/change-password.tsx b/frontend/src/features/user/components/change-password.tsx
deleted file mode 100644
index 418d40f..0000000
--- a/frontend/src/features/user/components/change-password.tsx
+++ /dev/null
@@ -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 (
-
-
- Password
- You can change your password here.
-
-
-
- );
-}
-
-function ChangePasswordDialog() {
- return (
-
-
- );
-}
-
-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
-
-function ChangePasswordForm() {
- const [isLoading, setIsLoading] = useState(false);
-
- const form = useForm({
- resolver: zodResolver(changePasswordSchema),
- defaultValues: {
- current: "",
- password: "",
- confirm_password: "",
- },
- });
-
- function onSubmit(data: ChangePasswordFormValues) {
- setIsLoading(true);
- console.log(data);
- }
-
- return (
-
-
- (
-
- Current password
-
-
-
-
-
- )}
- />
-
- (
-
- New password
-
-
-
-
-
- )}
- />
-
- (
-
- Repeat new password
-
-
-
-
-
- )}
- />
-
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/features/workspace/components/workspace-invite-dialog.tsx b/frontend/src/features/workspace/components/workspace-invite-dialog.tsx
deleted file mode 100644
index f39d32b..0000000
--- a/frontend/src/features/workspace/components/workspace-invite-dialog.tsx
+++ /dev/null
@@ -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 (
- <>
-
- >
- );
-}
diff --git a/frontend/src/features/workspace/components/workspace-invite-form.tsx b/frontend/src/features/workspace/components/workspace-invite-form.tsx
deleted file mode 100644
index 9612de1..0000000
--- a/frontend/src/features/workspace/components/workspace-invite-form.tsx
+++ /dev/null
@@ -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
-
-const defaultValues: Partial = {
- members: [
- { email: "user@example.com", role: "member" },
- ],
-};
-
-export function WorkspaceInviteForm() {
-
- const form = useForm({
- resolver: zodResolver(inviteFormSchema),
- defaultValues,
- mode: "onChange",
- });
-
- const { fields, append, remove } = useFieldArray({
- name: "members",
- control: form.control,
- });
-
- function onSubmit(data: InviteFormValues) {
- console.log(data);
- }
-
- return (
-
-
-
-
- {
- fields.map((field, index) => {
- const key = index.toString();
- return (
-
-
-
- {index === 0 && Email}
- (
-
-
-
-
-
-
- )}
- />
-
-
-
- {index === 0 && Role}
-
- (
-
-
-
-
- )} />
-
-
-
- {index != 0 &&
- }
- variant="secondary"
- onClick={() => remove(index)}
- />
- }
-
-
-
- );
- })
- }
-
-
-
-
-
-
-
-
-
-
-
- );
-
-}
diff --git a/frontend/src/features/workspace/components/workspace-invite-section.tsx b/frontend/src/features/workspace/components/workspace-invite-section.tsx
deleted file mode 100644
index f484e37..0000000
--- a/frontend/src/features/workspace/components/workspace-invite-section.tsx
+++ /dev/null
@@ -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("");
-
- 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 (
- <>
-
- Invite members
-
- Anyone with the link can join this workspace.
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/frontend/src/features/workspace/components/workspace-members-table.tsx b/frontend/src/features/workspace/components/workspace-members-table.tsx
deleted file mode 100644
index 9fb9bca..0000000
--- a/frontend/src/features/workspace/components/workspace-members-table.tsx
+++ /dev/null
@@ -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 &&
-
-
- Your workspace members will appear here.
-
-
- Name
- Email
- Role
-
-
-
-
- {
- data['users']?.map((user, index) => (
-
- {user.name}
- {user.email}
- {user.workspaceRole}
-
- ))
- }
-
-
-
- }
- >
- );
-}
diff --git a/frontend/src/features/workspace/components/workspace-name-form.tsx b/frontend/src/features/workspace/components/workspace-name-form.tsx
deleted file mode 100644
index ffe6cae..0000000
--- a/frontend/src/features/workspace/components/workspace-name-form.tsx
+++ /dev/null
@@ -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;
-
-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 = {
- name: currentUser?.workspace?.name,
- };
-
- const form = useForm({
- resolver: zodResolver(profileFormSchema),
- defaultValues,
- });
-
- async function onSubmit(data: Partial) {
- 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 (
-
-
- (
-
- Name
-
-
-
-
- Your workspace name.
-
-
-
- )}
- />
-
-
-
-
- );
-}
diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts
index ec79801..e69de29 100644
--- a/frontend/src/lib/utils.ts
+++ b/frontend/src/lib/utils.ts
@@ -1,6 +0,0 @@
-import { type ClassValue, clsx } from "clsx"
-import { twMerge } from "tailwind-merge"
-
-export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
-}
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
deleted file mode 100644
index 0377ea1..0000000
--- a/frontend/tailwind.config.js
+++ /dev/null
@@ -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")],
-}
\ No newline at end of file
|