├── app ├── api │ └── auth │ │ └── [...nextauth] │ │ └── route.ts ├── favicon.ico ├── fonts │ ├── GeistVF.woff │ └── GeistMonoVF.woff ├── (auth) │ ├── login │ │ └── page.tsx │ ├── register │ │ └── page.tsx │ └── layout.tsx ├── (protected) │ ├── upload │ │ ├── new │ │ │ ├── page.tsx │ │ │ └── components │ │ │ │ └── UploadNew.tsx │ │ ├── page.tsx │ │ └── components │ │ │ ├── Upload.tsx │ │ │ └── columns.tsx │ ├── profile │ │ └── page.tsx │ ├── layout.tsx │ └── receive │ │ ├── page.tsx │ │ └── components │ │ ├── Receive.tsx │ │ ├── columns.tsx │ │ └── CellAction.tsx ├── page.tsx ├── layout.tsx └── globals.css ├── .eslintrc.json ├── next.config.mjs ├── postcss.config.mjs ├── hooks └── use-current-user-server.ts ├── lib └── utils.ts ├── components ├── auth │ ├── LogoutButton.tsx │ ├── AuthCard.tsx │ ├── LoginForm.tsx │ └── RegisterForm.tsx ├── utils │ ├── ActionUtils.ts │ ├── ErrorUtils.ts │ ├── GlobalApiCall.ts │ └── handleApiError.ts ├── schema │ ├── profileType.ts │ └── authType.ts ├── ui │ ├── label.tsx │ ├── separator.tsx │ ├── popover.tsx │ ├── input.tsx │ ├── card.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── table.tsx │ ├── data-table.tsx │ ├── dialog.tsx │ ├── form.tsx │ └── select.tsx ├── profile │ ├── Profile.tsx │ ├── UserProfile.tsx │ └── PasswordChange.tsx └── header │ └── Header.tsx ├── components.json ├── nextauth.d.ts ├── .gitignore ├── routes.ts ├── tsconfig.json ├── middleware.ts ├── LICENSE ├── package.json ├── action ├── fileHandler.ts ├── authHandler.ts └── profileHandler.ts ├── tailwind.config.ts ├── auth.ts └── README.md /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | export { GET, POST } from '@/auth'; -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aarambh-darshan/file-share-frontend/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aarambh-darshan/file-share-frontend/HEAD/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aarambh-darshan/file-share-frontend/HEAD/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /hooks/use-current-user-server.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@/auth"; 2 | 3 | export const useCurrentUserServer = async () => { 4 | const session = await auth(); 5 | 6 | return session?.user; 7 | } -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /app/(auth)/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { LoginForm } from "@/components/auth/LoginForm" 2 | 3 | const LoginPage = () => { 4 | return ( 5 | 6 | ) 7 | } 8 | 9 | export default LoginPage; -------------------------------------------------------------------------------- /app/(auth)/register/page.tsx: -------------------------------------------------------------------------------- 1 | import { RegisterForm } from "@/components/auth/RegisterForm" 2 | 3 | const RegisterPage = () => { 4 | return ( 5 | 6 | ) 7 | } 8 | 9 | export default RegisterPage; -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | const AuthLayout = ({ 2 | children, 3 | }: Readonly<{ 4 | children: React.ReactNode; 5 | }>) => { 6 | return ( 7 |
8 | {children} 9 |
10 | ) 11 | } 12 | 13 | export default AuthLayout; -------------------------------------------------------------------------------- /app/(protected)/upload/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@/auth"; 2 | import { UploadNew } from "./components/UploadNew"; 3 | 4 | const UploadNewPage = async () => { 5 | const session = await auth(); 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } 12 | 13 | export default UploadNewPage; -------------------------------------------------------------------------------- /app/(protected)/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { getMe } from "@/action/profileHandler"; 2 | import { Profile } from "@/components/profile/Profile"; 3 | 4 | const ProfilePage = async () => { 5 | const userData = await getMe(); 6 | console.log(userData,'userData') 7 | return ( 8 |
9 | 10 |
11 | ) 12 | } 13 | 14 | export default ProfilePage; -------------------------------------------------------------------------------- /components/auth/LogoutButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Logout } from "@/action/authHandler"; 4 | 5 | interface LogoutButtonProps { 6 | children?: React.ReactNode; 7 | } 8 | 9 | export const LogoutButton = ({ children }: LogoutButtonProps) => { 10 | const onClick = () => { 11 | Logout(); 12 | } 13 | 14 | return ( 15 | 16 | {children} 17 | 18 | ) 19 | } -------------------------------------------------------------------------------- /app/(protected)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from "@/components/header/Header"; 2 | 3 | export const dynamic = 'force-dynamic'; 4 | export const revalidate = 0; 5 | 6 | const ProtectedLayout = ({ 7 | children, 8 | }: Readonly<{ 9 | children: React.ReactNode; 10 | }>) => { 11 | return ( 12 |
13 |
14 | {children} 15 |
16 | ) 17 | } 18 | 19 | export default ProtectedLayout; -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /components/utils/ActionUtils.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { handleApiError } from "./handleApiError"; 3 | 4 | export const withActionHandler = async (action: () => Promise) => { 5 | try { 6 | return await action(); 7 | } catch (error) { 8 | const { status, message, location } = handleApiError(error); 9 | 10 | if (location) { 11 | redirect(location); 12 | } 13 | 14 | return { 15 | status, 16 | message, 17 | }; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /nextauth.d.ts: -------------------------------------------------------------------------------- 1 | import { DefaultSession } from "next-auth"; 2 | 3 | // Extend the DefaultSession["user"] type with accessToken 4 | export type ExtendedUser = DefaultSession["user"] & { 5 | accessToken: string; 6 | } 7 | 8 | declare module "next-auth" { 9 | // Extend the User interface with accessToken 10 | interface User { 11 | token: string; 12 | } 13 | 14 | // Extend the Session interface with the extended User 15 | interface Session { 16 | user: ExtendedUser; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An array of routes that are used for authentication 3 | * These routes will redirect logged in users to /settings 4 | * @type { string[] } 5 | */ 6 | export const authRoutes = ["/login", "/register"]; 7 | 8 | /** 9 | * The prefix for API authentication routes 10 | * Routes that start with this prefix are used for API authentication purposes 11 | * @type { string } 12 | */ 13 | export const apiAuthPrefix = "/api/auth"; 14 | 15 | /** 16 | * The defult redirect path after logging in 17 | * @type { string } 18 | */ 19 | export const DEFAULT_LOGIN_REDIRECT = "/upload"; -------------------------------------------------------------------------------- /app/(protected)/upload/page.tsx: -------------------------------------------------------------------------------- 1 | import { send_file_list } from "@/action/fileHandler"; 2 | import { Upload } from "./components/Upload"; 3 | 4 | const UploadPage = async ({ 5 | searchParams, 6 | }: { 7 | searchParams: { 8 | [key: string]: string | string[] | undefined; 9 | }; 10 | }) => { 11 | const fileData = await send_file_list({ 12 | page: Number(searchParams.page) || 1, 13 | limit: Number(searchParams.limit) || 10, 14 | }); 15 | 16 | return ( 17 |
18 | 19 |
20 | ); 21 | }; 22 | 23 | export default UploadPage; 24 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { useState } from "react"; 5 | 6 | export default function Home() { 7 | const [isLoading, setIsLoading] = useState(false); 8 | 9 | const handleClick = () => { 10 | setIsLoading(true); 11 | // Simulate a network request 12 | setTimeout(() => { 13 | setIsLoading(false); 14 | }, 2000); 15 | }; 16 | 17 | return ( 18 |
19 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /components/utils/ErrorUtils.ts: -------------------------------------------------------------------------------- 1 | export class ApiError extends Error { 2 | status: number; 3 | 4 | constructor(status: number, message: string) { 5 | super(message); 6 | this.status = status; 7 | this.name = "ApiError"; 8 | } 9 | } 10 | 11 | export class RedirectError extends Error { 12 | status: number; 13 | location: string; 14 | 15 | constructor(status: number, location: string, message?: string) { 16 | super(message || `Redirect to ${location}`); 17 | this.status = status; 18 | this.location = location; 19 | this.name = "RedirectError"; // Set the name property for better error identification 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /app/(protected)/receive/page.tsx: -------------------------------------------------------------------------------- 1 | import { receive_file_list } from "@/action/fileHandler"; 2 | import { Receive } from "./components/Receive"; 3 | import { auth } from "@/auth"; 4 | 5 | 6 | const ReceivePage = async ({ 7 | searchParams 8 | }: { 9 | searchParams: { [key: string]: string | string[] | undefined } 10 | }) => { 11 | const fileData = await receive_file_list({ 12 | page: Number(searchParams.page) || 1, 13 | limit: Number(searchParams.limit) || 10, 14 | }); 15 | const session = await auth(); 16 | return ( 17 |
18 | 19 |
20 | ) 21 | } 22 | 23 | export default ReceivePage; -------------------------------------------------------------------------------- /components/schema/profileType.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const nameUpdateSchema = z.object({ 4 | name: z.string().min(1, { 5 | message: 'Name is required' 6 | }), 7 | email: z.string() 8 | }) 9 | 10 | export const passwordChangeSchema = z.object({ 11 | old_password: z.string() 12 | .min(6, { message: "Password should be at least 6 characters long" }), 13 | new_password: z.string() 14 | .min(6, { message: "Password should be at least 6 characters long" }), 15 | 16 | new_password_confirm: z.string() 17 | .min(1, { message: "Password confirmation is required" }), 18 | }).refine((data) => data.new_password === data.new_password_confirm, { 19 | message: "Passwords do not match", 20 | path: ["new_password_confirm"] 21 | }); -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /components/profile/Profile.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Separator } from "@radix-ui/react-separator" 4 | import { Card, CardContent, CardHeader, CardTitle } from "../ui/card" 5 | import { Userprofile } from "./UserProfile"; 6 | import { PasswordChange } from "./PasswordChange"; 7 | 8 | export interface UserDataProps { 9 | id: string; 10 | name: string; 11 | email: string; 12 | public_key: string | null; 13 | } 14 | 15 | 16 | export const Profile = ({ userData }: { userData: UserDataProps }) => { 17 | return ( 18 | 19 | 20 | Profile Management 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | } -------------------------------------------------------------------------------- /components/schema/authType.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | 4 | export const loginSchema = z.object({ 5 | email: z.string() 6 | .min(1, { message: "Email is required" }) 7 | .email({ message: "Invalid email address" }), 8 | 9 | password: z.string() 10 | .min(6, { message: "Password should be at least 6 characters long" }), 11 | }) 12 | 13 | 14 | export const registerSchema = z.object({ 15 | email: z.string() 16 | .min(1, { message: "Email is required" }) 17 | .email({ message: "Invalid email address" }), 18 | 19 | name: z.string() 20 | .min(1, { message: "Name is required" }), 21 | 22 | password: z.string() 23 | .min(6, { message: "Password should be at least 6 characters long" }), 24 | 25 | passwordConfirm: z.string() 26 | .min(1, { message: "Password confirmation is required" }), 27 | }).refine((data) => data.password === data.passwordConfirm, { 28 | message: "Passwords do not match", 29 | path: ["passwordConfirm"] 30 | }); 31 | -------------------------------------------------------------------------------- /app/(protected)/receive/components/Receive.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" 4 | import { Separator } from "@/components/ui/separator" 5 | import { useReceiveColumns, ReceiveColumnsType } from "./columns" 6 | import { DataTable } from "@/components/ui/data-table"; 7 | 8 | interface ReceiveProps { 9 | data: ReceiveColumnsType[], 10 | total: number, 11 | token: string | null 12 | } 13 | 14 | export const Receive = ({ data, total, token }: ReceiveProps) => { 15 | const columes = useReceiveColumns({ token: token }); 16 | return ( 17 | 18 | 19 | 20 | Receive Files 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | ) 33 | } -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@/auth"; 2 | import { 3 | apiAuthPrefix, 4 | authRoutes, 5 | DEFAULT_LOGIN_REDIRECT, 6 | } from "./routes"; 7 | import { NextResponse } from "next/server"; 8 | 9 | export default auth((req) => { 10 | const { nextUrl } = req; 11 | const isLoggedIn = req.auth?.user.accessToken ? true : false; 12 | 13 | const isApiAuthRoute = nextUrl.pathname.startsWith(apiAuthPrefix); 14 | const isAuthRoute = authRoutes.includes(nextUrl.pathname); 15 | 16 | if(nextUrl.pathname === '/') { 17 | return Response.redirect(new URL('/login', nextUrl)) 18 | } 19 | 20 | if (isApiAuthRoute) { 21 | return NextResponse.next(); 22 | } 23 | 24 | if (isAuthRoute) { 25 | if (isLoggedIn) { 26 | return Response.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl)); 27 | } 28 | return NextResponse.next(); 29 | } 30 | 31 | if (!isLoggedIn) { 32 | return Response.redirect( 33 | new URL('/login', nextUrl) 34 | ); 35 | } 36 | 37 | return NextResponse.next(); 38 | }); 39 | 40 | export const config = { 41 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], 42 | }; 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Aarambh Dev Hub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /components/auth/AuthCard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { Button } from "../ui/button"; 5 | import { Card, CardContent, CardFooter, CardHeader } from "../ui/card"; 6 | import { cn } from "@/lib/utils"; 7 | 8 | interface AuthCardProps { 9 | children: React.ReactNode; 10 | headerLabel: string; 11 | backButtonHref: string; 12 | backButtonLabel: string; 13 | className?: string; 14 | } 15 | 16 | export const AuthCard = ({ 17 | backButtonHref, 18 | backButtonLabel, 19 | children, 20 | headerLabel, 21 | className 22 | }: AuthCardProps) => { 23 | return ( 24 | 25 | 26 |
27 |

🔐SecureShare

28 |

{headerLabel}

29 |
30 |
31 | {children} 32 | 33 | 36 | 37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /components/utils/GlobalApiCall.ts: -------------------------------------------------------------------------------- 1 | import { useCurrentUserServer } from "@/hooks/use-current-user-server"; 2 | import { RedirectError } from "./ErrorUtils"; 3 | 4 | interface GlobalAPICallProps { 5 | url: string; 6 | options?: RequestInit; 7 | } 8 | 9 | export const GlobalApiCall = async ({ 10 | url, 11 | options = {}, 12 | }: GlobalAPICallProps) => { 13 | try { 14 | const session = await useCurrentUserServer(); 15 | 16 | const token = session?.accessToken ?? null; 17 | 18 | const response = await fetch(url, { 19 | ...options, 20 | credentials: "include", 21 | headers: { 22 | "Content-Type": "application/json", 23 | ...(token ? { Authorization: `Bearer ${token}` } : {}), 24 | ...options.headers, 25 | }, 26 | }); 27 | 28 | if (response.status === 401) { 29 | throw new RedirectError(302, "/logout", "session expired"); 30 | } 31 | 32 | if (!response.ok) { 33 | const errorText = await response.text(); 34 | throw new Error( 35 | `HTTP error! status: ${response.status}, message: ${errorText}` 36 | ); 37 | } 38 | 39 | return await response.json(); 40 | } catch (error) { 41 | console.error("fetch Error:", error); 42 | throw error; 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /components/header/Header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { LogoutButton } from "../auth/LogoutButton"; 5 | import { usePathname } from "next/navigation"; 6 | import { cn } from "@/lib/utils"; 7 | 8 | export const Header = () => { 9 | const pathName = usePathname(); 10 | 11 | return ( 12 |
13 |
14 | SecureShare 15 |
16 | 36 | 37 |
38 | Logout 39 |
40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /app/(protected)/upload/components/Upload.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 4 | import { UploadColumns, UploadColumnsType } from "./columns" 5 | import { Separator } from "@/components/ui/separator"; 6 | import { Button } from "@/components/ui/button"; 7 | import Link from "next/link"; 8 | import { DataTable } from "@/components/ui/data-table"; 9 | 10 | interface UploadProps { 11 | data: UploadColumnsType[], 12 | total: number, 13 | } 14 | 15 | export const Upload = ({ data, total }: UploadProps) => { 16 | return ( 17 | 18 | 19 | Upload Files 20 | 21 | 22 | 23 |
24 | 27 |
28 | 33 |
34 |
35 | ) 36 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | import { Toaster } from 'react-hot-toast'; 5 | import { auth } from "@/auth"; 6 | import { SessionProvider } from "next-auth/react"; 7 | import { ThemeProvider } from "next-themes"; 8 | 9 | const geistSans = localFont({ 10 | src: "./fonts/GeistVF.woff", 11 | variable: "--font-geist-sans", 12 | weight: "100 900", 13 | }); 14 | const geistMono = localFont({ 15 | src: "./fonts/GeistMonoVF.woff", 16 | variable: "--font-geist-mono", 17 | weight: "100 900", 18 | }); 19 | 20 | export const metadata: Metadata = { 21 | title: "Create Next App", 22 | description: "Generated by create next app", 23 | }; 24 | 25 | export default async function RootLayout({ 26 | children, 27 | }: Readonly<{ 28 | children: React.ReactNode; 29 | }>) { 30 | const session = await auth(); 31 | return ( 32 | 33 | 34 | 37 | 42 | 43 | {children} 44 | 45 | 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverAnchor = PopoverPrimitive.Anchor 13 | 14 | const PopoverContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 18 | 19 | 29 | 30 | )) 31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 32 | 33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secureshare_frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@hookform/resolvers": "^3.9.0", 13 | "@radix-ui/react-dialog": "^1.1.1", 14 | "@radix-ui/react-icons": "^1.3.0", 15 | "@radix-ui/react-label": "^2.1.0", 16 | "@radix-ui/react-popover": "^1.1.1", 17 | "@radix-ui/react-select": "^2.1.1", 18 | "@radix-ui/react-separator": "^1.1.0", 19 | "@radix-ui/react-slot": "^1.1.0", 20 | "@tanstack/react-table": "^8.20.5", 21 | "class-variance-authority": "^0.7.0", 22 | "clsx": "^2.1.1", 23 | "date-fns": "^3.6.0", 24 | "lucide-react": "^0.446.0", 25 | "next": "14.2.13", 26 | "next-auth": "^5.0.0-beta.22", 27 | "next-themes": "^0.3.0", 28 | "react": "^18", 29 | "react-day-picker": "^8.10.1", 30 | "react-dom": "^18", 31 | "react-hook-form": "^7.53.0", 32 | "react-hot-toast": "^2.4.1", 33 | "tailwind-merge": "^2.5.2", 34 | "tailwindcss-animate": "^1.0.7", 35 | "zod": "^3.23.8" 36 | }, 37 | "devDependencies": { 38 | "@types/node": "^20", 39 | "@types/react": "^18", 40 | "@types/react-dom": "^18", 41 | "eslint": "^8", 42 | "eslint-config-next": "14.2.13", 43 | "postcss": "^8", 44 | "tailwindcss": "^3.4.1", 45 | "typescript": "^5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /action/fileHandler.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { withActionHandler } from "@/components/utils/ActionUtils"; 4 | import { GlobalApiCall } from "@/components/utils/GlobalApiCall"; 5 | 6 | const API_BASE_URL = process.env.API_BASE_URL; 7 | 8 | export const send_file_list = async ({ 9 | page, 10 | limit 11 | }: { 12 | page: number, 13 | limit: number 14 | }) => { 15 | 16 | return withActionHandler(async () => { 17 | const response = await GlobalApiCall({ 18 | url: `${API_BASE_URL}/list/send?page=${page}&limit=${limit}`, 19 | options: { 20 | method: 'get', 21 | cache: 'no-store' 22 | } 23 | }) 24 | 25 | return response; 26 | }) 27 | } 28 | export const receive_file_list = async ({ 29 | page, 30 | limit 31 | }: { 32 | page: number, 33 | limit: number 34 | }) => { 35 | 36 | return withActionHandler(async () => { 37 | const response = await GlobalApiCall({ 38 | url: `${API_BASE_URL}/list/receive?page=${page}&limit=${limit}`, 39 | options: { 40 | method: 'get', 41 | cache: 'no-store' 42 | } 43 | }) 44 | 45 | return response; 46 | }) 47 | } 48 | 49 | export const searchEmail = async (query: string) => { 50 | return withActionHandler(async () => { 51 | const response = await GlobalApiCall({ 52 | url: `${API_BASE_URL}/users/search-emails?query=${query}`, 53 | options: { 54 | method: "get", 55 | cache: 'no-store' 56 | }, 57 | }); 58 | 59 | return response; 60 | }) 61 | } -------------------------------------------------------------------------------- /action/authHandler.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { AuthError } from "next-auth"; 4 | import { signIn, signOut } from "@/auth"; 5 | import { DEFAULT_LOGIN_REDIRECT } from "@/routes"; 6 | import { GlobalApiCall } from "@/components/utils/GlobalApiCall"; 7 | 8 | const API_BASE_URL = process.env.API_BASE_URL; 9 | 10 | export async function LoginApi({ 11 | email, 12 | password, 13 | }: { 14 | email: string; 15 | password: string; 16 | }) { 17 | try { 18 | await signIn("credentials", { 19 | email, 20 | password, 21 | redirectTo: DEFAULT_LOGIN_REDIRECT, 22 | }); 23 | } catch (error) { 24 | if (error instanceof AuthError) { 25 | switch (error.type) { 26 | case "CredentialsSignin": 27 | return { error: "Invalid Email or password!" }; 28 | default: 29 | return { error: "Something went wrong!" }; 30 | } 31 | } 32 | 33 | throw error; 34 | } 35 | } 36 | 37 | export async function RegisterApi({ 38 | name, 39 | email, 40 | password, 41 | passwordConfirm, 42 | }: { 43 | name: string; 44 | email: string; 45 | password: string; 46 | passwordConfirm: string; 47 | }) { 48 | 49 | try { 50 | const response = await GlobalApiCall({ 51 | url: `${API_BASE_URL}/auth/register`, 52 | options: { 53 | method: "post", 54 | body: JSON.stringify({ name, email, password, passwordConfirm }), 55 | cache: 'no-store' 56 | }, 57 | }); 58 | return response; 59 | } catch(error) { 60 | throw error; 61 | } 62 | 63 | } 64 | 65 | 66 | export async function Logout() { 67 | await signOut({ redirectTo: '/login' }); 68 | } -------------------------------------------------------------------------------- /action/profileHandler.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { withActionHandler } from "@/components/utils/ActionUtils"; 4 | import { GlobalApiCall } from "@/components/utils/GlobalApiCall" 5 | 6 | const API_BASE_URL = process.env.API_BASE_URL; 7 | 8 | export const getMe = async () => { 9 | return withActionHandler(async() => { 10 | const response = await GlobalApiCall({ 11 | url: `${API_BASE_URL}/users/me`, 12 | options: { 13 | method: 'get', 14 | cache: 'no-store' 15 | } 16 | }) 17 | 18 | return response; 19 | }) 20 | } 21 | 22 | export const updateUserName = async ({ name }: { name: string; }) => { 23 | return withActionHandler(async () => { 24 | const response = await GlobalApiCall({ 25 | url: `${API_BASE_URL}/users/name`, 26 | options: { 27 | method: 'put', 28 | body: JSON.stringify({name}), 29 | cache: 'no-store' 30 | } 31 | }) 32 | 33 | return response; 34 | }) 35 | } 36 | 37 | export const updateUserPassword = async ({ old_password, new_password, new_password_confirm }: { old_password: string; new_password: string; new_password_confirm: string; }) => { 38 | return withActionHandler(async () => { 39 | const response = await GlobalApiCall({ 40 | url: `${API_BASE_URL}/users/password`, 41 | options: { 42 | method: 'put', 43 | body: JSON.stringify({old_password, new_password, new_password_confirm}), 44 | cache: 'no-store' 45 | } 46 | }) 47 | 48 | return response; 49 | }) 50 | } -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | import { Eye, EyeOff } from "lucide-react"; 5 | 6 | export interface InputProps extends React.InputHTMLAttributes { 7 | enablePasswordToggle?: boolean; 8 | } 9 | 10 | const Input = React.forwardRef( 11 | ({ className, type, enablePasswordToggle, ...props }, ref) => { 12 | const [showPassword, setShowPassword] = React.useState(false); 13 | 14 | const currentType = enablePasswordToggle ? ( showPassword ? 'text': 'password' ): type; 15 | 16 | const handleTooglePassword = () => { 17 | setShowPassword((prev) => !prev); 18 | } 19 | 20 | return ( 21 |
22 | 31 | { 32 | enablePasswordToggle && ( 33 | 40 | ) 41 | } 42 |
43 | ) 44 | } 45 | ) 46 | Input.displayName = "Input" 47 | 48 | export { Input } 49 | -------------------------------------------------------------------------------- /app/(protected)/upload/components/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColumnDef } from "@tanstack/react-table" 4 | 5 | export type UploadColumnsType = { 6 | file_id: string; 7 | file_name: string; 8 | recipient_email: string; 9 | expiration_date: string; 10 | created_at: string; 11 | }; 12 | 13 | export const UploadColumns: ColumnDef[] = [ 14 | { 15 | accessorKey: "file_id", 16 | header: 'ID', 17 | }, 18 | { 19 | accessorKey: 'file_name', 20 | header: 'File Name', 21 | }, 22 | { 23 | accessorKey: 'recipient_email', 24 | header: 'Recipient Email', 25 | }, 26 | { 27 | accessorKey: 'expiration_date', 28 | header: 'Expiration Date', 29 | cell: ({ row }) => { 30 | const date = new Date(row.original.expiration_date); 31 | return date.toLocaleDateString('en-US', { 32 | weekday: 'short', // Short name for the day (e.g., "Fri") 33 | day: '2-digit', // Two-digit day (e.g., "27") 34 | month: 'short', // Short name for the month (e.g., "Sep") 35 | year: 'numeric', // Full numeric year (e.g., "2024") 36 | }); 37 | }, 38 | }, 39 | { 40 | accessorKey: 'created_at', 41 | header: 'Created At', 42 | cell: ({ row }) => { 43 | const date = new Date(row.original.created_at); 44 | return date.toLocaleDateString('en-US', { 45 | weekday: 'short', // Short name for the day (e.g., "Fri") 46 | day: '2-digit', // Two-digit day (e.g., "27") 47 | month: 'short', // Short name for the month (e.g., "Sep") 48 | year: 'numeric', // Full numeric year (e.g., "2024") 49 | }); 50 | }, 51 | } 52 | ] 53 | 54 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | background: 'hsl(var(--background))', 14 | foreground: 'hsl(var(--foreground))', 15 | card: { 16 | DEFAULT: 'hsl(var(--card))', 17 | foreground: 'hsl(var(--card-foreground))' 18 | }, 19 | popover: { 20 | DEFAULT: 'hsl(var(--popover))', 21 | foreground: 'hsl(var(--popover-foreground))' 22 | }, 23 | primary: { 24 | DEFAULT: 'hsl(var(--primary))', 25 | foreground: 'hsl(var(--primary-foreground))' 26 | }, 27 | secondary: { 28 | DEFAULT: 'hsl(var(--secondary))', 29 | foreground: 'hsl(var(--secondary-foreground))' 30 | }, 31 | muted: { 32 | DEFAULT: 'hsl(var(--muted))', 33 | foreground: 'hsl(var(--muted-foreground))' 34 | }, 35 | accent: { 36 | DEFAULT: 'hsl(var(--accent))', 37 | foreground: 'hsl(var(--accent-foreground))' 38 | }, 39 | destructive: { 40 | DEFAULT: 'hsl(var(--destructive))', 41 | foreground: 'hsl(var(--destructive-foreground))' 42 | }, 43 | border: 'hsl(var(--border))', 44 | input: 'hsl(var(--input))', 45 | ring: 'hsl(var(--ring))', 46 | chart: { 47 | '1': 'hsl(var(--chart-1))', 48 | '2': 'hsl(var(--chart-2))', 49 | '3': 'hsl(var(--chart-3))', 50 | '4': 'hsl(var(--chart-4))', 51 | '5': 'hsl(var(--chart-5))' 52 | } 53 | }, 54 | borderRadius: { 55 | lg: 'var(--radius)', 56 | md: 'calc(var(--radius) - 2px)', 57 | sm: 'calc(var(--radius) - 4px)' 58 | } 59 | } 60 | }, 61 | plugins: [require("tailwindcss-animate")], 62 | }; 63 | export default config; 64 | -------------------------------------------------------------------------------- /auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth'; 2 | import CredentialsProvider from 'next-auth/providers/credentials'; 3 | 4 | export const { 5 | handlers: { GET, POST }, 6 | auth, 7 | signIn, 8 | signOut, 9 | } = NextAuth({ 10 | pages: { 11 | signIn: "/login", 12 | }, 13 | trustHost: true, 14 | secret: process.env.NEXTAUTH_SECRET, 15 | providers: [ 16 | CredentialsProvider({ 17 | name: 'credentials', 18 | credentials: { 19 | email: { label: 'email', type: "email" }, 20 | password: { label: 'password', type: "password" }, 21 | }, 22 | async authorize(credentials) { 23 | const res = await fetch(`${process.env.API_BASE_URL}/auth/login`, { 24 | method: 'post', 25 | headers: { 26 | "Content-Type": "application/json", 27 | }, 28 | body: JSON.stringify({ 29 | email: credentials?.email, 30 | password: credentials?.password, 31 | }) 32 | }); 33 | 34 | const data = await res.json(); 35 | 36 | if (res.ok && data.token) { 37 | return { token: data.token} 38 | } else { 39 | return null; 40 | } 41 | }, 42 | }) 43 | ], 44 | session:{ 45 | strategy: 'jwt' 46 | }, 47 | callbacks: { 48 | async jwt({ token, user }) { 49 | if (user) { 50 | token.accessToken = user.token; 51 | } 52 | return token; 53 | }, 54 | async session({ session, token }) { 55 | session.user = { 56 | ...session.user, 57 | accessToken: token.accessToken as string, 58 | }; 59 | return session; 60 | } 61 | } 62 | }); -------------------------------------------------------------------------------- /components/utils/handleApiError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, RedirectError } from "./ErrorUtils"; 2 | 3 | export const handleApiError = ( 4 | error: unknown 5 | ): { status: number; message: string; location?: string } => { 6 | if (error instanceof ApiError) { 7 | // Custom application error 8 | return { status: error.status, message: error.message }; 9 | } else if (error instanceof RedirectError) { 10 | // Redirect error 11 | return { 12 | status: error.status, 13 | message: error.message, 14 | location: error.location, 15 | }; 16 | } else if (error instanceof Error) { 17 | // Check for specific error messages 18 | if (error.message.includes("Json deserialize error")) { 19 | return { 20 | status: 400, 21 | message: "Invalid data format received from the server.", 22 | }; 23 | } else if (error.message.includes("Old password is incorrect")) { 24 | return { status: 400, message: "Old password is incorrect" }; 25 | } else if (error.message.includes("HTTP error! status:")) { 26 | // Extract status and message from the error string 27 | const statusMatch = error.message.match(/status: (\d+)/); 28 | const messageMatch = error.message.match(/message: (.+)/); 29 | const status = statusMatch ? parseInt(statusMatch[1], 10) : 400; 30 | let message = messageMatch ? messageMatch[1] : "An error occurred."; 31 | 32 | // Attempt to parse the JSON string 33 | try { 34 | const jsonMessage = JSON.parse(message); 35 | message = jsonMessage.message || "An error occurred."; 36 | } catch (e) { 37 | // If parsing fails, use the original message 38 | message = message; 39 | console.log(e); 40 | } 41 | 42 | return { status, message }; 43 | } else { 44 | return { status: 400, message: error.message }; 45 | } 46 | } else { 47 | // Fallback for unexpected error types 48 | return { status: 500, message: "An unexpected error occurred." }; 49 | } 50 | }; -------------------------------------------------------------------------------- /app/(protected)/receive/components/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ColumnDef } from "@tanstack/react-table" 4 | import { CellAction } from "./CellAction"; 5 | 6 | export type ReceiveColumnsType = { 7 | file_id: string; 8 | file_name: string; 9 | recipient_email: string; 10 | expiration_date: string; 11 | created_at: string; 12 | }; 13 | 14 | export function useReceiveColumns({ token }: { token: string | null}): ColumnDef[] { 15 | 16 | return [ 17 | { 18 | accessorKey: "file_id", 19 | header: 'ID', 20 | }, 21 | { 22 | accessorKey: 'file_name', 23 | header: 'File Name', 24 | }, 25 | { 26 | accessorKey: 'sender_email', 27 | header: 'Sender Email', 28 | }, 29 | { 30 | accessorKey: 'expiration_date', 31 | header: 'Expiration Date', 32 | cell: ({ row }) => { 33 | const date = new Date(row.original.expiration_date); 34 | return date.toLocaleDateString('en-US', { 35 | weekday: 'short', // Short name for the day (e.g., "Fri") 36 | day: '2-digit', // Two-digit day (e.g., "27") 37 | month: 'short', // Short name for the month (e.g., "Sep") 38 | year: 'numeric', // Full numeric year (e.g., "2024") 39 | }); 40 | }, 41 | }, 42 | { 43 | accessorKey: 'created_at', 44 | header: 'Created At', 45 | cell: ({ row }) => { 46 | const date = new Date(row.original.created_at); 47 | return date.toLocaleDateString('en-US', { 48 | weekday: 'short', // Short name for the day (e.g., "Fri") 49 | day: '2-digit', // Two-digit day (e.g., "27") 50 | month: 'short', // Short name for the month (e.g., "Sep") 51 | year: 'numeric', // Full numeric year (e.g., "2024") 52 | }); 53 | }, 54 | }, 55 | { 56 | accessorKey: 'actions', 57 | header: "Actions", 58 | cell: ({ row }) => 59 | } 60 | ]; 61 | } 62 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |

53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |

61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: Arial, Helvetica, sans-serif; 21 | } 22 | 23 | @layer utilities { 24 | .text-balance { 25 | text-wrap: balance; 26 | } 27 | } 28 | 29 | @layer base { 30 | :root { 31 | --background: 0 0% 100%; 32 | --foreground: 240 10% 3.9%; 33 | --card: 0 0% 100%; 34 | --card-foreground: 240 10% 3.9%; 35 | --popover: 0 0% 100%; 36 | --popover-foreground: 240 10% 3.9%; 37 | --primary: 240 5.9% 10%; 38 | --primary-foreground: 0 0% 98%; 39 | --secondary: 240 4.8% 95.9%; 40 | --secondary-foreground: 240 5.9% 10%; 41 | --muted: 240 4.8% 95.9%; 42 | --muted-foreground: 240 3.8% 46.1%; 43 | --accent: 240 4.8% 95.9%; 44 | --accent-foreground: 240 5.9% 10%; 45 | --destructive: 0 84.2% 60.2%; 46 | --destructive-foreground: 0 0% 98%; 47 | --border: 240 5.9% 90%; 48 | --input: 240 5.9% 90%; 49 | --ring: 240 10% 3.9%; 50 | --chart-1: 12 76% 61%; 51 | --chart-2: 173 58% 39%; 52 | --chart-3: 197 37% 24%; 53 | --chart-4: 43 74% 66%; 54 | --chart-5: 27 87% 67%; 55 | --radius: 0.5rem; 56 | } 57 | .dark { 58 | --background: 240 10% 3.9%; 59 | --foreground: 0 0% 98%; 60 | --card: 240 10% 3.9%; 61 | --card-foreground: 0 0% 98%; 62 | --popover: 240 10% 3.9%; 63 | --popover-foreground: 0 0% 98%; 64 | --primary: 0 0% 98%; 65 | --primary-foreground: 240 5.9% 10%; 66 | --secondary: 240 3.7% 15.9%; 67 | --secondary-foreground: 0 0% 98%; 68 | --muted: 240 3.7% 15.9%; 69 | --muted-foreground: 240 5% 64.9%; 70 | --accent: 240 3.7% 15.9%; 71 | --accent-foreground: 0 0% 98%; 72 | --destructive: 0 62.8% 30.6%; 73 | --destructive-foreground: 0 0% 98%; 74 | --border: 240 3.7% 15.9%; 75 | --input: 240 3.7% 15.9%; 76 | --ring: 240 4.9% 83.9%; 77 | --chart-1: 220 70% 50%; 78 | --chart-2: 160 60% 45%; 79 | --chart-3: 30 80% 55%; 80 | --chart-4: 280 65% 60%; 81 | --chart-5: 340 75% 55%; 82 | } 83 | } 84 | 85 | @layer base { 86 | * { 87 | @apply border-border; 88 | } 89 | body { 90 | @apply bg-background text-foreground; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | import { cn } from "@/lib/utils"; 5 | import { Loader2 } from "lucide-react"; // Assuming you are using this for the loader icon 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | isLoading?: boolean; // Add isLoading prop 42 | } 43 | 44 | const Button = React.forwardRef( 45 | ( 46 | { 47 | className, 48 | variant, 49 | size, 50 | asChild = false, 51 | isLoading = false, 52 | children, 53 | ...props 54 | }, 55 | ref 56 | ) => { 57 | if (asChild) { 58 | // When asChild is true, use Slot and pass only the children 59 | return ( 60 | 65 | {children} 66 | 67 | ); 68 | } else { 69 | // When asChild is false, use button and render Loader2 if loading 70 | return ( 71 | 80 | ); 81 | } 82 | } 83 | ); 84 | 85 | Button.displayName = "Button"; 86 | 87 | export { Button, buttonVariants }; 88 | -------------------------------------------------------------------------------- /components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons" 5 | import { DayPicker } from "react-day-picker" 6 | 7 | import { cn } from "@/lib/utils" 8 | import { buttonVariants } from "@/components/ui/button" 9 | 10 | export type CalendarProps = React.ComponentProps 11 | 12 | function Calendar({ 13 | className, 14 | classNames, 15 | showOutsideDays = true, 16 | ...props 17 | }: CalendarProps) { 18 | return ( 19 | .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" 43 | : "[&:has([aria-selected])]:rounded-md" 44 | ), 45 | day: cn( 46 | buttonVariants({ variant: "ghost" }), 47 | "h-8 w-8 p-0 font-normal aria-selected:opacity-100" 48 | ), 49 | day_range_start: "day-range-start", 50 | day_range_end: "day-range-end", 51 | day_selected: 52 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", 53 | day_today: "bg-accent text-accent-foreground", 54 | day_outside: 55 | "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30", 56 | day_disabled: "text-muted-foreground opacity-50", 57 | day_range_middle: 58 | "aria-selected:bg-accent aria-selected:text-accent-foreground", 59 | day_hidden: "invisible", 60 | ...classNames, 61 | }} 62 | components={{ 63 | IconLeft: ({ }) => , 64 | IconRight: ({ }) => , 65 | }} 66 | {...props} 67 | /> 68 | ) 69 | } 70 | Calendar.displayName = "Calendar" 71 | 72 | export { Calendar } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js Frontend for File Share with End-to-End Encryption 2 | 3 | [![Watch the video](https://img.youtube.com/vi/t5w2dauFmhM/maxresdefault.jpg)](https://youtu.be/t5w2dauFmhM) 4 | 5 | 6 | This is the frontend for the file sharing project, built using Next.js, with authentication handled by Auth.js and the UI components provided by ShadCN. 7 | 8 | ## Table of Contents 9 | 10 | - [Features](#features) 11 | - [Technologies Used](#technologies-used) 12 | - [Getting Started](#getting-started) 13 | - [Project Structure](#project-structure) 14 | - [Available Scripts](#available-scripts) 15 | - [Contributing](#contributing) 16 | - [License](#license) 17 | - [Donations](#donations) 18 | 19 | ## Features 20 | 21 | - **File Upload**: Upload and share files securely with end-to-end encryption. 22 | - **Authentication**: Secure user login and registration powered by Auth.js. 23 | - **Responsive UI**: A modern, responsive design using ShadCN for consistent and reusable UI components. 24 | - **File Listing**: Send and receive file lists between users. 25 | 26 | ## Technologies Used 27 | 28 | - **Next.js**: The React framework for building fast and scalable applications with server-side rendering and static site generation. 29 | - **Auth.js**: Authentication solution to handle secure login and user sessions. 30 | - **ShadCN**: A customizable and consistent UI component library built on Radix and Tailwind CSS for creating beautiful UIs quickly. 31 | - **Tailwind CSS**: A utility-first CSS framework for rapid UI development. 32 | - **TypeScript**: Superset of JavaScript that provides static typing, ensuring a robust and scalable codebase. 33 | 34 | ## Getting Started 35 | 36 | To get a local copy of this project up and running, follow these steps: 37 | 38 | ### Prerequisites 39 | 40 | - **Node.js** (16.x or higher) and **npm** (or **yarn**) installed. You can download Node.js [here](https://nodejs.org/). 41 | 42 | ### Installation 43 | 44 | 1. Clone the repository: 45 | 46 | ```bash 47 | git clone https://github.com/AarambhDevHub/file-share-frontend.git 48 | cd file-share-frontend 49 | ``` 50 | 51 | 2. Create a .env file in the root of the project with the following variables: 52 | 53 | ``` 54 | API_BASE_URL=http://localhost:8000/api 55 | NEXTAUTH_SECRET=b06401cc634e3695e1aca5f08002377d948e937d3a4dd4772e4e8c5d5f58b8a2 56 | NEXT_PUBLIC_APP_URL=http://localhost:3000 57 | ``` 58 | 3. Install the necessary dependencies: 59 | 60 | ``` 61 | npm install 62 | ``` 63 | 64 | 4. Start the development server: 65 | 66 | ``` 67 | npm run dev 68 | ``` 69 | The app will be available at http://localhost:3000. 70 | 71 | ## Available Scripts 72 | 73 | In the project directory, you can run: 74 | 75 | - **`npm run dev`**: Runs the app in development mode. Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 76 | - **`npm run build`**: Builds the app for production in the `out` folder. It bundles React in production mode and optimizes the build for the best performance. 77 | - **`npm run start`**: Starts the production server after building the project. 78 | 79 | ## License 80 | 81 | This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details. 82 | 83 | ## Donations 84 | 85 | If you find this project useful and would like to support its continued development, you can make a donation via [Buy Me a Coffee](https://buymeacoffee.com/aarambhdevhub). 86 | 87 | Thank you for your support! 88 | -------------------------------------------------------------------------------- /components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )) 17 | Table.displayName = "Table" 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )) 25 | TableHeader.displayName = "TableHeader" 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )) 37 | TableBody.displayName = "TableBody" 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | tr]:last:border-b-0", 47 | className 48 | )} 49 | {...props} 50 | /> 51 | )) 52 | TableFooter.displayName = "TableFooter" 53 | 54 | const TableRow = React.forwardRef< 55 | HTMLTableRowElement, 56 | React.HTMLAttributes 57 | >(({ className, ...props }, ref) => ( 58 | 66 | )) 67 | TableRow.displayName = "TableRow" 68 | 69 | const TableHead = React.forwardRef< 70 | HTMLTableCellElement, 71 | React.ThHTMLAttributes 72 | >(({ className, ...props }, ref) => ( 73 |
[role=checkbox]]:translate-y-[2px]", 77 | className 78 | )} 79 | {...props} 80 | /> 81 | )) 82 | TableHead.displayName = "TableHead" 83 | 84 | const TableCell = React.forwardRef< 85 | HTMLTableCellElement, 86 | React.TdHTMLAttributes 87 | >(({ className, ...props }, ref) => ( 88 | [role=checkbox]]:translate-y-[2px]", 92 | className 93 | )} 94 | {...props} 95 | /> 96 | )) 97 | TableCell.displayName = "TableCell" 98 | 99 | const TableCaption = React.forwardRef< 100 | HTMLTableCaptionElement, 101 | React.HTMLAttributes 102 | >(({ className, ...props }, ref) => ( 103 |
108 | )) 109 | TableCaption.displayName = "TableCaption" 110 | 111 | export { 112 | Table, 113 | TableHeader, 114 | TableBody, 115 | TableFooter, 116 | TableHead, 117 | TableRow, 118 | TableCell, 119 | TableCaption, 120 | } 121 | -------------------------------------------------------------------------------- /components/auth/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTransition } from "react"; 4 | import { AuthCard } from "./AuthCard"; 5 | import { useForm } from "react-hook-form"; 6 | import { z } from "zod"; 7 | import { loginSchema } from "../schema/authType"; 8 | import { zodResolver } from '@hookform/resolvers/zod'; 9 | import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../ui/form"; 10 | import { Input } from "../ui/input"; 11 | import { Button } from "../ui/button"; 12 | import { LoginApi } from "@/action/authHandler"; 13 | import toast from "react-hot-toast"; 14 | 15 | export const LoginForm = () => { 16 | const [isPanding, startTransistion] = useTransition(); 17 | 18 | const form = useForm>({ 19 | resolver: zodResolver(loginSchema), 20 | defaultValues: { 21 | email: '', 22 | password: '' 23 | } 24 | }) 25 | 26 | const onSubmit = (values: z.infer) => { 27 | startTransistion(() => { 28 | LoginApi(values) 29 | .then((response) => { 30 | if(response?.error) { 31 | toast.error(response.error); 32 | } 33 | }) 34 | .catch((error) => { 35 | console.log(error); 36 | }) 37 | }) 38 | } 39 | 40 | return( 41 | 46 |
47 | 48 |
49 | ( 53 | 54 | Email 55 | 56 | 62 | 63 | 64 | 65 | )} 66 | /> 67 | ( 71 | 72 | Password 73 | 74 | 81 | 82 | 83 | 84 | )} 85 | /> 86 |
87 | 90 |
91 | 92 | 93 |
94 | ) 95 | } -------------------------------------------------------------------------------- /components/profile/UserProfile.tsx: -------------------------------------------------------------------------------- 1 | import { useForm } from "react-hook-form"; 2 | import { UserDataProps } from "./Profile"; 3 | import { z } from "zod"; 4 | import { nameUpdateSchema } from "../schema/profileType"; 5 | import { zodResolver } from "@hookform/resolvers/zod"; 6 | import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../ui/form"; 7 | import { Input } from "../ui/input"; 8 | import { Button } from "../ui/button"; 9 | import { useTransition } from "react"; 10 | import { updateUserName } from "@/action/profileHandler"; 11 | import toast from "react-hot-toast"; 12 | 13 | 14 | export const Userprofile = ({ userData }: { userData: UserDataProps }) => { 15 | const [isPanding, startTransition] = useTransition(); 16 | 17 | const form = useForm>({ 18 | resolver: zodResolver(nameUpdateSchema), 19 | defaultValues: { 20 | email: userData.email, 21 | name: userData.name, 22 | } 23 | }) 24 | 25 | const onSubmit = (values: z.infer) => { 26 | startTransition(() => { 27 | updateUserName({ name: values.name }) 28 | .then((response) => { 29 | if (response.status === 400){ 30 | toast.error(response.message) 31 | } 32 | 33 | if (response.status == 'success') { 34 | toast.success(`User name update Successful!`) 35 | } 36 | }) 37 | .catch((error) => { 38 | console.log(error); 39 | }) 40 | }) 41 | } 42 | 43 | return ( 44 |
45 | 46 | Update User Name 47 | 48 |
49 | 50 |
51 | ( 55 | 56 | Email 57 | 58 | 64 | 65 | 66 | 67 | )} 68 | /> 69 | ( 73 | 74 | Name 75 | 76 | 81 | 82 | 83 | 84 | )} 85 | /> 86 | 89 |
90 |
91 | 92 |
93 | ) 94 | } -------------------------------------------------------------------------------- /components/ui/data-table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | ColumnDef, 5 | flexRender, 6 | getCoreRowModel, 7 | useReactTable, 8 | getPaginationRowModel, 9 | } from "@tanstack/react-table"; 10 | 11 | import { 12 | Table, 13 | TableBody, 14 | TableCell, 15 | TableHead, 16 | TableHeader, 17 | TableRow, 18 | } from "@/components/ui/table"; 19 | import { Button } from "@/components/ui/button"; 20 | import { useEffect, useState } from "react"; 21 | import { useRouter, useSearchParams } from "next/navigation"; 22 | 23 | interface DataTableProps { 24 | columns: ColumnDef[]; 25 | data: TData[]; 26 | totalCount: number; // Total count for pagination 27 | } 28 | 29 | export function DataTable({ 30 | columns, 31 | data, 32 | totalCount, 33 | }: DataTableProps) { 34 | 35 | const router = useRouter(); 36 | const searchParams = useSearchParams(); 37 | 38 | const initialPage = Number(searchParams.get("page")) || 1; 39 | const [page, setPage] = useState(initialPage); 40 | const limit = 10; // Adjust based on your needs 41 | 42 | const table = useReactTable({ 43 | data, 44 | columns, 45 | getCoreRowModel: getCoreRowModel(), 46 | getPaginationRowModel: getPaginationRowModel(), 47 | }); 48 | 49 | useEffect(() => { 50 | 51 | const params = new URLSearchParams(window.location.search); 52 | params.set("page", String(page)) 53 | params.set("limit", String(limit)) 54 | 55 | router.push(`${window.location.pathname}?${params.toString()}`) 56 | },[page, limit, router]) 57 | 58 | return ( 59 |
60 |
61 | 62 | 63 | {table.getHeaderGroups().map((headerGroup) => ( 64 | 65 | {headerGroup.headers.map((header) => { 66 | return ( 67 | 68 | {header.isPlaceholder 69 | ? null 70 | : flexRender( 71 | header.column.columnDef.header, 72 | header.getContext() 73 | )} 74 | 75 | ); 76 | })} 77 | 78 | ))} 79 | 80 | 81 | {table.getRowModel().rows?.length ? ( 82 | table.getRowModel().rows.map((row) => ( 83 | 87 | {row.getVisibleCells().map((cell) => ( 88 | 89 | {flexRender( 90 | cell.column.columnDef.cell, 91 | cell.getContext() 92 | )} 93 | 94 | ))} 95 | 96 | )) 97 | ) : ( 98 | 99 | 103 | No results. 104 | 105 | 106 | )} 107 | 108 |
109 |
110 |
111 | 119 | 128 |
129 |
130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { Cross2Icon } from "@radix-ui/react-icons" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogTrigger, 116 | DialogClose, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { Slot } from "@radix-ui/react-slot" 6 | import { 7 | Controller, 8 | ControllerProps, 9 | FieldPath, 10 | FieldValues, 11 | FormProvider, 12 | useFormContext, 13 | } from "react-hook-form" 14 | 15 | import { cn } from "@/lib/utils" 16 | import { Label } from "@/components/ui/label" 17 | 18 | const Form = FormProvider 19 | 20 | type FormFieldContextValue< 21 | TFieldValues extends FieldValues = FieldValues, 22 | TName extends FieldPath = FieldPath 23 | > = { 24 | name: TName 25 | } 26 | 27 | const FormFieldContext = React.createContext( 28 | {} as FormFieldContextValue 29 | ) 30 | 31 | const FormField = < 32 | TFieldValues extends FieldValues = FieldValues, 33 | TName extends FieldPath = FieldPath 34 | >({ 35 | ...props 36 | }: ControllerProps) => { 37 | return ( 38 | 39 | 40 | 41 | ) 42 | } 43 | 44 | const useFormField = () => { 45 | const fieldContext = React.useContext(FormFieldContext) 46 | const itemContext = React.useContext(FormItemContext) 47 | const { getFieldState, formState } = useFormContext() 48 | 49 | const fieldState = getFieldState(fieldContext.name, formState) 50 | 51 | if (!fieldContext) { 52 | throw new Error("useFormField should be used within ") 53 | } 54 | 55 | const { id } = itemContext 56 | 57 | return { 58 | id, 59 | name: fieldContext.name, 60 | formItemId: `${id}-form-item`, 61 | formDescriptionId: `${id}-form-item-description`, 62 | formMessageId: `${id}-form-item-message`, 63 | ...fieldState, 64 | } 65 | } 66 | 67 | type FormItemContextValue = { 68 | id: string 69 | } 70 | 71 | const FormItemContext = React.createContext( 72 | {} as FormItemContextValue 73 | ) 74 | 75 | const FormItem = React.forwardRef< 76 | HTMLDivElement, 77 | React.HTMLAttributes 78 | >(({ className, ...props }, ref) => { 79 | const id = React.useId() 80 | 81 | return ( 82 | 83 |
84 | 85 | ) 86 | }) 87 | FormItem.displayName = "FormItem" 88 | 89 | const FormLabel = React.forwardRef< 90 | React.ElementRef, 91 | React.ComponentPropsWithoutRef 92 | >(({ className, ...props }, ref) => { 93 | const { error, formItemId } = useFormField() 94 | 95 | return ( 96 |