From c17c233ba4b917f64a1a2ebad86b63865057a4a6 Mon Sep 17 00:00:00 2001 From: Mohammad Yaseen Date: Tue, 16 Dec 2025 10:03:26 +0530 Subject: [PATCH] intial commit --- .env.example | 3 + .gitignore | 38 + DESIGN_PRINCIPLES.md | 1163 +++++ MIGRATION_GUIDE.md | 125 + MIXED_CONTENT_FIX.md | 101 + NGROK_FIX.md | 69 + PERFORMANCE_ANALYSIS.md | 229 + app/api/footer/route.ts | 68 + app/api/image-proxy/route.ts | 79 + app/api/logo/route.ts | 31 + app/api/main-hero/route.ts | 32 + app/api/offerings/route.ts | 33 + app/api/testimonials/route.ts | 31 + app/api/trusted-by/route.ts | 38 + app/api/whysfs/route.ts | 50 + app/globals.css | 132 + app/layout.tsx | 26 + app/page.tsx | 21 + components.json | 21 + components/footer.tsx | 357 ++ components/hero-fallback.json | 3 + components/hero-section.tsx | 144 + components/main-hero.tsx | 324 ++ components/navbar.tsx | 180 + components/our-offerings.tsx | 137 + components/testimonials.tsx | 236 + components/theme-provider.tsx | 11 + components/trusted-by.tsx | 174 + components/ui/accordion.tsx | 66 + components/ui/alert-dialog.tsx | 157 + components/ui/alert.tsx | 66 + components/ui/aspect-ratio.tsx | 11 + components/ui/avatar.tsx | 53 + components/ui/badge.tsx | 46 + components/ui/breadcrumb.tsx | 109 + components/ui/button-group.tsx | 83 + components/ui/button.tsx | 60 + components/ui/calendar.tsx | 213 + components/ui/card.tsx | 92 + components/ui/carousel.tsx | 241 + components/ui/chart.tsx | 353 ++ components/ui/checkbox.tsx | 32 + components/ui/collapsible.tsx | 33 + components/ui/command.tsx | 184 + components/ui/context-menu.tsx | 252 + components/ui/dialog.tsx | 143 + components/ui/drawer.tsx | 135 + components/ui/dropdown-menu.tsx | 257 ++ components/ui/empty.tsx | 104 + components/ui/field.tsx | 244 + components/ui/form.tsx | 167 + components/ui/hover-card.tsx | 44 + components/ui/input-group.tsx | 169 + components/ui/input-otp.tsx | 77 + components/ui/input.tsx | 21 + components/ui/item.tsx | 193 + components/ui/kbd.tsx | 28 + components/ui/label.tsx | 24 + components/ui/menubar.tsx | 276 ++ components/ui/navigation-menu.tsx | 166 + components/ui/pagination.tsx | 127 + components/ui/popover.tsx | 48 + components/ui/progress.tsx | 31 + components/ui/radio-group.tsx | 45 + components/ui/resizable.tsx | 56 + components/ui/scroll-area.tsx | 58 + components/ui/select.tsx | 185 + components/ui/separator.tsx | 28 + components/ui/sheet.tsx | 139 + components/ui/sidebar.tsx | 726 +++ components/ui/skeleton.tsx | 13 + components/ui/slider.tsx | 63 + components/ui/sonner.tsx | 25 + components/ui/spinner.tsx | 16 + components/ui/switch.tsx | 31 + components/ui/table.tsx | 116 + components/ui/tabs.tsx | 66 + components/ui/textarea.tsx | 18 + components/ui/toast.tsx | 129 + components/ui/toaster.tsx | 35 + components/ui/toggle-group.tsx | 73 + components/ui/toggle.tsx | 47 + components/ui/tooltip.tsx | 61 + components/ui/use-mobile.tsx | 19 + components/ui/use-toast.ts | 191 + env.example | 21 + home_page_srs/Group 197 (1).svg | 293 ++ home_page_srs/Group 197 (2).png | Bin 0 -> 572769 bytes hooks/use-mobile.ts | 19 + hooks/use-toast.ts | 191 + lib/utils.ts | 66 + next.config.mjs | 31 + package-lock.json | 4061 +++++++++++++++++ package.json | 77 + pnpm-lock.yaml | 5 + postcss.config.mjs | 8 + public/favicon.ico | Bin 0 -> 384 bytes public/metaimages/apple-icon.png | Bin 0 -> 2626 bytes public/metaimages/icon-dark-32x32.png | Bin 0 -> 585 bytes public/metaimages/icon-light-32x32.png | Bin 0 -> 566 bytes public/metaimages/icon.svg | 26 + public/metaimages/images/logo.png | Bin 0 -> 3166 bytes public/metaimages/images/partnerSchool.png | Bin 0 -> 1065404 bytes public/metaimages/logo.png | Bin 0 -> 3166 bytes public/metaimages/placeholder-logo.png | Bin 0 -> 568 bytes public/metaimages/placeholder-logo.svg | 1 + public/metaimages/placeholder-user.jpg | Bin 0 -> 1635 bytes public/metaimages/placeholder.jpg | Bin 0 -> 1064 bytes public/metaimages/placeholder.svg | 1 + public/metaimages/professional-man.png | Bin 0 -> 778052 bytes .../professional-woman-portrait.jpg | Bin 0 -> 43670 bytes public/metaimages/professional-woman.png | Bin 0 -> 586208 bytes scripts/README.md | 85 + scripts/migrate-strapi.js | 245 + scripts/strapi-export/all-content.json | 41 + scripts/strapi-export/schoolforschools.json | 38 + .../strapi-export/whyschoolforschools.json | 1 + styles/globals.css | 125 + tsconfig.json | 41 + 119 files changed, 15677 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 DESIGN_PRINCIPLES.md create mode 100644 MIGRATION_GUIDE.md create mode 100644 MIXED_CONTENT_FIX.md create mode 100644 NGROK_FIX.md create mode 100644 PERFORMANCE_ANALYSIS.md create mode 100644 app/api/footer/route.ts create mode 100644 app/api/image-proxy/route.ts create mode 100644 app/api/logo/route.ts create mode 100644 app/api/main-hero/route.ts create mode 100644 app/api/offerings/route.ts create mode 100644 app/api/testimonials/route.ts create mode 100644 app/api/trusted-by/route.ts create mode 100644 app/api/whysfs/route.ts create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 components.json create mode 100644 components/footer.tsx create mode 100644 components/hero-fallback.json create mode 100644 components/hero-section.tsx create mode 100644 components/main-hero.tsx create mode 100644 components/navbar.tsx create mode 100644 components/our-offerings.tsx create mode 100644 components/testimonials.tsx create mode 100644 components/theme-provider.tsx create mode 100644 components/trusted-by.tsx create mode 100644 components/ui/accordion.tsx create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/aspect-ratio.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/breadcrumb.tsx create mode 100644 components/ui/button-group.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/calendar.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/carousel.tsx create mode 100644 components/ui/chart.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/collapsible.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/context-menu.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/drawer.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/empty.tsx create mode 100644 components/ui/field.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/hover-card.tsx create mode 100644 components/ui/input-group.tsx create mode 100644 components/ui/input-otp.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/item.tsx create mode 100644 components/ui/kbd.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/menubar.tsx create mode 100644 components/ui/navigation-menu.tsx create mode 100644 components/ui/pagination.tsx create mode 100644 components/ui/popover.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/radio-group.tsx create mode 100644 components/ui/resizable.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/sidebar.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/slider.tsx create mode 100644 components/ui/sonner.tsx create mode 100644 components/ui/spinner.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/toast.tsx create mode 100644 components/ui/toaster.tsx create mode 100644 components/ui/toggle-group.tsx create mode 100644 components/ui/toggle.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 components/ui/use-mobile.tsx create mode 100644 components/ui/use-toast.ts create mode 100644 env.example create mode 100644 home_page_srs/Group 197 (1).svg create mode 100644 home_page_srs/Group 197 (2).png create mode 100644 hooks/use-mobile.ts create mode 100644 hooks/use-toast.ts create mode 100644 lib/utils.ts create mode 100644 next.config.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 postcss.config.mjs create mode 100644 public/favicon.ico create mode 100644 public/metaimages/apple-icon.png create mode 100644 public/metaimages/icon-dark-32x32.png create mode 100644 public/metaimages/icon-light-32x32.png create mode 100644 public/metaimages/icon.svg create mode 100644 public/metaimages/images/logo.png create mode 100644 public/metaimages/images/partnerSchool.png create mode 100644 public/metaimages/logo.png create mode 100644 public/metaimages/placeholder-logo.png create mode 100644 public/metaimages/placeholder-logo.svg create mode 100644 public/metaimages/placeholder-user.jpg create mode 100644 public/metaimages/placeholder.jpg create mode 100644 public/metaimages/placeholder.svg create mode 100644 public/metaimages/professional-man.png create mode 100644 public/metaimages/professional-woman-portrait.jpg create mode 100644 public/metaimages/professional-woman.png create mode 100644 scripts/README.md create mode 100644 scripts/migrate-strapi.js create mode 100644 scripts/strapi-export/all-content.json create mode 100644 scripts/strapi-export/schoolforschools.json create mode 100644 scripts/strapi-export/whyschoolforschools.json create mode 100644 styles/globals.css create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7c7fffc --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# Strapi Configuration +# Copy this file to .env.local and update with your actual Strapi URL +NEXT_PUBLIC_STRAPI_BASE_URL=https://your-strapi-url.com diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e1e125f --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + + diff --git a/DESIGN_PRINCIPLES.md b/DESIGN_PRINCIPLES.md new file mode 100644 index 0000000..40c5ab7 --- /dev/null +++ b/DESIGN_PRINCIPLES.md @@ -0,0 +1,1163 @@ +# UI Design Principles Documentation + +## School For Schools Interface Design System + +This document serves as a comprehensive reference guide for understanding and implementing the design principles, visual elements, and user experience patterns used throughout the School For Schools interface. + +--- + +## Table of Contents + +1. [Core Design Principles](#core-design-principles) +2. [Visual Design Elements](#visual-design-elements) +3. [Component Patterns](#component-patterns) +4. [User Experience Principles](#user-experience-principles) +5. [Implementation Examples](#implementation-examples) +6. [Design System Architecture](#design-system-architecture) + +--- + +## Core Design Principles + +### 1. Minimalism & Clarity + +The interface prioritizes **clarity over decoration**. This philosophy manifests through: + +- **Clean layouts** with generous white space +- **Focused content** that eliminates visual noise +- **Purposeful elements** where every component serves a clear function +- **Reduced cognitive load** through simplified navigation and information architecture + +**Rationale**: Educational platforms require users to process complex information. Minimalist design reduces distractions and helps users focus on essential content. + +### 2. Visual Hierarchy + +Content is organized through **systematic visual weighting**: + +- **Typography scale** creates clear information levels (headings, body, captions) +- **Color contrast** distinguishes primary actions from secondary elements +- **Spatial relationships** guide the eye through content flow +- **Size and weight** emphasize importance without overwhelming + +**Implementation**: The design uses a consistent type scale (4xl-7xl for hero headings, base for body text) and strategic use of the brand orange (`#F48120`) for emphasis. + +### 3. Consistency & Standards + +**Unified design language** across all components: + +- **Shared color palette** applied consistently +- **Standardized spacing** using Tailwind's spacing scale +- **Component patterns** reused throughout the interface +- **Interaction patterns** that users can predict and learn + +**Rationale**: Consistency reduces learning curve and builds user confidence. Once users understand one section, they can navigate the entire platform intuitively. + +### 4. Accessibility First + +Design decisions prioritize **inclusive user experience**: + +- **Semantic HTML** structure for screen readers +- **Color contrast ratios** that meet WCAG standards +- **Keyboard navigation** support throughout +- **Focus indicators** visible for keyboard users +- **ARIA labels** where appropriate + +### 5. Progressive Enhancement + +The interface is built with **graceful degradation**: + +- **Core functionality** works without JavaScript +- **Enhanced interactions** layer on top of base functionality +- **Responsive design** adapts to all device sizes +- **Performance optimization** ensures fast load times + +--- + +## Visual Design Elements + +### Color Palette + +The design system uses a **dual-tone color approach** with OKLCH color space for modern color management and perceptual uniformity. + +#### Brand Colors + +```css +--color-orange: #f48120 /* Primary accent, CTAs, hover states */ +--color-dark-grey: #353535 /* Primary text, headings */ +--color-black: #000000 /* Deep emphasis */ +--color-hero-bg: #eaeaea /* Section backgrounds, cards */ +``` + +**Usage Guidelines**: + +- **Orange (`#F48120`)**: Used sparingly for primary actions, hover states, active navigation items, and key accents. Creates visual interest without overwhelming. +- **Dark Grey (`#353535`)**: Primary text color, headings, and important UI elements. Provides excellent readability. +- **Hero Background (`#EAEAEA`)**: Section backgrounds and card containers. Creates subtle separation without harsh contrast. +- **Black (`#000000`)**: Maximum emphasis, reserved for critical text and deep UI elements. + +#### Semantic Color System + +The system includes a comprehensive semantic color palette based on OKLCH: + +**Light Mode**: +- `--background`: Pure white (`oklch(1 0 0)`) +- `--foreground`: Near-black (`oklch(0.145 0 0)`) +- `--primary`: Dark (`oklch(0.205 0 0)`) +- `--secondary`: Light grey (`oklch(0.97 0 0)`) +- `--muted`: Light grey (`oklch(0.97 0 0)`) +- `--accent`: Light grey (`oklch(0.97 0 0)`) +- `--border`: Light border (`oklch(0.922 0 0)`) +- `--destructive`: Red accent (`oklch(0.577 0.245 27.325)`) + +**Dark Mode**: +- Complete color inversion while maintaining contrast ratios +- Perceptually uniform transitions between light and dark themes +- Consistent brand orange remains prominent + +#### Color Usage Examples + +```typescript +// Primary Action Button +className="bg-[#F48120] hover:bg-[#F48120]/90" + +// Text Emphasis +className="text-[#353535]" + +// Section Background +className="bg-[#EAEAEA]" + +// Card Container +className="bg-white border border-gray-300" +``` + +### Typography + +#### Font Family + +**Primary Font**: `DIN Alternate` + +```css +--font-sans: "DIN Alternate", "Geist", "Geist Fallback" +``` + +**Rationale**: DIN Alternate provides: +- **Professional authority** appropriate for educational technology +- **Excellent readability** at various sizes +- **Geometric clarity** that aligns with technical content +- **Modern aesthetic** without being overly decorative + +**Fallback Chain**: Geist and system fallbacks ensure graceful degradation across platforms. + +#### Type Scale + +The typography system uses a **responsive scale** that adapts to screen size: + +| Element | Mobile | Tablet | Desktop | Usage | +|---------|--------|--------|---------|-------| +| Hero Heading | `text-5xl` | `text-6xl` | `text-7xl` | Main page titles | +| Section Heading | `text-4xl` | `text-5xl` | `text-6xl` | Section titles | +| Card Title | `text-lg` | `text-xl` | `text-2xl` | Card headings | +| Body Text | `text-sm` | `text-base` | `text-base` | Paragraph content | +| Small Text | `text-xs` | `text-sm` | `text-sm` | Captions, meta | + +**Implementation Example**: + +```tsx +

+ Learning +

+``` + +#### Font Weights + +- **400 (Regular)**: Body text, descriptions +- **700 (Bold)**: Headings, emphasis, important information + +**Usage Pattern**: Minimal weight variation maintains clarity while creating hierarchy through size and color. + +#### Line Height + +- **Tight** (`leading-tight`): Headings for compact, impactful display +- **Relaxed** (`leading-relaxed`): Body text for comfortable reading +- **Normal**: Default for UI elements + +### Spacing & Layout System + +#### Container System + +The layout uses a **constrained width container system**: + +```tsx +// Standard Container +
+ +// Wide Container (for hero sections) +
+ +// Extra Wide Container +
+``` + +**Rationale**: +- Prevents content from stretching too wide on large screens +- Maintains optimal reading line length +- Creates consistent margins across sections + +#### Responsive Padding + +Padding scales with screen size to optimize space usage: + +- **Mobile**: `px-4` (1rem / 16px) +- **Tablet**: `sm:px-6` (1.5rem / 24px) +- **Desktop**: `lg:px-8` (2rem / 32px) + +#### Vertical Spacing + +Section spacing follows a consistent rhythm: + +```tsx +// Standard Section +className="py-12 md:py-20 lg:py-24" + +// Compact Section +className="py-8 md:py-16" + +// Hero Section +className="py-16 md:py-24" +``` + +**Principle**: Vertical rhythm creates visual breathing room and clearly separates content sections. + +#### Grid System + +The interface uses **CSS Grid** for two-dimensional layouts and **Flexbox** for one-dimensional alignment: + +```tsx +// Responsive Grid (Why SFS Section) +
+ +// Card Grid (Offerings) +
+ +// Testimonial Grid +
+``` + +**Breakpoint Strategy**: +- **Mobile First**: Base styles target mobile, then enhance for larger screens +- **Breakpoints**: `sm:` (640px), `md:` (768px), `lg:` (1024px) +- **Gap Scaling**: Gap sizes increase with container size + +### Visual Hierarchy Techniques + +#### Size Hierarchy + +1. **Primary**: Hero headings (7xl) - Most important content +2. **Secondary**: Section headings (4xl-6xl) - Section identification +3. **Tertiary**: Card titles (lg-xl) - Content grouping +4. **Body**: Paragraph text (base) - Content consumption +5. **Meta**: Captions, labels (xs-sm) - Supporting information + +#### Color Hierarchy + +- **High Contrast**: Brand orange on key interactive elements +- **Medium Contrast**: Dark grey for primary content +- **Low Contrast**: Muted colors for secondary information + +#### Spatial Hierarchy + +- **Grouping**: Related items grouped with consistent spacing +- **Separation**: Sections separated by background colors or significant spacing +- **Alignment**: Consistent alignment creates visual structure + +#### Motion Hierarchy + +Animations are **purposeful and restrained**: + +- **Micro-interactions**: Hover states, transitions (300ms duration) +- **Content transitions**: Animated content changes (500-800ms) +- **Page transitions**: Smooth, non-distracting animations + +--- + +## Component Patterns + +### 1. Navigation Pattern + +The navigation follows a **horizontal menu pattern** with mobile-first responsive behavior: + +**Desktop**: +- Horizontal menu with equal spacing (`space-x-8`) +- Active state indicated by orange color +- Hover transitions for feedback + +**Mobile**: +- Hamburger menu icon +- Collapsible drawer navigation +- Full-width touch targets + +```12:48:components/navbar.tsx + {/* Desktop Navigation */} +
+ {navItems.map((item) => ( + + {item.label} + + ))} +
+``` + +**Design Decisions**: +- **Fixed height** (`h-20`) maintains consistency across pages +- **Border separation** (`border-b border-gray-200`) subtly separates navigation from content +- **Transition animations** (`transition-colors`) provide smooth feedback + +### 2. Hero Section Pattern + +Hero sections use a **split-screen layout** with animated content: + +**Characteristics**: +- Large, bold typography (5xl-7xl) +- Animated content rotation (4.5s intervals) +- SVG iconography with drawing animations +- Fixed-height containers to prevent layout shift + +```100:130:components/main-hero.tsx +
+
+
+ + {/* --- Left Column: Text --- */} +
+

+ Learning +

+ + {/* Fixed Height Text Container to prevent jumping */} +
+ + +``` + +**Key Features**: +- **Fixed-height container** prevents content jumping during animations +- **Fade + slide transitions** create smooth content changes +- **Responsive grid** adapts from stacked (mobile) to side-by-side (desktop) + +### 3. Feature Card Pattern + +Feature cards use a **hover transformation pattern**: + +```97:112:components/hero-section.tsx +
+
+
+ +
+
+

+ {feature.title} +

+

+ {feature.description} +

+
+``` + +**Pattern Elements**: +- **Group hover**: Entire card responds to hover +- **Color transformation**: Background shifts from grey to orange +- **Text inversion**: Text changes to white for contrast +- **Smooth transitions**: 300ms duration for polished feel +- **Minimum height**: Ensures consistent card sizing + +### 4. Offering Card Pattern + +Larger offering cards use an **aspect-ratio square pattern**: + +```33:63:components/our-offerings.tsx +
+ {/* Icon */} +
+ +
+ + {/* Title */} +

+ {offering.title} +

+ + {/* Description */} +

+ {offering.description} +

+ + {/* Link */} + + {offering.linkText} + +
+``` + +**Design Decisions**: +- **Square aspect ratio**: Creates visual balance and consistency +- **Centered content**: Uses flexbox to center content vertically +- **Icon → Title → Description → Link**: Clear information hierarchy +- **Dark hover state**: Inverts to dark background for dramatic effect + +### 5. Testimonial Carousel Pattern + +Testimonials use a **sliding window carousel** with navigation controls: + +```104:201:components/testimonials.tsx +
+
+

+ Testimonials +

+ +
+ {/* Navigation Buttons */} + + + + + {/* Carousel Viewport */} +
+
+ + {visibleTestimonials.map((testimonial) => ( + + {/* + Card Design: + - White background + - Padding around + - Inner border that changes color on hover + */} +
+
+``` + +**Pattern Features**: +- **External navigation**: Buttons positioned outside content area +- **Sliding animation**: Smooth fade + slide transitions +- **Border hover effect**: Border color changes on hover +- **Fixed card height**: Prevents layout shift during transitions +- **Touch-friendly**: Large hit targets for mobile users + +### 6. Infinite Marquee Pattern + +The "Trusted By" section uses a **seamless infinite scroll**: + +```22:60:components/trusted-by.tsx + {/* Infinite Marquee Container */} +
+ {/* We need two sets of logos for seamless looping */} + + {/* Set 1 */} + {SCHOOLS.map((school) => ( +
+ Partner School +
+ ))} + {/* Set 2 (Duplicate) */} + {SCHOOLS.map((school) => ( +
+ Partner School +
+ ))} +
+
+``` + +**Implementation Strategy**: +- **Duplicate content**: Two identical sets of logos +- **50% translation**: Animation moves exactly one set width +- **Linear easing**: Creates seamless, continuous motion +- **Performance**: Uses GPU-accelerated transforms + +### 7. Button Component Pattern + +Buttons follow a **variant-based system** using class-variance-authority: + +```7:37:components/ui/button.tsx +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: + 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + secondary: + 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: + 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', + 'icon-sm': 'size-8', + 'icon-lg': 'size-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +) +``` + +**Design Principles**: +- **Accessibility**: Focus states, ARIA support, keyboard navigation +- **Consistency**: Standardized sizes and spacing +- **Flexibility**: Multiple variants for different contexts +- **Icon support**: Automatic spacing adjustments for icons + +### 8. Card Component Pattern + +Cards provide a **composable container system**: + +```5:15:components/ui/card.tsx +function Card({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} +``` + +**Composition Pattern**: +- **Card**: Base container +- **CardHeader**: Header section with title/description +- **CardTitle**: Heading within card +- **CardDescription**: Supporting text +- **CardContent**: Main content area +- **CardFooter**: Footer actions/info + +**Usage**: Allows flexible composition while maintaining consistent styling. + +--- + +## User Experience Principles + +### Navigation Structure + +#### Information Architecture + +The site follows a **shallow navigation hierarchy**: + +``` +Home +├── About +├── ERP +├── Tech Lab Setup +├── Resources +└── Contact +``` + +**Rationale**: Flat structure reduces cognitive load and allows quick access to any section. + +#### Navigation Patterns + +1. **Primary Navigation**: Horizontal menu (desktop), hamburger (mobile) +2. **Section Navigation**: Anchor links for long pages +3. **Breadcrumbs**: Not implemented (shallow structure doesn't require them) +4. **Footer Navigation**: Quick links for common destinations + +#### Mobile Navigation + +- **Hamburger menu**: Collapses navigation to save screen space +- **Full-width items**: Touch-friendly targets +- **Smooth transitions**: Slide-in animation for drawer +- **Overlay behavior**: Menu overlays content when open + +### Interaction Patterns + +#### Hover States + +**Consistent hover feedback** across all interactive elements: + +- **Links**: Color change to brand orange (300ms transition) +- **Buttons**: Background opacity change (10% darker) +- **Cards**: Background color transformation + shadow +- **Icons**: Color change matching context + +**Implementation**: + +```tsx +// Link hover +className="text-[#353535] hover:text-[#F48120] transition-colors" + +// Button hover +className="bg-[#353535] hover:bg-[#F48120] transition-colors" + +// Card hover +className="group hover:bg-[#F48120] transition-colors duration-300" +``` + +#### Click Feedback + +- **Visual feedback**: Immediate color change on click +- **Transition animations**: Smooth state changes +- **Loading states**: Spinners or disabled states for async actions +- **Success feedback**: Visual confirmation when actions complete + +#### Animation Philosophy + +**Principle**: Animations should feel **natural and purposeful**, not decorative. + +**Guidelines**: +- **Duration**: 300ms for micro-interactions, 500-800ms for content transitions +- **Easing**: `ease-out` for most transitions (feels responsive) +- **Reduced motion**: Respects `prefers-reduced-motion` media query +- **Performance**: Uses GPU-accelerated properties (transform, opacity) + +**Example Implementation**: + +```tsx +// Smooth content transition + +``` + +### Accessibility Considerations + +#### Semantic HTML + +All components use **appropriate semantic elements**: + +- `
+ + {/* Mobile: Search icon and menu button */} +
+ {/* Mobile Search Icon */} + + {/* Mobile menu button */} + +
+
+
+ + {/* Mobile Navigation */} + {isOpen && ( +
+
+ {navItems.map((item) => ( + + {item.label} + + ))} +
+
+ )} + + ) +} diff --git a/components/our-offerings.tsx b/components/our-offerings.tsx new file mode 100644 index 0000000..f333536 --- /dev/null +++ b/components/our-offerings.tsx @@ -0,0 +1,137 @@ +import { FlaskConical, Smartphone } from "lucide-react" +import Link from "next/link" + +type Offering = { + id: number | string + title: string + description: string + info_footer: string + svg?: string | null + // Fallback icon key (not used when svg exists) + icon?: typeof FlaskConical +} + +async function fetchOfferings(): Promise { + try { + // For server components, we can fetch directly from Strapi since there's no CORS issue + // But for consistency, we'll use the API route + const baseUrl = process.env.NEXT_PUBLIC_STRAPI_BASE_URL || "http://160.187.167.213" + const apiUrl = `${baseUrl.replace(/\/+$/, "")}/api/offerings` + + const res = await fetch(apiUrl, { + headers: { Accept: "application/json" }, + next: { revalidate: 300 }, + }) + + if (!res.ok) throw new Error(`Offerings API status ${res.status}`) + + const json = await res.json() + const items = Array.isArray(json?.data) ? [...json.data].reverse() : [] + + return items.map((item: any, index: number) => { + // Strapi v4: attributes, but sample payload has fields at root; handle both + const attrs = item?.attributes ?? item ?? {} + + // Optional fallback icon rotation if SVG missing + const fallbackIcons = [FlaskConical, Smartphone] + + return { + id: item?.id ?? attrs?.id ?? index, + title: attrs?.title ?? "", + description: attrs?.description ?? "", + info_footer: attrs?.info_footer ?? "", + svg: attrs?.svg_icon ?? null, + icon: fallbackIcons[index % fallbackIcons.length], + } satisfies Offering + }) + } catch (error) { + console.error("Failed to load offerings:", error) + // Fallback to empty array if API fails + return [] + } +} + +async function fetchOfferingHeading() { + try { + // For server components, we can fetch directly from Strapi since there's no CORS issue + const baseUrl = process.env.NEXT_PUBLIC_STRAPI_BASE_URL || "http://160.187.167.213" + const apiUrl = `${baseUrl.replace(/\/+$/, "")}/api/offering-text` + + const res = await fetch(apiUrl, { + headers: { Accept: "application/json" }, + next: { revalidate: 300 }, + }) + + if (!res.ok) throw new Error(`Offering heading API status ${res.status}`) + const json = await res.json() + + // Handle both Strapi shapes (with or without attributes wrapper) + const attrs = json?.data?.attributes ?? json?.data ?? {} + return attrs?.offering || "Our Offerings" + } catch (error) { + console.error("Failed to load offering heading:", error) + return "Our Offerings" + } +} + +export default async function OurOfferings() { + const heading = await fetchOfferingHeading() + const offerings = await fetchOfferings() + + return ( +
+
+

+ {heading} +

+ +
+ {offerings.map((offering) => ( +
+ {/* Icon */} +
+ {offering.svg ? ( + + ) : ( + offering.icon && ( + + ) + )} +
+ + {/* Title */} +

+ {offering.title} +

+ + {/* Description */} +

+ {offering.description} +

+ + {/* Info footer / link text */} + + {offering.info_footer} + +
+ ))} +
+
+
+ ) +} diff --git a/components/testimonials.tsx b/components/testimonials.tsx new file mode 100644 index 0000000..4d22ab0 --- /dev/null +++ b/components/testimonials.tsx @@ -0,0 +1,236 @@ +"use client" + +import Image from "next/image" +import { useState, useEffect } from "react" +import { ChevronLeft, ChevronRight, Star } from "lucide-react" +import { motion, AnimatePresence } from "framer-motion" +import { ensureHttpsImageUrl } from "@/lib/utils" + +interface Testimonial { + id: number | string + name: string + title: string + organization: string + image: string + text: string + rating: number +} + +export default function Testimonials() { + const [testimonials, setTestimonials] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [startIndex, setStartIndex] = useState(0) + const itemsPerPage = 3 + + // Fetch testimonials from API + useEffect(() => { + const fetchTestimonials = async () => { + try { + // Use Next.js API route instead of direct Strapi call + const res = await fetch("/api/testimonials", { + headers: { Accept: "application/json" }, + }) + + if (!res.ok) throw new Error(`Testimonials API status ${res.status}`) + const json = await res.json() + + const items = Array.isArray(json?.data) ? json.data : [] + const mapped = items + .filter((item: any) => { + // Only include items that have an avatar image + const attrs = item?.attributes ?? item ?? {} + const avatarUrl = + attrs?.avatar?.data?.attributes?.url || + attrs?.avatar?.data?.url || + attrs?.avatar?.url || + attrs?.avatar || + "" + return !!avatarUrl + }) + .map((item: any, index: number): Testimonial => { + const attrs = item?.attributes ?? item ?? {} + + // Normalize rating 0-5 + const rawRating = Number(attrs?.rating ?? 0) + const rating = Number.isFinite(rawRating) + ? Math.min(5, Math.max(0, Math.round(rawRating))) + : 0 + + // Resolve media URL - handle both nested and direct formats + const avatarData = attrs?.avatar?.data || attrs?.avatar + const avatarAttrs = avatarData?.attributes || avatarData || {} + const avatarUrl = avatarAttrs?.url || attrs?.avatar?.url || "" + + // Ensure HTTPS to prevent Mixed Content errors + const image = ensureHttpsImageUrl(avatarUrl) + + return { + id: item?.id ?? attrs?.id ?? index, + name: attrs?.name ?? "", + title: attrs?.role ?? "", + organization: attrs?.company ?? "", + text: attrs?.quote ?? "", + rating, + image, + } + }) + + setTestimonials(mapped) + setError(null) + } catch (err: any) { + console.error("Failed to load testimonials:", err) + setTestimonials([]) + setError("Failed to load testimonials") + } finally { + setIsLoading(false) + } + } + + fetchTestimonials() + }, []) + + const nextSlide = () => { + if (testimonials.length === 0) return + setStartIndex((prev) => (prev + 1) % Math.max(1, testimonials.length)) + } + + const prevSlide = () => { + if (testimonials.length === 0) return + setStartIndex((prev) => (prev === 0 ? testimonials.length - 1 : prev - 1)) + } + + // Sliding window over testimonials + const visibleTestimonials: Testimonial[] = [] + const visibleCount = Math.min(itemsPerPage, testimonials.length) + for (let i = 0; i < visibleCount; i++) { + const index = (startIndex + i) % Math.max(1, testimonials.length || 1) + const item = testimonials[index] + if (item) visibleTestimonials.push(item) + } + + const handleNext = () => { + setStartIndex((prev) => (prev + 1) % testimonials.length) + } + + const handlePrev = () => { + setStartIndex((prev) => (prev - 1 + testimonials.length) % testimonials.length) + } + + return ( +
+
+

+ Testimonials +

+ +
+ {/* Navigation Buttons */} + + + + + {/* Carousel Viewport */} +
+ {isLoading && ( +
Loading testimonials...
+ )} + {!isLoading && testimonials.length === 0 && ( +
+ No testimonials available at the moment. +
+ )} +
+ + {visibleTestimonials.map((testimonial, index) => ( + 0 ? 'hidden lg:block' : ''} ${index > 1 ? 'xl:block hidden lg:hidden' : ''}`} + > + {/* + Card Design: + - White background + - Padding around + - Inner border that changes color on hover + */} +
+
+ + {/* Profile Image */} + {testimonial.image && ( +
+ {testimonial.name} +
+ )} + + {/* Star Rating */} +
+ {Array.from({ length: 5 }).map((_, i) => { + const isFilled = i < testimonial.rating + return ( + + ) + })} +
+ + {/* Text - Serif font as per design image */} +

+ {testimonial.text} +

+ + {/* Divider */} +
+ + {/* Author Info */} +
+

+ {testimonial.name} +

+
+

{testimonial.title}

+

{testimonial.organization}

+
+
+
+
+
+ ))} +
+
+
+
+
+
+ ) +} diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx new file mode 100644 index 0000000..55c2f6e --- /dev/null +++ b/components/theme-provider.tsx @@ -0,0 +1,11 @@ +'use client' + +import * as React from 'react' +import { + ThemeProvider as NextThemesProvider, + type ThemeProviderProps, +} from 'next-themes' + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children} +} diff --git a/components/trusted-by.tsx b/components/trusted-by.tsx new file mode 100644 index 0000000..9f429c6 --- /dev/null +++ b/components/trusted-by.tsx @@ -0,0 +1,174 @@ +"use client" + +import Image from "next/image" +import { useEffect, useState } from "react" +import { motion } from "framer-motion" +import { ensureHttpsImageUrl } from "@/lib/utils" + +interface Logo { + id: string + src: string + alt: string +} + + +export default function TrustedBy() { + const [heading, setHeading] = useState("Trusted by leading schools worldwide") + const [subtext, setSubtext] = useState( + "Join the growing community of schools that trust our platform to transform education through technology." + ) + const [logos, setLogos] = useState([]) + + useEffect(() => { + const loadTexts = async () => { + try { + // Use Next.js API route instead of direct Strapi calls + const res = await fetch("/api/trusted-by", { + headers: { Accept: "application/json" }, + }) + + if (!res.ok) { + throw new Error(`Trusted-by API status ${res.status}`) + } + + const data = await res.json() + + // Extract heading + if (data.heading) { + const headingJson = data.heading + const text = + headingJson?.data?.attributes?.text ?? + headingJson?.data?.text ?? + headingJson?.data?.TEXT ?? + headingJson?.data?.attributes?.TEXT + if (text) setHeading(text) + } + + // Extract subtext + if (data.subtext) { + const subtextJson = data.subtext + const text = + subtextJson?.data?.attributes?.text ?? + subtextJson?.data?.text ?? + subtextJson?.data?.TEXT ?? + subtextJson?.data?.attributes?.TEXT + if (text) setSubtext(text) + } + } catch (err) { + console.error("Failed to load trusted-by texts:", err) + } + } + + loadTexts() + }, []) + + useEffect(() => { + const loadLogos = async () => { + try { + // Use Next.js API route instead of direct Strapi call + const res = await fetch("/api/trusted-by", { + headers: { Accept: "application/json" }, + }) + if (!res.ok) throw new Error(`Trusted-by API status ${res.status}`) + const data = await res.json() + + if (!data.logos) return + + const json = data.logos + const entries = Array.isArray(json?.data) ? json.data : [] + + // Collect all media URLs from trusted_schools relation + const collected: Logo[] = [] + entries.forEach((item: any, idx: number) => { + const attrs = item?.attributes ?? item ?? {} + const media = + attrs?.trusted_schools?.data || + attrs?.trusted_schools || // handle array directly (as seen in response) + attrs?.trusted_school?.data || + attrs?.trustedSchools?.data + const mediaArray = Array.isArray(media) ? media : Array.isArray(media?.data) ? media.data : [] + + mediaArray.forEach((m: any, mIdx: number) => { + const a = m?.attributes ?? m ?? {} + const url = + a?.url || + a?.formats?.medium?.url || + a?.formats?.small?.url || + a?.formats?.thumbnail?.url || + "" + if (!url) return + // Ensure HTTPS to prevent Mixed Content errors + const absolute = ensureHttpsImageUrl(url) + collected.push({ + id: `${item?.id ?? idx}-${m?.id ?? mIdx}`, + src: absolute, + alt: a?.alternativeText || "Trusted school", + }) + }) + }) + + if (collected.length > 0) { + setLogos(collected) + } + // Don't set fallback - user wants only Strapi images + } catch (err) { + console.error("Failed to load trusted schools logos:", err) + // Don't set fallback - user wants only Strapi images + } + } + + loadLogos() + }, []) + + const renderedLogos = logos + + return ( +
+
+

+ {heading} +

+

+ {subtext} +

+
+ + {renderedLogos.length > 0 && ( +
+
+ + {renderedLogos.concat(renderedLogos).map((item, idx) => ( +
+ {item.alt} +
+ ))} +
+
+
+ )} +
+ ) +} diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx new file mode 100644 index 0000000..e538a33 --- /dev/null +++ b/components/ui/accordion.tsx @@ -0,0 +1,66 @@ +'use client' + +import * as React from 'react' +import * as AccordionPrimitive from '@radix-ui/react-accordion' +import { ChevronDownIcon } from 'lucide-react' + +import { cn } from '@/lib/utils' + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180', + className, + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..9704452 --- /dev/null +++ b/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +'use client' + +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' + +import { cn } from '@/lib/utils' +import { buttonVariants } from '@/components/ui/button' + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 0000000..e6751ab --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const alertVariants = cva( + 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', + { + variants: { + variant: { + default: 'bg-card text-card-foreground', + destructive: + 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<'div'> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/components/ui/aspect-ratio.tsx b/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..40bb120 --- /dev/null +++ b/components/ui/aspect-ratio.tsx @@ -0,0 +1,11 @@ +'use client' + +import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio' + +function AspectRatio({ + ...props +}: React.ComponentProps) { + return +} + +export { AspectRatio } diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..aa98465 --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +'use client' + +import * as React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' + +import { cn } from '@/lib/utils' + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..fc4126b --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', + secondary: + 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', + destructive: + 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<'span'> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'span' + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..1750ff2 --- /dev/null +++ b/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { ChevronRight, MoreHorizontal } from 'lucide-react' + +import { cn } from '@/lib/utils' + +function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) { + return