├── .eslintrc.json
├── src
├── app
│ ├── auth
│ │ ├── page.tsx
│ │ ├── sign-in
│ │ │ └── loading.tsx
│ │ ├── sign-up
│ │ │ └── loading.tsx
│ │ ├── account-created
│ │ │ └── loading.tsx
│ │ ├── forgot-password
│ │ │ └── loading.tsx
│ │ ├── reset-password
│ │ │ └── loading.tsx
│ │ ├── password-reset-confirm
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ ├── components
│ │ │ └── AuthFormWrapper.tsx
│ │ ├── check-email
│ │ │ └── page.tsx
│ │ └── email-sent
│ │ │ └── page.tsx
│ ├── not-authorized.tsx
│ ├── page.tsx
│ ├── loading.tsx
│ ├── team
│ │ ├── loading.tsx
│ │ ├── page.tsx
│ │ └── components
│ │ │ ├── FilterToggle.tsx
│ │ │ ├── TeamClient.tsx
│ │ │ ├── UserTable.tsx
│ │ │ └── OrganizationName.tsx
│ ├── contacts
│ │ ├── loading.tsx
│ │ ├── page.tsx
│ │ └── components
│ │ │ └── ContactsTableRow.tsx
│ ├── documents
│ │ ├── loading.tsx
│ │ ├── [documentId]
│ │ │ ├── loading.tsx
│ │ │ ├── page.tsx
│ │ │ └── components
│ │ │ │ ├── FilterToggle.tsx
│ │ │ │ ├── CustomBarChart.tsx
│ │ │ │ └── InfoTableHeader.tsx
│ │ └── components
│ │ │ ├── CustomAccordion.tsx
│ │ │ ├── DocumentsTableHeader.tsx
│ │ │ ├── LinkDetailsAccordion.tsx
│ │ │ └── NewLinkDialog.tsx
│ ├── profile
│ │ ├── loading.tsx
│ │ └── page.tsx
│ ├── settings
│ │ ├── loading.tsx
│ │ ├── page.tsx
│ │ └── components
│ │ │ ├── SettingsTabs.tsx
│ │ │ └── ColorPickerBox.tsx
│ ├── documentAccess
│ │ └── [linkId]
│ │ │ ├── loading.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── components
│ │ │ ├── AccessError.tsx
│ │ │ └── FileDisplay.tsx
│ │ │ └── page.tsx
│ ├── api
│ │ ├── _services
│ │ │ ├── index.ts
│ │ │ └── errorService.ts
│ │ ├── auth
│ │ │ ├── [...nextauth]
│ │ │ │ └── route.ts
│ │ │ ├── verify
│ │ │ │ └── route.ts
│ │ │ ├── password
│ │ │ │ ├── reset
│ │ │ │ │ └── route.ts
│ │ │ │ └── forgot
│ │ │ │ │ └── route.ts
│ │ │ └── register
│ │ │ │ └── route.ts
│ │ ├── documents
│ │ │ └── [documentId]
│ │ │ │ ├── links
│ │ │ │ ├── [linkId]
│ │ │ │ │ └── route.ts
│ │ │ │ └── route.ts
│ │ │ │ ├── visitors
│ │ │ │ └── route.ts
│ │ │ │ └── route.ts
│ │ ├── profile
│ │ │ ├── route.ts
│ │ │ ├── changeName
│ │ │ │ └── route.ts
│ │ │ └── changePassword
│ │ │ │ └── route.ts
│ │ ├── public_links
│ │ │ └── [linkId]
│ │ │ │ ├── route.ts
│ │ │ │ └── access
│ │ │ │ └── route.ts
│ │ └── contacts
│ │ │ └── route.ts
│ ├── layout.tsx
│ ├── not-found.tsx
│ └── providers.tsx
├── shared
│ ├── types
│ │ ├── global.d.ts
│ │ └── next-auth.d.ts
│ ├── models
│ │ ├── authModels.ts
│ │ ├── index.ts
│ │ ├── userModels.ts
│ │ ├── documentModels.ts
│ │ └── linkModels.ts
│ ├── config
│ │ ├── visitorFieldsConfig.ts
│ │ ├── fileIcons.ts
│ │ └── routesConfig.ts
│ └── utils
│ │ └── index.ts
├── providers
│ ├── toast
│ │ ├── toastTypes.ts
│ │ └── ToastProvider.tsx
│ ├── query
│ │ └── QueryProvider.tsx
│ └── auth
│ │ └── AuthWrapper.tsx
├── hooks
│ ├── useModal.ts
│ ├── useToast.ts
│ ├── documents
│ │ ├── useFetchDocuments.ts
│ │ ├── useDeleteDocument.ts
│ │ ├── useUploadDocument.ts
│ │ └── useCreateLink.ts
│ ├── documentAccess
│ │ ├── useDocumentAccess.ts
│ │ └── useVisitorSubmission.ts
│ ├── contacts
│ │ └── useFetchContacts.ts
│ ├── index.ts
│ ├── useDocumentDetail.ts
│ ├── useDocumentData.ts
│ ├── useDocumentAnalytics.ts
│ └── useSort.ts
├── lib
│ └── prisma.ts
├── components
│ ├── loaders
│ │ ├── LoadingSpinner.tsx
│ │ ├── LoadingButton.tsx
│ │ ├── CustomCircularProgress.tsx
│ │ └── EmptyState.tsx
│ ├── input
│ │ ├── CustomCheckbox.tsx
│ │ ├── Dropdown.tsx
│ │ ├── PasswordValidation.tsx
│ │ └── FormInput.tsx
│ ├── common
│ │ ├── EnvironmentBadge.tsx
│ │ └── Toast.tsx
│ ├── navigation
│ │ ├── NavLink.tsx
│ │ └── Paginator.tsx
│ └── index.ts
├── theme
│ └── customTypesTheme.d.ts
└── icons
│ ├── charts
│ └── BarChartIcon.tsx
│ ├── shapes
│ └── SquareIcon.tsx
│ ├── arrows
│ ├── ArrowNarrowLeftIcon.tsx
│ ├── ArrowNarrowRightIcon.tsx
│ ├── ChevronUpIcon.tsx
│ ├── ChevronDownIcon.tsx
│ ├── ChevronRightIcon.tsx
│ └── ChevronSelectorVerticalIcon.tsx
│ ├── general
│ ├── CheckIcon.tsx
│ ├── XCloseIcon.tsx
│ ├── CheckSquareIcon.tsx
│ ├── LogOutIcon.tsx
│ ├── MenuIcon.tsx
│ ├── UploadCloudIcon.tsx
│ ├── LinkBrokenIcon.tsx
│ ├── LinkIcon.tsx
│ ├── TrashIcon.tsx
│ ├── XCircleIcon.tsx
│ ├── EyeIcon.tsx
│ ├── CopyIcon.tsx
│ ├── SettingsIcon.tsx
│ ├── EyeOffIcon.tsx
│ ├── HomeIcon.tsx
│ ├── CheckCircleIcon.tsx
│ └── SaveIcon.tsx
│ ├── alerts
│ └── AlertCircleIcon.tsx
│ ├── files
│ ├── FileIcon.tsx
│ ├── VideoIcon.tsx
│ ├── AudioIcon.tsx
│ ├── GeneralIcon.tsx
│ ├── ImageIcon.tsx
│ ├── ZIPIcon.tsx
│ ├── TextIcon.tsx
│ ├── FileDownloadIcon.tsx
│ ├── ExcelIcon.tsx
│ └── PPTIcon.tsx
│ ├── users
│ ├── UserIcon.tsx
│ └── UsersIcon.tsx
│ ├── security
│ ├── LockIcon.tsx
│ └── KeyIcon.tsx
│ ├── communication
│ └── MailIcon.tsx
│ └── editor
│ └── PencilIcon.tsx
├── .prettierignore
├── middleware.ts
├── .prettierrc
├── .gitignore
├── tsconfig.json
├── next.config.mjs
├── .vscode
└── settings.json
├── prisma
└── seed.ts
├── scripts
└── prepare-env.js
└── package.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/auth/page.tsx:
--------------------------------------------------------------------------------
1 | export default function Home() {
2 | return
Auth
;
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/not-authorized.tsx:
--------------------------------------------------------------------------------
1 | export default function Home() {
2 | return Not Authorized
;
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '@mui/material';
2 |
3 | export default function Home() {
4 | return Home page;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from '@/components';
2 |
3 | export default function PageLoader() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/team/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from '@/components';
2 |
3 | export default function PageLoader() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/contacts/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from '@/components';
2 |
3 | export default function PageLoader() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/documents/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from '@/components';
2 |
3 | export default function PageLoader() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/profile/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from '@/components';
2 |
3 | export default function PageLoader() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/settings/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from '@/components';
2 |
3 | export default function PageLoader() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/auth/sign-in/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from '@/components';
2 |
3 | export default function PageLoader() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/auth/sign-up/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from '@/components';
2 |
3 | export default function PageLoader() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/auth/account-created/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from '@/components';
2 |
3 | export default function PageLoader() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/auth/forgot-password/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from '@/components';
2 |
3 | export default function PageLoader() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/auth/reset-password/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from '@/components';
2 |
3 | export default function PageLoader() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/shared/types/global.d.ts:
--------------------------------------------------------------------------------
1 | // global.d.ts
2 | import { PrismaClient } from '@prisma/client';
3 |
4 | declare global {
5 | var prisma: PrismaClient | undefined;
6 | }
7 |
8 | export {};
9 |
--------------------------------------------------------------------------------
/src/app/documentAccess/[linkId]/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from '@/components';
2 |
3 | export default function PageLoader() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/documents/[documentId]/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from '@/components';
2 |
3 | export default function PageLoader() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/auth/password-reset-confirm/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from '@/components';
2 |
3 | export default function PageLoader() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/profile/page.tsx:
--------------------------------------------------------------------------------
1 | import { Container } from '@mui/material';
2 | import ProfileForm from './components/ProfileForm';
3 |
4 | export default function ProfilePage() {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/providers/toast/toastTypes.ts:
--------------------------------------------------------------------------------
1 | export type ToastVariant = 'success' | 'error' | 'warning' | 'info';
2 |
3 | export interface ToastMessage {
4 | id: string; // Unique identifier for each toast
5 | message: string;
6 | variant?: ToastVariant;
7 | autoHide?: boolean;
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/api/_services/index.ts:
--------------------------------------------------------------------------------
1 | export { DocumentService } from './documentService';
2 | export { authService } from './authService';
3 | export { createErrorResponse } from './errorService';
4 | export { LinkService } from './linkService';
5 | export { emailService } from './emailService';
6 |
--------------------------------------------------------------------------------
/src/hooks/useModal.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | export function useModal() {
4 | const [isOpen, setIsOpen] = useState(false);
5 |
6 | const openModal = () => setIsOpen(true);
7 | const closeModal = () => setIsOpen(false);
8 |
9 | return { isOpen, openModal, closeModal };
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from 'next-auth';
2 | import { authOptions } from '@/lib/authOptions';
3 |
4 | const nextAuthHandler = NextAuth(authOptions) as unknown as (req: Request) => Promise;
5 |
6 | export const GET = nextAuthHandler;
7 | export const POST = nextAuthHandler;
8 |
--------------------------------------------------------------------------------
/src/hooks/useToast.ts:
--------------------------------------------------------------------------------
1 | import { useToastContext } from '@/providers/toast/ToastProvider';
2 | import { ToastMessage } from '@/providers/toast/toastTypes';
3 |
4 | export const useToast = () => {
5 | const { showToast } = useToastContext();
6 | return { showToast: (toast: Omit) => showToast(toast) };
7 | };
8 |
--------------------------------------------------------------------------------
/src/app/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import { Container, Divider } from '@mui/material';
2 | import BrandingSetting from './components/BrandingSetting';
3 | import SettingsTabs from './components/SettingsTabs';
4 |
5 | export default function SettingsPage() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/contacts/page.tsx:
--------------------------------------------------------------------------------
1 | import { Container, Typography } from '@mui/material';
2 | import ContactsTable from './components/ContactsTable';
3 |
4 | export default function ContactsPage() {
5 | return (
6 |
7 |
10 | All contacts
11 |
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore dependencies and builds
2 | node_modules
3 | dist
4 | .next
5 | out
6 | build
7 | coverage
8 |
9 | # Ignore specific files
10 | package-lock.json
11 | yarn.lock
12 | README.md
13 |
14 | # Ignore public assets
15 | public
16 | public/**/*.*
17 | static
18 | static/**/*
19 |
20 | # Ignore environment files
21 | .env
22 | .env.local
23 | .env.*.local
24 |
25 | # Ignore logs
26 | *.log
27 |
--------------------------------------------------------------------------------
/src/app/documents/[documentId]/page.tsx:
--------------------------------------------------------------------------------
1 | import { Container } from '@mui/material';
2 | import DocumentView from './components/DocumentView';
3 |
4 | export default async function page(props: { params: Promise<{ documentId: string }> }) {
5 | const { documentId } = await props.params;
6 |
7 | return (
8 |
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/lib/prisma.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client';
2 |
3 | const globalForPrisma = global as unknown as { prisma: PrismaClient };
4 |
5 | const prisma =
6 | globalForPrisma.prisma ||
7 | new PrismaClient({
8 | log: process.env.ENABLE_DEBUG_LOGS === 'true' ? ['query', 'info', 'warn', 'error'] : [],
9 | });
10 |
11 | if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
12 |
13 | export default prisma;
14 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { withAuth } from 'next-auth/middleware';
2 |
3 | export default withAuth({
4 | pages: {
5 | signIn: '/auth/sign-in', // Redirect to /sign-in for all unauthenticated routes
6 | },
7 | });
8 |
9 | export const config = {
10 | matcher: [
11 | '/((?!register|auth/sign-up|auth/forgot-password|auth/account-created|auth/password-reset-confirm|auth/check-email|auth/reset-password|documentAccess/[a-f0-9-]{36})/.*.*|auth/reset-password/.*)',
12 | ],
13 | };
14 |
--------------------------------------------------------------------------------
/src/app/api/_services/errorService.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 |
3 | /**
4 | * Creates a consistent JSON error response.
5 | * @param message The error message
6 | * @param status HTTP status code
7 | * @param details Optional error details
8 | */
9 | export function createErrorResponse(message: string, status: number, details?: any) {
10 | console.error(`[${new Date().toISOString()}] ${message}`, details);
11 | return NextResponse.json({ message }, { status });
12 | }
13 |
--------------------------------------------------------------------------------
/src/shared/types/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | // next-auth.d.ts
2 | import NextAuth from 'next-auth';
3 |
4 | declare module 'next-auth' {
5 | interface Session {
6 | user: {
7 | id: string;
8 | userId: string;
9 | role: string;
10 | firstName: string;
11 | lastName: string;
12 | email: string;
13 | image?: string;
14 | };
15 | }
16 |
17 | interface User {
18 | id: string;
19 | userId: string;
20 | role: string;
21 | firstName: string;
22 | lastName: string;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/loaders/LoadingSpinner.tsx:
--------------------------------------------------------------------------------
1 | import { Box, CircularProgress } from '@mui/material';
2 |
3 | export default function LoadingSpinner() {
4 | return (
5 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/shared/models/authModels.ts:
--------------------------------------------------------------------------------
1 | import { UserRole } from '@prisma/client';
2 |
3 | // =========== REGISTER PAYLOAD ===========
4 |
5 | export interface RegisterPayload {
6 | email: string;
7 | password: string;
8 | firstName: string;
9 | lastName: string;
10 | role: UserRole;
11 | }
12 |
13 | // =========== REGISTER RESULT ===========
14 |
15 | export interface RegisterResult {
16 | success: boolean;
17 | message: string;
18 | userId?: string;
19 | verificationToken?: string;
20 | emailFail?: boolean; // partial success scenario
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/settings/components/SettingsTabs.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Tab, Tabs } from '@mui/material';
4 | import { useState } from 'react';
5 |
6 | export default function SettingsTabs() {
7 | const [tabValue, setTabValue] = useState(0);
8 |
9 | const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
10 | setTabValue(newValue);
11 | };
12 | return (
13 | <>
14 |
18 |
19 |
20 | >
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/hooks/documents/useFetchDocuments.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { useQuery } from '@tanstack/react-query';
3 | import { DocumentType } from '@/shared/models';
4 |
5 | interface DocumentResponse {
6 | documents: DocumentType[];
7 | }
8 |
9 | const fetchDocuments = async (): Promise => {
10 | const response = await axios.get('/api/documents');
11 |
12 | return response.data;
13 | };
14 |
15 | const useFetchDocuments = () => {
16 | return useQuery({
17 | queryKey: ['documents'],
18 | queryFn: fetchDocuments,
19 | });
20 | };
21 |
22 | export default useFetchDocuments;
23 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "bracketSameLine": true,
4 | "bracketSpacing": true,
5 | "endOfLine": "lf",
6 | "printWidth": 100,
7 | "semi": true,
8 | "singleQuote": true,
9 | "tabWidth": 2,
10 | "useTabs": true,
11 | "trailingComma": "all",
12 | "jsxSingleQuote": true,
13 | "singleAttributePerLine": true,
14 | "htmlWhitespaceSensitivity": "css",
15 | "proseWrap": "preserve",
16 | "quoteProps": "as-needed",
17 | "embeddedLanguageFormatting": "auto",
18 | "overrides": [
19 | {
20 | "files": "*.json",
21 | "options": {
22 | "printWidth": 80,
23 | "tabWidth": 2
24 | }
25 | }
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/src/hooks/documents/useDeleteDocument.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { useMutation, useQueryClient } from '@tanstack/react-query';
3 |
4 | const deleteDocumentById = async (documentId: string): Promise => {
5 | const response = await axios.delete(`/api/documents/${documentId}`);
6 |
7 | return response.data;
8 | };
9 |
10 | const useDeleteDocument = () => {
11 | const queryClient = useQueryClient();
12 |
13 | return useMutation({
14 | mutationFn: deleteDocumentById,
15 | onSuccess: () => {
16 | queryClient.invalidateQueries({ queryKey: ['documents'] });
17 | },
18 | });
19 | };
20 |
21 | export default useDeleteDocument;
22 |
--------------------------------------------------------------------------------
/src/providers/query/QueryProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 |
5 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
6 | import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
7 |
8 | interface Props {
9 | children: React.ReactNode;
10 | }
11 |
12 | const QueryProvider = ({ children }: Props) => {
13 | const [queryClient] = React.useState(() => new QueryClient());
14 |
15 | return (
16 |
17 |
18 | {children}
19 |
20 | );
21 | };
22 |
23 | export default QueryProvider;
24 |
--------------------------------------------------------------------------------
/src/hooks/documentAccess/useDocumentAccess.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { useQuery } from '@tanstack/react-query';
3 | import { QueryFunctionContext } from '@tanstack/react-query';
4 |
5 | const fetchDocumentDetails = async ({ queryKey }: QueryFunctionContext) => {
6 | const [_, linkId] = queryKey as [string, string];
7 | const response = await axios.get(`/api/public_links/${linkId}`);
8 |
9 | return response.data;
10 | };
11 |
12 | const useDocumentAccess = (linkId: string) => {
13 | return useQuery({
14 | queryKey: ['documentAccess', linkId],
15 | queryFn: fetchDocumentDetails,
16 | retry: false,
17 | });
18 | };
19 |
20 | export default useDocumentAccess;
21 |
--------------------------------------------------------------------------------
/src/app/auth/components/AuthFormWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Container } from '@mui/material';
2 | import { ReactNode } from 'react';
3 | import { BackgroundIcon } from '@/icons';
4 |
5 | const AuthFormWrapper = ({ children }: { children: ReactNode }) => {
6 | return (
7 | <>
8 |
9 |
12 |
18 | {children}
19 |
20 |
21 | >
22 | );
23 | };
24 |
25 | export default AuthFormWrapper;
26 |
--------------------------------------------------------------------------------
/src/shared/models/index.ts:
--------------------------------------------------------------------------------
1 | export type { RegisterPayload } from './authModels';
2 | export type { RegisterResult } from './authModels';
3 |
4 | export type { DocumentType } from './documentModels';
5 | export type { BarDataItem } from './documentModels';
6 |
7 | export type { LinkType } from './linkModels';
8 | export type { LinkFormValues } from './linkModels';
9 | export type { CreateDocumentLinkPayload } from './linkModels';
10 | export type { InviteRecipientsPayload } from './linkModels';
11 | export type { LinkData } from './linkModels';
12 | export type { LinkDetail } from './linkModels';
13 |
14 | export type { User } from './userModels';
15 | export type { Contact } from './userModels';
16 |
--------------------------------------------------------------------------------
/src/shared/config/visitorFieldsConfig.ts:
--------------------------------------------------------------------------------
1 | const visitorFieldKeys = ['name', 'email'] as const;
2 |
3 | type VisitorFieldKey = (typeof visitorFieldKeys)[number];
4 | type VisitorFieldsConfigByKey = Record;
5 |
6 | export interface VisitorField {
7 | key: VisitorFieldKey;
8 | label: string;
9 | placeholder: string;
10 | }
11 |
12 | export const visitorFieldsConfig: VisitorField[] = [
13 | { key: 'name', label: 'Name', placeholder: 'Your Name' },
14 | { key: 'email', label: 'Email', placeholder: 'your_email@bluewave.com' },
15 | ];
16 |
17 | export const visitorFieldsConfigByKey: VisitorFieldsConfigByKey = Object.groupBy(
18 | visitorFieldsConfig,
19 | (item) => item.key,
20 | );
21 |
--------------------------------------------------------------------------------
/src/app/team/page.tsx:
--------------------------------------------------------------------------------
1 | import { Container, Divider, Typography } from '@mui/material';
2 | import OrganizationName from './components/OrganizationName';
3 | import TeamClient from './components/TeamClient';
4 |
5 | export default function TeamPage() {
6 | return (
7 |
8 | Team
9 |
10 |
13 | Set up your team here.
14 |
15 |
16 |
17 |
18 |
19 |
22 | Team Members
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/.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 | # next.js
10 | /.next/
11 | /out/
12 |
13 | # Ignore settings.json files
14 | /.vscode
15 | .vscode/settings.json
16 |
17 | # debug
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
22 | # Ignore all real .env files
23 | env/
24 | .env
25 | .env.local
26 | .env.production
27 | .env.development
28 |
29 | # vercel
30 | .vercel
31 |
32 | md/
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | # Seed Data
39 | /src/seed
40 |
41 | # Ignore the entire migrations folder temporarily
42 | prisma/migrations/
43 |
44 |
--------------------------------------------------------------------------------
/src/hooks/documents/useUploadDocument.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { useMutation, useQueryClient } from '@tanstack/react-query';
3 |
4 | const uploadDocument = async (formData: FormData) => {
5 | const response = await axios.post('/api/documents', formData);
6 |
7 | return response.data;
8 | };
9 |
10 | const useUploadDocument = () => {
11 | const queryClient = useQueryClient();
12 |
13 | return useMutation({
14 | mutationFn: async (formData: FormData) => uploadDocument(formData),
15 | onSuccess: () => {
16 | queryClient.invalidateQueries({ queryKey: ['documents'] });
17 | },
18 | onError: (error) => {
19 | console.error('Error adding document: ', error);
20 | },
21 | });
22 | };
23 |
24 | export default useUploadDocument;
25 |
--------------------------------------------------------------------------------
/src/hooks/documentAccess/useVisitorSubmission.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { useMutation } from '@tanstack/react-query';
3 |
4 | const submitVisitorDetails = async ({ linkId, payload }: { linkId: string; payload: any }) => {
5 | const response = await axios.post(`/api/public_links/${linkId}/access`, payload);
6 |
7 | return response.data;
8 | };
9 |
10 | const useVisitorSubmission = () => {
11 | const mutation = useMutation({
12 | mutationFn: submitVisitorDetails,
13 | onError: (error) => {
14 | console.error('Error submitting visitor details: ', error);
15 | },
16 | });
17 |
18 | return {
19 | mutateAsync: mutation.mutateAsync,
20 | isPending: mutation.isPending,
21 | error: mutation.error,
22 | };
23 | };
24 |
25 | export default useVisitorSubmission;
26 |
--------------------------------------------------------------------------------
/src/app/api/auth/verify/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { authService } from '@/app/api/_services/authService';
3 |
4 | /**
5 | * GET /api/auth/verify?token=... or userId=...
6 | */
7 | export async function GET(req: NextRequest) {
8 | try {
9 | const { searchParams } = new URL(req.url);
10 | const token = searchParams.get('token') || undefined;
11 | const userId = searchParams.get('userId') || undefined;
12 |
13 | const result = await authService.verifyUser(token, userId);
14 |
15 | return NextResponse.json({ message: result.message }, { status: result.statusCode });
16 | } catch (err) {
17 | console.error('[verify] Error verifying email:', err);
18 | return NextResponse.json({ message: 'Internal server error' }, { status: 500 });
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/shared/models/userModels.ts:
--------------------------------------------------------------------------------
1 | // =========== USER TYPE ===========
2 |
3 | export interface User {
4 | user_id: number;
5 | name: string;
6 | email: string;
7 | role: 'Administrator' | 'Member';
8 | createdAt: string;
9 | // ... etc
10 | }
11 |
12 | // =========== VISITOR DETAIL ===========
13 |
14 | export interface Contact {
15 | id: number;
16 | name: string; // Combined first + last name
17 | email: string; // If LinkVisitors has an email field
18 | document_id: string; // The document_id from DB
19 | lastActivity: Date; //The date/time of their last activity
20 | lastViewedLink: string; //The last link or friendly name they viewed
21 | totalVisits: number; //Total visits for that email across the user's links
22 | downloads: number;
23 | duration: string;
24 | completion: string;
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 | import { Inter } from 'next/font/google';
3 | import React from 'react';
4 | import Providers from './providers';
5 | import EnvironmentBadge from '@/components/common/EnvironmentBadge';
6 |
7 | const inter = Inter({ subsets: ['latin'] });
8 |
9 | export const metadata: Metadata = {
10 | title: 'Blw-Datahall',
11 | description: 'Share documents safely with your team and customers',
12 | };
13 |
14 | interface RootLayoutProps {
15 | children: React.ReactNode;
16 | }
17 |
18 | export default function RootLayout({ children }: RootLayoutProps) {
19 | return (
20 |
21 |
22 |
23 | {children}
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/team/components/FilterToggle.tsx:
--------------------------------------------------------------------------------
1 | import { ToggleButton, ToggleButtonGroup } from '@mui/material';
2 |
3 | interface Props {
4 | currentFilter: 'All' | 'Administrator' | 'Member';
5 | onFilterChange: (role: 'All' | 'Administrator' | 'Member') => void;
6 | }
7 |
8 | const FilterToggle = ({ currentFilter, onFilterChange }: Props) => (
9 | {
13 | if (newRole !== null) {
14 | onFilterChange(newRole);
15 | }
16 | }}
17 | aria-label='Filter by role'
18 | size='small'>
19 | All
20 | Administrator
21 | Member
22 |
23 | );
24 |
25 | export default FilterToggle;
26 |
--------------------------------------------------------------------------------
/src/components/input/CustomCheckbox.tsx:
--------------------------------------------------------------------------------
1 | import { Checkbox, CheckboxProps, FormControlLabel } from '@mui/material';
2 | import { SquareIcon, CheckSquareIcon } from '@/icons';
3 |
4 | import { ChangeEvent } from 'react';
5 |
6 | interface CustomCheckboxProps extends CheckboxProps {
7 | checked: boolean;
8 | onChange: (event: ChangeEvent) => void;
9 | label: string;
10 | name: string;
11 | }
12 |
13 | const CustomCheckbox = ({ checked, onChange, label, name, ...props }: CustomCheckboxProps) => (
14 | }
18 | checkedIcon={}
19 | checked={checked}
20 | name={name}
21 | onChange={onChange}
22 | {...props}
23 | />
24 | }
25 | label={label}
26 | />
27 | );
28 |
29 | export default CustomCheckbox;
30 |
--------------------------------------------------------------------------------
/src/hooks/contacts/useFetchContacts.ts:
--------------------------------------------------------------------------------
1 | import { Contact } from '@/shared/models';
2 | import { useQuery } from '@tanstack/react-query';
3 | import axios from 'axios';
4 |
5 | export default function useFetchContacts() {
6 | const fetchContacts = async (): Promise => {
7 | const response = await axios.get('/api/contacts');
8 | return response.data.data;
9 | };
10 |
11 | return useQuery({
12 | queryKey: ['contacts'], // Caching key
13 | queryFn: fetchContacts, // Function to fetch data
14 | staleTime: 1000 * 30, // Data stays fresh for 30 seconds before being marked stale
15 | refetchInterval: 1000 * 60, // Background refetch every 60 seconds
16 | refetchOnWindowFocus: true, // Refetch when user focuses the window
17 | refetchOnReconnect: true, // Refetch when the user reconnects to the internet
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/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 | "@/*": ["./src/*"],
22 | "@lib/*": ["./lib/*"]
23 | },
24 | "target": "ES2017"
25 | },
26 | "include": [
27 | "next-env.d.ts",
28 | "**/*.ts",
29 | "**/*.tsx",
30 | ".next/types/**/*.ts",
31 | "customTypesTheme.d.ts",
32 | "src/app/register/page.jsx",
33 | "src/app/register/page.jsx"
34 | ],
35 | "exclude": ["node_modules"]
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/documents/[documentId]/components/FilterToggle.tsx:
--------------------------------------------------------------------------------
1 | import { ToggleButton, ToggleButtonGroup } from '@mui/material';
2 |
3 | interface FilterToggleProps {
4 | currentFilter: 'fromStart' | 'last30Days' | 'last7Days';
5 | onFilterChange: (period: 'fromStart' | 'last30Days' | 'last7Days') => void;
6 | }
7 |
8 | const FilterToggle = ({ currentFilter, onFilterChange }: FilterToggleProps) => (
9 | {
13 | if (newPeriod !== null) {
14 | onFilterChange(newPeriod);
15 | }
16 | }}
17 | aria-label='Filter by period'>
18 | From start
19 | Last 30 days
20 | Last 7 days
21 |
22 | );
23 |
24 | export default FilterToggle;
25 |
--------------------------------------------------------------------------------
/src/app/contacts/components/ContactsTableRow.tsx:
--------------------------------------------------------------------------------
1 | import { TableCell, TableRow, Typography } from '@mui/material';
2 |
3 | import { Contact } from '@/shared/models';
4 | import { formatDateTime } from '@/shared/utils';
5 |
6 | interface Props {
7 | contact: Contact;
8 | }
9 |
10 | export default function ContactsTableRow({ contact }: Props) {
11 | return (
12 |
13 |
14 | {contact.name ? contact.name : 'N/A'}
15 |
16 | {contact.email ? contact.email : 'N/A'}
17 |
18 | {contact.lastViewedLink}
19 |
20 | {formatDateTime(contact.lastActivity, { includeTime: true })}
21 |
22 | {contact.totalVisits}
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/shared/models/documentModels.ts:
--------------------------------------------------------------------------------
1 | import { FileType } from '@/shared/config/fileIcons';
2 | import { LinkDetail } from './linkModels';
3 |
4 | // =========== DOCUMENT TYPE ===========
5 |
6 | export interface DocumentType {
7 | document_id: string; // The unique DB identifier (cuid)
8 | fileName: string;
9 | filePath: string;
10 | fileType: FileType;
11 | size: number;
12 | createdAt: string; // ISO string
13 | updatedAt: string; // ISO string
14 | uploader: {
15 | name: string;
16 | avatar: string | null;
17 | };
18 | links: number; // The count of Link[]
19 | viewers: number; // The sum of all LinkVisitors for all links
20 | views: number; // Potential total doc views (0 if not tracked)
21 | createdLinks?: LinkDetail[]; // If you want to store link details
22 | }
23 |
24 | // ====== CHART TYPE ======
25 |
26 | export interface BarDataItem {
27 | month: string;
28 | Views: number;
29 | Downloads: number;
30 | date: Date;
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/documentAccess/[linkId]/layout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Typography, Box, Link } from '@mui/material';
3 | import { BlueWaveLogo } from '@/components';
4 |
5 | interface RootLayoutProps {
6 | children: React.ReactNode;
7 | }
8 |
9 | export default function RootLayout({ children }: RootLayoutProps) {
10 | return (
11 |
18 |
19 |
23 |
24 | {children}
25 |
29 | Need help?
30 |
33 | Contact Support
34 |
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/api/documents/[documentId]/links/[linkId]/route.ts:
--------------------------------------------------------------------------------
1 | import { authService, createErrorResponse, LinkService } from '@/app/api/_services';
2 | import { NextRequest, NextResponse } from 'next/server';
3 |
4 | /**
5 | * DELETE /api/documents/[documentId]/links/[documentLinkId]
6 | * Removes a link if the user owns it.
7 | */
8 | export async function DELETE(
9 | req: NextRequest,
10 | props: { params: Promise<{ documentLinkId: string }> },
11 | ) {
12 | try {
13 | const userId = await authService.authenticate();
14 | const { documentLinkId } = await props.params;
15 | const deleted = await LinkService.deleteLink(userId, documentLinkId);
16 |
17 | if (!deleted) {
18 | return createErrorResponse('Link not found or access denied.', 404);
19 | }
20 |
21 | return NextResponse.json({ message: 'Link deleted successfully.' }, { status: 200 });
22 | } catch (error) {
23 | return createErrorResponse('Server error while deleting link.', 500, error);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | // Ensures these server-only packages are included in the server bundle.
4 | serverExternalPackages: ['@prisma/client', 'bcrypt'],
5 | // Enables additional checks and warnings in development mode.
6 | reactStrictMode: false,
7 |
8 | webpack: (config) => {
9 | // Prevents Webpack from resolving the 'canvas' module.
10 | // This is useful when deploying to platforms like Vercel
11 | config.resolve.alias.canvas = false;
12 |
13 | return config;
14 | },
15 |
16 | typescript: {
17 | // Ensures the build fails if there are TypeScript errors.
18 | ignoreBuildErrors: false,
19 | },
20 | // Prepares the Next.js app for production with a self-contained output.
21 | // Useful for Docker deployments or when running the app outside of Vercel.
22 | // It bundles all necessary dependencies into a 'standalone' folder.
23 | output: 'standalone',
24 | };
25 |
26 | export default nextConfig;
27 |
--------------------------------------------------------------------------------
/src/components/common/EnvironmentBadge.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Box, Chip, Typography } from '@mui/material';
4 | import { ChipProps } from '@mui/material';
5 |
6 | const actualEnv = process.env.NEXT_PUBLIC_DEPLOYMENT_ENVIRONMENT ?? 'unknown';
7 |
8 | const envMap: Record = {
9 | local: { label: 'Dev', color: 'warning' }, // green
10 | development: { label: 'Preview', color: 'success' }, // yellow
11 | production: { label: 'BETA', color: 'primary' }, // red
12 | };
13 |
14 | const EnvironmentBadge = () => {
15 | const { label, color } = envMap[actualEnv] ?? { label: actualEnv, color: '#616161' };
16 |
17 | return (
18 |
31 | );
32 | };
33 |
34 | export default EnvironmentBadge;
35 |
--------------------------------------------------------------------------------
/src/app/api/auth/password/reset/route.ts:
--------------------------------------------------------------------------------
1 | // src/app/api/auth/password/reset/route.ts
2 | import { authService } from '@/app/api/_services/authService';
3 | import { NextRequest, NextResponse } from 'next/server';
4 |
5 | /**
6 | * POST /api/auth/password/reset
7 | * Expects: { token, password }
8 | */
9 | export async function POST(req: NextRequest) {
10 | try {
11 | const { token, password } = await req.json();
12 | if (!token || !password) {
13 | return NextResponse.json(
14 | { message: 'Token and new password are required.' },
15 | { status: 400 },
16 | );
17 | }
18 |
19 | const result = await authService.resetUserPassword(token, password);
20 | if (!result.success) {
21 | return NextResponse.json({ message: result.message }, { status: 400 });
22 | }
23 |
24 | return NextResponse.json({ message: result.message }, { status: 200 });
25 | } catch (error) {
26 | console.error('[reset] Error updating password:', error);
27 | return NextResponse.json({ message: 'Internal server error' }, { status: 500 });
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/documents/[documentId]/components/CustomBarChart.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { BarDataItem } from '@/shared/models';
4 |
5 | import { BarChart } from '@mui/x-charts/BarChart';
6 |
7 | interface CustomBarChartProps {
8 | data: { month: string; Views: number; Downloads: number; date: Date }[];
9 | }
10 |
11 | export default function CustomBarChart({ data }: CustomBarChartProps) {
12 | const chartData = data;
13 |
14 | return (
15 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/shared/utils/index.ts:
--------------------------------------------------------------------------------
1 | export type { FormatDateTimeOptions } from './dateUtils';
2 | export { formatDateTime } from './dateUtils';
3 | export { computeExpirationDays } from './dateUtils';
4 |
5 | export type { FormatFileSizeOptions } from './fileUtils';
6 | export { formatFileSize } from './fileUtils';
7 | export { parseFileSize } from './fileUtils';
8 |
9 | export { splitName } from './stringUtils';
10 | export { convertTransparencyToHex } from './stringUtils';
11 | export { sortFields } from './stringUtils';
12 |
13 | export type { ValidationRule } from './validators';
14 | export { requiredFieldRule } from './validators';
15 | export { validEmailRule } from './validators';
16 | export { validateEmails } from './validators';
17 | export { validateEmailsRule } from './validators';
18 | export { minLengthRule } from './validators';
19 | export { hasSpecialCharRule } from './validators';
20 | export { passwordValidationRule } from './validators';
21 | export { confirmPasswordRule } from './validators';
22 | export { getPasswordChecks } from './validators';
23 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export { default as useFetchContacts } from './contacts/useFetchContacts';
2 |
3 | export { default as useFetchDocuments } from './documents/useFetchDocuments';
4 | export { default as useUploadDocument } from './documents/useUploadDocument';
5 | export { default as useDeleteDocument } from './documents/useDeleteDocument';
6 |
7 | export { default as useCreateLink } from './documents/useCreateLink';
8 |
9 | export { default as useDocumentAnalytics } from './useDocumentAnalytics';
10 | export { default as useDocumentData } from './useDocumentData';
11 | export { default as useDocumentDetail } from './useDocumentDetail';
12 | export { useFormSubmission } from './useFormSubmission';
13 | export { useModal } from './useModal';
14 | export { useSort } from './useSort';
15 | export { useToast } from './useToast';
16 | export { useValidatedFormData } from './useValidatedFormData';
17 | export { default as useDocumentAccess } from './documentAccess/useDocumentAccess';
18 | export { default as useVisitorSubmission } from './documentAccess/useVisitorSubmission';
19 |
--------------------------------------------------------------------------------
/src/components/navigation/NavLink.tsx:
--------------------------------------------------------------------------------
1 | import { Typography, TypographyProps } from '@mui/material';
2 | import NextLink from 'next/link';
3 |
4 | interface NavLinkProps extends TypographyProps {
5 | href: string;
6 | linkText: string;
7 | color?: string;
8 | prefetch?: boolean;
9 | }
10 |
11 | const NavLink = ({
12 | href,
13 | linkText,
14 | color = 'text.brand',
15 | prefetch = false,
16 | ...props
17 | }: NavLinkProps) => {
18 | return (
19 |
25 |
33 |
41 | {linkText}
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default NavLink;
49 |
--------------------------------------------------------------------------------
/src/app/api/profile/route.ts:
--------------------------------------------------------------------------------
1 | import { authService } from '../_services/authService';
2 | import prisma from '@/lib/prisma';
3 | import { NextRequest, NextResponse } from 'next/server';
4 |
5 | export async function GET(req: NextRequest) {
6 | try {
7 | // Authenticate the user
8 | const userId = await authService.authenticate();
9 |
10 | // Get the user’s info from the database
11 | const user = await prisma.user.findUnique({
12 | where: { user_id: userId },
13 | select: {
14 | email: true,
15 | first_name: true,
16 | last_name: true,
17 | },
18 | });
19 |
20 | if (!user) {
21 | return NextResponse.json({ error: 'User not found' }, { status: 404 });
22 | }
23 |
24 | // Return the user’s info
25 | return NextResponse.json(
26 | {
27 | email: user.email,
28 | firstName: user.first_name,
29 | lastName: user.last_name,
30 | },
31 | { status: 200 },
32 | );
33 | } catch (error) {
34 | console.error('Error fetching user info:', error);
35 | return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/shared/config/fileIcons.ts:
--------------------------------------------------------------------------------
1 | import {
2 | PDFIcon,
3 | WordIcon,
4 | ExcelIcon,
5 | PPTIcon,
6 | ZIPIcon,
7 | TextIcon,
8 | ImageIcon,
9 | AudioIcon,
10 | VideoIcon,
11 | GeneralIcon,
12 | } from '@/icons';
13 |
14 | // =========== ENUMS & CONFIGS ===========
15 |
16 | export const FileTypeConfig = {
17 | 'application/pdf': PDFIcon,
18 | 'application/msword': WordIcon,
19 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': WordIcon,
20 | 'application/vnd.ms-excel': ExcelIcon,
21 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ExcelIcon,
22 | 'application/vnd.ms-powerpoint': PPTIcon,
23 | 'application/vnd.openxmlformats-officedocument.presentationml.presentation': PPTIcon,
24 | 'application/zip': ZIPIcon,
25 | 'text/plain': TextIcon,
26 | 'image/png': ImageIcon,
27 | 'image/jpeg': ImageIcon,
28 | 'image/gif': ImageIcon,
29 | 'audio/mpeg': AudioIcon,
30 | 'audio/wav': AudioIcon,
31 | 'video/mp4': VideoIcon,
32 | 'video/x-msvideo': VideoIcon,
33 | General: GeneralIcon,
34 | } as const;
35 |
36 | export type FileType = keyof typeof FileTypeConfig;
37 |
--------------------------------------------------------------------------------
/src/app/documentAccess/[linkId]/components/AccessError.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Box, Typography } from '@mui/material';
4 |
5 | import { LinkBrokenIcon } from '@/icons';
6 |
7 | interface AccessErrorProps {
8 | message: string;
9 | description?: string;
10 | }
11 |
12 | const AccessError: React.FC = (props) => {
13 | return (
14 |
19 |
23 |
29 |
32 | {props.message}
33 |
34 |
35 |
36 |
37 | The link you used is no longer active. If you believe this is an error, please contact the
38 | document owner.
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default AccessError;
46 |
--------------------------------------------------------------------------------
/src/hooks/documents/useCreateLink.ts:
--------------------------------------------------------------------------------
1 | import { CreateDocumentLinkPayload, LinkType } from '@/shared/models';
2 | import { useMutation, useQueryClient } from '@tanstack/react-query';
3 | import axios from 'axios';
4 |
5 | interface CreateLinkParams {
6 | documentId: string;
7 | payload: CreateDocumentLinkPayload;
8 | }
9 |
10 | interface DocumentLinkResponse {
11 | message: string;
12 | link: LinkType;
13 | }
14 |
15 | export default function useCreateLink() {
16 | const queryClient = useQueryClient();
17 |
18 | const mutation = useMutation({
19 | // Returns a promise that resolves to the response data, thus fixing the useFormSubmission issue
20 | mutationFn: async ({
21 | documentId,
22 | payload,
23 | }: CreateLinkParams): Promise => {
24 | const response = await axios.post(`/api/documents/${documentId}/links`, payload);
25 | return response.data;
26 | },
27 | onSuccess: () => {
28 | queryClient.invalidateQueries({ queryKey: ['links'] });
29 | },
30 | });
31 |
32 | return {
33 | mutateAsync: mutation.mutateAsync,
34 | isPending: mutation.isPending,
35 | error: mutation.error,
36 | };
37 | }
38 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | // General Editor Settings
3 | "editor.formatOnSave": true,
4 | "prettier.requireConfig": true,
5 | "prettier.configPath": "./.prettierrc",
6 | // Default Formatters for Supported Languages
7 | "[typescript]": {
8 | "editor.defaultFormatter": "esbenp.prettier-vscode"
9 | },
10 | "[typescriptreact]": {
11 | "editor.defaultFormatter": "esbenp.prettier-vscode"
12 | },
13 | "[javascript]": {
14 | "editor.defaultFormatter": "esbenp.prettier-vscode"
15 | },
16 | "[html]": {
17 | "editor.defaultFormatter": "esbenp.prettier-vscode"
18 | },
19 | "[scss]": {
20 | "editor.defaultFormatter": "esbenp.prettier-vscode"
21 | },
22 | "[css]": {
23 | "editor.defaultFormatter": "esbenp.prettier-vscode"
24 | },
25 | "search.exclude": {
26 | "node_modules": true,
27 | "dist": true,
28 | "build": true,
29 | ".next": true
30 | },
31 | // Disable Automatic Formatting in Large Files
32 | "editor.largeFileOptimizations": true,
33 | // Trim Trailing Whitespace on Save
34 | "files.trimTrailingWhitespace": true,
35 | }
--------------------------------------------------------------------------------
/src/app/api/profile/changeName/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse, NextRequest } from 'next/server';
2 | import prisma from '@/lib/prisma';
3 |
4 | export async function POST(req: NextRequest) {
5 | try {
6 | const { email, firstName, lastName } = await req.json();
7 |
8 | // Check if the user exists
9 | const user = await prisma.user.findUnique({ where: { email } });
10 |
11 | if (!user) {
12 | return NextResponse.json({ error: 'User not found' }, { status: 404 });
13 | }
14 |
15 | const updateData: { first_name?: string; last_name?: string } = {};
16 | if (firstName) {
17 | updateData.first_name = firstName;
18 | updateData.last_name = lastName;
19 | }
20 |
21 | if (Object.keys(updateData).length > 0) {
22 | await prisma.user.update({
23 | where: { email },
24 | data: { first_name: firstName, last_name: lastName },
25 | });
26 | }
27 |
28 | return NextResponse.json({ message: 'Name changed successfully' }, { status: 200 });
29 | } catch (error) {
30 | console.error('Error updating name', error);
31 | return NextResponse.json(
32 | { error: 'An error occurred while updating the name' },
33 | { status: 500 },
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Typography } from '@mui/material';
2 | import Link from 'next/link';
3 |
4 | import { AlertCircleIcon } from '@/icons';
5 |
6 | export default function NotFound() {
7 | return (
8 |
22 |
23 |
27 |
28 |
29 |
32 | Oops! Page Not Found
33 |
34 |
37 | The page you’re looking for doesn’t exist. It might have been removed, or the URL might be
38 | incorrect.
39 |
40 |
43 |
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/auth/check-email/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Box, Typography, Button } from '@mui/material';
3 | import { useRouter } from 'next/navigation';
4 | import AuthFormWrapper from '../components/AuthFormWrapper';
5 | import { BlueWaveLogo } from '@/components';
6 |
7 | export default function VerificationSent() {
8 | const router = useRouter();
9 |
10 | const handleGoToSignIn = () => {
11 | router.push('/auth/sign-in'); // Navigate to sign-in page
12 | };
13 |
14 | return (
15 |
16 |
17 |
21 |
22 |
23 |
26 | 📬 Check Your Inbox!
27 |
28 |
29 |
33 | We've sent you an email with a verification link. Click the link to complete your
34 | registration. If you don't see it in your inbox, please check your spam or junk folder.
35 |
36 |
37 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/app/api/profile/changePassword/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse, NextRequest } from 'next/server';
2 | import prisma from '@/lib/prisma';
3 | import bcryptjs from 'bcryptjs';
4 |
5 | export async function POST(req: NextRequest) {
6 | try {
7 | const { email, currentPassword, newPassword } = await req.json();
8 | const user = await prisma.user.findUnique({
9 | where: { email },
10 | });
11 |
12 | // Check if the user exists
13 | if (!user) {
14 | return NextResponse.json({ error: 'User not found' }, { status: 404 });
15 | }
16 |
17 | const isPasswordValid = await bcryptjs.compare(currentPassword, user.password!);
18 | if (!isPasswordValid) {
19 | return NextResponse.json({ error: 'Current password is incorrect' }, { status: 401 });
20 | }
21 | const hashedPassword = await bcryptjs.hash(newPassword, 10);
22 |
23 | await prisma.user.update({
24 | where: { email },
25 | data: { password: hashedPassword },
26 | });
27 |
28 | return NextResponse.json({ message: 'Password updated successfully' }, { status: 200 });
29 | } catch (error) {
30 | console.error('Error updating password:', error);
31 | return NextResponse.json(
32 | { error: 'An error occurred while updating the password' },
33 | { status: 500 },
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Breadcrumb } from './common/Breadcrumb';
2 | export { default as Toast } from './common/Toast';
3 |
4 | export { default as CustomUploader } from './fileHandling/CustomUploader';
5 | export { default as PDFViewer } from './fileHandling/PDFViewer';
6 |
7 | export { default as CustomCheckbox } from './input/CustomCheckbox';
8 | export { default as Dropdown } from './input/Dropdown';
9 | export { default as FormInput } from './input/FormInput';
10 | export { default as PasswordValidation } from './input/PasswordValidation';
11 |
12 | export { default as BlueWaveLogo } from './layout/BlueWaveLogo';
13 | export { default as DropdownMenu } from './layout/DropdownMenu';
14 | export { default as Sidebar } from './layout/Sidebar';
15 |
16 | export { default as CustomCircularProgress } from './loaders/CustomCircularProgress';
17 | export { default as EmptyState } from './loaders/EmptyState';
18 | export { default as LoadingButton } from './loaders/LoadingButton';
19 | export { default as LoadingSpinner } from './loaders/LoadingSpinner';
20 |
21 | export { default as ModalWrapper } from './modals/ModalWrapper';
22 |
23 | export { default as NavLink } from './navigation/NavLink';
24 | export { default as Paginator } from './navigation/Paginator';
25 |
--------------------------------------------------------------------------------
/src/components/loaders/LoadingButton.tsx:
--------------------------------------------------------------------------------
1 | // components/LoadingButton.tsx
2 | import { Button, CircularProgress, ButtonProps } from '@mui/material';
3 | import { FormEvent } from 'react';
4 |
5 | interface LoadingButtonProps extends ButtonProps {
6 | loading: boolean;
7 | buttonText: string;
8 | loadingText?: string;
9 | fullWidth?: boolean;
10 | onClick?: (event: FormEvent) => void;
11 | type?: 'button' | 'submit' | 'reset';
12 | variant?: 'text' | 'outlined' | 'contained';
13 | color?: 'primary' | 'secondary' | 'error';
14 | }
15 |
16 | const LoadingButton = ({
17 | loading,
18 | buttonText,
19 | loadingText = 'Loading...',
20 | fullWidth = true,
21 | onClick,
22 | type = 'submit',
23 | variant = 'contained',
24 | color = 'primary',
25 | ...props
26 | }: LoadingButtonProps) => {
27 | return (
28 |
41 | ) : null
42 | }
43 | {...props}>
44 | {loading ? loadingText : buttonText}
45 |
46 | );
47 | };
48 |
49 | export default LoadingButton;
50 |
--------------------------------------------------------------------------------
/src/app/documents/components/CustomAccordion.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Accordion,
3 | AccordionDetails,
4 | AccordionProps,
5 | AccordionSummary,
6 | Typography,
7 | } from '@mui/material';
8 |
9 | import { ChevronRightIcon } from '@/icons';
10 |
11 | interface CustomAccordionProps extends AccordionProps {
12 | title: string;
13 | }
14 |
15 | const CustomAccordion = ({ title, children, ...props }: CustomAccordionProps) => {
16 | return (
17 |
20 | }>
33 |
38 | {title}
39 |
40 |
41 | {children}
42 |
43 | );
44 | };
45 |
46 | export default CustomAccordion;
47 |
--------------------------------------------------------------------------------
/src/app/documents/components/DocumentsTableHeader.tsx:
--------------------------------------------------------------------------------
1 | import { TableCell, TableRow, TableSortLabel } from '@mui/material';
2 |
3 | import { DocumentType } from '@/shared/models';
4 |
5 | import { ChevronDownIcon, ChevronSelectorVerticalIcon } from '@/icons';
6 |
7 | interface Props {
8 | orderBy: keyof DocumentType | undefined;
9 | orderDirection: 'asc' | 'desc' | undefined;
10 | onSort: (property: keyof DocumentType) => void;
11 | }
12 |
13 | const DocumentsTableHeader = ({ orderBy, orderDirection, onSort }: Props) => (
14 |
15 |
16 | DOCUMENT
17 |
18 | onSort('uploader')}
22 | hideSortIcon={false}
23 | IconComponent={
24 | orderDirection === undefined ? ChevronSelectorVerticalIcon : ChevronDownIcon
25 | }>
26 | UPLOADER
27 |
28 |
29 | ANALYTICS
30 | LINK
31 | ACTION
32 |
33 | );
34 |
35 | export default DocumentsTableHeader;
36 |
--------------------------------------------------------------------------------
/src/app/documentAccess/[linkId]/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { use } from 'react';
4 |
5 | import { Box, Button, Typography } from '@mui/material';
6 | import AccessPage from './components/AccessPage';
7 |
8 | const LinkIdPage = ({ params }: { params: Promise<{ linkId: string }> }) => {
9 | const { linkId } = use(params);
10 |
11 | const [showFileAccess, setShowFileAccess] = React.useState(false);
12 |
13 | const handleConfirmClick = () => {
14 | setShowFileAccess(true);
15 | };
16 |
17 | return (
18 | <>
19 | {!showFileAccess ? (
20 |
27 |
28 |
31 | A secure file has been shared with you
32 |
33 |
34 | Please confirm your identity to access this document
35 |
36 |
37 |
42 |
43 | ) : (
44 |
45 | )}
46 | >
47 | );
48 | };
49 |
50 | export default LinkIdPage;
51 |
--------------------------------------------------------------------------------
/src/app/providers.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { SessionProvider } from 'next-auth/react';
3 | import { useEffect, useState } from 'react';
4 |
5 | import { CssBaseline, ThemeProvider } from '@mui/material';
6 | import { AppRouterCacheProvider } from '@mui/material-nextjs/v13-appRouter';
7 |
8 | import { LoadingSpinner } from '@/components';
9 | import AuthWrapper from '@/providers/auth/AuthWrapper';
10 | import { ToastProvider } from '@/providers/toast/ToastProvider';
11 | import QueryProvider from '@/providers/query/QueryProvider';
12 |
13 | import globalTheme from '@/theme/globalTheme';
14 |
15 | export default function Providers({ children }: { children: React.ReactNode }) {
16 | const [isHydrated, setIsHydrated] = useState(false);
17 |
18 | useEffect(() => {
19 | setIsHydrated(true);
20 | }, []);
21 |
22 | if (!isHydrated) {
23 | // Show a loading spinner while the client-side is hydrating
24 | return ;
25 | }
26 |
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 | {children}
35 |
36 |
37 |
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/theme/customTypesTheme.d.ts:
--------------------------------------------------------------------------------
1 | import '@mui/material/styles';
2 |
3 | declare module '@mui/material' {
4 | interface Palette {
5 | border: {
6 | light: string;
7 | dark: string;
8 | };
9 | }
10 |
11 | interface SimplePaletteColorOptions {
12 | text?: string;
13 | }
14 |
15 | interface PaletteOptions {
16 | border?: {
17 | light: string;
18 | dark: string;
19 | };
20 | }
21 |
22 | interface ThemeOptions {
23 | shape?: {
24 | borderRadius: number;
25 | borderThick?: number;
26 | boxShadow?: string;
27 | };
28 | customShadows?: {
29 | menu?: string;
30 | };
31 | }
32 | }
33 |
34 | declare module '@mui/material/styles' {
35 | interface TypeBackground {
36 | content: string;
37 | alt: string;
38 | brand: string;
39 | fill: string;
40 | error: string;
41 | }
42 |
43 | interface Theme {
44 | customShadows: {
45 | menu: string;
46 | };
47 | }
48 |
49 | // Extend the Variant type
50 | interface TypographyVariants {
51 | subtitle3: React.CSSProperties;
52 | }
53 |
54 | // Extend the Variant type for theme options
55 | interface TypographyVariantsOptions {
56 | subtitle3?: React.CSSProperties;
57 | }
58 | }
59 |
60 | // For components using Typography
61 | declare module '@mui/material/Typography' {
62 | interface TypographyPropsVariantOverrides {
63 | subtitle3: true;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/hooks/useDocumentDetail.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { useState, useEffect, useCallback } from 'react';
3 |
4 | import { DocumentType } from '@/shared/models';
5 |
6 | interface UseDocumentDetailReturn {
7 | document: DocumentType | null;
8 | isLoading: boolean;
9 | error: string | null;
10 | refetch: () => void;
11 | }
12 |
13 | export default function useDocumentDetail(documentId: string): UseDocumentDetailReturn {
14 | const [document, setDocument] = useState(null);
15 | const [isLoading, setIsLoading] = useState(true);
16 | const [error, setError] = useState(null);
17 |
18 | const fetchDocumentDetails = useCallback(async () => {
19 | if (!documentId) {
20 | setError('No document ID provided.');
21 | setIsLoading(false);
22 | return;
23 | }
24 | try {
25 | setIsLoading(true);
26 | setError(null);
27 |
28 | const response = await axios.get(`/api/documents/${documentId}`);
29 | setDocument(response.data.document);
30 | } catch (err: any) {
31 | setError(err.response?.data?.error || 'Error fetching document details');
32 | } finally {
33 | setIsLoading(false);
34 | }
35 | }, [documentId]);
36 |
37 | useEffect(() => {
38 | fetchDocumentDetails();
39 | }, [fetchDocumentDetails]);
40 |
41 | return { document, isLoading, error, refetch: fetchDocumentDetails };
42 | }
43 |
--------------------------------------------------------------------------------
/src/app/api/documents/[documentId]/visitors/route.ts:
--------------------------------------------------------------------------------
1 | import { authService, DocumentService, createErrorResponse } from '@/app/api/_services';
2 | import { NextRequest, NextResponse } from 'next/server';
3 |
4 | /**
5 | * GET /api/documents/[documentId]/visitors
6 | * Lists visitors across all links for a doc.
7 | */
8 | export async function GET(req: NextRequest, props: { params: Promise<{ documentId: string }> }) {
9 | try {
10 | const userId = await authService.authenticate();
11 | const { documentId } = await props.params;
12 | const linkVisitors = await DocumentService.getDocumentVisitors(userId, documentId);
13 | if (linkVisitors === null) {
14 | return createErrorResponse('Document not found or access denied.', 404);
15 | }
16 |
17 | if (linkVisitors.length === 0) {
18 | return NextResponse.json({ visitors: [] }, { status: 200 });
19 | }
20 |
21 | const visitors = linkVisitors.map((visitor) => ({
22 | id: visitor.id,
23 | documentId: documentId,
24 | name: `${visitor.firstName} ${visitor.lastName}`.trim(),
25 | email: visitor.email,
26 | lastActivity: visitor.updatedAt,
27 | downloads: 0,
28 | duration: 0,
29 | completion: 0,
30 | }));
31 |
32 | return NextResponse.json({ visitors }, { status: 200 });
33 | } catch (error) {
34 | return createErrorResponse('Server error while fetching visitors.', 500, error);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/icons/charts/BarChartIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface BarChartIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | }
8 |
9 | /**
10 | * A reusable SVG icon component for rendering an icon.
11 | *
12 | * @param {number} [width=14] - The width of the icon in pixels. Optional.
13 | * @param {number} [height=14] - The height of the icon in pixels. Optional.
14 | * @param {string} [color='#939393'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
15 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
16 | *
17 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
18 | */
19 |
20 | const BarChartIcon: FC = ({
21 | width = 14,
22 | height = 14,
23 | color = '#939393',
24 | ...props
25 | }) => {
26 | return (
27 |
43 | );
44 | };
45 |
46 | export default BarChartIcon;
47 |
--------------------------------------------------------------------------------
/src/icons/shapes/SquareIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface SquareIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | }
8 |
9 | /**
10 | * A reusable SVG icon component for rendering an icon.
11 | *
12 | * @param {number} [width=20] - The width of the icon in pixels. Optional.
13 | * @param {number} [height=20] - The height of the icon in pixels. Optional.
14 | * @param {string} [color='#98A2B3'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
15 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
16 | *
17 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
18 | */
19 |
20 | const SquareIcon: FC = ({
21 | width = 20,
22 | height = 20,
23 | color = '#98A2B3',
24 | ...props
25 | }) => {
26 | return (
27 |
41 | );
42 | };
43 |
44 | export default SquareIcon;
45 |
--------------------------------------------------------------------------------
/src/app/api/public_links/[linkId]/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { LinkService, createErrorResponse } from '@/app/api/_services';
3 |
4 | /**
5 | * GET /api/public_links/[linkId]
6 | */
7 | export async function GET(req: NextRequest, props: { params: Promise<{ linkId: string }> }) {
8 | try {
9 | const { linkId } = await props.params;
10 | if (!linkId) {
11 | return createErrorResponse('Link ID is required.', 400);
12 | }
13 |
14 | const link = await LinkService.getPublicLink(linkId);
15 | console.log('🚀 ~ GET ~ link:', link);
16 | console.log('Link ID:', linkId);
17 | if (!link) {
18 | console.log(`Link not found: ${linkId}`);
19 | return NextResponse.json({ message: 'Link not found' }, { status: 404 });
20 | }
21 |
22 | // Check expiration
23 | if (link.expirationTime && new Date(link.expirationTime) <= new Date()) {
24 | return NextResponse.json({ message: 'Link is expired' }, { status: 410 });
25 | }
26 |
27 | // If link is public, we see if it requires user details or password
28 | const isPasswordProtected = !!link.password;
29 | const visitorFields = link.visitorFields;
30 |
31 | return NextResponse.json(
32 | {
33 | message: 'Link is valid',
34 | data: {
35 | isPasswordProtected,
36 | visitorFields,
37 | },
38 | },
39 | { status: 200 },
40 | );
41 | } catch (error) {
42 | return createErrorResponse('Server error while fetching link.', 500, error);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/icons/arrows/ArrowNarrowLeftIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface ArrowNarrowLeftIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | }
8 |
9 | /**
10 | * A reusable SVG icon component for rendering an icon.
11 | *
12 | * @param {number} [width=14] - The width of the icon in pixels. Optional.
13 | * @param {number} [height=14] - The height of the icon in pixels. Optional.
14 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
15 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
16 | *
17 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
18 | */
19 |
20 | const ArrowNarrowLeftIcon: FC = ({
21 | width = 14,
22 | height = 14,
23 | color = '#667085',
24 | ...props
25 | }) => {
26 | return (
27 |
43 | );
44 | };
45 |
46 | export default ArrowNarrowLeftIcon;
47 |
--------------------------------------------------------------------------------
/src/icons/arrows/ArrowNarrowRightIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface ArrowNarrowRightIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | }
8 |
9 | /**
10 | * A reusable SVG icon component for rendering an icon.
11 | *
12 | * @param {number} [width=14] - The width of the icon in pixels. Optional.
13 | * @param {number} [height=14] - The height of the icon in pixels. Optional.
14 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
15 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
16 | *
17 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
18 | */
19 |
20 | const ArrowNarrowRightIcon: FC = ({
21 | width = 14,
22 | height = 14,
23 | color = '#667085',
24 | ...props
25 | }) => {
26 | return (
27 |
43 | );
44 | };
45 |
46 | export default ArrowNarrowRightIcon;
47 |
--------------------------------------------------------------------------------
/src/components/loaders/CustomCircularProgress.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Box, CircularProgress, CircularProgressProps, Typography } from '@mui/material';
4 | import { useEffect } from 'react';
5 |
6 | const CircularProgressWithLabel = (props: CircularProgressProps & { value: number }) => {
7 | return (
8 |
9 |
13 |
24 | {`${Math.round(props.value)}%`}
28 |
29 |
30 | );
31 | };
32 |
33 | interface CustomCircularProgressProps {
34 | progress: number;
35 | handleProgress: React.Dispatch>;
36 | }
37 |
38 | const CustomCircularProgress = ({ progress, handleProgress }: CustomCircularProgressProps) => {
39 | useEffect(() => {
40 | const timer = setInterval(() => {
41 | handleProgress((prevProgress) => (prevProgress >= 100 ? 0 : prevProgress + 10));
42 | }, 800);
43 | return () => {
44 | clearInterval(timer);
45 | };
46 | }, [handleProgress]);
47 |
48 | return ;
49 | };
50 |
51 | export default CustomCircularProgress;
52 |
--------------------------------------------------------------------------------
/src/icons/general/CheckIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface CheckIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=18] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=15] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=1] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const CheckIcon: FC = ({
23 | width = 18,
24 | height = 15,
25 | color = '#667085',
26 | strokeWidth = 1,
27 | ...props
28 | }) => {
29 | return (
30 |
47 | );
48 | };
49 |
50 | export default CheckIcon;
51 |
--------------------------------------------------------------------------------
/src/components/input/Dropdown.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { MenuItem, Select, SelectChangeEvent, SelectProps } from '@mui/material';
4 | import { useState } from 'react';
5 |
6 | interface Props {
7 | options: { value: string; label: string }[];
8 | initialValue: string;
9 | onValueChange?: (newValue: string) => void;
10 | isSelectFullWidth?: boolean;
11 | px?: number;
12 | py?: number;
13 | minWidth?: number;
14 | variant?: SelectProps['variant'];
15 | }
16 |
17 | const Dropdown = ({
18 | options,
19 | initialValue,
20 | onValueChange,
21 | variant,
22 | isSelectFullWidth = false,
23 | minWidth = 195,
24 | px = 8,
25 | py = 4,
26 | }: Props) => {
27 | const [value, setValue] = useState(initialValue);
28 |
29 | const handleChange = (event: SelectChangeEvent) => {
30 | const newValue = event.target.value as string;
31 | setValue(newValue);
32 |
33 | if (onValueChange) {
34 | onValueChange(newValue);
35 | }
36 | };
37 |
38 | return (
39 |
60 | );
61 | };
62 |
63 | export default Dropdown;
64 |
--------------------------------------------------------------------------------
/src/icons/general/XCloseIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface XCloseIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=36] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=36] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#98A2B3'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=1.66667] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const XCloseIcon: FC = ({
23 | width = 36,
24 | height = 36,
25 | color = '#98A2B3',
26 | strokeWidth = 1.66667,
27 | ...props
28 | }) => {
29 | return (
30 |
47 | );
48 | };
49 |
50 | export default XCloseIcon;
51 |
--------------------------------------------------------------------------------
/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | // import { PrismaClient } from '@prisma/client';
2 |
3 | // const prisma = new PrismaClient();
4 |
5 | // import { getLinkData } from '../src/seed/link';
6 | // import { getLinkVisitorData } from '../src/seed/linkVisitor';
7 | // import { getDocumentData } from '../src/seed/document';
8 | // import { getUserData } from '../src/seed/user';
9 |
10 | // async function main() {
11 | // console.log('Seeding database...');
12 |
13 | // // 1) Seed USERS
14 | // const usersData = await getUserData();
15 | // await prisma.user.createMany({ data: usersData });
16 | // console.log(`Seeded ${usersData.length} users.`);
17 |
18 | // // 2) Seed DOCUMENTS
19 | // const documentsData = getDocumentData();
20 | // await prisma.document.createMany({ data: documentsData });
21 | // console.log(`Seeded ${documentsData.length} documents.`);
22 |
23 | // // 3) Seed LINKS
24 | // const linksData = getLinkData();
25 | // await prisma.link.createMany({ data: linksData });
26 | // console.log(`Seeded ${linksData.length} links.`);
27 |
28 | // // 4) Seed LINK VISITORS
29 | // const linkVisitorsData = getLinkVisitorData();
30 | // await prisma.linkVisitors.createMany({ data: linkVisitorsData });
31 | // console.log(`Seeded ${linkVisitorsData.length} link visitors.`);
32 |
33 | // console.log('Seeding completed successfully.');
34 | // }
35 |
36 | // main()
37 | // .catch((error) => {
38 | // console.error('Error during seeding:', error);
39 | // process.exit(1);
40 | // })
41 | // .finally(async () => {
42 | // await prisma.$disconnect();
43 | // });
44 |
--------------------------------------------------------------------------------
/src/icons/arrows/ChevronUpIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface ChevronUpIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=20] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=20] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=1.66667] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const ChevronUpIcon: FC = ({
23 | width = 20,
24 | height = 20,
25 | color = '#667085',
26 | strokeWidth = 1.66667,
27 | ...props
28 | }) => {
29 | return (
30 |
47 | );
48 | };
49 |
50 | export default ChevronUpIcon;
51 |
--------------------------------------------------------------------------------
/src/icons/arrows/ChevronDownIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface ChevronDownIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=15] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=15] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#9A9B9B'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=1.66667] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const ChevronDownIcon: FC = ({
23 | width = 15,
24 | height = 15,
25 | color = '#9A9B9B',
26 | strokeWidth = 1.66667,
27 | ...props
28 | }) => {
29 | return (
30 |
47 | );
48 | };
49 |
50 | export default ChevronDownIcon;
51 |
--------------------------------------------------------------------------------
/src/icons/arrows/ChevronRightIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface ChevronRightIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=20] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=20] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=1.66667] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const ChevronRightIcon: FC = ({
23 | width = 20,
24 | height = 20,
25 | color = '#667085',
26 | strokeWidth = 1.66667,
27 | ...props
28 | }) => {
29 | return (
30 |
47 | );
48 | };
49 |
50 | export default ChevronRightIcon;
51 |
--------------------------------------------------------------------------------
/src/hooks/useDocumentData.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import axios from 'axios';
4 | import { useCallback, useEffect, useState } from 'react';
5 |
6 | import { Contact, LinkDetail } from '@/shared/models';
7 |
8 | interface UseDocumentDataReturn {
9 | data: LinkDetail[] | Contact[];
10 | loading: boolean;
11 | error: string | null;
12 | refetch: () => void;
13 | }
14 |
15 | export default function useDocumentData(
16 | documentId: string,
17 | variant: 'linkTable' | 'visitorTable',
18 | ): UseDocumentDataReturn {
19 | const [data, setData] = useState([]);
20 | const [loading, setLoading] = useState(true);
21 | const [error, setError] = useState(null);
22 |
23 | const fetchData = useCallback(async () => {
24 | if (!documentId) {
25 | setError('No document ID provided');
26 | setLoading(false);
27 | return;
28 | }
29 | try {
30 | setLoading(true);
31 | setError(null);
32 |
33 | const url =
34 | variant === 'linkTable'
35 | ? `/api/documents/${documentId}/links`
36 | : `/api/documents/${documentId}/visitors`;
37 |
38 | const res = await axios.get(url);
39 | if (variant === 'linkTable') {
40 | setData(res.data.links || []);
41 | } else {
42 | setData(res.data.visitors || []);
43 | }
44 | } catch (err: any) {
45 | setError(err.response?.data?.error || 'Error fetching data');
46 | } finally {
47 | setLoading(false);
48 | }
49 | }, [documentId, variant]);
50 |
51 | useEffect(() => {
52 | fetchData();
53 | }, [fetchData]);
54 |
55 | return { data, loading, error, refetch: fetchData };
56 | }
57 |
--------------------------------------------------------------------------------
/src/icons/alerts/AlertCircleIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface AlertCircleIconProps extends SVGProps {
4 | color?: string;
5 | strokeWidth?: number;
6 | }
7 |
8 | /**
9 | * A reusable SVG icon component for rendering an icon.
10 | *
11 | * @param {number} [width=80] - The width of the icon in pixels. Optional.
12 | * @param {number} [height=80] - The height of the icon in pixels. Optional.
13 | * @param {string} [color='#db504a'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
14 | * @param {number} [strokeWidth=2] - The stroke width of the icon's path. Optional.
15 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
16 | *
17 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
18 | */
19 |
20 | const AlertCircleIcon: FC = ({
21 | width = 80,
22 | height = 80,
23 | color = '#db504a',
24 | strokeWidth = 2,
25 | ...props
26 | }) => {
27 | return (
28 |
45 | );
46 | };
47 |
48 | export default AlertCircleIcon;
49 |
--------------------------------------------------------------------------------
/src/app/api/public_links/[linkId]/access/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 |
3 | import { createErrorResponse, LinkService } from '@/app/api/_services';
4 |
5 | /**
6 | * POST /api/public_links/[linkId]/access
7 | */
8 | export async function POST(req: NextRequest, props: { params: Promise<{ linkId: string }> }) {
9 | try {
10 | const { linkId } = await props.params;
11 | const { firstName, lastName, email, password } = await req.json();
12 |
13 | // 1) Retrieve link
14 | const link = await LinkService.getPublicLink(linkId);
15 | if (!link) {
16 | return createErrorResponse('Link not found', 404);
17 | }
18 |
19 | // 2) Check expiration
20 | if (link.expirationTime && new Date(link.expirationTime) <= new Date()) {
21 | return createErrorResponse('Link is expired', 410);
22 | }
23 |
24 | // 3) If password is required, verify
25 | const passwordOk = await LinkService.verifyLinkPassword(link, password);
26 | if (!passwordOk) {
27 | return createErrorResponse('Invalid password', 401);
28 | }
29 |
30 | // 4) Log visitor.
31 | await LinkService.logVisitor(linkId, firstName, lastName, email);
32 |
33 | // 5) Get a signed URL for the doc
34 | try {
35 | const { fileName, signedUrl, size } = await LinkService.getSignedFileFromLink(linkId);
36 | return NextResponse.json({
37 | message: 'File access granted',
38 | data: { signedUrl, fileName, size },
39 | });
40 | } catch (err) {
41 | return createErrorResponse('Error retrieving file', 400, err);
42 | }
43 | } catch (error) {
44 | return createErrorResponse('Server error while accessing link', 500, error);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/scripts/prepare-env.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const dotenv = require('dotenv');
4 |
5 | const cliArg = process.argv.find((arg) => arg.startsWith('--env='));
6 | const cliEnv = cliArg ? cliArg.split('=')[1] : null;
7 |
8 | const DEPLOYMENT_ENV = cliEnv || process.env.DEPLOYMENT_ENVIRONMENT || 'local';
9 |
10 | const rootDir = path.resolve(__dirname, '..');
11 | const envDir = path.join(rootDir, 'env');
12 | const outputEnvPath = path.join(rootDir, '.env');
13 |
14 | function loadEnv(filePath) {
15 | if (!fs.existsSync(filePath)) return {};
16 | const result = dotenv.parse(fs.readFileSync(filePath));
17 | return result;
18 | }
19 |
20 | function mergeEnvs(...envObjects) {
21 | return Object.assign({}, ...envObjects);
22 | }
23 |
24 | function writeEnvFile(envObj, targetPath) {
25 | const contents = Object.entries(envObj)
26 | .map(([key, val]) => `${key}="${val.replace(/"/g, '\\"')}"`)
27 | .join('\n');
28 | fs.writeFileSync(targetPath, contents);
29 | console.log(`✅ Generated .env from DEPLOYMENT_ENVIRONMENT=${DEPLOYMENT_ENV}`);
30 | }
31 |
32 | function run() {
33 | let envConfig = {};
34 |
35 | switch (DEPLOYMENT_ENV) {
36 | case 'production':
37 | envConfig = loadEnv(path.join(envDir, '.env.production'));
38 | break;
39 | case 'development':
40 | envConfig = loadEnv(path.join(envDir, '.env.development'));
41 | break;
42 | case 'local':
43 | default:
44 | const dev = loadEnv(path.join(envDir, '.env.development'));
45 | const local = loadEnv(path.join(envDir, '.env.local'));
46 | envConfig = mergeEnvs(dev, local); // local overrides dev
47 | break;
48 | }
49 |
50 | writeEnvFile(envConfig, outputEnvPath);
51 | }
52 |
53 | run();
54 |
--------------------------------------------------------------------------------
/src/icons/general/CheckSquareIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface CheckSquareIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=20] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=20] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='white'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=2] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const CheckSquareIcon: FC = ({
23 | width = 20,
24 | height = 20,
25 | color = 'white',
26 | strokeWidth = 2,
27 | ...props
28 | }) => {
29 | return (
30 |
51 | );
52 | };
53 |
54 | export default CheckSquareIcon;
55 |
--------------------------------------------------------------------------------
/src/providers/toast/ToastProvider.tsx:
--------------------------------------------------------------------------------
1 | import { Toast } from '@/components';
2 | import { ToastMessage } from './toastTypes';
3 |
4 | import React, { createContext, useContext, useState, ReactNode } from 'react';
5 |
6 | interface ToastContextValue {
7 | showToast: (toast: Omit) => void;
8 | }
9 |
10 | const ToastContext = createContext(undefined);
11 |
12 | export const ToastProvider = ({ children }: { children: ReactNode }) => {
13 | const [toasts, setToasts] = useState([]);
14 |
15 | const showToast = (toast: Omit) => {
16 | const id = Math.random().toString(36).substr(2, 9); // Generate a unique ID
17 | setToasts((prev) => [...prev, { id, ...toast }]);
18 |
19 | // Auto-remove toast after a duration
20 | if (toast.autoHide !== false) {
21 | setTimeout(() => removeToast(id), 6000);
22 | }
23 | };
24 |
25 | const removeToast = (id: string) => {
26 | setToasts((prev) => prev.filter((toast) => toast.id !== id));
27 | };
28 |
29 | return (
30 |
31 | {children}
32 | {/* Render active toasts */}
33 | {toasts.map(({ id, message, variant, autoHide }, index) => (
34 | removeToast(id)}
40 | autoHide={autoHide}
41 | index={index}
42 | />
43 | ))}
44 |
45 | );
46 | };
47 |
48 | export const useToastContext = () => {
49 | const context = useContext(ToastContext);
50 | if (!context) {
51 | throw new Error('useToastContext must be used within a ToastProvider');
52 | }
53 | return context;
54 | };
55 |
--------------------------------------------------------------------------------
/src/app/documents/components/LinkDetailsAccordion.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Typography } from '@mui/material';
2 | import React from 'react';
3 |
4 | import { CustomCheckbox, FormInput } from '@/components';
5 |
6 | interface Props {
7 | formValues: any;
8 | handleInputChange: (event: React.ChangeEvent) => void;
9 | }
10 |
11 | const LinkDetailsAccordion = ({ formValues, handleInputChange }: Props) => {
12 | return (
13 |
14 | {/* Link URL
15 |
18 | This is an automatically generated link address.
19 |
20 |
21 |
25 | {}}
30 | placeholder=''
31 | />
32 |
33 |
34 |
35 | */}
36 |
37 |
42 | Link Alias
43 |
51 |
52 |
59 |
60 |
61 | );
62 | };
63 |
64 | export default LinkDetailsAccordion;
65 |
--------------------------------------------------------------------------------
/src/icons/files/FileIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface FileIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | }
8 |
9 | /**
10 | * A reusable SVG icon component for rendering an icon.
11 | *
12 | * @param {number} [width=24] - The width of the icon in pixels. Optional.
13 | * @param {number} [height=24] - The height of the icon in pixels. Optional.
14 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
15 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
16 | *
17 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
18 | */
19 |
20 | const FileIcon: FC = ({ width = 24, height = 24, color = '#667085', ...props }) => {
21 | return (
22 |
38 | );
39 | };
40 |
41 | export default FileIcon;
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blw-datahall",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "predev": "node scripts/prepare-env.js --env=local",
7 | "dev": "next dev",
8 | "build": "next build",
9 | "start": "next start",
10 | "lint": "next lint",
11 | "prepare-env": "node scripts/prepare-env.js",
12 | "prisma:generate": "npm run prepare-env && npx prisma generate",
13 | "prisma:migrate": "npm run prepare-env && npx prisma migrate dev",
14 | "prisma:studio": "npm run prepare-env && npx prisma studio"
15 | },
16 | "dependencies": {
17 | "@emotion/cache": "^11.14.0",
18 | "@emotion/react": "^11.14.0",
19 | "@emotion/styled": "^11.14.0",
20 | "@mui/icons-material": "^6.4.6",
21 | "@mui/material": "^6.4.6",
22 | "@mui/material-nextjs": "^6.4.3",
23 | "@mui/x-charts": "^7.27.1",
24 | "@prisma/client": "^6.4.1",
25 | "@supabase/supabase-js": "^2.49.1",
26 | "@tanstack/react-query": "^5.66.11",
27 | "@tanstack/react-query-devtools": "^5.66.11",
28 | "axios": "^1.8.1",
29 | "bcryptjs": "^3.0.2",
30 | "canvas": "^3.1.0",
31 | "date-fns": "^4.1.0",
32 | "dotenv": "^16.4.7",
33 | "next": "^15.2.0",
34 | "next-auth": "^4.24.11",
35 | "pdfjs-dist": "^4.10.38",
36 | "react": "^19.0.0",
37 | "react-color": "^2.19.3",
38 | "react-dom": "^19.0.0",
39 | "react-dropzone": "^14.3.8",
40 | "resend": "^4.1.2"
41 | },
42 | "devDependencies": {
43 | "@types/bcryptjs": "^2.4.6",
44 | "@types/node": "^22.13.5",
45 | "@types/react": "^19.0.10",
46 | "@types/react-color": "^3.0.13",
47 | "@types/react-dom": "^19.0.4",
48 | "eslint": "^9.21.0",
49 | "eslint-config-next": "^15.2.0",
50 | "prettier": "^3.5.2",
51 | "prisma": "^6.4.1",
52 | "ts-node": "^10.9.2",
53 | "tsx": "^4.19.3",
54 | "typescript": "^5.7.3"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/icons/general/LogOutIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface LogOutIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=16] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=16] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=1.5] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const LogOutIcon: FC = ({
23 | width = 16,
24 | height = 16,
25 | color = '#667085',
26 | strokeWidth = 1.5,
27 | ...props
28 | }) => {
29 | return (
30 |
47 | );
48 | };
49 |
50 | export default LogOutIcon;
51 |
--------------------------------------------------------------------------------
/src/components/navigation/Paginator.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Pagination, PaginationItem } from '@mui/material';
2 |
3 | import { ArrowNarrowLeftIcon, ArrowNarrowRightIcon } from '@/icons';
4 |
5 | interface PaginatorProps {
6 | page: number;
7 | totalPages: number;
8 | onPageChange: (newPage: number) => void;
9 | pageSize: number;
10 | totalItems: number;
11 | }
12 |
13 | const Paginator = ({ page, totalPages, onPageChange, pageSize, totalItems }: PaginatorProps) => {
14 | const handlePageChange = (_event: any, newPage: number) => {
15 | onPageChange(newPage);
16 | };
17 |
18 | if (totalItems <= pageSize) {
19 | return null;
20 | }
21 |
22 | return (
23 |
29 | }
33 | onClick={() => onPageChange(page > 1 ? page - 1 : page)}
34 | disabled={page === 1}
35 | sx={{ minWidth: '8rem' }}>
36 | Previous
37 |
38 |
39 | (
49 |
53 | )}
54 | />
55 | }
59 | onClick={() => onPageChange(page < totalPages ? page + 1 : page)}
60 | disabled={page === totalPages}
61 | sx={{ minWidth: '8rem' }}>
62 | Next
63 |
64 |
65 | );
66 | };
67 |
68 | export default Paginator;
69 |
--------------------------------------------------------------------------------
/src/icons/files/VideoIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface VideoIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | }
7 |
8 | /**
9 | * A reusable SVG icon component for rendering an icon.
10 | *
11 | * @param {number} [width=25] - The width of the icon in pixels. Optional.
12 | * @param {number} [height=25] - The height of the icon in pixels. Optional.
13 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
14 | *
15 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
16 | */
17 |
18 | const VideoIcon: FC = ({ width = 25, height = 25, ...props }) => {
19 | return (
20 |
36 | );
37 | };
38 |
39 | export default VideoIcon;
40 |
--------------------------------------------------------------------------------
/src/icons/general/MenuIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface MenuIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=20] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=20] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#344054ab'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=2] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const MenuIcon: FC = ({
23 | width = 20,
24 | height = 20,
25 | color = '#344054ab',
26 | strokeWidth = 2,
27 | ...props
28 | }) => {
29 | return (
30 |
68 | );
69 | };
70 |
71 | export default MenuIcon;
72 |
--------------------------------------------------------------------------------
/src/icons/general/UploadCloudIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface UploadCloudIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=21] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=20] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#344054'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=1.66667] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const UploadCloudIcon: FC = ({
23 | width = 21,
24 | height = 20,
25 | color = '#344054',
26 | strokeWidth = 1.66667,
27 | ...props
28 | }) => (
29 |
46 | );
47 |
48 | export default UploadCloudIcon;
49 |
--------------------------------------------------------------------------------
/src/icons/users/UserIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface UserIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=24] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=24] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=1] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const UserIcon: FC = ({
23 | width = 24,
24 | height = 24,
25 | color = '#667085',
26 | strokeWidth = 1,
27 | ...props
28 | }) => {
29 | return (
30 |
47 | );
48 | };
49 |
50 | export default UserIcon;
51 |
--------------------------------------------------------------------------------
/src/app/documents/components/NewLinkDialog.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Box, Chip, Dialog, DialogContent, DialogTitle, IconButton } from '@mui/material';
4 |
5 | import { CheckIcon, CopyIcon, LinkIcon } from '@/icons';
6 |
7 | interface NewLinkDialogProps {
8 | linkUrl: string;
9 | onClose: () => void;
10 | }
11 |
12 | export default function NewLinkDialog({ linkUrl, onClose }: NewLinkDialogProps) {
13 | const [isLinkCopied, setIsLinkCopied] = React.useState(false);
14 |
15 | function handleLinkCopy() {
16 | if (linkUrl) {
17 | navigator.clipboard.writeText(linkUrl);
18 | setIsLinkCopied(true);
19 | setTimeout(() => setIsLinkCopied(false), 3000);
20 | }
21 | }
22 |
23 | const open = Boolean(linkUrl);
24 |
25 | return (
26 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/src/icons/general/LinkBrokenIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface LinkBrokenIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=24] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=24] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#FF4747'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=2] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const LinkBrokenIcon: FC = ({
23 | width = 24,
24 | height = 24,
25 | color = '#FF4747',
26 | strokeWidth = 2,
27 | ...props
28 | }) => {
29 | return (
30 |
47 | );
48 | };
49 |
50 | export default LinkBrokenIcon;
51 |
--------------------------------------------------------------------------------
/src/app/api/auth/register/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { authService } from '@/app/api/_services/authService';
3 | // import { emailService } from '../../_services/emailService';
4 |
5 | export async function POST(req: NextRequest) {
6 | try {
7 | const { email, password, firstName, lastName, role } = await req.json();
8 |
9 | const result = await authService.registerUser({
10 | email,
11 | password,
12 | firstName,
13 | lastName,
14 | role,
15 | });
16 |
17 | if (!result.success) {
18 | return NextResponse.json({ message: result.message }, { status: 409 });
19 | }
20 |
21 | // Attempt to send verification email
22 | const verificationLink = `${process.env.APP_PROTOCOL}://${process.env.APP_DOMAIN}/auth/account-created/?token=${result.verificationToken}`;
23 |
24 | // const emailResp = await emailService.sendVerificationEmail({
25 | // toEmail: email,
26 | // username: firstName,
27 | // verificationLink,
28 | // });
29 |
30 | // if (!emailResp.success) {
31 | // // Partial success: user was created but email sending failed
32 | // console.error('[register] Email sending failed:', emailResp.error);
33 | // return NextResponse.json(
34 | // {
35 | // success: true,
36 | // emailFail: true,
37 | // userId: result.userId,
38 | // message: 'User created, but verification email failed. Please contact admin.',
39 | // },
40 | // { status: 200 },
41 | // );
42 | // }
43 |
44 | // Full success
45 | return NextResponse.json(
46 | {
47 | success: true,
48 | message: 'Verification email sent. Please check your inbox.',
49 | token: result.verificationToken,
50 | },
51 | { status: 200 },
52 | );
53 | } catch (error) {
54 | console.error('[register] Error creating user:', error);
55 | return NextResponse.json({ message: 'Internal server error' }, { status: 500 });
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/shared/config/routesConfig.ts:
--------------------------------------------------------------------------------
1 | // THIS IS NOT FUNCTIONAL YET, JUST A TEMPLATE
2 |
3 | /**
4 | * A list of public route patterns, expressed as regex strings.
5 | *
6 | * Explanation:
7 | * - '^auth/sign-up$' means exactly "/auth/sign-up".
8 | * (We remove the leading slash when matching, see isPublicRoute below.)
9 | * - '^auth/reset-password/.+' means any path that starts with /auth/reset-password/
10 | * and then has additional segments.
11 | * - '^links/[a-f0-9-]{36}' matches "/links/uuid" where uuid is a 36-char hex string.
12 | */
13 | const PUBLIC_ROUTE_PATTERNS = [
14 | '^auth/sign-up$',
15 | '^auth/forgot-password$',
16 | '^auth/reset-password$',
17 | '^auth/account-created$',
18 | '^auth/password-reset-confirm$',
19 | '^auth/check-email$',
20 | '^links/[a-f0-9-]{36}', // dynamic link route
21 | '^auth/reset-password/.+', // dynamic reset-password route
22 | ];
23 |
24 | /**
25 | * Checks if a given pathname is public by testing against the regex patterns.
26 | *
27 | * @param pathname - the current route path (e.g. "/auth/sign-up" or "/documents/123").
28 | */
29 | export function isPublicRoute(pathname: string): boolean {
30 | // Remove leading slash (e.g. "/auth/sign-up" => "auth/sign-up")
31 | const path = pathname.startsWith('/') ? pathname.slice(1) : pathname;
32 |
33 | return PUBLIC_ROUTE_PATTERNS.some((pattern) => {
34 | const regex = new RegExp(pattern, 'i'); // 'i' = case-insensitive
35 | return regex.test(path);
36 | });
37 | }
38 |
39 | /**
40 | * Generates one big negative-lookahead pattern for NextAuth middleware.
41 | *
42 | * For example, if PUBLIC_ROUTE_PATTERNS = ["^auth/sign-up$", "^links/[a-f0-9-]{36}"],
43 | * we'll produce something like:
44 | * ^/(?!(auth/sign-up$|links/[a-f0-9-]{36})).*
45 | *
46 | * This means: "Match anything that does NOT start with one of these patterns."
47 | */
48 | export const NEGATIVE_LOOKAHEAD_REGEX = `^/(?!(${PUBLIC_ROUTE_PATTERNS.join('|')})).*`;
49 |
--------------------------------------------------------------------------------
/src/icons/users/UsersIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface UsersIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=16] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=16] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=2.2] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const UsersIcon: FC = ({
23 | width = 16,
24 | height = 16,
25 | color = '#667085',
26 | strokeWidth = 2.2,
27 | ...props
28 | }) => {
29 | return (
30 |
47 | );
48 | };
49 |
50 | export default UsersIcon;
51 |
--------------------------------------------------------------------------------
/src/components/input/PasswordValidation.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 |
3 | import { Box, Typography } from '@mui/material';
4 |
5 | import { CheckCircleIcon, XCircleIcon } from '@/icons';
6 |
7 | import { getPasswordChecks } from '@/shared/utils';
8 |
9 | interface PasswordValidationProps {
10 | passwordValue: string;
11 | isBlur?: boolean;
12 | }
13 |
14 | const PasswordValidation: FC = ({ passwordValue, isBlur }) => {
15 | const { isLengthValid, hasUppercaseLetter, hasSymbol } = getPasswordChecks(passwordValue);
16 |
17 | return (
18 |
23 | {/* Has at least 8 characters */}
24 |
28 | {passwordValue && !isLengthValid && isBlur ? (
29 |
30 | ) : (
31 |
32 | )}
33 | Must be at least 8 characters
34 |
35 |
36 | {/* Has at least one uppercase letter */}
37 |
41 | {passwordValue && !hasUppercaseLetter && isBlur ? (
42 |
43 | ) : (
44 |
45 | )}
46 | Must contain at least one uppercase letter.
47 |
48 |
49 | {/* Has at least one symbol */}
50 |
54 | {passwordValue && !hasSymbol && isBlur ? (
55 |
56 | ) : (
57 |
58 | )}
59 | Must Include at least one symbol.
60 |
61 |
62 | );
63 | };
64 |
65 | export default PasswordValidation;
66 |
--------------------------------------------------------------------------------
/src/icons/general/LinkIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface LinkIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | disabled?: boolean;
9 | }
10 |
11 | /**
12 | * A reusable SVG icon component for rendering an icon.
13 | *
14 | * @param {number} [width=16] - The width of the icon in pixels. Optional.
15 | * @param {number} [height=16] - The height of the icon in pixels. Optional.
16 | * @param {string} [color='#344054'] - The stroke color of the icon when it is active. Accepts any valid CSS color value. Optional.
17 | * @param {number} [strokeWidth=2] - The stroke width of the icon's path. Optional.
18 | * @param {boolean} [disabled=false] - Indicates whether the icon is in a disabled state. Optional.
19 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
20 | *
21 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
22 | */
23 |
24 | const LinkIcon: FC = ({
25 | width = 16,
26 | height = 16,
27 | color = '#344054',
28 | strokeWidth = 2,
29 | disabled = false,
30 | ...props
31 | }) => {
32 | return (
33 |
50 | );
51 | };
52 |
53 | export default LinkIcon;
54 |
--------------------------------------------------------------------------------
/src/icons/general/TrashIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface TrashIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=19] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=20] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=1.5] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const TrashIcon: FC = ({
23 | width = 19,
24 | height = 20,
25 | color = '#667085',
26 | strokeWidth = 1.5,
27 | ...props
28 | }) => {
29 | return (
30 |
47 | );
48 | };
49 |
50 | export default TrashIcon;
51 |
--------------------------------------------------------------------------------
/src/icons/general/XCircleIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | type IconColor = 'error' | 'success' | 'disabled' | 'outline';
4 |
5 | const colorMap: Record = {
6 | error: '#F04438',
7 | success: '#067647',
8 | disabled: '#D0D5DD',
9 | outline: '#344054',
10 | };
11 |
12 | interface XCircleIconProps extends SVGProps {
13 | width?: number;
14 | height?: number;
15 | color?: IconColor;
16 | }
17 |
18 | /**
19 | * A reusable SVG icon component for rendering an icon.
20 | *
21 | * @param {number} [width=24] - The width of the icon in pixels. Optional.
22 | * @param {number} [height=24] - The height of the icon in pixels. Optional.
23 | * @param {IconColor} [color='disabled'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
24 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
25 | *
26 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
27 | */
28 |
29 | const XCircleIcon: FC = ({
30 | width = 24,
31 | height = 24,
32 | color = 'disabled',
33 | ...props
34 | }) => {
35 | const isOutline = color === 'outline';
36 | const fillColor = isOutline ? 'none' : colorMap[color];
37 | const strokeColor = isOutline ? colorMap['outline'] : colorMap[color];
38 |
39 | return (
40 |
65 | );
66 | };
67 |
68 | export default XCircleIcon;
69 |
--------------------------------------------------------------------------------
/src/icons/general/EyeIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface EyeIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=24] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=24] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=1] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const EyeIcon: FC = ({
23 | width = 24,
24 | height = 24,
25 | color = '#667085',
26 | strokeWidth = 1,
27 | ...props
28 | }) => {
29 | return (
30 |
56 | );
57 | };
58 |
59 | export default EyeIcon;
60 |
--------------------------------------------------------------------------------
/src/app/settings/components/ColorPickerBox.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { SketchPicker } from 'react-color';
3 |
4 | import { Box, Dialog, IconButton, TextField } from '@mui/material';
5 |
6 | import { convertTransparencyToHex } from '@/shared/utils';
7 |
8 | export default function ColorPickerBox() {
9 | const [pickerColor, setPickerColor] = useState('#ffffff');
10 | const [showPicker, setShowPicker] = useState(false);
11 |
12 | const handleColorChange = (newColor: any) => {
13 | //Concat the 2-digit hex as a transparency number to newColor.hex
14 | const transparentColor = newColor.hex.concat(convertTransparencyToHex(newColor.rgb.a));
15 | setPickerColor(transparentColor); //Push the changed color to color state
16 | };
17 |
18 | const handleInputChange = (event: React.ChangeEvent) => {
19 | setPickerColor(event.target.value); //Push the hex code
20 | };
21 |
22 | //Open and close a color picker
23 | const togglePicker = () => {
24 | setShowPicker(!showPicker);
25 | };
26 |
27 | return (
28 |
36 |
47 |
59 |
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/src/icons/files/AudioIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface AudioIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | }
7 |
8 | /**
9 | * A reusable SVG icon component for rendering an icon.
10 | *
11 | * @param {number} [width=25] - The width of the icon in pixels. Optional.
12 | * @param {number} [height=25] - The height of the icon in pixels. Optional.
13 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
14 | *
15 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
16 | */
17 |
18 | const AudioIcon: FC = ({ width = 25, height = 25, ...props }) => {
19 | return (
20 |
36 | );
37 | };
38 |
39 | export default AudioIcon;
40 |
--------------------------------------------------------------------------------
/src/icons/arrows/ChevronSelectorVerticalIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface ChevronSelectorVerticalIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=16] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=16] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#9A9B9B'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=1.33333] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const ChevronSelectorVerticalIcon: FC = ({
23 | width = 16,
24 | height = 16,
25 | color = '#9A9B9B',
26 | strokeWidth = 1.33333,
27 | ...props
28 | }) => {
29 | return (
30 |
65 | );
66 | };
67 |
68 | export default ChevronSelectorVerticalIcon;
69 |
--------------------------------------------------------------------------------
/src/icons/files/GeneralIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface GeneralIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=25] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=25] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='white'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=1.90667] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const GeneralIcon: FC = ({
23 | width = 25,
24 | height = 25,
25 | color = 'white',
26 | strokeWidth = 1.90667,
27 | ...props
28 | }) => {
29 | return (
30 |
51 | );
52 | };
53 |
54 | export default GeneralIcon;
55 |
--------------------------------------------------------------------------------
/src/icons/files/ImageIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface ImageIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | }
7 |
8 | /**
9 | * A reusable SVG icon component for rendering an icon.
10 | *
11 | * @param {number} [width=25] - The width of the icon in pixels. Optional.
12 | * @param {number} [height=25] - The height of the icon in pixels. Optional.
13 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
14 | *
15 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
16 | */
17 |
18 | const ImageIcon: FC = ({ width = 25, height = 25, ...props }) => {
19 | return (
20 |
36 | );
37 | };
38 |
39 | export default ImageIcon;
40 |
--------------------------------------------------------------------------------
/src/icons/general/CopyIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 | interface CopyIconProps extends SVGProps {
3 | width?: number;
4 | height?: number;
5 | color?: string;
6 | }
7 |
8 | /**
9 | * A reusable SVG icon component for rendering an icon.
10 | *
11 | * @param {number} [width=15] - The width of the icon in pixels. Optional.
12 | * @param {number} [height=15] - The height of the icon in pixels. Optional.
13 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
14 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
15 | *
16 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
17 | */
18 |
19 | const CopyIcon: FC = ({ width = 15, height = 15, color = '#667085', ...props }) => {
20 | return (
21 |
37 | );
38 | };
39 |
40 | export default CopyIcon;
41 |
--------------------------------------------------------------------------------
/src/app/api/auth/password/forgot/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 |
3 | import { authService, emailService } from '@/app/api/_services';
4 |
5 | /**
6 | * POST /api/auth/password/forgot
7 | * Expects: { email }
8 | */
9 | export async function POST(req: NextRequest) {
10 | try {
11 | const { email } = await req.json();
12 |
13 | if (!email || typeof email !== 'string') {
14 | return NextResponse.json({ message: 'Email is required.' }, { status: 400 });
15 | }
16 |
17 | // 1) Create token
18 | const result = await authService.createPasswordResetToken(email);
19 | if (!result.success) {
20 | return NextResponse.json({ message: result.message }, { status: 400 });
21 | }
22 |
23 | // 2) Build reset URL
24 | const resetPasswordUrl = `${process.env.APP_PROTOCOL}://${process.env.APP_DOMAIN}/auth/reset-password?token=${result.token}&email=${encodeURIComponent(
25 | result.userEmail!,
26 | )}`;
27 |
28 | // // 3) Send the email (if SEND_EMAILS === 'true')
29 | // const emailResp = await emailService.sendResetPasswordEmail({
30 | // toEmail: result.userEmail!,
31 | // username: result.userName!,
32 | // resetUrl: resetPasswordUrl,
33 | // });
34 |
35 | // if (!emailResp.success) {
36 | // console.error('[forgot] Error sending email:', emailResp.error);
37 | // return NextResponse.json({ message: 'Internal server error' }, { status: 500 });
38 | // }
39 |
40 | // 4) Return different info if not sending emails
41 | if (process.env.SEND_EMAILS !== 'true') {
42 | // In dev, skip actual email sending
43 | return NextResponse.json(
44 | {
45 | success: true,
46 | message: 'Mail sending is disabled; returning reset URL for dev usage.',
47 | url: resetPasswordUrl,
48 | },
49 | { status: 201 },
50 | );
51 | }
52 |
53 | // Production or emailing is enabled
54 | return NextResponse.json(
55 | { success: true, message: 'Mail sent. Please check your inbox.' },
56 | { status: 201 },
57 | );
58 | } catch (err) {
59 | console.error('[forgot] Error in password/forgot route:', err);
60 | return NextResponse.json({ message: 'Internal server error' }, { status: 500 });
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/icons/general/SettingsIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface SettingsIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | }
8 |
9 | /**
10 | * A reusable SVG icon component for rendering an icon.
11 | *
12 | * @param {number} [width=24] - The width of the icon in pixels. Optional.
13 | * @param {number} [height=24] - The height of the icon in pixels. Optional.
14 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
15 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
16 | *
17 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
18 | */
19 |
20 | const SettingsIcon: FC = ({
21 | width = 24,
22 | height = 24,
23 | color = '#667085',
24 | ...props
25 | }) => {
26 | return (
27 |
45 | );
46 | };
47 |
48 | export default SettingsIcon;
49 |
--------------------------------------------------------------------------------
/src/components/loaders/EmptyState.tsx:
--------------------------------------------------------------------------------
1 | import { Container, Box, Button, Typography } from '@mui/material';
2 | import { ReactNode } from 'react';
3 | import { EmptyStateIcon } from '@/icons';
4 |
5 | interface EmptyStateProps {
6 | message: string;
7 | icon?: ReactNode;
8 | buttonText?: string;
9 | buttonAction?: () => void;
10 | children?: ReactNode;
11 | }
12 |
13 | /**
14 | * A reusable empty state component for empty items, such as tables, that are completely out of data.
15 | * For example, in the parent component, we can handle the following code:
16 | * {!data.length && } />}
17 | *
18 | * @param {string} [message] - The message of empty states. Required.
19 | * @param {ReactNode} [icon= ] - The icon of empty states. Optional.
20 | * @param {string} [buttonText] - The button text of empty states. Optional.
21 | * @param {() => void} [buttonAction] - The button action of empty states. Optional.
22 | * @param {ReactNode} [children] - Custom JSX elements for more complex empty states (e.g., multiple buttons or additional text). Optional.
23 | *
24 | * @returns {JSX.Element} A scalable and responsive design including a message, icon, button, or passed children.
25 | */
26 |
27 | export default function EmptyState({
28 | message,
29 | icon = ,
30 | buttonText,
31 | buttonAction,
32 | children,
33 | }: EmptyStateProps) {
34 | return (
35 |
42 | {icon && (
43 |
46 | {icon}
47 |
48 | )}
49 |
50 | {message}
51 |
52 | {buttonText && buttonAction ? (
53 |
54 |
59 |
60 | ) : (
61 | children && {children}
62 | )}
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/src/icons/files/ZIPIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface ZIPIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | }
7 |
8 | /**
9 | * A reusable SVG icon component for rendering an icon.
10 | *
11 | * @param {number} [width=25] - The width of the icon in pixels. Optional.
12 | * @param {number} [height=25] - The height of the icon in pixels. Optional.
13 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
14 | *
15 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
16 | */
17 |
18 | const ZIPIcon: FC = ({ width = 25, height = 25, ...props }) => {
19 | return (
20 |
45 | );
46 | };
47 |
48 | export default ZIPIcon;
49 |
--------------------------------------------------------------------------------
/src/icons/security/LockIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface LockIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=28] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=28] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#344054'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=2] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const LockIcon: FC = ({
23 | width = 28,
24 | height = 28,
25 | color = '#344054',
26 | strokeWidth = 2,
27 | ...props
28 | }) => {
29 | return (
30 |
47 | );
48 | };
49 |
50 | export default LockIcon;
51 |
--------------------------------------------------------------------------------
/src/icons/general/EyeOffIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface EyeOffIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=24] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=24] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=1] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const EyeOffIcon: FC = ({
23 | width = 24,
24 | height = 24,
25 | color = '#667085',
26 | strokeWidth = 1,
27 | ...props
28 | }) => {
29 | return (
30 |
47 | );
48 | };
49 |
50 | export default EyeOffIcon;
51 |
--------------------------------------------------------------------------------
/src/icons/files/TextIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface TextIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | }
7 |
8 | /**
9 | * A reusable SVG icon component for rendering an icon.
10 | *
11 | * @param {number} [width=25] - The width of the icon in pixels. Optional.
12 | * @param {number} [height=25] - The height of the icon in pixels. Optional.
13 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
14 | *
15 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
16 | */
17 |
18 | const TextIcon: FC = ({ width = 25, height = 25, ...props }) => {
19 | return (
20 |
36 | );
37 | };
38 |
39 | export default TextIcon;
40 |
--------------------------------------------------------------------------------
/src/components/input/FormInput.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * FormInput.tsx
3 | * ----------------------------------------------------------------------------
4 | * A unified reusable input component built on top of MUI's TextField.
5 | *
6 | * NOTES:
7 | * - Supports all standard TextFieldProps (type, disabled, etc.).
8 | * - "label" is rendered above the TextField as a separate Typography element.
9 | * - "errorMessage" will display inline below the TextField if provided.
10 | * - "minWidth" is applied via sx props for basic styling.
11 | */
12 |
13 | import React, { FC } from 'react';
14 | import { Box, TextField, Typography, TextFieldProps } from '@mui/material';
15 |
16 | interface FormInputProps extends Omit {
17 | /** Optional label rendered above the TextField. */
18 | label?: string;
19 |
20 | /** The unique identifier for this field (used for id/name). */
21 | id: string;
22 |
23 | /** An inline error message displayed below the TextField if validation fails. */
24 | errorMessage?: string;
25 |
26 | /** An optional custom minimum width for the TextField, in pixels. */
27 | minWidth?: number;
28 |
29 | /** An optional custom minimum height for the TextField helpertext */
30 | minHeight?: number;
31 | }
32 |
33 | const FormInput: FC = ({
34 | label,
35 | id,
36 | errorMessage = '',
37 | minWidth,
38 | minHeight = '1.5em',
39 | fullWidth = true,
40 | size = 'small',
41 | // Any other TextField props
42 | ...props
43 | }) => {
44 | const displayError = Boolean(errorMessage);
45 |
46 | return (
47 |
48 | {/* Render a top label if provided */}
49 | {label && (
50 |
53 | {label}
54 |
55 | )}
56 |
57 |
77 |
78 | );
79 | };
80 |
81 | export default FormInput;
82 |
--------------------------------------------------------------------------------
/src/hooks/useDocumentAnalytics.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { BarDataItem } from '@/shared/models';
4 |
5 | import { useState, useEffect, useMemo } from 'react';
6 |
7 | // import { mockGraphData } from '@/seed/analytics';
8 |
9 | export type FilterPeriod = 'fromStart' | 'last30Days' | 'last7Days';
10 |
11 | interface UseDocumentAnalyticsReturn {
12 | filteredData: BarDataItem[];
13 | isLoading: boolean;
14 | error: string | null;
15 | filterPeriod: FilterPeriod;
16 | setFilterPeriod: (period: FilterPeriod) => void;
17 | }
18 |
19 | export default function useDocumentAnalytics(documentId: string): UseDocumentAnalyticsReturn {
20 | const [analyticsData, setAnalyticsData] = useState([]);
21 | const [filterPeriod, setFilterPeriod] = useState('fromStart');
22 | const [isLoading, setIsLoading] = useState(false);
23 | const [error, setError] = useState(null);
24 |
25 | useEffect(() => {
26 | if (!documentId) return;
27 |
28 | const fetchAnalytics = async () => {
29 | try {
30 | setIsLoading(true);
31 | setError(null);
32 |
33 | // In a real scenario, you might do:
34 | // const { data } = await axios.get(`/api/analytics?documentId=${documentId}`);
35 | // setAnalyticsData(data);
36 |
37 | // setAnalyticsData(mockGraphData);
38 | } catch (err: any) {
39 | setError('Error fetching analytics data');
40 | } finally {
41 | setIsLoading(false);
42 | }
43 | };
44 |
45 | fetchAnalytics();
46 | }, [documentId]);
47 |
48 | // Filter logic
49 | const filteredData = useMemo(() => {
50 | if (!analyticsData.length) return [];
51 | const currentDate = new Date();
52 |
53 | switch (filterPeriod) {
54 | case 'last30Days':
55 | return analyticsData.filter(
56 | (item) => currentDate.getTime() - item.date.getTime() <= 30 * 24 * 60 * 60 * 1000,
57 | );
58 | case 'last7Days':
59 | return analyticsData.filter(
60 | (item) => currentDate.getTime() - item.date.getTime() <= 7 * 24 * 60 * 60 * 1000,
61 | );
62 | case 'fromStart':
63 | default:
64 | return analyticsData;
65 | }
66 | }, [filterPeriod, analyticsData]);
67 |
68 | return {
69 | filteredData,
70 | isLoading,
71 | error,
72 | filterPeriod,
73 | setFilterPeriod,
74 | };
75 | }
76 |
--------------------------------------------------------------------------------
/src/icons/general/HomeIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface HomeIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=20] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=20] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=1.5] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const HomeIcon: FC = ({
23 | width = 20,
24 | height = 20,
25 | color = '#667085',
26 | strokeWidth = 1.5,
27 | ...props
28 | }) => {
29 | return (
30 |
47 | );
48 | };
49 |
50 | export default HomeIcon;
51 |
--------------------------------------------------------------------------------
/src/shared/models/linkModels.ts:
--------------------------------------------------------------------------------
1 | // =========== LINK TYPE ===========
2 |
3 | export interface LinkType {
4 | id: number;
5 | linkId: string;
6 | documentId: string;
7 | userId: string;
8 | alias?: string;
9 | linkUrl: string;
10 | isPublic: boolean;
11 | visitorFields?: string[];
12 | expirationTime?: string;
13 | password?: string;
14 | updatedAt: string;
15 | createdAt: string;
16 | }
17 |
18 | // =========== LINK FORM VALUES ===========
19 |
20 | export interface LinkFormValues {
21 | password?: string;
22 | isPublic: boolean;
23 | alias?: string;
24 | expirationTime?: string;
25 | requirePassword: boolean;
26 | expirationEnabled: boolean;
27 | requireUserDetails: boolean;
28 | visitorFields?: string[];
29 | contactEmails?: { label: string; id: number }[];
30 | selectFromContact: boolean;
31 | otherEmails?: string;
32 | sendToOthers: boolean;
33 | }
34 |
35 | // =========== LINK PAYLOAD ===========
36 |
37 | export interface CreateDocumentLinkPayload {
38 | documentId: string;
39 | alias?: string; // Alias for the link
40 | isPublic: boolean;
41 | expirationTime?: string; // ISO string format
42 | expirationEnabled?: boolean;
43 | requirePassword?: boolean;
44 | password?: string;
45 | requireUserDetails?: boolean;
46 | visitorFields?: string[]; // Array of required visitor details
47 | contactEmails?: { label: string; id: number }[];
48 | selectFromContact: boolean;
49 | otherEmails?: string;
50 | sendToOthers: boolean;
51 | }
52 |
53 | // =========== INVITE RECIPIENTS PAYLOAD ===========
54 |
55 | export interface InviteRecipientsPayload {
56 | linkUrl: string;
57 | recipients: string[];
58 | }
59 |
60 | // =========== LINK DATA ===========
61 |
62 | export interface LinkData {
63 | isPasswordProtected?: boolean;
64 | requiredUserDetailsOption?: number;
65 | signedUrl?: string;
66 | fileName?: string;
67 | size?: number;
68 | }
69 |
70 | // =========== LINK DETAIL ===========
71 |
72 | export interface LinkDetail {
73 | documentLinkId: string; // unique string
74 | alias: string; // The links's friendly name
75 | document_id: string; // The document_id from DB
76 | createdLink: string; // The linkUrl from DB
77 | lastActivity: Date; // The link's updatedAt
78 | linkViews: number; // If you track actual link views, you can use a real value
79 | }
80 |
--------------------------------------------------------------------------------
/src/icons/communication/MailIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface MailIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=28] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=28] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#344054'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=2] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const MailIcon: FC = ({
23 | width = 28,
24 | height = 28,
25 | color = '#344054',
26 | strokeWidth = 2,
27 | ...props
28 | }) => {
29 | return (
30 |
47 | );
48 | };
49 |
50 | export default MailIcon;
51 |
--------------------------------------------------------------------------------
/src/icons/files/FileDownloadIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface FileDownloadIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=28] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=28] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=2] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const FileDownloadIcon: FC = ({
23 | width = 28,
24 | height = 28,
25 | color = '#667085',
26 | strokeWidth = 2,
27 | ...props
28 | }) => {
29 | return (
30 |
47 | );
48 | };
49 |
50 | export default FileDownloadIcon;
51 |
--------------------------------------------------------------------------------
/src/app/documents/[documentId]/components/InfoTableHeader.tsx:
--------------------------------------------------------------------------------
1 | import { TableCell, TableRow, TableSortLabel } from '@mui/material';
2 |
3 | import { Contact, LinkDetail } from '@/shared/models';
4 |
5 | import { ChevronDownIcon, ChevronSelectorVerticalIcon } from '@/icons';
6 |
7 | type SortableKeys = 'lastActivity';
8 |
9 | interface InfoTableHeaderProps {
10 | variant?: 'linkTable' | 'visitorTable';
11 | orderBy?: keyof LinkDetail | Contact | undefined;
12 | orderDirection?: 'asc' | 'desc' | undefined;
13 | onSort?: (property: SortableKeys) => void;
14 | }
15 |
16 | export default function InfoTableHeader({
17 | variant,
18 | orderBy,
19 | orderDirection,
20 | onSort,
21 | }: InfoTableHeaderProps) {
22 | const handleHeaderClick = () => {
23 | if (onSort) {
24 | onSort('lastActivity');
25 | }
26 | };
27 |
28 | const sortIcon = orderDirection === undefined ? ChevronSelectorVerticalIcon : ChevronDownIcon;
29 |
30 | if (variant === 'linkTable') {
31 | return (
32 |
33 | LINK
34 |
35 |
41 | LAST VIEWED
42 |
43 |
44 | VIEWS
45 | ACTION
46 |
47 | );
48 | }
49 |
50 | // visitorTable
51 | return (
52 |
53 | VISITOR
54 |
55 |
61 | LAST VIEWED
62 |
63 |
64 | DOWNLOADS
65 | DURATION
66 | COMPLETION
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/src/icons/general/CheckCircleIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | type IconColor = 'error' | 'success' | 'disabled' | 'outline' | 'primaryOutline';
4 |
5 | const colorMap: Record = {
6 | error: '#F04438',
7 | success: '#067647',
8 | disabled: '#D0D5DD',
9 | outline: '#344054',
10 | primaryOutline: '#1570ef',
11 | };
12 |
13 | interface CheckCircleIconProps extends SVGProps {
14 | width?: number;
15 | height?: number;
16 | color?: IconColor;
17 | }
18 |
19 | /**
20 | * A reusable SVG icon component for rendering an icon.
21 | *
22 | * @param {number} [width=24] - The width of the icon in pixels. Optional.
23 | * @param {number} [height=24] - The height of the icon in pixels. Optional.
24 | * @param {IconColor} [color='disabled'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
25 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
26 | *
27 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
28 | */
29 |
30 | const CheckCircleIcon: FC = ({
31 | width = 24,
32 | height = 24,
33 | color = 'disabled',
34 | ...props
35 | }) => {
36 | const isOutline = color === 'outline';
37 | const isPrimaryOutline = color === 'primaryOutline';
38 | const fillColor = isOutline || isPrimaryOutline ? 'none' : colorMap[color];
39 | const strokeColor = isOutline
40 | ? colorMap['outline']
41 | : isPrimaryOutline
42 | ? colorMap['primaryOutline']
43 | : 'none';
44 |
45 | return (
46 |
74 | );
75 | };
76 |
77 | export default CheckCircleIcon;
78 |
--------------------------------------------------------------------------------
/src/components/common/Toast.tsx:
--------------------------------------------------------------------------------
1 | import { Alert, Box, IconButton, Slide, SlideProps, Snackbar } from '@mui/material';
2 | import { ReactNode } from 'react';
3 | import NavLink from '../navigation/NavLink';
4 | import { ToastVariant } from '@/providers/toast/toastTypes';
5 | import { XCloseIcon } from '@/icons';
6 |
7 | interface BaseToastProps {
8 | variant?: ToastVariant;
9 | autoHide?: boolean;
10 | index?: number;
11 | }
12 |
13 | type ToastWithMessage = BaseToastProps & {
14 | message: string;
15 | toastLink?: string;
16 | toastLinkText?: string;
17 | children?: never;
18 | };
19 |
20 | type ToastWithChildren = BaseToastProps & {
21 | children: ReactNode;
22 | message?: never;
23 | toastLink?: never;
24 | toastLinkText?: never;
25 | };
26 |
27 | type ToastProps = ToastWithMessage | ToastWithChildren;
28 |
29 | const SlideTransition = (props: SlideProps) => {
30 | return (
31 |
35 | );
36 | };
37 |
38 | export default function Toast({
39 | variant = 'info',
40 | message,
41 | toastLink,
42 | toastLinkText = 'Learn more',
43 | autoHide = true,
44 | open,
45 | hideToast,
46 | children,
47 | index = 0,
48 | }: ToastProps & { open: boolean; hideToast: () => void }) {
49 | const action = (
50 |
51 |
52 |
53 | );
54 |
55 | return (
56 |
57 |
70 |
75 | {message ? (
76 |
81 | {message}{' '}
82 | {toastLink && (
83 |
87 | )}
88 |
89 | ) : (
90 | children
91 | )}
92 |
93 |
94 |
95 | );
96 | }
97 |
--------------------------------------------------------------------------------
/src/app/auth/password-reset-confirm/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { signIn } from 'next-auth/react';
4 | import { useRouter, useSearchParams } from 'next/navigation';
5 | import { useEffect, useState } from 'react';
6 |
7 | import { Box, Typography } from '@mui/material';
8 |
9 | import { CheckCircleIcon } from '@/icons';
10 |
11 | import { LoadingButton } from '@/components';
12 | import AuthFormWrapper from '../components/AuthFormWrapper';
13 |
14 | export default function PasswordResetConfirm() {
15 | const [loading, setLoading] = useState(false);
16 | const [signedIn, setSignedIn] = useState(false);
17 |
18 | const router = useRouter();
19 | const searchParams = useSearchParams();
20 |
21 | const email = searchParams.get('email');
22 | const password = searchParams.get('password');
23 |
24 | // Attempt sign-in
25 | const handleSignIn = async () => {
26 | setLoading(true);
27 |
28 | if (email && password) {
29 | const signInResult = await signIn('credentials', {
30 | redirect: false,
31 | email,
32 | password,
33 | });
34 |
35 | if (signInResult?.error) {
36 | console.error('Sign-in failed:', signInResult.error);
37 | } else {
38 | setSignedIn(true);
39 | }
40 | }
41 |
42 | setLoading(false);
43 | };
44 |
45 | // If sign-in is successful, redirect to /documents
46 | useEffect(() => {
47 | if (signedIn) {
48 | router.push('/documents');
49 | }
50 | }, [signedIn, router]);
51 |
52 | return (
53 |
54 |
63 |
68 |
69 |
70 |
73 | Password Reset Successfully!
74 |
75 |
76 |
79 | Your password has been successfully reset. Click below to log in magically.
80 |
81 |
82 |
89 |
90 | );
91 | }
92 |
--------------------------------------------------------------------------------
/src/app/documentAccess/[linkId]/components/FileDisplay.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Typography, Box, Button } from '@mui/material';
4 |
5 | import { useToast } from '@/hooks';
6 |
7 | import { formatFileSize } from '@/shared/utils';
8 |
9 | interface FilePageProps {
10 | signedUrl: string;
11 | fileName: string;
12 | size: number;
13 | }
14 |
15 | const FileDisplay: React.FC = ({ signedUrl, fileName, size }) => {
16 | const { showToast } = useToast();
17 |
18 | const handleDownload = async () => {
19 | try {
20 | const response = await fetch(signedUrl);
21 | const blob = await response.blob();
22 | const url = window.URL.createObjectURL(blob);
23 |
24 | const link = document.createElement('a');
25 | link.href = url;
26 | link.download = fileName;
27 | link.click();
28 |
29 | window.URL.revokeObjectURL(url);
30 | showToast({ message: 'File downloaded successfully', variant: 'success' });
31 | } catch (error) {
32 | console.error('Error downloading the file:', error);
33 | showToast({
34 | message: 'Error downloading the file. Please try again.',
35 | variant: 'error',
36 | });
37 | }
38 | };
39 |
40 | return (
41 |
42 |
45 | File is ready for download
46 |
47 |
48 | Thanks for verifying your details. You can now download the document.
49 |
50 |
56 | Document:
57 |
60 | {fileName} ({formatFileSize(size)})
61 |
62 |
63 |
68 |
75 |
80 |
81 |
82 | );
83 | };
84 |
85 | export default FileDisplay;
86 |
--------------------------------------------------------------------------------
/src/icons/security/KeyIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface KeyIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=28] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=28] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#344054'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=2] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const KeyIcon: FC = ({
23 | width = 28,
24 | height = 28,
25 | color = '#344054',
26 | strokeWidth = 2,
27 | ...props
28 | }) => {
29 | return (
30 |
47 | );
48 | };
49 |
50 | export default KeyIcon;
51 |
--------------------------------------------------------------------------------
/src/app/api/contacts/route.ts:
--------------------------------------------------------------------------------
1 | import prisma from '@/lib/prisma';
2 | import { NextRequest, NextResponse } from 'next/server';
3 | import { authService } from '../_services/authService';
4 |
5 | export async function GET(req: NextRequest): Promise {
6 | try {
7 | const userId = await authService.authenticate();
8 |
9 | const userLinks = await prisma.documentLink.findMany({
10 | where: { createdByUserId: userId },
11 | select: { documentLinkId: true },
12 | });
13 | if (!userLinks.length) {
14 | return NextResponse.json({ data: [] }, { status: 200 });
15 | }
16 |
17 | const linkIds = userLinks.map((l) => l.documentLinkId);
18 |
19 | const visitors = await prisma.documentLinkVisitor.groupBy({
20 | by: ['email'],
21 | where: {
22 | documentLinkId: { in: linkIds },
23 | },
24 | _count: {
25 | email: true,
26 | },
27 | _max: {
28 | updatedAt: true,
29 | },
30 | });
31 |
32 | const visitorDetails = await Promise.all(
33 | visitors.map(async (visitor) => {
34 | const lastVisit = await prisma.documentLinkVisitor.findFirst({
35 | where: {
36 | email: visitor.email,
37 | documentLinkId: { in: linkIds },
38 | },
39 | orderBy: { updatedAt: 'desc' },
40 | include: {
41 | documentLink: true,
42 | },
43 | });
44 |
45 | if (!lastVisit) {
46 | return null;
47 | }
48 |
49 | const firstName = lastVisit.firstName?.trim() || null;
50 | const lastName = lastVisit.lastName?.trim() || null;
51 | const fullName =
52 | firstName || lastName ? `${firstName || ''} ${lastName || ''}`.trim() : null;
53 |
54 | return {
55 | id: lastVisit.id,
56 | name: fullName,
57 | email: visitor.email || null,
58 | lastViewedLink: lastVisit.documentLink?.alias || lastVisit.documentLink?.linkUrl || null,
59 | lastActivity: lastVisit.updatedAt || null,
60 | totalVisits: visitor._count.email || 0,
61 | };
62 | }),
63 | );
64 |
65 | const contacts = visitorDetails.filter(Boolean);
66 |
67 | return NextResponse.json({ data: contacts }, { status: 200 });
68 | } catch (error) {
69 | return createErrorResponse('Server error.', 500, error);
70 | }
71 | }
72 |
73 | function createErrorResponse(message: string, status: number, details?: any) {
74 | console.error(`[${new Date().toISOString()}] ${message}`, details);
75 | return NextResponse.json({ error: message, details }, { status });
76 | }
77 |
--------------------------------------------------------------------------------
/src/icons/files/ExcelIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface ExcelIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | }
7 |
8 | /**
9 | * A reusable SVG icon component for rendering an icon.
10 | *
11 | * @param {number} [width=25] - The width of the icon in pixels. Optional.
12 | * @param {number} [height=25] - The height of the icon in pixels. Optional.
13 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
14 | *
15 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
16 | */
17 |
18 | const ExcelIcon: FC = ({ width = 25, height = 25, ...props }) => {
19 | return (
20 |
36 | );
37 | };
38 |
39 | export default ExcelIcon;
40 |
--------------------------------------------------------------------------------
/src/app/team/components/TeamClient.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useEffect, useState } from 'react';
3 |
4 | import { Box, Button } from '@mui/material';
5 |
6 | import { ModalWrapper } from '@/components';
7 |
8 | import { dummyTeams } from './dummyTeams';
9 |
10 | import FilterToggle from './FilterToggle';
11 | import UserTable from './UserTable';
12 |
13 | import { useModal } from '@/hooks';
14 |
15 | import { User } from '@/shared/models';
16 |
17 | export default function TeamClient() {
18 | const inviteModal = useModal();
19 |
20 | const [filterRole, setFilterRole] = useState<'All' | 'Administrator' | 'Member'>('All');
21 | const [page, setPage] = useState(1);
22 | const [users, setUsers] = useState([]);
23 | const [totalUsers, setTotalUsers] = useState(0);
24 | const pageSize = 6;
25 |
26 | useEffect(() => {
27 | const fetchUsers = () => {
28 | const filteredUsers =
29 | filterRole === 'All' ? dummyTeams : dummyTeams.filter((user) => user.role === filterRole);
30 |
31 | setTotalUsers(filteredUsers.length);
32 | setUsers(filteredUsers.slice((page - 1) * pageSize, page * pageSize));
33 | };
34 |
35 | fetchUsers();
36 | }, [filterRole, page]);
37 |
38 | const handleFilterChange = (role: 'All' | 'Administrator' | 'Member') => {
39 | setFilterRole(role);
40 | setPage(1); // Reset to page 1 when the filter changes
41 | };
42 |
43 | return (
44 | <>
45 |
49 |
53 |
59 |
60 |
61 |
69 |
70 |
71 |
82 | >
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/src/hooks/useSort.ts:
--------------------------------------------------------------------------------
1 | import { useState, useMemo } from 'react';
2 |
3 | type SortFunction = (a: T, b: T, orderDirection: 'asc' | 'desc' | undefined) => number;
4 |
5 | export function useSort(
6 | data: T[],
7 | initialKey: keyof T | undefined = undefined,
8 | customSort?: SortFunction,
9 | ) {
10 | const [orderDirection, setOrderDirection] = useState<'asc' | 'desc' | undefined>(undefined);
11 | const [orderBy, setOrderBy] = useState(initialKey);
12 |
13 | const sortedData = useMemo(() => {
14 | if (orderDirection && orderBy) {
15 | return [...data].sort((a, b) => {
16 | // 1) If a custom sort function is provided, use it.
17 | if (customSort) {
18 | return customSort(a, b, orderDirection);
19 | }
20 |
21 | let aValue: any = a[orderBy];
22 | let bValue: any = b[orderBy];
23 |
24 | // 2) If both values are Date objects, compare by getTime().
25 | if (aValue instanceof Date && bValue instanceof Date) {
26 | return orderDirection === 'asc'
27 | ? aValue.getTime() - bValue.getTime()
28 | : bValue.getTime() - aValue.getTime();
29 | }
30 |
31 | // 3) Handle nested objects with a `.name` property, or strings
32 | if (typeof aValue === 'object' && aValue !== null) {
33 | aValue = aValue.name?.toUpperCase() ?? '';
34 | } else if (typeof aValue === 'string') {
35 | aValue = aValue.toUpperCase();
36 | }
37 | if (typeof bValue === 'object' && bValue !== null) {
38 | bValue = bValue.name?.toUpperCase() ?? '';
39 | } else if (typeof bValue === 'string') {
40 | bValue = bValue.toUpperCase();
41 | }
42 |
43 | // 4) Default string/number comparison
44 | if (orderDirection === 'asc') {
45 | return aValue < bValue ? -1 : 1;
46 | } else if (orderDirection === 'desc') {
47 | return aValue > bValue ? -1 : 1;
48 | }
49 | return 0;
50 | });
51 | }
52 | return data;
53 | }, [data, orderDirection, orderBy, customSort]);
54 |
55 | const handleSortRequest = (property: keyof T) => {
56 | if (orderBy === property) {
57 | // Toggle direction or reset
58 | if (orderDirection === 'asc') {
59 | setOrderDirection('desc');
60 | } else if (orderDirection === 'desc') {
61 | setOrderDirection(undefined);
62 | setOrderBy(undefined);
63 | } else {
64 | setOrderDirection('asc');
65 | }
66 | } else {
67 | setOrderBy(property);
68 | setOrderDirection('asc');
69 | }
70 | };
71 |
72 | return { sortedData, orderDirection, orderBy, handleSortRequest };
73 | }
74 |
--------------------------------------------------------------------------------
/src/app/team/components/UserTable.tsx:
--------------------------------------------------------------------------------
1 | import { Dropdown, Paginator } from '@/components';
2 |
3 | import { TrashIcon } from '@/icons';
4 |
5 | import { User } from '@/shared/models';
6 |
7 | import {
8 | Paper,
9 | Table,
10 | TableBody,
11 | TableCell,
12 | TableContainer,
13 | TableHead,
14 | TableRow,
15 | Typography,
16 | Box,
17 | } from '@mui/material';
18 |
19 | import IconButton from '@mui/material/IconButton';
20 |
21 | interface Props {
22 | users: User[];
23 | page: number;
24 | setPage: (page: number) => void;
25 | filterRole: 'All' | 'Administrator' | 'Member';
26 | pageSize: number;
27 | totalUsers: number;
28 | }
29 |
30 | const UserTable = ({ users, page, setPage, pageSize, totalUsers }: Props) => (
31 | <>
32 |
33 |
34 |
35 |
36 | Name
37 | Email
38 | Role
39 | Action
40 |
41 |
42 |
43 | {users.map((user, index) => (
44 |
45 |
46 | {user.name}
47 | Created {user.createdAt}
48 |
49 | {user.email}
50 |
51 | {
59 | console.log(`Role changed to ${newRole} for user ${user.name}`);
60 | }}
61 | />
62 |
63 |
64 |
65 |
70 |
71 |
72 |
73 | ))}
74 |
75 |
76 |
77 |
84 | >
85 | );
86 |
87 | export default UserTable;
88 |
--------------------------------------------------------------------------------
/src/icons/files/PPTIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface PPTIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | }
7 |
8 | /**
9 | * A reusable SVG icon component for rendering an icon.
10 | *
11 | * @param {number} [width=25] - The width of the icon in pixels. Optional.
12 | * @param {number} [height=25] - The height of the icon in pixels. Optional.
13 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
14 | *
15 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
16 | */
17 |
18 | const PPTIcon: FC = ({ width = 25, height = 25, ...props }) => {
19 | return (
20 |
36 | );
37 | };
38 |
39 | export default PPTIcon;
40 |
--------------------------------------------------------------------------------
/src/icons/editor/PencilIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface PencilIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=16] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=16] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=1.5] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const PencilIcon: FC = ({
23 | width = 16,
24 | height = 16,
25 | color = '#667085',
26 | strokeWidth = 1.5,
27 | ...props
28 | }) => {
29 | return (
30 |
58 | );
59 | };
60 |
61 | export default PencilIcon;
62 |
--------------------------------------------------------------------------------
/src/app/api/documents/[documentId]/links/route.ts:
--------------------------------------------------------------------------------
1 | import { authService, createErrorResponse, LinkService } from '@/app/api/_services';
2 | import { NextRequest, NextResponse } from 'next/server';
3 |
4 | /**
5 | * GET /api/documents/[documentId]/links
6 | * Returns links for a doc owned by the user.
7 | */
8 | export async function GET(req: NextRequest, props: { params: Promise<{ documentId: string }> }) {
9 | try {
10 | const userId = await authService.authenticate();
11 | const { documentId } = await props.params;
12 | const links = await LinkService.getDocumentLinks(userId, documentId);
13 | if (links === null) {
14 | return createErrorResponse('Document not found or access denied.', 404);
15 | }
16 |
17 | const result = links.map((link) => ({
18 | id: link.id,
19 | documentId: link.documentId,
20 | linkId: link.documentLinkId,
21 | alias: link.alias,
22 | createdLink: link.linkUrl,
23 | lastViewed: link.updatedAt,
24 | linkViews: 0,
25 | }));
26 |
27 | return NextResponse.json({ links: result }, { status: 200 });
28 | } catch (error) {
29 | return createErrorResponse('Server error while fetching links.', 500, error);
30 | }
31 | }
32 |
33 | /**
34 | * POST /api/documents/[documentId]/links
35 | * Creates a new link for the doc.
36 | */
37 | export async function POST(req: NextRequest, props: { params: Promise<{ documentId: string }> }) {
38 | const params = await props.params;
39 | try {
40 | const userId = await authService.authenticate();
41 | const body = await req.json();
42 |
43 | // Attempt creation
44 | try {
45 | const newLink = await LinkService.createLinkForDocument(userId, params.documentId, body);
46 |
47 | if (!newLink) {
48 | return createErrorResponse('Document not found or access denied.', 404);
49 | }
50 | return NextResponse.json(
51 | { message: 'Link created successfully.', link: newLink },
52 | { status: 201 },
53 | );
54 | } catch (createErr) {
55 | // Check if we threw an "EXPIRATION_PAST" error
56 | if (createErr instanceof Error && createErr.message === 'EXPIRATION_PAST') {
57 | return createErrorResponse('Expiration time cannot be in the past.', 400);
58 | }
59 | if (createErr instanceof Error && createErr.message === 'FRIENDLY_NAME_CONFLICT') {
60 | return createErrorResponse(
61 | 'This alias is already in use. Please choose a different link alias.',
62 | 409,
63 | );
64 | }
65 | throw createErr; // rethrow
66 | }
67 | } catch (error) {
68 | return createErrorResponse('Server error while creating link', 500, error);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/icons/general/SaveIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, SVGProps } from 'react';
2 |
3 | interface SaveIconProps extends SVGProps {
4 | width?: number;
5 | height?: number;
6 | color?: string;
7 | strokeWidth?: number;
8 | }
9 |
10 | /**
11 | * A reusable SVG icon component for rendering an icon.
12 | *
13 | * @param {number} [width=20] - The width of the icon in pixels. Optional.
14 | * @param {number} [height=20] - The height of the icon in pixels. Optional.
15 | * @param {string} [color='#667085'] - The stroke color of the icon. Accepts any valid CSS color value. Optional.
16 | * @param {number} [strokeWidth=2] - The stroke width of the icon's path. Optional.
17 | * @param {SVGProps} props - Additional SVG props such as `className`, `style`, or custom attributes.
18 | *
19 | * @returns {JSX.Element} A scalable vector graphic (SVG) element representing the icon.
20 | */
21 |
22 | const SaveIcon: FC = ({
23 | width = 20,
24 | height = 20,
25 | color = '#667085',
26 | strokeWidth = 2,
27 | ...props
28 | }) => {
29 | return (
30 |
47 | );
48 | };
49 |
50 | export default SaveIcon;
51 |
--------------------------------------------------------------------------------
/src/app/team/components/OrganizationName.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { PencilIcon, SaveIcon } from '@/icons';
4 | import { Box, IconButton, TextField, Typography } from '@mui/material';
5 | import { useEffect, useRef, useState } from 'react';
6 |
7 | const OrganizationName = () => {
8 | const [isEditing, setIsEditing] = useState(false);
9 | const [companyName, setCompanyName] = useState('Bluewave Labs');
10 | const inputRef = useRef(null);
11 |
12 | const handleEditClick = () => {
13 | setIsEditing(true);
14 | };
15 |
16 | const handleSaveClick = () => {
17 | setIsEditing(false);
18 | // Add code here to save the updated name to the backend if needed
19 | console.log('Saved:', companyName);
20 | };
21 |
22 | const handleNameChange = (event: React.ChangeEvent) => {
23 | setCompanyName(event.target.value);
24 | };
25 |
26 | // Moves cursor to the end of Organization Name Textfield (Weird Requirment on figma)
27 | useEffect(() => {
28 | if (isEditing && inputRef.current) {
29 | const input = inputRef.current;
30 | input.focus();
31 | input.setSelectionRange(input.value.length, input.value.length);
32 | }
33 | }, [isEditing]);
34 |
35 | return (
36 |
41 | Organization name
42 |
46 | {isEditing ? (
47 |
69 | ) : (
70 |
78 | {companyName}
79 |
80 | )}
81 |
84 | {isEditing ? : }
85 |
86 |
87 |
88 | );
89 | };
90 |
91 | export default OrganizationName;
92 |
--------------------------------------------------------------------------------
/src/app/api/documents/[documentId]/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { authService, DocumentService, createErrorResponse } from '../../_services';
3 |
4 | /**
5 | * GET /api/documents/[documentId]
6 | * Returns metadata for a single doc (ownership enforced).
7 | */
8 | export async function GET(req: NextRequest, props: { params: Promise<{ documentId: string }> }) {
9 | try {
10 | const userId = await authService.authenticate();
11 | const { documentId } = await props.params;
12 | const doc = await DocumentService.getDocumentById(userId, documentId);
13 | if (!doc) {
14 | return createErrorResponse('Document not found or access denied.', 404);
15 | }
16 |
17 | const responsePayload = {
18 | ...doc,
19 | uploader: {
20 | name: `${doc.User.first_name} ${doc.User.last_name}`,
21 | avatar: null,
22 | },
23 | links: 0,
24 | viewers: 0,
25 | views: 0,
26 | };
27 |
28 | return NextResponse.json({ document: responsePayload }, { status: 200 });
29 | } catch (error) {
30 | return createErrorResponse('Server error while fetching document.', 500, error);
31 | }
32 | }
33 |
34 | /**
35 | * PATCH /api/documents/[documentId]
36 | */
37 | export async function PATCH(req: NextRequest, props: { params: Promise<{ documentId: string }> }) {
38 | const { documentId } = await props.params;
39 | try {
40 | const userId = await authService.authenticate();
41 | const body = await req.json();
42 |
43 | const updatedDoc = await DocumentService.updateDocument(userId, documentId, {
44 | fileName: body.fileName,
45 | });
46 | if (!updatedDoc) {
47 | return createErrorResponse('Document not found or access denied.', 404);
48 | }
49 |
50 | return NextResponse.json(
51 | { message: 'Document updated successfully.', document: updatedDoc },
52 | { status: 200 },
53 | );
54 | } catch (error) {
55 | return createErrorResponse('Server error while updating document.', 500, error);
56 | }
57 | }
58 |
59 | /**
60 | * DELETE /api/documents/[documentId]
61 | */
62 | export async function DELETE(req: NextRequest, props: { params: Promise<{ documentId: string }> }) {
63 | const { documentId } = await props.params;
64 | try {
65 | const userId = await authService.authenticate();
66 |
67 | const deletedDoc = await DocumentService.deleteDocument(userId, documentId);
68 | if (!deletedDoc) {
69 | return createErrorResponse('Document not found or access denied.', 404);
70 | }
71 |
72 | return NextResponse.json({ message: 'Document deleted successfully.' }, { status: 200 });
73 | } catch (error) {
74 | return createErrorResponse('Server error while deleting document.', 500, error);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/app/auth/email-sent/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { NavLink } from '@/components';
4 |
5 | import { Box, Container, Link, Typography } from '@mui/material';
6 |
7 | import NextLink from 'next/link';
8 | import { FormEvent, useState } from 'react';
9 |
10 | import { MailIcon } from '@/icons';
11 |
12 | export default function EmailSent() {
13 | const [email, setEmail] = useState('your_email@bluewave.ca');
14 | const [resending, setResending] = useState(false);
15 | const [resendSuccess, setResendSuccess] = useState(false);
16 |
17 | const handleResendEmail = (event: FormEvent) => {
18 | event.preventDefault();
19 | setResending(true);
20 | // Simulate resending reset email
21 | setTimeout(() => {
22 | setResending(false);
23 | setResendSuccess(true);
24 | }, 5000); // Mock API delay
25 | };
26 |
27 | return (
28 |
31 |
37 |
46 |
47 |
48 |
49 |
52 | Check your email
53 |
54 |
55 |
59 | We sent a password reset link to {email}
60 |
61 |
62 |
63 | Didn’t receive the email?{' '}
64 |
70 | {resending ? 'Resending...' : 'Click to resend'}
71 |
72 |
73 |
74 |
79 |
80 | {resendSuccess && (
81 |
85 | A reset email has been sent again to {email}.
86 |
87 | )}
88 | {/* Temporary Link to reset Password */}
89 |
92 | Password Reset
93 |
94 |
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/src/providers/auth/AuthWrapper.tsx:
--------------------------------------------------------------------------------
1 | import SignIn from '@/app/auth/sign-in/page';
2 |
3 | import { Sidebar } from '@/components';
4 |
5 | import { Box, CircularProgress } from '@mui/material';
6 | import { useSession } from 'next-auth/react';
7 | import { usePathname } from 'next/navigation';
8 | import { ReactNode, useEffect, useState } from 'react';
9 |
10 | export default function AuthWrapper({ children }: { children: ReactNode }) {
11 | const { data: session, status } = useSession();
12 | const pathname = usePathname(); // Get the current route path
13 |
14 | // Define the public routes
15 | const publicRoutes = [
16 | '/auth/sign-up',
17 | '/auth/forgot-password',
18 | '/auth/reset-password',
19 | '/auth/account-created',
20 | '/auth/password-reset-confirm',
21 | '/auth/check-email',
22 | '/auth/sign-in',
23 | ];
24 |
25 | // Check if the current path starts with /auth/reset-password, which is dynamic
26 | const isResetPassFormRoute =
27 | pathname.startsWith('/auth/reset-password') && pathname.includes('reset-password');
28 |
29 | const isLinksUuidRoute =
30 | pathname.startsWith('/documentAccess/') &&
31 | /^[a-f0-9-]{36}$/.test(pathname.split('/documentAccess/')[1]);
32 | // Local state to handle loading state
33 | const [isLoading, setIsLoading] = useState(true);
34 |
35 | // useEffect to handle session status changes
36 | useEffect(() => {
37 | if (status === 'loading') {
38 | setIsLoading(true);
39 | } else {
40 | setIsLoading(false);
41 | }
42 | }, [status]);
43 |
44 | // Show a loading state while fetching the session
45 | if (isLoading) {
46 | return (
47 |
52 |
53 |
54 | );
55 | }
56 |
57 | if (isLinksUuidRoute) {
58 | return <>{children}>;
59 | }
60 |
61 | // If the user is trying to access a restricted route and is not signed in, redirect to the sign-in page
62 | if (!session && !publicRoutes.includes(pathname) && !isResetPassFormRoute) {
63 | // Redirect the user to the sign-in page with a callback URL
64 | return ;
65 | }
66 |
67 | // Render authenticated layout only when session is authenticated
68 | if (publicRoutes.includes(pathname) || isResetPassFormRoute) {
69 | return <>{children}>;
70 | }
71 |
72 | return (
73 | <>
74 |
79 |
80 |
84 | {children}
85 |
86 |
87 | >
88 | );
89 | }
90 |
--------------------------------------------------------------------------------