+>(({ 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/tabs.tsx b/frontend/src/components/ui/tabs.tsx
new file mode 100644
index 0000000..c99280c
--- /dev/null
+++ b/frontend/src/components/ui/tabs.tsx
@@ -0,0 +1,52 @@
+import * as React from 'react'
+import * as TabsPrimitive from '@radix-ui/react-tabs'
+import { cn } from '@/lib/utils'
+
+const Tabs = TabsPrimitive.Root
+
+const TabsList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/frontend/src/components/ui/textarea.tsx b/frontend/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..9818ff9
--- /dev/null
+++ b/frontend/src/components/ui/textarea.tsx
@@ -0,0 +1,21 @@
+import * as React from 'react'
+import { cn } from '@/lib/utils'
+
+const Textarea = React.forwardRef<
+ HTMLTextAreaElement,
+ React.ComponentProps<'textarea'>
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+Textarea.displayName = 'Textarea'
+
+export { Textarea }
diff --git a/frontend/src/components/ui/toast.tsx b/frontend/src/components/ui/toast.tsx
new file mode 100644
index 0000000..e6044eb
--- /dev/null
+++ b/frontend/src/components/ui/toast.tsx
@@ -0,0 +1,126 @@
+import * as React from 'react'
+import * as ToastPrimitives from '@radix-ui/react-toast'
+import { cva, type VariantProps } from 'class-variance-authority'
+import { X } from 'lucide-react'
+import { cn } from '@/lib/utils'
+
+const ToastProvider = ToastPrimitives.Provider
+
+const ToastViewport = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastViewport.displayName = ToastPrimitives.Viewport.displayName
+
+const toastVariants = cva(
+ 'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
+ {
+ variants: {
+ variant: {
+ default: 'border bg-background text-foreground',
+ destructive:
+ 'destructive group border-destructive bg-destructive text-destructive-foreground',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ }
+)
+
+const Toast = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, variant, ...props }, ref) => {
+ return (
+
+ )
+})
+Toast.displayName = ToastPrimitives.Root.displayName
+
+const ToastAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastAction.displayName = ToastPrimitives.Action.displayName
+
+const ToastClose = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+ToastClose.displayName = ToastPrimitives.Close.displayName
+
+const ToastTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastTitle.displayName = ToastPrimitives.Title.displayName
+
+const ToastDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastDescription.displayName = ToastPrimitives.Description.displayName
+
+type ToastProps = React.ComponentPropsWithoutRef
+
+type ToastActionElement = React.ReactElement
+
+export {
+ type ToastProps,
+ type ToastActionElement,
+ ToastProvider,
+ ToastViewport,
+ Toast,
+ ToastTitle,
+ ToastDescription,
+ ToastClose,
+ ToastAction,
+}
diff --git a/frontend/src/components/ui/toaster.tsx b/frontend/src/components/ui/toaster.tsx
new file mode 100644
index 0000000..49fd8c9
--- /dev/null
+++ b/frontend/src/components/ui/toaster.tsx
@@ -0,0 +1,33 @@
+import { useToast } from '@/hooks/use-toast'
+import {
+ Toast,
+ ToastClose,
+ ToastDescription,
+ ToastProvider,
+ ToastTitle,
+ ToastViewport,
+} from '@/components/ui/toast'
+
+export function Toaster() {
+ const { toasts } = useToast()
+
+ return (
+
+ {toasts.map(function ({ id, title, description, action, ...props }) {
+ return (
+
+
+ {title && {title} }
+ {description && (
+ {description}
+ )}
+
+ {action}
+
+
+ )
+ })}
+
+
+ )
+}
diff --git a/frontend/src/components/ui/tooltip.tsx b/frontend/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000..e320a64
--- /dev/null
+++ b/frontend/src/components/ui/tooltip.tsx
@@ -0,0 +1,29 @@
+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/config/fonts.ts b/frontend/src/config/fonts.ts
new file mode 100644
index 0000000..773fcf8
--- /dev/null
+++ b/frontend/src/config/fonts.ts
@@ -0,0 +1,28 @@
+/**
+ * List of available font names (visit the url`/settings/appearance`).
+ * This array is used to generate Tailwind's `safelist` inside 'tailwind.config.js' and 'appearance-form.tsx'
+ * to prevent dynamic font classes (e.g., `font-inter`, `font-manrope`) from being removed during purging.
+ *
+ * 📝 How to Add a New Font:
+ * 1. Add the font name here.
+ * 2. Update the ` ` tag in 'index.html' to include the new font from Google Fonts (or any other source).
+ * 3. Add new fontFamily 'tailwind.config.js'
+ *
+ * Example:
+ * fonts.ts → Add 'roboto' to this array.
+ * index.html → Add Google Fonts link for Roboto.
+ * tailwind.config.js → Add the new font inside `theme.extend.fontFamily`.
+ * ```ts
+ * theme: {
+ * // other configs
+ * extend: {
+ * fontFamily: {
+ * inter: ['Inter', ...fontFamily.sans],
+ * manrope: ['Manrope', ...fontFamily.sans],
+ * roboto: ['Roboto', ...fontFamily.sans], // Add new font here
+ * }
+ * }
+ * }
+ * ```
+ */
+export const fonts = ['inter', 'manrope', 'system'] as const
diff --git a/frontend/src/context/font-context.tsx b/frontend/src/context/font-context.tsx
new file mode 100644
index 0000000..36e2a7e
--- /dev/null
+++ b/frontend/src/context/font-context.tsx
@@ -0,0 +1,48 @@
+import React, { createContext, useContext, useEffect, useState } from 'react'
+import { fonts } from '@/config/fonts'
+
+type Font = (typeof fonts)[number]
+
+interface FontContextType {
+ font: Font
+ setFont: (font: Font) => void
+}
+
+const FontContext = createContext(undefined)
+
+export const FontProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ const [font, _setFont] = useState(() => {
+ const savedFont = localStorage.getItem('font')
+ return fonts.includes(savedFont as Font) ? (savedFont as Font) : fonts[0]
+ })
+
+ useEffect(() => {
+ const applyFont = (font: string) => {
+ const root = document.documentElement
+ root.classList.forEach((cls) => {
+ if (cls.startsWith('font-')) root.classList.remove(cls)
+ })
+ root.classList.add(`font-${font}`)
+ }
+
+ applyFont(font)
+ }, [font])
+
+ const setFont = (font: Font) => {
+ localStorage.setItem('font', font)
+ _setFont(font)
+ }
+
+ return {children}
+}
+
+// eslint-disable-next-line react-refresh/only-export-components
+export const useFont = () => {
+ const context = useContext(FontContext)
+ if (!context) {
+ throw new Error('useFont must be used within a FontProvider')
+ }
+ return context
+}
diff --git a/frontend/src/context/search-context.tsx b/frontend/src/context/search-context.tsx
new file mode 100644
index 0000000..ed9e6ad
--- /dev/null
+++ b/frontend/src/context/search-context.tsx
@@ -0,0 +1,46 @@
+import React from 'react'
+import { CommandMenu } from '@/components/command-menu'
+
+interface SearchContextType {
+ open: boolean
+ setOpen: React.Dispatch>
+}
+
+const SearchContext = React.createContext(null)
+
+interface Props {
+ children: React.ReactNode
+}
+
+export function SearchProvider({ children }: Props) {
+ const [open, setOpen] = React.useState(false)
+
+ React.useEffect(() => {
+ const down = (e: KeyboardEvent) => {
+ if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
+ e.preventDefault()
+ setOpen((open) => !open)
+ }
+ }
+ document.addEventListener('keydown', down)
+ return () => document.removeEventListener('keydown', down)
+ }, [])
+
+ return (
+
+ {children}
+
+
+ )
+}
+
+// eslint-disable-next-line react-refresh/only-export-components
+export const useSearch = () => {
+ const searchContext = React.useContext(SearchContext)
+
+ if (!searchContext) {
+ throw new Error('useSearch has to be used within ')
+ }
+
+ return searchContext
+}
diff --git a/frontend/src/context/theme-context.tsx b/frontend/src/context/theme-context.tsx
new file mode 100644
index 0000000..1cbd24b
--- /dev/null
+++ b/frontend/src/context/theme-context.tsx
@@ -0,0 +1,82 @@
+import { createContext, useContext, useEffect, useState } from 'react'
+
+type Theme = 'dark' | 'light' | 'system'
+
+type ThemeProviderProps = {
+ children: React.ReactNode
+ defaultTheme?: Theme
+ storageKey?: string
+}
+
+type ThemeProviderState = {
+ theme: Theme
+ setTheme: (theme: Theme) => void
+}
+
+const initialState: ThemeProviderState = {
+ theme: 'system',
+ setTheme: () => null,
+}
+
+const ThemeProviderContext = createContext(initialState)
+
+export function ThemeProvider({
+ children,
+ defaultTheme = 'system',
+ storageKey = 'vite-ui-theme',
+ ...props
+}: ThemeProviderProps) {
+ const [theme, _setTheme] = useState(
+ () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
+ )
+
+ useEffect(() => {
+ const root = window.document.documentElement
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
+
+ const applyTheme = (theme: Theme) => {
+ root.classList.remove('light', 'dark') // Remove existing theme classes
+ const systemTheme = mediaQuery.matches ? 'dark' : 'light'
+ const effectiveTheme = theme === 'system' ? systemTheme : theme
+ root.classList.add(effectiveTheme) // Add the new theme class
+ }
+
+ const handleChange = () => {
+ if (theme === 'system') {
+ applyTheme('system')
+ }
+ }
+
+ applyTheme(theme)
+
+ mediaQuery.addEventListener('change', handleChange)
+
+ return () => mediaQuery.removeEventListener('change', handleChange)
+ }, [theme])
+
+ const setTheme = (theme: Theme) => {
+ localStorage.setItem(storageKey, theme)
+ _setTheme(theme)
+ }
+
+ const value = {
+ theme,
+ setTheme,
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+// eslint-disable-next-line react-refresh/only-export-components
+export const useTheme = () => {
+ const context = useContext(ThemeProviderContext)
+
+ if (context === undefined)
+ throw new Error('useTheme must be used within a ThemeProvider')
+
+ return context
+}
diff --git a/frontend/src/features/apps/data/apps.tsx b/frontend/src/features/apps/data/apps.tsx
new file mode 100644
index 0000000..72bb94b
--- /dev/null
+++ b/frontend/src/features/apps/data/apps.tsx
@@ -0,0 +1,110 @@
+import {
+ IconBrandDiscord,
+ IconBrandDocker,
+ IconBrandFigma,
+ IconBrandGithub,
+ IconBrandGitlab,
+ IconBrandGmail,
+ IconBrandMedium,
+ IconBrandNotion,
+ IconBrandSkype,
+ IconBrandSlack,
+ IconBrandStripe,
+ IconBrandTelegram,
+ IconBrandTrello,
+ IconBrandWhatsapp,
+ IconBrandZoom,
+} from '@tabler/icons-react'
+
+export const apps = [
+ {
+ name: 'Telegram',
+ logo: ,
+ connected: false,
+ desc: 'Connect with Telegram for real-time communication.',
+ },
+ {
+ name: 'Notion',
+ logo: ,
+ connected: true,
+ desc: 'Effortlessly sync Notion pages for seamless collaboration.',
+ },
+ {
+ name: 'Figma',
+ logo: ,
+ connected: true,
+ desc: 'View and collaborate on Figma designs in one place.',
+ },
+ {
+ name: 'Trello',
+ logo: ,
+ connected: false,
+ desc: 'Sync Trello cards for streamlined project management.',
+ },
+ {
+ name: 'Slack',
+ logo: ,
+ connected: false,
+ desc: 'Integrate Slack for efficient team communication',
+ },
+ {
+ name: 'Zoom',
+ logo: ,
+ connected: true,
+ desc: 'Host Zoom meetings directly from the dashboard.',
+ },
+ {
+ name: 'Stripe',
+ logo: ,
+ connected: false,
+ desc: 'Easily manage Stripe transactions and payments.',
+ },
+ {
+ name: 'Gmail',
+ logo: ,
+ connected: true,
+ desc: 'Access and manage Gmail messages effortlessly.',
+ },
+ {
+ name: 'Medium',
+ logo: ,
+ connected: false,
+ desc: 'Explore and share Medium stories on your dashboard.',
+ },
+ {
+ name: 'Skype',
+ logo: ,
+ connected: false,
+ desc: 'Connect with Skype contacts seamlessly.',
+ },
+ {
+ name: 'Docker',
+ logo: ,
+ connected: false,
+ desc: 'Effortlessly manage Docker containers on your dashboard.',
+ },
+ {
+ name: 'GitHub',
+ logo: ,
+ connected: false,
+ desc: 'Streamline code management with GitHub integration.',
+ },
+ {
+ name: 'GitLab',
+ logo: ,
+ connected: false,
+ desc: 'Efficiently manage code projects with GitLab integration.',
+ },
+ {
+ name: 'Discord',
+ logo: ,
+ connected: false,
+ desc: 'Connect with Discord for seamless team communication.',
+ },
+ {
+ name: 'WhatsApp',
+ logo: ,
+ connected: false,
+ desc: 'Easily integrate WhatsApp for direct messaging.',
+ },
+]
diff --git a/frontend/src/features/apps/index.tsx b/frontend/src/features/apps/index.tsx
new file mode 100644
index 0000000..00d76ca
--- /dev/null
+++ b/frontend/src/features/apps/index.tsx
@@ -0,0 +1,144 @@
+import { useState } from 'react'
+import {
+ IconAdjustmentsHorizontal,
+ IconSortAscendingLetters,
+ IconSortDescendingLetters,
+} from '@tabler/icons-react'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select'
+import { Separator } from '@/components/ui/separator'
+import { Header } from '@/components/layout/header'
+import { Main } from '@/components/layout/main'
+import { ProfileDropdown } from '@/components/profile-dropdown'
+import { Search } from '@/components/search'
+import { ThemeSwitch } from '@/components/theme-switch'
+import { apps } from './data/apps'
+
+const appText = new Map([
+ ['all', 'All Apps'],
+ ['connected', 'Connected'],
+ ['notConnected', 'Not Connected'],
+])
+
+export default function Apps() {
+ const [sort, setSort] = useState('ascending')
+ const [appType, setAppType] = useState('all')
+ const [searchTerm, setSearchTerm] = useState('')
+
+ const filteredApps = apps
+ .sort((a, b) =>
+ sort === 'ascending'
+ ? a.name.localeCompare(b.name)
+ : b.name.localeCompare(a.name)
+ )
+ .filter((app) =>
+ appType === 'connected'
+ ? app.connected
+ : appType === 'notConnected'
+ ? !app.connected
+ : true
+ )
+ .filter((app) => app.name.toLowerCase().includes(searchTerm.toLowerCase()))
+
+ return (
+ <>
+ {/* ===== Top Heading ===== */}
+
+
+ {/* ===== Content ===== */}
+
+
+
+ App Integrations
+
+
+ Here's a list of your apps for the integration!
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+ {appText.get(appType)}
+
+
+ All Apps
+ Connected
+ Not Connected
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ascending
+
+
+
+
+
+ Descending
+
+
+
+
+
+
+
+ {filteredApps.map((app) => (
+
+
+
+ {app.logo}
+
+
+ {app.connected ? 'Connected' : 'Connect'}
+
+
+
+
{app.name}
+
{app.desc}
+
+
+ ))}
+
+
+ >
+ )
+}
diff --git a/frontend/src/features/auth/auth-layout.tsx b/frontend/src/features/auth/auth-layout.tsx
new file mode 100644
index 0000000..aefdfba
--- /dev/null
+++ b/frontend/src/features/auth/auth-layout.tsx
@@ -0,0 +1,28 @@
+interface Props {
+ children: React.ReactNode
+}
+
+export default function AuthLayout({ children }: Props) {
+ return (
+
+ )
+}
diff --git a/frontend/src/features/auth/forgot-password/components/forgot-password-form.tsx b/frontend/src/features/auth/forgot-password/components/forgot-password-form.tsx
new file mode 100644
index 0000000..d105ef4
--- /dev/null
+++ b/frontend/src/features/auth/forgot-password/components/forgot-password-form.tsx
@@ -0,0 +1,70 @@
+import { HTMLAttributes, useState } from 'react'
+import { z } from 'zod'
+import { useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { cn } from '@/lib/utils'
+import { Button } from '@/components/ui/button'
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@/components/ui/form'
+import { Input } from '@/components/ui/input'
+
+type ForgotFormProps = HTMLAttributes
+
+const formSchema = z.object({
+ email: z
+ .string()
+ .min(1, { message: 'Please enter your email' })
+ .email({ message: 'Invalid email address' }),
+})
+
+export function ForgotForm({ className, ...props }: ForgotFormProps) {
+ const [isLoading, setIsLoading] = useState(false)
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: { email: '' },
+ })
+
+ function onSubmit(data: z.infer) {
+ setIsLoading(true)
+ // eslint-disable-next-line no-console
+ console.log(data)
+
+ setTimeout(() => {
+ setIsLoading(false)
+ }, 3000)
+ }
+
+ return (
+
+ )
+}
diff --git a/frontend/src/features/auth/forgot-password/index.tsx b/frontend/src/features/auth/forgot-password/index.tsx
new file mode 100644
index 0000000..035c1ad
--- /dev/null
+++ b/frontend/src/features/auth/forgot-password/index.tsx
@@ -0,0 +1,33 @@
+import { Link } from '@tanstack/react-router'
+import { Card } from '@/components/ui/card'
+import AuthLayout from '../auth-layout'
+import { ForgotForm } from './components/forgot-password-form'
+
+export default function ForgotPassword() {
+ return (
+
+
+
+
+ Forgot Password
+
+
+ Enter your registered email and we will send you a link to
+ reset your password.
+
+
+
+
+ Don't have an account?{' '}
+
+ Sign up
+
+ .
+
+
+
+ )
+}
diff --git a/frontend/src/features/auth/otp/components/otp-form.tsx b/frontend/src/features/auth/otp/components/otp-form.tsx
new file mode 100644
index 0000000..11aa1b5
--- /dev/null
+++ b/frontend/src/features/auth/otp/components/otp-form.tsx
@@ -0,0 +1,95 @@
+import { HTMLAttributes, useState } from 'react'
+import { z } from 'zod'
+import { useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { useNavigate } from '@tanstack/react-router'
+import { cn } from '@/lib/utils'
+import { toast } from '@/hooks/use-toast'
+import { Button } from '@/components/ui/button'
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormMessage,
+} from '@/components/ui/form'
+import { Input } from '@/components/ui/input'
+import { Separator } from '@/components/ui/separator'
+import { PinInput, PinInputField } from '@/components/pin-input'
+
+type OtpFormProps = HTMLAttributes
+
+const formSchema = z.object({
+ otp: z.string().min(1, { message: 'Please enter your otp code.' }),
+})
+
+export function OtpForm({ className, ...props }: OtpFormProps) {
+ const navigate = useNavigate()
+ const [isLoading, setIsLoading] = useState(false)
+ const [disabledBtn, setDisabledBtn] = useState(true)
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: { otp: '' },
+ })
+
+ function onSubmit(data: z.infer) {
+ setIsLoading(true)
+ toast({
+ title: 'You submitted the following values:',
+ description: (
+
+ {JSON.stringify(data, null, 2)}
+
+ ),
+ })
+
+ setTimeout(() => {
+ setIsLoading(false)
+ navigate({ to: '/' })
+ }, 1000)
+ }
+
+ return (
+
+ )
+}
diff --git a/frontend/src/features/auth/otp/index.tsx b/frontend/src/features/auth/otp/index.tsx
new file mode 100644
index 0000000..9d188cf
--- /dev/null
+++ b/frontend/src/features/auth/otp/index.tsx
@@ -0,0 +1,33 @@
+import { Link } from '@tanstack/react-router'
+import { Card } from '@/components/ui/card'
+import AuthLayout from '../auth-layout'
+import { OtpForm } from './components/otp-form'
+
+export default function Otp() {
+ return (
+
+
+
+
+ Two-factor Authentication
+
+
+ Please enter the authentication code. We have sent the
+ authentication code to your email.
+
+
+
+
+ Haven't received it?{' '}
+
+ Resend a new code.
+
+ .
+
+
+
+ )
+}
diff --git a/frontend/src/features/auth/sign-in/components/user-auth-form.tsx b/frontend/src/features/auth/sign-in/components/user-auth-form.tsx
new file mode 100644
index 0000000..ae7139a
--- /dev/null
+++ b/frontend/src/features/auth/sign-in/components/user-auth-form.tsx
@@ -0,0 +1,135 @@
+import { HTMLAttributes, useState } from 'react'
+import { z } from 'zod'
+import { useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { Link } from '@tanstack/react-router'
+import { IconBrandFacebook, IconBrandGithub } from '@tabler/icons-react'
+import { cn } from '@/lib/utils'
+import { Button } from '@/components/ui/button'
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@/components/ui/form'
+import { Input } from '@/components/ui/input'
+import { PasswordInput } from '@/components/password-input'
+
+type UserAuthFormProps = HTMLAttributes
+
+const formSchema = z.object({
+ email: z
+ .string()
+ .min(1, { message: 'Please enter your email' })
+ .email({ message: 'Invalid email address' }),
+ password: z
+ .string()
+ .min(1, {
+ message: 'Please enter your password',
+ })
+ .min(7, {
+ message: 'Password must be at least 7 characters long',
+ }),
+})
+
+export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
+ const [isLoading, setIsLoading] = useState(false)
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ email: '',
+ password: '',
+ },
+ })
+
+ function onSubmit(data: z.infer) {
+ setIsLoading(true)
+ // eslint-disable-next-line no-console
+ console.log(data)
+
+ setTimeout(() => {
+ setIsLoading(false)
+ }, 3000)
+ }
+
+ return (
+
+ )
+}
diff --git a/frontend/src/features/auth/sign-in/index.tsx b/frontend/src/features/auth/sign-in/index.tsx
new file mode 100644
index 0000000..dbec8d2
--- /dev/null
+++ b/frontend/src/features/auth/sign-in/index.tsx
@@ -0,0 +1,37 @@
+import { Card } from '@/components/ui/card'
+import AuthLayout from '../auth-layout'
+import { UserAuthForm } from './components/user-auth-form'
+
+export default function SignIn() {
+ return (
+
+
+
+
Login
+
+ Enter your email and password below
+ to log into your account
+
+
+
+
+ By clicking login, you agree to our{' '}
+
+ Terms of Service
+ {' '}
+ and{' '}
+
+ Privacy Policy
+
+ .
+
+
+
+ )
+}
diff --git a/frontend/src/features/auth/sign-in/sign-in-2.tsx b/frontend/src/features/auth/sign-in/sign-in-2.tsx
new file mode 100644
index 0000000..276a779
--- /dev/null
+++ b/frontend/src/features/auth/sign-in/sign-in-2.tsx
@@ -0,0 +1,75 @@
+import ViteLogo from '@/assets/vite.svg'
+import { UserAuthForm } from './components/user-auth-form'
+
+export default function SignIn2() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ “This template has saved me countless hours of work and
+ helped me deliver stunning designs to my clients faster than ever
+ before.”
+
+
+
+
+
+
+
+
+
Login
+
+ Enter your email and password below
+ to log into your account
+
+
+
+
+ By clicking login, you agree to our{' '}
+
+ Terms of Service
+ {' '}
+ and{' '}
+
+ Privacy Policy
+
+ .
+
+
+
+
+ )
+}
diff --git a/frontend/src/features/auth/sign-up/components/sign-up-form.tsx b/frontend/src/features/auth/sign-up/components/sign-up-form.tsx
new file mode 100644
index 0000000..26387fe
--- /dev/null
+++ b/frontend/src/features/auth/sign-up/components/sign-up-form.tsx
@@ -0,0 +1,146 @@
+import { HTMLAttributes, useState } from 'react'
+import { z } from 'zod'
+import { useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { IconBrandFacebook, IconBrandGithub } from '@tabler/icons-react'
+import { cn } from '@/lib/utils'
+import { Button } from '@/components/ui/button'
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@/components/ui/form'
+import { Input } from '@/components/ui/input'
+import { PasswordInput } from '@/components/password-input'
+
+type SignUpFormProps = HTMLAttributes
+
+const formSchema = z
+ .object({
+ email: z
+ .string()
+ .min(1, { message: 'Please enter your email' })
+ .email({ message: 'Invalid email address' }),
+ password: z
+ .string()
+ .min(1, {
+ message: 'Please enter your password',
+ })
+ .min(7, {
+ message: 'Password must be at least 7 characters long',
+ }),
+ confirmPassword: z.string(),
+ })
+ .refine((data) => data.password === data.confirmPassword, {
+ message: "Passwords don't match.",
+ path: ['confirmPassword'],
+ })
+
+export function SignUpForm({ className, ...props }: SignUpFormProps) {
+ const [isLoading, setIsLoading] = useState(false)
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ email: '',
+ password: '',
+ confirmPassword: '',
+ },
+ })
+
+ function onSubmit(data: z.infer) {
+ setIsLoading(true)
+ // eslint-disable-next-line no-console
+ console.log(data)
+
+ setTimeout(() => {
+ setIsLoading(false)
+ }, 3000)
+ }
+
+ return (
+
+ )
+}
diff --git a/frontend/src/features/auth/sign-up/index.tsx b/frontend/src/features/auth/sign-up/index.tsx
new file mode 100644
index 0000000..0629bea
--- /dev/null
+++ b/frontend/src/features/auth/sign-up/index.tsx
@@ -0,0 +1,46 @@
+import { Link } from '@tanstack/react-router'
+import { Card } from '@/components/ui/card'
+import AuthLayout from '../auth-layout'
+import { SignUpForm } from './components/sign-up-form'
+
+export default function SignUp() {
+ return (
+
+
+
+
+ Create an account
+
+
+ Enter your email and password to create an account.
+ Already have an account?{' '}
+
+ Sign In
+
+
+
+
+
+ By creating an account, you agree to our{' '}
+
+ Terms of Service
+ {' '}
+ and{' '}
+
+ Privacy Policy
+
+ .
+
+
+
+ )
+}
diff --git a/frontend/src/features/chats/components/new-chat.tsx b/frontend/src/features/chats/components/new-chat.tsx
new file mode 100644
index 0000000..978b482
--- /dev/null
+++ b/frontend/src/features/chats/components/new-chat.tsx
@@ -0,0 +1,138 @@
+import { useEffect, useState } from 'react'
+import { IconCheck, IconX } from '@tabler/icons-react'
+import { toast } from '@/hooks/use-toast'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from '@/components/ui/command'
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
+import { ChatUser } from '../data/chat-types'
+
+type User = Omit
+
+type Props = {
+ users: User[]
+ open: boolean
+ onOpenChange: (open: boolean) => void
+}
+export function NewChat({ users, onOpenChange, open }: Props) {
+ const [selectedUsers, setSelectedUsers] = useState([])
+
+ const handleSelectUser = (user: User) => {
+ if (!selectedUsers.find((u) => u.id === user.id)) {
+ setSelectedUsers([...selectedUsers, user])
+ } else {
+ handleRemoveUser(user.id)
+ }
+ }
+
+ const handleRemoveUser = (userId: string) => {
+ setSelectedUsers(selectedUsers.filter((user) => user.id !== userId))
+ }
+
+ useEffect(() => {
+ if (!open) {
+ setSelectedUsers([])
+ }
+ }, [open])
+
+ const onSubmit = () => {
+ toast({
+ title: 'You submitted the following values:',
+ description: (
+
+
+ {JSON.stringify(selectedUsers, null, 2)}
+
+
+ ),
+ })
+ }
+
+ return (
+
+
+
+ New message
+
+
+
+ To:
+ {selectedUsers.map((user) => (
+
+ {user.fullName}
+ {
+ if (e.key === 'Enter') {
+ handleRemoveUser(user.id)
+ }
+ }}
+ onClick={() => handleRemoveUser(user.id)}
+ >
+
+
+
+ ))}
+
+
+
+
+ No people found.
+
+ {users.map((user) => (
+ handleSelectUser(user)}
+ className='flex items-center justify-between gap-2'
+ >
+
+
+
+
+ {user.fullName}
+
+
+ {user.username}
+
+
+
+
+ {selectedUsers.find((u) => u.id === user.id) && (
+
+ )}
+
+ ))}
+
+
+
+
+ Chat
+
+
+
+
+ )
+}
diff --git a/frontend/src/features/chats/data/chat-types.ts b/frontend/src/features/chats/data/chat-types.ts
new file mode 100644
index 0000000..442254a
--- /dev/null
+++ b/frontend/src/features/chats/data/chat-types.ts
@@ -0,0 +1,4 @@
+import { conversations } from './convo.json'
+
+export type ChatUser = (typeof conversations)[number]
+export type Convo = ChatUser['messages'][number]
diff --git a/frontend/src/features/chats/data/convo.json b/frontend/src/features/chats/data/convo.json
new file mode 100644
index 0000000..310160f
--- /dev/null
+++ b/frontend/src/features/chats/data/convo.json
@@ -0,0 +1,309 @@
+{
+ "conversations": [
+ {
+ "id": "conv1",
+ "profile": "https://randomuser.me/api/portraits/men/32.jpg",
+ "username": "alex_dev",
+ "fullName": "Alex John",
+ "title": "Senior Backend Dev",
+ "messages": [
+ {
+ "sender": "You",
+ "message": "See you later, Alex!",
+ "timestamp": "2024-08-24T11:15:15"
+ },
+ {
+ "sender": "Alex",
+ "message": "Alright, talk to you later!",
+ "timestamp": "2024-08-24T11:11:30"
+ },
+ {
+ "sender": "You",
+ "message": "For sure. Anyway, I should get back to reviewing the project.",
+ "timestamp": "2024-08-23T09:26:50"
+ },
+ {
+ "sender": "Alex",
+ "message": "Yeah, let me know what you think.",
+ "timestamp": "2024-08-23T09:25:15"
+ },
+ {
+ "sender": "You",
+ "message": "Oh, nice! I've been waiting for that. I'll check it out later.",
+ "timestamp": "2024-08-23T09:24:30"
+ },
+ {
+ "sender": "Alex",
+ "message": "They've added a dark mode option! It looks really sleek.",
+ "timestamp": "2024-08-23T09:23:10"
+ },
+ {
+ "sender": "You",
+ "message": "No, not yet. What's new?",
+ "timestamp": "2024-08-23T09:22:00"
+ },
+ {
+ "sender": "Alex",
+ "message": "By the way, have you seen the new feature update?",
+ "timestamp": "2024-08-23T09:21:05"
+ },
+ {
+ "sender": "You",
+ "message": "Will do! Thanks, Alex.",
+ "timestamp": "2024-08-23T09:20:10"
+ },
+ {
+ "sender": "Alex",
+ "message": "Great! Let me know if you need any help.",
+ "timestamp": "2024-08-23T09:19:20"
+ },
+ {
+ "sender": "You",
+ "message": "Almost done. Just need to review a few things.",
+ "timestamp": "2024-08-23T09:18:45"
+ },
+ {
+ "sender": "Alex",
+ "message": "I'm good, thanks! Did you finish the project?",
+ "timestamp": "2024-08-23T09:17:10"
+ },
+ {
+ "sender": "You",
+ "message": "Hey Alex, I'm doing well! How about you?",
+ "timestamp": "2024-08-23T09:16:30"
+ },
+ {
+ "sender": "Alex",
+ "message": "Hey Bob, how are you doing?",
+ "timestamp": "2024-08-23T09:15:00"
+ }
+ ]
+ },
+ {
+ "id": "conv2",
+ "profile": "https://randomuser.me/api/portraits/women/45.jpg",
+ "username": "taylor.codes",
+ "fullName": "Taylor Grande",
+ "title": "Tech Lead",
+ "messages": [
+ {
+ "sender": "Taylor",
+ "message": "Yeah, it's really well-explained. You should give it a try.",
+ "timestamp": "2024-08-23T10:35:00"
+ },
+ {
+ "sender": "You",
+ "message": "Not yet, is it good?",
+ "timestamp": "2024-08-23T10:32:00"
+ },
+ {
+ "sender": "Taylor",
+ "message": "Hey, did you check out that new tutorial?",
+ "timestamp": "2024-08-23T10:30:00"
+ }
+ ]
+ },
+ {
+ "id": "conv3",
+ "profile": "https://randomuser.me/api/portraits/men/54.jpg",
+ "username": "john_stack",
+ "fullName": "John Doe",
+ "title": "QA",
+ "messages": [
+ {
+ "sender": "You",
+ "message": "Yep, see ya. 👋🏼",
+ "timestamp": "2024-08-22T18:59:00"
+ },
+ {
+ "sender": "John",
+ "message": "Great, see you then!",
+ "timestamp": "2024-08-22T18:55:00"
+ },
+ {
+ "sender": "You",
+ "message": "Yes, same time as usual. I'll send the invite shortly.",
+ "timestamp": "2024-08-22T18:50:00"
+ },
+ {
+ "sender": "John",
+ "message": "Are we still on for the meeting tomorrow?",
+ "timestamp": "2024-08-22T18:45:00"
+ }
+ ]
+ },
+ {
+ "id": "conv4",
+ "profile": "https://randomuser.me/api/portraits/women/29.jpg",
+ "username": "megan_frontend",
+ "fullName": "Megan Flux",
+ "title": "Jr Developer",
+ "messages": [
+ {
+ "sender": "You",
+ "message": "Sure ✌🏼",
+ "timestamp": "2024-08-23T11:30:00"
+ },
+ {
+ "sender": "Megan",
+ "message": "Thanks, appreciate it!",
+ "timestamp": "2024-08-23T11:30:00"
+ },
+ {
+ "sender": "You",
+ "message": "Sure thing! I'll take a look in the next hour.",
+ "timestamp": "2024-08-23T11:25:00"
+ },
+ {
+ "sender": "Megan",
+ "message": "Hey! Do you have time to review my PR today?",
+ "timestamp": "2024-08-23T11:20:00"
+ }
+ ]
+ },
+ {
+ "id": "conv5",
+ "profile": "https://randomuser.me/api/portraits/men/72.jpg",
+ "username": "dev_david",
+ "fullName": "David Brown",
+ "title": "Senior UI/UX Designer",
+ "messages": [
+ {
+ "sender": "You",
+ "message": "Great, I'll review them now!",
+ "timestamp": "2024-08-23T12:00:00"
+ },
+ {
+ "sender": "David",
+ "message": "Just sent you the files. Let me know if you need any changes.",
+ "timestamp": "2024-08-23T11:58:00"
+ },
+ {
+ "sender": "David",
+ "message": "I finished the design for the dashboard. Thoughts?",
+ "timestamp": "2024-08-23T11:55:00"
+ }
+ ]
+ },
+ {
+ "id": "conv6",
+ "profile": "https://randomuser.me/api/portraits/women/68.jpg",
+ "username": "julia.design",
+ "fullName": "Julia Carter",
+ "title": "Product Designer",
+ "messages": [
+ {
+ "sender": "Julia",
+ "message": "Same here! It's coming together nicely.",
+ "timestamp": "2024-08-22T14:10:00"
+ },
+ {
+ "sender": "You",
+ "message": "I'm really excited to see the final product!",
+ "timestamp": "2024-08-22T14:15:00"
+ },
+ {
+ "sender": "You",
+ "message": "How's the project looking on your end?",
+ "timestamp": "2024-08-22T14:05:00"
+ }
+ ]
+ },
+ {
+ "id": "conv7",
+ "profile": "https://randomuser.me/api/portraits/men/24.jpg",
+ "username": "brad_dev",
+ "fullName": "Brad Wilson",
+ "title": "CEO",
+ "messages": [
+ {
+ "sender": "Brad",
+ "message": "Got it! Thanks for the update.",
+ "timestamp": "2024-08-23T15:45:00"
+ },
+ {
+ "sender": "You",
+ "message": "The release has been delayed to next week.",
+ "timestamp": "2024-08-23T15:40:00"
+ },
+ {
+ "sender": "Brad",
+ "message": "Hey, any news on the release?",
+ "timestamp": "2024-08-23T15:35:00"
+ }
+ ]
+ },
+ {
+ "id": "conv8",
+ "profile": "https://randomuser.me/api/portraits/women/34.jpg",
+ "username": "katie_ui",
+ "fullName": "Katie Lee",
+ "title": "QA",
+ "messages": [
+ {
+ "sender": "Katie",
+ "message": "I'll join the call in a few minutes.",
+ "timestamp": "2024-08-23T09:50:00"
+ },
+ {
+ "sender": "You",
+ "message": "Perfect! We'll start as soon as you're in.",
+ "timestamp": "2024-08-23T09:48:00"
+ },
+ {
+ "sender": "Katie",
+ "message": "Is the meeting still on?",
+ "timestamp": "2024-08-23T09:45:00"
+ }
+ ]
+ },
+ {
+ "id": "conv9",
+ "profile": "https://randomuser.me/api/portraits/men/67.jpg",
+ "username": "matt_fullstack",
+ "fullName": "Matt Green",
+ "title": "Full-stack Dev",
+ "messages": [
+ {
+ "sender": "Matt",
+ "message": "Sure thing, I'll send over the updates shortly.",
+ "timestamp": "2024-08-23T10:25:00"
+ },
+ {
+ "sender": "You",
+ "message": "Could you update the backend as well?",
+ "timestamp": "2024-08-23T10:23:00"
+ },
+ {
+ "sender": "Matt",
+ "message": "The frontend updates are done. How does it look?",
+ "timestamp": "2024-08-23T10:20:00"
+ }
+ ]
+ },
+ {
+ "id": "conv10",
+ "profile": "https://randomuser.me/api/portraits/women/56.jpg",
+ "username": "sophie_dev",
+ "fullName": "Sophie Alex",
+ "title": "Jr. Frontend Dev",
+ "messages": [
+ {
+ "sender": "You",
+ "message": "Thanks! I'll review your code and get back to you.",
+ "timestamp": "2024-08-23T16:10:00"
+ },
+ {
+ "sender": "Sophie",
+ "message": "Let me know if you need anything else.",
+ "timestamp": "2024-08-23T16:05:00"
+ },
+ {
+ "sender": "Sophie",
+ "message": "The feature is implemented. Can you review it?",
+ "timestamp": "2024-08-23T16:00:00"
+ }
+ ]
+ }
+ ]
+}
diff --git a/frontend/src/features/chats/index.tsx b/frontend/src/features/chats/index.tsx
new file mode 100644
index 0000000..2a41a76
--- /dev/null
+++ b/frontend/src/features/chats/index.tsx
@@ -0,0 +1,346 @@
+import { useState } from 'react'
+import { Fragment } from 'react/jsx-runtime'
+import { format } from 'date-fns'
+import {
+ IconArrowLeft,
+ IconDotsVertical,
+ IconEdit,
+ IconMessages,
+ IconPaperclip,
+ IconPhone,
+ IconPhotoPlus,
+ IconPlus,
+ IconSearch,
+ IconSend,
+ IconVideo,
+} from '@tabler/icons-react'
+import { cn } from '@/lib/utils'
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
+import { Button } from '@/components/ui/button'
+import { ScrollArea } from '@/components/ui/scroll-area'
+import { Separator } from '@/components/ui/separator'
+import { Header } from '@/components/layout/header'
+import { Main } from '@/components/layout/main'
+import { ProfileDropdown } from '@/components/profile-dropdown'
+import { Search } from '@/components/search'
+import { ThemeSwitch } from '@/components/theme-switch'
+import { NewChat } from './components/new-chat'
+import { type ChatUser, type Convo } from './data/chat-types'
+// Fake Data
+import { conversations } from './data/convo.json'
+
+export default function Chats() {
+ const [search, setSearch] = useState('')
+ const [selectedUser, setSelectedUser] = useState(null)
+ const [mobileSelectedUser, setMobileSelectedUser] = useState(
+ null
+ )
+ const [createConversationDialogOpened, setCreateConversationDialog] =
+ useState(false)
+
+ // Filtered data based on the search query
+ const filteredChatList = conversations.filter(({ fullName }) =>
+ fullName.toLowerCase().includes(search.trim().toLowerCase())
+ )
+
+ const currentMessage = selectedUser?.messages.reduce(
+ (acc: Record, obj) => {
+ const key = format(obj.timestamp, 'd MMM, yyyy')
+
+ // Create an array for the category if it doesn't exist
+ if (!acc[key]) {
+ acc[key] = []
+ }
+
+ // Push the current object to the array
+ acc[key].push(obj)
+
+ return acc
+ },
+ {}
+ )
+
+ const users = conversations.map(({ messages, ...user }) => user)
+
+ return (
+ <>
+ {/* ===== Top Heading ===== */}
+
+
+
+
+ {/* Left Side */}
+
+
+
+
+
Inbox
+
+
+
+
setCreateConversationDialog(true)}
+ className='rounded-lg'
+ >
+
+
+
+
+
+
+ Search
+ setSearch(e.target.value)}
+ />
+
+
+
+
+ {filteredChatList.map((chatUsr) => {
+ const { id, profile, username, messages, fullName } = chatUsr
+ const lastConvo = messages[0]
+ const lastMsg =
+ lastConvo.sender === 'You'
+ ? `You: ${lastConvo.message}`
+ : lastConvo.message
+ return (
+
+ {
+ setSelectedUser(chatUsr)
+ setMobileSelectedUser(chatUsr)
+ }}
+ >
+
+
+
+ {username}
+
+
+
+ {fullName}
+
+
+ {lastMsg}
+
+
+
+
+
+
+ )
+ })}
+
+
+
+ {/* Right Side */}
+ {selectedUser ? (
+
+ {/* Top Part */}
+
+ {/* Left */}
+
+
setMobileSelectedUser(null)}
+ >
+
+
+
+
+
+ {selectedUser.username}
+
+
+
+ {selectedUser.fullName}
+
+
+ {selectedUser.title}
+
+
+
+
+
+ {/* Right */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Conversation */}
+
+
+
+
+ {currentMessage &&
+ Object.keys(currentMessage).map((key) => (
+
+ {currentMessage[key].map((msg, index) => (
+
+ {msg.message}{' '}
+
+ {format(msg.timestamp, 'h:mm a')}
+
+
+ ))}
+ {key}
+
+ ))}
+
+
+
+
+
+
+ ) : (
+
+
+
+
+
+
+
Your messages
+
+ Send a message to start a chat.
+
+
+
setCreateConversationDialog(true)}
+ >
+ Send message
+
+
+
+ )}
+
+
+
+ >
+ )
+}
diff --git a/frontend/src/features/dashboard/components/overview.tsx b/frontend/src/features/dashboard/components/overview.tsx
new file mode 100644
index 0000000..befcd3b
--- /dev/null
+++ b/frontend/src/features/dashboard/components/overview.tsx
@@ -0,0 +1,81 @@
+import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from 'recharts'
+
+const data = [
+ {
+ name: 'Jan',
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: 'Feb',
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: 'Mar',
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: 'Apr',
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: 'May',
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: 'Jun',
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: 'Jul',
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: 'Aug',
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: 'Sep',
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: 'Oct',
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: 'Nov',
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+ {
+ name: 'Dec',
+ total: Math.floor(Math.random() * 5000) + 1000,
+ },
+]
+
+export function Overview() {
+ return (
+
+
+
+ `$${value}`}
+ />
+
+
+
+ )
+}
diff --git a/frontend/src/features/dashboard/components/recent-sales.tsx b/frontend/src/features/dashboard/components/recent-sales.tsx
new file mode 100644
index 0000000..92a1548
--- /dev/null
+++ b/frontend/src/features/dashboard/components/recent-sales.tsx
@@ -0,0 +1,83 @@
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
+
+export function RecentSales() {
+ return (
+
+
+
+
+ OM
+
+
+
+
Olivia Martin
+
+ olivia.martin@email.com
+
+
+
+$1,999.00
+
+
+
+
+
+ JL
+
+
+
+
Jackson Lee
+
+ jackson.lee@email.com
+
+
+
+$39.00
+
+
+
+
+
+ IN
+
+
+
+
Isabella Nguyen
+
+ isabella.nguyen@email.com
+
+
+
+$299.00
+
+
+
+
+
+
+ WK
+
+
+
+
William Kim
+
will@email.com
+
+
+$99.00
+
+
+
+
+
+
+ SD
+
+
+
+
Sofia Davis
+
+ sofia.davis@email.com
+
+
+
+$39.00
+
+
+
+ )
+}
diff --git a/frontend/src/features/dashboard/index.tsx b/frontend/src/features/dashboard/index.tsx
new file mode 100644
index 0000000..5c98c32
--- /dev/null
+++ b/frontend/src/features/dashboard/index.tsx
@@ -0,0 +1,216 @@
+import { Button } from '@/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card'
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import { Header } from '@/components/layout/header'
+import { Main } from '@/components/layout/main'
+import { TopNav } from '@/components/layout/top-nav'
+import { ProfileDropdown } from '@/components/profile-dropdown'
+import { Search } from '@/components/search'
+import { ThemeSwitch } from '@/components/theme-switch'
+import { Overview } from './components/overview'
+import { RecentSales } from './components/recent-sales'
+
+export default function Dashboard() {
+ return (
+ <>
+ {/* ===== Top Heading ===== */}
+
+
+ {/* ===== Main ===== */}
+
+
+
Dashboard
+
+ Download
+
+
+
+
+
+ Overview
+
+ Analytics
+
+
+ Reports
+
+
+ Notifications
+
+
+
+
+
+
+
+
+ Total Revenue
+
+
+
+
+
+
+ $45,231.89
+
+ +20.1% from last month
+
+
+
+
+
+
+ Subscriptions
+
+
+
+
+
+
+
+
+ +2350
+
+ +180.1% from last month
+
+
+
+
+
+ Sales
+
+
+
+
+
+
+ +12,234
+
+ +19% from last month
+
+
+
+
+
+
+ Active Now
+
+
+
+
+
+
+ +573
+
+ +201 since last hour
+
+
+
+
+
+
+
+ Overview
+
+
+
+
+
+
+
+ Recent Sales
+
+ You made 265 sales this month.
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+const topNav = [
+ {
+ title: 'Overview',
+ href: 'dashboard/overview',
+ isActive: true,
+ disabled: false,
+ },
+ {
+ title: 'Customers',
+ href: 'dashboard/customers',
+ isActive: false,
+ disabled: true,
+ },
+ {
+ title: 'Products',
+ href: 'dashboard/products',
+ isActive: false,
+ disabled: true,
+ },
+ {
+ title: 'Settings',
+ href: 'dashboard/settings',
+ isActive: false,
+ disabled: true,
+ },
+]
diff --git a/frontend/src/features/errors/forbidden.tsx b/frontend/src/features/errors/forbidden.tsx
new file mode 100644
index 0000000..5a2c4a6
--- /dev/null
+++ b/frontend/src/features/errors/forbidden.tsx
@@ -0,0 +1,25 @@
+import { useNavigate, useRouter } from '@tanstack/react-router'
+import { Button } from '@/components/ui/button'
+
+export default function ForbiddenError() {
+ const navigate = useNavigate()
+ const { history } = useRouter()
+ return (
+
+
+
403
+
Access Forbidden
+
+ You don't have necessary permission
+ to view this resource.
+
+
+ history.go(-1)}>
+ Go Back
+
+ navigate({ to: '/' })}>Back to Home
+
+
+
+ )
+}
diff --git a/frontend/src/features/errors/general-error.tsx b/frontend/src/features/errors/general-error.tsx
new file mode 100644
index 0000000..aadbb65
--- /dev/null
+++ b/frontend/src/features/errors/general-error.tsx
@@ -0,0 +1,36 @@
+import { useNavigate, useRouter } from '@tanstack/react-router'
+import { cn } from '@/lib/utils'
+import { Button } from '@/components/ui/button'
+
+interface GeneralErrorProps extends React.HTMLAttributes {
+ minimal?: boolean
+}
+
+export default function GeneralError({
+ className,
+ minimal = false,
+}: GeneralErrorProps) {
+ const navigate = useNavigate()
+ const { history } = useRouter()
+ return (
+
+
+ {!minimal && (
+
500
+ )}
+
Oops! Something went wrong {`:')`}
+
+ We apologize for the inconvenience. Please try again later.
+
+ {!minimal && (
+
+ history.go(-1)}>
+ Go Back
+
+ navigate({ to: '/' })}>Back to Home
+
+ )}
+
+
+ )
+}
diff --git a/frontend/src/features/errors/maintenance-error.tsx b/frontend/src/features/errors/maintenance-error.tsx
new file mode 100644
index 0000000..9c1dbe1
--- /dev/null
+++ b/frontend/src/features/errors/maintenance-error.tsx
@@ -0,0 +1,19 @@
+import { Button } from '@/components/ui/button'
+
+export default function MaintenanceError() {
+ return (
+
+
+
503
+
Website is under maintenance!
+
+ The site is not available at the moment.
+ We'll be back online shortly.
+
+
+ Learn more
+
+
+
+ )
+}
diff --git a/frontend/src/features/errors/not-found-error.tsx b/frontend/src/features/errors/not-found-error.tsx
new file mode 100644
index 0000000..dc55a5d
--- /dev/null
+++ b/frontend/src/features/errors/not-found-error.tsx
@@ -0,0 +1,25 @@
+import { useNavigate, useRouter } from '@tanstack/react-router'
+import { Button } from '@/components/ui/button'
+
+export default function NotFoundError() {
+ const navigate = useNavigate()
+ const { history } = useRouter()
+ return (
+
+
+
404
+
Oops! Page Not Found!
+
+ It seems like the page you're looking for
+ does not exist or might have been removed.
+
+
+ history.go(-1)}>
+ Go Back
+
+ navigate({ to: '/' })}>Back to Home
+
+
+
+ )
+}
diff --git a/frontend/src/features/errors/unauthorized-error.tsx b/frontend/src/features/errors/unauthorized-error.tsx
new file mode 100644
index 0000000..c4fb6f1
--- /dev/null
+++ b/frontend/src/features/errors/unauthorized-error.tsx
@@ -0,0 +1,25 @@
+import { useNavigate, useRouter } from '@tanstack/react-router'
+import { Button } from '@/components/ui/button'
+
+export default function UnauthorisedError() {
+ const navigate = useNavigate()
+ const { history } = useRouter()
+ return (
+
+
+
401
+
Unauthorized Access
+
+ Please log in with the appropriate credentials to access this
+ resource.
+
+
+ history.go(-1)}>
+ Go Back
+
+ navigate({ to: '/' })}>Back to Home
+
+
+
+ )
+}
diff --git a/frontend/src/features/settings/account/account-form.tsx b/frontend/src/features/settings/account/account-form.tsx
new file mode 100644
index 0000000..29e6341
--- /dev/null
+++ b/frontend/src/features/settings/account/account-form.tsx
@@ -0,0 +1,217 @@
+import { z } from 'zod'
+import { format } from 'date-fns'
+import { useForm } from 'react-hook-form'
+import { CalendarIcon, CaretSortIcon, CheckIcon } from '@radix-ui/react-icons'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { cn } from '@/lib/utils'
+import { toast } from '@/hooks/use-toast'
+import { Button } from '@/components/ui/button'
+import { Calendar } from '@/components/ui/calendar'
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from '@/components/ui/command'
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@/components/ui/form'
+import { Input } from '@/components/ui/input'
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover'
+
+const languages = [
+ { label: 'English', value: 'en' },
+ { label: 'French', value: 'fr' },
+ { label: 'German', value: 'de' },
+ { label: 'Spanish', value: 'es' },
+ { label: 'Portuguese', value: 'pt' },
+ { label: 'Russian', value: 'ru' },
+ { label: 'Japanese', value: 'ja' },
+ { label: 'Korean', value: 'ko' },
+ { label: 'Chinese', value: 'zh' },
+] as const
+
+const accountFormSchema = z.object({
+ name: z
+ .string()
+ .min(2, {
+ message: 'Name must be at least 2 characters.',
+ })
+ .max(30, {
+ message: 'Name must not be longer than 30 characters.',
+ }),
+ dob: z.date({
+ required_error: 'A date of birth is required.',
+ }),
+ language: z.string({
+ required_error: 'Please select a language.',
+ }),
+})
+
+type AccountFormValues = z.infer
+
+// This can come from your database or API.
+const defaultValues: Partial = {
+ name: '',
+}
+
+export function AccountForm() {
+ const form = useForm({
+ resolver: zodResolver(accountFormSchema),
+ defaultValues,
+ })
+
+ function onSubmit(data: AccountFormValues) {
+ toast({
+ title: 'You submitted the following values:',
+ description: (
+
+ {JSON.stringify(data, null, 2)}
+
+ ),
+ })
+ }
+
+ return (
+