Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Frontend #90

Merged
merged 8 commits into from
Feb 24, 2025
8 changes: 7 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,30 @@
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^4.1.1",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@tailwindcss/vite": "^4.0.6",
"axios": "^1.7.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^8.5.2",
"lucide-react": "^0.475.0",
"next-themes": "^0.4.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"react-router-dom": "^7.1.5",
"sonner": "^2.0.1",
"tailwind-merge": "^3.0.1",
"tailwindcss": "^4.0.6",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.2"
},
"devDependencies": {
"@eslint/js": "^9.19.0",
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/api/axios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import axios from 'axios'

export const axiosInstance = axios.create({
baseURL: 'http://localhost:3000',
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
});
39 changes: 39 additions & 0 deletions frontend/src/components/password-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as React from 'react'
import { Eye, EyeOff } from 'lucide-react';
import { cn } from '@/lib/utils'
import { Button } from './ui/button'

type PasswordInputProps = Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'type'
>

const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
({ className, disabled, ...props }, ref) => {
const [showPassword, setShowPassword] = React.useState(false)
return (
<div className={cn('relative rounded-md', className)}>
<input
type={showPassword ? 'text' : 'password'}
className='flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50'
ref={ref}
disabled={disabled}
{...props}
/>
<Button
type='button'
size='icon'
variant='ghost'
disabled={disabled}
className='absolute right-1 top-1/2 h-6 w-6 -translate-y-1/2 rounded-md text-muted-foreground'
onClick={() => setShowPassword((prev) => !prev)}
>
{showPassword ? <Eye size={18} /> : <EyeOff size={18} />}
</Button>
</div>
)
}
)
PasswordInput.displayName = 'PasswordInput'

export { PasswordInput }
175 changes: 175 additions & 0 deletions frontend/src/components/ui/form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import * as React from 'react'
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from 'react-hook-form'
import * as LabelPrimitive from '@radix-ui/react-label'
import { Slot } from '@radix-ui/react-slot'
import { cn } from '@/lib/utils'
import { Label } from '@/components/ui/label'

const Form = FormProvider

type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}

const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)

const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}

const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()

const fieldState = getFieldState(fieldContext.name, formState)

if (!fieldContext) {
throw new Error('useFormField should be used within <FormField>')
}

const { id } = itemContext

return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}

type FormItemContextValue = {
id: string
}

const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)

const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()

return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn('space-y-2', className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = 'FormItem'

const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()

return (
<Label
ref={ref}
className={cn(error && 'text-destructive', className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = 'FormLabel'

const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()

return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = 'FormControl'

const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()

return (
<p
ref={ref}
id={formDescriptionId}
className={cn('text-[0.8rem] text-muted-foreground', className)}
{...props}
/>
)
})
FormDescription.displayName = 'FormDescription'

const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children

if (!body) {
return null
}

return (
<p
ref={ref}
id={formMessageId}
className={cn('text-[0.8rem] font-medium text-destructive', className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = 'FormMessage'

export {
useFormField,

Check warning on line 167 in frontend/src/components/ui/form.tsx

View workflow job for this annotation

GitHub Actions / lint

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}
27 changes: 27 additions & 0 deletions frontend/src/components/ui/sonner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"

const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()

return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground font-medium",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground font-medium",
},
}}
{...props}
/>
)
}

export { Toaster }
13 changes: 0 additions & 13 deletions frontend/src/components/user/auth/AuthLeftPanel.tsx

This file was deleted.

66 changes: 66 additions & 0 deletions frontend/src/components/user/auth/components/FormLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Separator } from "@/components/ui/separator"
import { UserAuthForm } from "./user-auth-form"
import Button from "@/components/ui/button"
import { Github } from "lucide-react"
import { useState } from "react"


export const FormLayout = () => {

const [authState, setAuthState] = useState<'login' | 'register'>('login');

return (
<div className='lg:p-8'>
<div className='mx-auto flex w-full flex-col justify-center space-y-2 sm:w-[350px]'>
<div className='flex flex-col space-y-2 text-left'>
<h1 className='text-2xl font-semibold tracking-tight'>{authState === 'login' ? "Welcome Back!" : "Create an Account"}</h1>
<p className='text-sm text-muted-foreground'>
{authState === 'login' ? "Please enter your email and password to log in." : "Please fill in the details to create your account."}
</p>
</div>
<UserAuthForm
authState={authState}
onStateChange={setAuthState}
/>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<Separator />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>

<div className="flex gap-2 ">
<Button variant="outline" className="w-full">
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
fill="#4285F4"
/>
<path
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
fill="#34A853"
/>
<path
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
fill="#FBBC05"
/>
<path
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
fill="#EA4335"
/>
</svg>
Google
</Button>
<Button variant="outline" className="w-full">
<Github className="mr-2 h-4 w-4" />
Github
</Button>
</div>
</div>
</div>
)
}
19 changes: 19 additions & 0 deletions frontend/src/components/user/auth/components/SideLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useTheme } from "../../common/theme-provider"
import LightLogo from '@/assets/Logo Light.svg'
import DarkLogo from '@/assets/Logo Dark.svg'

export const SideLayout = () => {
const { theme } = useTheme()
return (
<div className='relative hidden h-full flex-col bg-muted p-10 text-white dark:border-r lg:flex'>
<div className='absolute inset-0 bg-zinc-900' />
<div className='relative z-20 flex items-center text-lg font-medium'>
<img
src={theme == 'light' ? LightLogo : DarkLogo}
className='mr-2 h-20 w-20'
alt='Inker'
/>
</div>
</div>
)
}
Loading
Loading