diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index 912b77db..333388ec 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -1,9 +1,19 @@ export { TextInput } from './TextInput'; -export { TextArea } from './TextArea'; export { Dropdown } from './Dropdown'; export { FileUpload } from './FileUpload'; export { InfoCard } from './InfoCard'; export { Button } from './Button'; -export { FormContainer } from './FormContainer'; export { ScrollLink } from './ScrollLink'; export { AnchorLinks } from './AnchorLinks'; +export { TextArea } from './TextArea'; +export { + PageLayout, + Section, + Grid, + Footer, + Header, + FormContainer, + DashboardTemplate, + AdminDashboard, + UserDashboard +} from './layout'; \ No newline at end of file diff --git a/frontend/src/components/layout/components/Container.tsx b/frontend/src/components/layout/components/Container.tsx new file mode 100644 index 00000000..457fdc2e --- /dev/null +++ b/frontend/src/components/layout/components/Container.tsx @@ -0,0 +1,27 @@ +import { LayoutContainerProps } from './types'; +import { widths } from './utils'; + +export const Container: React.FC = ({ + children, + width = 'normal', + background = 'bg-white', + fullHeight = false, + className = '', + id, + testId, +}) => { + return ( +
+ {children} +
+ ); +}; diff --git a/frontend/src/components/layout/components/Footer.tsx b/frontend/src/components/layout/components/Footer.tsx new file mode 100644 index 00000000..2226f9ac --- /dev/null +++ b/frontend/src/components/layout/components/Footer.tsx @@ -0,0 +1,22 @@ +import { ReactNode } from 'react'; +import { Container } from './Container'; +import { Stack } from './Stack'; + +interface FooterProps { + children: ReactNode; + className?: string; +} + +const Footer: React.FC = ({ children, className = '' }) => { + return ( + +
+ + {children} + +
+
+ ); +}; + +export { Footer }; \ No newline at end of file diff --git a/frontend/src/components/layout/components/FormContainer.tsx b/frontend/src/components/layout/components/FormContainer.tsx new file mode 100644 index 00000000..108c935a --- /dev/null +++ b/frontend/src/components/layout/components/FormContainer.tsx @@ -0,0 +1,31 @@ +import { ReactNode } from 'react'; + +interface FormContainerProps { + children: ReactNode; + title?: string; + description?: string; + className?: string; +} + +const FormContainer: React.FC = ({ + children, + title, + description, + className = '' +}) => { + return ( +
+ {(title || description) && ( +
+ {title &&

{title}

} + {description &&

{description}

} +
+ )} +
+ {children} +
+
+ ); +}; + +export { FormContainer }; \ No newline at end of file diff --git a/frontend/src/components/layout/components/Grid.tsx b/frontend/src/components/layout/components/Grid.tsx new file mode 100644 index 00000000..69755de5 --- /dev/null +++ b/frontend/src/components/layout/components/Grid.tsx @@ -0,0 +1,40 @@ +import { ReactNode } from 'react'; + +type GridColumns = 1 | 2 | 3 | 4 | 6 | 12; +type GridGap = 'none' | 'small' | 'normal' | 'large'; + +interface GridProps { + children: ReactNode; + columns?: GridColumns; + gap?: GridGap; + className?: string; +} + +const gapClasses: Record = { + none: 'gap-0', + small: 'gap-2', + normal: 'gap-4', + large: 'gap-8' +}; + +const Grid: React.FC = ({ + children, + columns = 3, + gap = 'normal', + className = '' +}) => { + return ( +
2 ? `lg:grid-cols-${columns}` : ''} + ${gapClasses[gap]} + ${className} + `}> + {children} +
+ ); +}; + +export { Grid }; \ No newline at end of file diff --git a/frontend/src/components/layout/components/Header.tsx b/frontend/src/components/layout/components/Header.tsx new file mode 100644 index 00000000..660594f6 --- /dev/null +++ b/frontend/src/components/layout/components/Header.tsx @@ -0,0 +1,16 @@ +import { ReactNode } from 'react'; + +interface HeaderProps { + children: ReactNode; + className?: string; +} + +const Header: React.FC = ({ children, className = '' }) => { + return ( +
+ {children} +
+ ); +}; + +export { Header }; \ No newline at end of file diff --git a/frontend/src/components/layout/components/PageLayout.tsx b/frontend/src/components/layout/components/PageLayout.tsx new file mode 100644 index 00000000..5679970f --- /dev/null +++ b/frontend/src/components/layout/components/PageLayout.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from 'react'; +import { Container } from './Container'; +import { Stack } from './Stack'; + +interface PageLayoutProps { + children: ReactNode; + className?: string; +} + +export const PageLayout: React.FC = ({ children, className = '' }) => { + return ( + + + {children} + + + ); +}; \ No newline at end of file diff --git a/frontend/src/components/layout/components/Section.tsx b/frontend/src/components/layout/components/Section.tsx new file mode 100644 index 00000000..6ed8c3f6 --- /dev/null +++ b/frontend/src/components/layout/components/Section.tsx @@ -0,0 +1,63 @@ +import { ReactNode } from 'react'; + +type SectionWidth = 'full' | 'wide' | 'normal' | 'narrow'; +type SectionPadding = 'none' | 'small' | 'normal' | 'large'; +type SectionAlignment = 'left' | 'center' | 'right'; + +interface SectionProps { + children: ReactNode; + width?: SectionWidth; + padding?: SectionPadding; + align?: SectionAlignment; + className?: string; + background?: string; + container?: boolean; // new prop to control container width +} + +// predefined width classes +const widthClasses: Record = { + full: 'w-full', + wide: 'max-w-7xl', + normal: 'max-w-5xl', + narrow: 'max-w-3xl' +}; + +// predefined padding classes +const paddingClasses: Record = { + none: 'p-0', + small: 'py-4 px-4', + normal: 'py-8 px-6', + large: 'py-16 px-8' +}; + +const Section: React.FC = ({ + children, + width = 'normal', + padding = 'normal', + align = 'center', + className = '', + background = 'bg-white', + container = true // default to using container +}) => { + const content = container ? ( +
+ {children} +
+ ) : children; + + return ( +
+ {content} +
+ ); +}; + +export { Section }; \ No newline at end of file diff --git a/frontend/src/components/layout/components/Stack.tsx b/frontend/src/components/layout/components/Stack.tsx new file mode 100644 index 00000000..5e5c14f7 --- /dev/null +++ b/frontend/src/components/layout/components/Stack.tsx @@ -0,0 +1,42 @@ +import { BaseLayoutProps, LayoutSpacingProps, LayoutAlignmentProps } from './types'; +import { getSpacingClasses, gaps } from './utils'; + +interface StackProps extends BaseLayoutProps, LayoutSpacingProps, LayoutAlignmentProps {} + +export const Stack: React.FC = ({ + children, + direction = 'column', + align = 'left', + justify = 'start', + gap = 'md', + padding, + margin, + className = '', + id, + testId, +}) => { + const paddingClasses = getSpacingClasses('p', padding); + const marginClasses = getSpacingClasses('m', margin); + + return ( +
+ {children} +
+ ); +}; diff --git a/frontend/src/components/layout/components/types.ts b/frontend/src/components/layout/components/types.ts new file mode 100644 index 00000000..519a3651 --- /dev/null +++ b/frontend/src/components/layout/components/types.ts @@ -0,0 +1,39 @@ +import { ReactNode } from 'react'; + +export type Spacing = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'; +export type Width = 'full' | 'screen' | 'wide' | 'normal' | 'narrow' | 'content'; +export type Alignment = 'left' | 'center' | 'right'; +export type Direction = 'row' | 'column'; +export type Gap = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + +export interface BaseLayoutProps { + children: ReactNode; + className?: string; + id?: string; + testId?: string; +} + +export interface LayoutSpacingProps { + padding?: Spacing | { x?: Spacing; y?: Spacing }; + margin?: Spacing | { x?: Spacing; y?: Spacing }; + gap?: Gap; +} + +export interface LayoutAlignmentProps { + align?: Alignment; + justify?: 'start' | 'center' | 'end' | 'between' | 'around'; + direction?: Direction; +} + +export interface LayoutContainerProps extends BaseLayoutProps { + width?: Width; + background?: string; + fullHeight?: boolean; +} + +export interface LayoutGridProps extends BaseLayoutProps, LayoutSpacingProps { + columns?: number | { sm?: number; md?: number; lg?: number }; + rows?: number; + autoFit?: boolean; + minChildWidth?: string; +} diff --git a/frontend/src/components/layout/components/utils.ts b/frontend/src/components/layout/components/utils.ts new file mode 100644 index 00000000..131df96e --- /dev/null +++ b/frontend/src/components/layout/components/utils.ts @@ -0,0 +1,58 @@ +import { Spacing, Width, Gap } from './types'; + +export const spacing: Record = { + none: '0', + xs: '0.25rem', + sm: '0.5rem', + md: '1rem', + lg: '1.5rem', + xl: '2rem', + '2xl': '3rem', +}; + +export const widths: Record = { + full: 'w-full', + screen: 'w-screen max-w-[100vw]', + wide: 'max-w-7xl', + normal: 'max-w-5xl', + narrow: 'max-w-3xl', + content: 'max-w-prose', +}; + +export const gaps: Record = { + none: 'gap-0', + xs: 'gap-1', + sm: 'gap-2', + md: 'gap-4', + lg: 'gap-6', + xl: 'gap-8', +}; + +export const getSpacingClasses = ( + type: 'p' | 'm', + spacing?: Spacing | { x?: Spacing; y?: Spacing } +): string => { + if (!spacing) return ''; + + if (typeof spacing === 'string') { + return `${type}-${spacing}`; + } + + const { x, y } = spacing; + return `${y ? `${type}y-${y}` : ''} ${x ? `${type}x-${x}` : ''}`.trim(); +}; + +export const getResponsiveGridColumns = ( + columns?: number | { sm?: number; md?: number; lg?: number } +): string => { + if (!columns) return ''; + + if (typeof columns === 'number') { + return `grid-cols-${columns}`; + } + + const { sm, md, lg } = columns; + return `${sm ? `sm:grid-cols-${sm}` : ''} ${md ? `md:grid-cols-${md}` : ''} ${ + lg ? `lg:grid-cols-${lg}` : '' + }`.trim(); +}; diff --git a/frontend/src/components/layout/index.ts b/frontend/src/components/layout/index.ts new file mode 100644 index 00000000..421bd6e2 --- /dev/null +++ b/frontend/src/components/layout/index.ts @@ -0,0 +1,16 @@ +// Base Components +export * from './components/Container'; +export * from './components/Stack'; +export * from './components/Grid'; +export * from './components/Section'; +export * from './components/PageLayout'; +export * from './components/Footer'; +export * from './components/Header'; +export * from './components/FormContainer'; + +// Templates +export * from './templates/DashboardTemplate'; + +// Page Layouts +export * from './pages/AdminDashboard'; +export * from './pages/UserDashboard'; diff --git a/frontend/src/components/layout/pages/AdminDashboard.tsx b/frontend/src/components/layout/pages/AdminDashboard.tsx new file mode 100644 index 00000000..c23181cf --- /dev/null +++ b/frontend/src/components/layout/pages/AdminDashboard.tsx @@ -0,0 +1,45 @@ +import React, { ReactNode } from "react"; +import { FiFolder, FiBook, FiUsers, FiSettings } from "react-icons/fi"; +import { DashboardTemplate } from "../templates/DashboardTemplate"; + +const adminMenuItems = [ + { label: "Projects", path: "/admin/projects", icon: }, + { label: "Resources", path: "/admin/resources", icon: }, + { label: "Users", path: "/admin/users", icon: }, + { label: "Settings", path: "/admin/settings", icon: }, +]; + +const adminNavTabs = [ + { label: "All Projects", path: "/admin/projects" }, + { label: "Pending", path: "/admin/projects/pending" }, + { label: "Approved", path: "/admin/projects/approved" }, +]; + +interface AdminDashboardProps { + children: ReactNode; +} + +export const AdminDashboard: React.FC = ({ children }) => { + const adminActions = ( + <> + + + + ); + + return ( + Admin Panel} + > + {children} + + ); +}; diff --git a/frontend/src/components/layout/pages/UserDashboard.tsx b/frontend/src/components/layout/pages/UserDashboard.tsx new file mode 100644 index 00000000..eb232fcf --- /dev/null +++ b/frontend/src/components/layout/pages/UserDashboard.tsx @@ -0,0 +1,43 @@ +import React, { ReactNode } from "react"; +import { FiFolder, FiBook, FiStar, FiUser } from "react-icons/fi"; +import { DashboardTemplate } from "../templates/DashboardTemplate"; + +const userMenuItems = [ + { label: "My Projects", path: "/projects", icon: }, + { label: "Resources", path: "/resources", icon: }, + { label: "Favorites", path: "/favorites", icon: }, + { label: "Profile", path: "/profile", icon: }, +]; + +const userNavTabs = [ + { label: "All Projects", path: "/projects" }, + { label: "Drafts", path: "/drafts" }, +]; + +interface UserDashboardProps { + children: ReactNode; +} + +export const UserDashboard: React.FC = ({ children }) => { + const userActions = ( + <> + + + + ); + + return ( + + {children} + + ); +}; diff --git a/frontend/src/components/layout/templates/DashboardTemplate.tsx b/frontend/src/components/layout/templates/DashboardTemplate.tsx new file mode 100644 index 00000000..f529cd6c --- /dev/null +++ b/frontend/src/components/layout/templates/DashboardTemplate.tsx @@ -0,0 +1,104 @@ +import React, { ReactNode } from "react"; +import { Link, useLocation } from "react-router-dom"; +import { PageLayout } from "../components/PageLayout"; +import { Stack } from "../components/Stack"; + +interface MenuItem { + label: string; + path: string; + icon: ReactNode; +} + +interface DashboardTemplateProps { + children: ReactNode; + menuItems: MenuItem[]; + logo?: ReactNode; + navTabs?: Array<{ + label: string; + path: string; + }>; + actions?: ReactNode; +} + +export const DashboardTemplate: React.FC = ({ + children, + menuItems, + logo =

Logo

, + navTabs = [], + actions, +}) => { + const location = useLocation(); + + return ( + + {/* top navbar */} +
+
+ + + {/* logo */} + {logo} + + {/* navigation tabs */} + {navTabs.length > 0 && ( + + {navTabs.map((tab) => ( + + {tab.label} + + ))} + + )} + + + {/* right side actions */} + {actions && ( + + {actions} + + )} + +
+
+ + {/* main content area */} +
+ {/* sidebar */} +
+ +
+ + {/* main content */} +
+ {children} +
+
+
+ ); +}; diff --git a/frontend/src/pages/AdminDashboard.tsx b/frontend/src/pages/AdminDashboard.tsx new file mode 100644 index 00000000..3cf5a629 --- /dev/null +++ b/frontend/src/pages/AdminDashboard.tsx @@ -0,0 +1,70 @@ +import { AdminDashboard, Section, Grid, FormContainer } from '@components'; +import { Button } from '@components'; + +const AdminDashboardPage = () => { + return ( + + {/* main content */} +
+
+
+ + {/* left column */} +
+ +
+
+

Total Users

+

1,234

+
+
+

Active Projects

+

567

+
+
+
+ + +
+
+
+
+

Project XYZ

+

Awaiting review

+
+
+
+
+
+

Project ABC

+

Pending verification

+
+
+
+ +
+ + {/* right column */} +
+ +
+ + + + +
+
+
+ +
+
+
+
+ ); +}; + +export { AdminDashboardPage }; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx new file mode 100644 index 00000000..ac459f63 --- /dev/null +++ b/frontend/src/pages/Dashboard.tsx @@ -0,0 +1,99 @@ +import { UserDashboard, Section, Grid, Footer, Header, FormContainer } from '@components'; +import { Button } from '@components'; + +const DashboardPage = () => { + return ( + + {/* header */} +
+
+
+

Dashboard

+ +
+
+
+ + {/* main content */} +
+
+
+ + {/* left column */} +
+ +
+
+

Stat 1

+

123

+
+
+

Stat 2

+

456

+
+
+
+ + +
+
+
+
+

Item 1

+

2 hours ago

+
+
+
+
+
+

Item 2

+

4 hours ago

+
+
+
+ +
+ + {/* right column */} +
+ +
+ + + + +
+
+
+ +
+
+
+ + {/* footer */} +
+
+
+

Footer text

+
+
+
+
+ ); +}; + +export { DashboardPage }; \ No newline at end of file diff --git a/frontend/src/pages/Landing.tsx b/frontend/src/pages/Landing.tsx index 6b4d4f4d..8eb6f104 100644 --- a/frontend/src/pages/Landing.tsx +++ b/frontend/src/pages/Landing.tsx @@ -5,6 +5,8 @@ import { Button, Dropdown, AnchorLinks, + PageLayout, + Section } from '@components'; // sample industries data @@ -18,64 +20,71 @@ const industries = [ ]; const Landing: React.FC = () => { - const [selectedIndustry, setSelectedIndustry] = useState< - (typeof industries)[0] | null - >(null); + const [selectedIndustry, setSelectedIndustry] = useState(null); return ( -
-

Landing

-
- -
- - -
-
- - - - -
-
- { - console.log(l, e); - }} - links={[ - { - label: 'dropdown', - target: '#dropdown', - }, - { - label: 'buttons', - target: '#buttons', - }, - ]} - > - {(link) => ( - - {link.label} - - )} - -
-
+ +
+

Landing

+ +
+ + + + + + +
+ +
+ + + + +
+
+ +
+ { + console.log(l, e); + }} + links={[ + { + label: 'dropdown', + target: '#dropdown', + }, + { + label: 'buttons', + target: '#buttons', + }, + ]} + > + {(link) => ( + + {link.label} + + )} + +
+
+
); }; -export { Landing }; +export { Landing }; \ No newline at end of file diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts index cd53d11d..d69247ea 100644 --- a/frontend/src/pages/index.ts +++ b/frontend/src/pages/index.ts @@ -1,2 +1,4 @@ export { Landing } from './Landing'; -export { Register } from './Register'; \ No newline at end of file +export { DashboardPage } from './Dashboard'; +export { AdminDashboardPage } from './AdminDashboard'; +export { Register } from './Register'; diff --git a/frontend/src/utils/Router.tsx b/frontend/src/utils/Router.tsx index b7e53d87..5800f165 100644 --- a/frontend/src/utils/Router.tsx +++ b/frontend/src/utils/Router.tsx @@ -1,11 +1,28 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; -import { Landing, Register } from '@pages' +import { Landing, DashboardPage, AdminDashboardPage, Register } from '@pages'; const Router = () => ( } /> } /> + + {/* User routes */} + } /> + } /> + } /> + } /> + } /> + } /> + + {/* Admin routes */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> ); diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 74c3cf27..ba4a646a 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -10,6 +10,7 @@ "paths": { "@": ["./src/*"], "@components": ["./src/components/index"], + "@components/layout": ["src/components/layout/index"], "@hooks": ["./src/hooks/index"], "@pages": ["./src/pages/index"], "@utils": ["./src/utils/index"], diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 74fb23d7..7468d0e7 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -9,6 +9,7 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), '@assets': path.resolve(__dirname, './src/assets'), '@components': path.resolve(__dirname, './src/components'), + '@components/layout': path.resolve(__dirname, './src/components/layout'), '@pages': path.resolve(__dirname, './src/pages'), '@utils': path.resolve(__dirname, './src/utils'), '@t': path.resolve(__dirname, './src/types'), diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index fefb7340..13b97f7f 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -3,30 +3,31 @@ import react from '@vitejs/plugin-react'; import path from 'path'; export default defineConfig({ - plugins: [react()], - test: { - environment: 'jsdom', - globals: true, - setupFiles: './src/test/setup.ts', - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html'], - exclude: [ - 'node_modules', - 'src/test/setup.ts', - 'vite.config.ts', - 'eslint.config.js', - 'vitest.config.ts', - ], - }, + plugins: [react()], + test: { + environment: 'jsdom', + globals: true, + setupFiles: './src/test/setup.ts', + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: [ + 'node_modules', + 'src/test/setup.ts', + 'vite.config.ts', + 'eslint.config.js', + 'vitest.config.ts', + ], }, - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - '@assets': path.resolve(__dirname, './src/assets'), - '@components': path.resolve(__dirname, './src/components'), - '@pages': path.resolve(__dirname, './src/pages'), - '@utils': path.resolve(__dirname, './src/utils'), - }, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + '@assets': path.resolve(__dirname, './src/assets'), + '@components': path.resolve(__dirname, './src/components'), + '@components/layout': path.resolve(__dirname, './src/components/layout'), + '@pages': path.resolve(__dirname, './src/pages'), + '@utils': path.resolve(__dirname, './src/utils'), }, + }, });