From e1578c13311905022bb91ecff667bcf99d59145f Mon Sep 17 00:00:00 2001 From: Joaquim Costa Date: Thu, 30 Jul 2026 23:13:55 -0400 Subject: [PATCH 01/11] chore(web): remove unused components, dead code, and obsolete exports --- apps/web/package.json | 6 - .../(main)/design-system/_data/components.ts | 357 ------------------ .../src/components/admin/mdx-engine/index.ts | 2 - .../admin/mdx-engine/mdx-commit-button.tsx | 104 ----- .../admin/mdx-engine/mdx-preview-modal.tsx | 266 ------------- .../src/components/catalyst-ui/fieldset.tsx | 42 --- .../src/components/catalyst-ui/popover.tsx | 29 -- .../src/components/contact/contact-form.tsx | 0 apps/web/src/components/content/search.tsx | 194 ---------- .../landing/hero-section-atmospheric.tsx | 192 ---------- .../components/landing/map-teaser-section.tsx | 8 +- .../homepage-newsletter-section.tsx | 251 ------------ .../web/src/components/ui/page-transition.tsx | 60 --- .../src/components/ui/video-hero-section.tsx | 178 --------- .../features/map/components/map-canvas.tsx | 2 +- apps/web/src/hooks/use-smooth-scroll.ts | 90 ----- apps/web/src/hooks/useFilterUrlSync.ts | 136 ------- apps/web/src/lib/content/mdx-components.tsx | 2 - apps/web/src/lib/mocks/directory.ts | 165 -------- apps/web/src/lib/mocks/index.ts | 37 -- apps/web/src/lib/mocks/user.ts | 169 --------- apps/web/src/schemas/index.ts | 1 - apps/web/src/schemas/mediaMetadataSchema.ts | 49 --- apps/web/src/schemas/storySchema.ts | 28 -- apps/web/velite.config.ts | 12 - 25 files changed, 5 insertions(+), 2375 deletions(-) delete mode 100644 apps/web/src/app/(main)/design-system/_data/components.ts delete mode 100644 apps/web/src/components/admin/mdx-engine/index.ts delete mode 100644 apps/web/src/components/admin/mdx-engine/mdx-commit-button.tsx delete mode 100644 apps/web/src/components/admin/mdx-engine/mdx-preview-modal.tsx delete mode 100644 apps/web/src/components/contact/contact-form.tsx delete mode 100644 apps/web/src/components/content/search.tsx delete mode 100644 apps/web/src/components/landing/hero-section-atmospheric.tsx delete mode 100644 apps/web/src/components/newsletter/homepage-newsletter-section.tsx delete mode 100644 apps/web/src/components/ui/page-transition.tsx delete mode 100644 apps/web/src/components/ui/video-hero-section.tsx delete mode 100644 apps/web/src/hooks/use-smooth-scroll.ts delete mode 100644 apps/web/src/hooks/useFilterUrlSync.ts delete mode 100644 apps/web/src/lib/mocks/directory.ts delete mode 100644 apps/web/src/lib/mocks/index.ts delete mode 100644 apps/web/src/lib/mocks/user.ts delete mode 100644 apps/web/src/schemas/mediaMetadataSchema.ts delete mode 100644 apps/web/src/schemas/storySchema.ts diff --git a/apps/web/package.json b/apps/web/package.json index ac75314f..2a571cb0 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -54,7 +54,6 @@ "lucide-react": "^1.24.0", "mapbox-gl": "^3.26.0", "next": "16.2.10", - "next-router-mock": "^1.0.5", "react": "19.2.7", "react-dom": "19.2.7", "react-dropzone": "^17.0.0", @@ -63,10 +62,8 @@ "react-markdown": "^10.1.0", "react-syntax-highlighter": "^16.1.1", "recharts": "^3.9.2", - "rehype-pretty-code": "^0.14.4", "resend": "^6.17.2", "sharp": "^0.35.3", - "shiki": "^4.3.1", "supercluster": "^8.0.1", "use-supercluster": "^1.2.0", "velite": "^0.4.0", @@ -74,9 +71,6 @@ "zustand": "^5.0.14" }, "devDependencies": { - "@eslint/eslintrc": "^3", - "@microsoft/eslint-formatter-sarif": "^3.1.0", - "@next/bundle-analyzer": "16.2.10", "@playwright/test": "^1.61.1", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/postcss": "^4", diff --git a/apps/web/src/app/(main)/design-system/_data/components.ts b/apps/web/src/app/(main)/design-system/_data/components.ts deleted file mode 100644 index b1caaacb..00000000 --- a/apps/web/src/app/(main)/design-system/_data/components.ts +++ /dev/null @@ -1,357 +0,0 @@ -/** - * Component metadata for the design system gallery. - * Used by specimen components to display import paths and descriptions. - */ - -export interface ComponentExample { - name: string; - description: string; - importPath: string; - source: "custom" | "catalyst"; -} - -export const buttonComponents: ComponentExample[] = [ - { - name: "AnimatedButton", - description: - "Primary action button with Framer Motion animations. Supports variants (primary, secondary, outline, ghost), sizes, loading states, and icons.", - importPath: "@/components/ui/animated-button", - source: "custom", - }, -]; - -export const inputComponents: ComponentExample[] = [ - { - name: "Input", - description: - "Text input field with validation states and theming. Based on HeadlessUI.", - importPath: "@/components/catalyst-ui/input", - source: "catalyst", - }, - { - name: "InputGroup", - description: - "Container for input with icons. Positions icons automatically.", - importPath: "@/components/catalyst-ui/input", - source: "catalyst", - }, -]; - -export const checkboxComponents: ComponentExample[] = [ - { - name: "Checkbox", - description: - "Checkbox with 22 color variants. Supports checked, indeterminate, and disabled states.", - importPath: "@/components/catalyst-ui/checkbox", - source: "catalyst", - }, - { - name: "CheckboxField", - description: "Layout wrapper for checkbox with label and description.", - importPath: "@/components/catalyst-ui/checkbox", - source: "catalyst", - }, - { - name: "CheckboxGroup", - description: "Container for multiple related checkboxes.", - importPath: "@/components/catalyst-ui/checkbox", - source: "catalyst", - }, -]; - -export const feedbackComponents: ComponentExample[] = [ - { - name: "Banner", - description: - "Promotional banner with gradient background. Supports default and high-contrast tones.", - importPath: "@/components/ui/banner", - source: "custom", - }, - { - name: "LoadingSpinner", - description: - "Animated loading indicator with three spinning dots. Available in sm, md, lg sizes.", - importPath: "@/components/ui/loading-spinner", - source: "custom", - }, - { - name: "LoadingDots", - description: "Inline loading dots animation for text contexts.", - importPath: "@/components/ui/loading-spinner", - source: "custom", - }, - { - name: "LoadingPulse", - description: "Subtle pulse animation wrapper for skeleton states.", - importPath: "@/components/ui/loading-spinner", - source: "custom", - }, - { - name: "ConfirmationDialog", - description: - "Modal dialog for destructive actions. Supports default, warning, and danger variants.", - importPath: "@/components/ui/confirmation-dialog", - source: "custom", - }, -]; - -export const cardComponents: ComponentExample[] = [ - { - name: "Card", - description: - "Base card component with Calm Premium tokens. Optional hoverable prop adds lift animation.", - importPath: "@/components/ui/card", - source: "custom", - }, - { - name: "DirectoryCard", - description: - "Project-specific card for directory entries. Includes image, category badge, bookmark button, and metadata.", - importPath: "@/components/directory/directory-card", - source: "custom", - }, -]; - -export const toastComponents: ComponentExample[] = [ - { - name: "Toast", - description: - "Toast notification with 4 semantic variants (success, error, info, warning). Supports action buttons, keyboard dismissal (Esc), and hover-pause for auto-dismiss.", - importPath: "@/components/ui/toast/toast", - source: "custom", - }, - { - name: "useToast", - description: - "Fluent builder hook for creating toasts. Chain methods: .success().action().duration().id().show()", - importPath: "@/hooks/use-toast", - source: "custom", - }, -]; - -export const badgeComponents: ComponentExample[] = [ - { - name: "Badge", - description: - "Inline status indicator with 5 color variants (blue, green, yellow, red, zinc). Uses design system brand colors.", - importPath: "@/components/catalyst-ui/badge", - source: "catalyst", - }, - { - name: "BadgeButton", - description: - "Interactive badge that can be a button or link. Includes focus ring and hover state.", - importPath: "@/components/catalyst-ui/badge", - source: "catalyst", - }, -]; - -export const dialogComponents: ComponentExample[] = [ - { - name: "Dialog", - description: - "Modal dialog with HeadlessUI. Supports 9 size variants from xs to 5xl. Mobile-responsive as bottom sheet.", - importPath: "@/components/catalyst-ui/dialog", - source: "catalyst", - }, - { - name: "DialogTitle", - description: "Dialog header with proper heading semantics.", - importPath: "@/components/catalyst-ui/dialog", - source: "catalyst", - }, - { - name: "DialogDescription", - description: "Descriptive text beneath the dialog title.", - importPath: "@/components/catalyst-ui/dialog", - source: "catalyst", - }, - { - name: "DialogBody", - description: "Container for dialog content with proper spacing.", - importPath: "@/components/catalyst-ui/dialog", - source: "catalyst", - }, - { - name: "DialogActions", - description: "Footer container for action buttons with responsive layout.", - importPath: "@/components/catalyst-ui/dialog", - source: "catalyst", - }, -]; - -export const skeletonComponents: ComponentExample[] = [ - { - name: "DirectoryCardSkeleton", - description: - "Skeleton loading state matching DirectoryCard layout. Uses semantic color tokens for dark mode.", - importPath: "@/components/ui/directory-card-skeleton", - source: "custom", - }, - { - name: "DirectoryGridSkeleton", - description: - "Grid of skeleton cards for directory pages. Configurable count.", - importPath: "@/components/ui/directory-grid-skeleton", - source: "custom", - }, -]; - -export const layoutComponents: ComponentExample[] = [ - { - name: "PageHeader", - description: - "Animated page header with title, optional subtitle, heading level (h1/h2), size variants, and bougainvillea accent bar.", - importPath: "@/components/ui/page-header", - source: "custom", - }, -]; - -export const navigationComponents: ComponentExample[] = [ - { - name: "MobileBottomNav", - description: - "Fixed bottom navigation for mobile. Thumb-zone optimized with iOS safe area support. Auto-hides on detail pages.", - importPath: "@/components/navigation/mobile-bottom-nav", - source: "custom", - }, -]; - -export const toolbarComponents: ComponentExample[] = [ - { - name: "ContentActionToolbar", - description: - "Responsive toolbar for content pages. Desktop: fixed left-rail. Mobile: FAB with expandable menu. Includes share, reactions, print, suggest.", - importPath: "@/components/content/content-action-toolbar", - source: "custom", - }, - { - name: "ContentActionDesktop", - description: - "Desktop variant of toolbar. Fixed left-rail with vertical action stack.", - importPath: - "@/components/content/content-action-toolbar/content-action-desktop", - source: "custom", - }, - { - name: "ContentActionFAB", - description: - "Mobile variant of toolbar. 56×56px FAB that expands upward with stagger animation.", - importPath: - "@/components/content/content-action-toolbar/content-action-fab", - source: "custom", - }, -]; - -export const avatarComponents: ComponentExample[] = [ - { - name: "Avatar", - description: - "User profile image with initials fallback, size variants (xs-xl), and optional status indicator.", - importPath: "@/components/ui/avatar", - source: "custom", - }, - { - name: "AvatarGroup", - description: - "Stacked avatar display with configurable max count and overflow indicator (+N).", - importPath: "@/components/ui/avatar", - source: "custom", - }, - { - name: "AvatarButton", - description: - "Clickable avatar with expanded touch target (44×44px) and focus ring.", - importPath: "@/components/ui/avatar", - source: "custom", - }, -]; - -export const overlayComponents: ComponentExample[] = [ - { - name: "Tooltip", - description: - "Simple text hint on hover/focus. Position variants: top, bottom, left, right.", - importPath: "@/components/ui/tooltip", - source: "custom", - }, - { - name: "Popover", - description: - "Rich content panel that opens on click. Auto-positioning with HeadlessUI anchor.", - importPath: "@/components/catalyst-ui/popover", - source: "catalyst", - }, - { - name: "Dropdown", - description: - "Action menu with items, dividers, icons, and keyboard shortcuts.", - importPath: "@/components/catalyst-ui/dropdown", - source: "catalyst", - }, - { - name: "DropdownItem", - description: - "Menu item supporting buttons, links, icons, disabled, and destructive states.", - importPath: "@/components/catalyst-ui/dropdown", - source: "catalyst", - }, -]; - -export const tabsComponents: ComponentExample[] = [ - { - name: "TabGroup", - description: - "HeadlessUI Tab component. Container for tabs with managed state.", - importPath: "@headlessui/react", - source: "catalyst", - }, - { - name: "TabList", - description: "Container for tab buttons with flex layout.", - importPath: "@headlessui/react", - source: "catalyst", - }, - { - name: "Tab", - description: - "Individual tab button. Supports data-selected for active styling.", - importPath: "@headlessui/react", - source: "catalyst", - }, - { - name: "TabPanels / TabPanel", - description: "Container and individual panels for tab content.", - importPath: "@headlessui/react", - source: "catalyst", - }, -]; - -export const paginationComponents: ComponentExample[] = [ - { - name: "Pagination", - description: - "Page navigation with prev/next buttons, page numbers, and ellipsis for large ranges. Size variants: sm, md, lg.", - importPath: "local pattern", - source: "custom", - }, -]; - -export const allComponents = [ - ...buttonComponents, - ...inputComponents, - ...checkboxComponents, - ...feedbackComponents, - ...cardComponents, - ...toastComponents, - ...badgeComponents, - ...dialogComponents, - ...skeletonComponents, - ...layoutComponents, - ...navigationComponents, - ...toolbarComponents, - ...avatarComponents, - ...overlayComponents, - ...tabsComponents, - ...paginationComponents, -]; diff --git a/apps/web/src/components/admin/mdx-engine/index.ts b/apps/web/src/components/admin/mdx-engine/index.ts deleted file mode 100644 index c3f047df..00000000 --- a/apps/web/src/components/admin/mdx-engine/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { MdxPreviewModal } from "./mdx-preview-modal"; -export { MdxCommitButton } from "./mdx-commit-button"; diff --git a/apps/web/src/components/admin/mdx-engine/mdx-commit-button.tsx b/apps/web/src/components/admin/mdx-engine/mdx-commit-button.tsx deleted file mode 100644 index 2be419fd..00000000 --- a/apps/web/src/components/admin/mdx-engine/mdx-commit-button.tsx +++ /dev/null @@ -1,104 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { FileCode, Loader2 } from "lucide-react"; -import { MdxPreviewModal } from "./mdx-preview-modal"; -import { generateMdx, commitMdx } from "@/lib/api"; -import type { MdxContent } from "@/types/admin"; - -interface MdxCommitButtonProps { - storyId: string; - storyTitle: string; -} - -export function MdxCommitButton({ storyId, storyTitle }: MdxCommitButtonProps) { - const [isGenerating, setIsGenerating] = useState(false); - const [isCommitting, setIsCommitting] = useState(false); - const [mdxContent, setMdxContent] = useState(null); - const [isModalOpen, setIsModalOpen] = useState(false); - - const handleGenerate = async () => { - setIsGenerating(true); - - try { - const content = await generateMdx(storyId); - setMdxContent(content); - setIsModalOpen(true); - } catch (err) { - console.error("Failed to generate MDX:", err); - // Show error toast/notification - alert( - `Error: ${err instanceof Error ? err.message : "Failed to generate MDX content"}` - ); - } finally { - setIsGenerating(false); - } - }; - - const handleCommit = async (mdxSource: string) => { - setIsCommitting(true); - - try { - const result = await commitMdx( - storyId, - mdxSource, - `Archive story: ${storyTitle}` - ); - - // Success - close modal and show success message - setIsModalOpen(false); - setMdxContent(null); - - // Show success toast/notification - alert( - `MDX committed successfully!\nPath: ${result.mdxPath}\nCommitted by: ${result.committedBy}` - ); - } catch (err) { - console.error("Failed to commit MDX:", err); - // Show error toast/notification - alert( - `Error: ${err instanceof Error ? err.message : "Failed to commit MDX content"}` - ); - } finally { - setIsCommitting(false); - } - }; - - const handleCloseModal = () => { - if (!isCommitting) { - setIsModalOpen(false); - setMdxContent(null); - } - }; - - return ( - <> - - - - - ); -} diff --git a/apps/web/src/components/admin/mdx-engine/mdx-preview-modal.tsx b/apps/web/src/components/admin/mdx-engine/mdx-preview-modal.tsx deleted file mode 100644 index 23a5ead7..00000000 --- a/apps/web/src/components/admin/mdx-engine/mdx-preview-modal.tsx +++ /dev/null @@ -1,266 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { - Dialog, - DialogBackdrop, - DialogPanel, - DialogTitle, -} from "@headlessui/react"; -import { - X, - CheckCircle, - AlertTriangle, - FileText, - Calendar, - User, - MapPin, -} from "lucide-react"; -import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; -import { dark } from "react-syntax-highlighter/dist/esm/styles/prism"; -import type { MdxContent } from "@/types/admin"; -import { ConfirmationDialog } from "@/components/ui/confirmation-dialog"; - -interface MdxPreviewModalProps { - mdxContent: MdxContent | null; - isOpen: boolean; - onClose: () => void; - onCommit: (mdxSource: string) => void; - isCommitting?: boolean; -} - -export function MdxPreviewModal({ - mdxContent, - isOpen, - onClose, - onCommit, - isCommitting = false, -}: MdxPreviewModalProps) { - const [showValidationDetails, setShowValidationDetails] = useState(false); - const [showValidationConfirm, setShowValidationConfirm] = useState(false); - - if (!mdxContent) return null; - - const handleCommit = () => { - if (!mdxContent.schemaValid) { - setShowValidationConfirm(true); - return; - } - onCommit(mdxContent.mdxSource); - }; - - const handleValidationConfirm = () => { - setShowValidationConfirm(false); - onCommit(mdxContent.mdxSource); - }; - - return ( - - - -
-
- - {/* Header */} -
-
-
- - - MDX Archival Engine - -
-

- Preview and commit generated MDX content -

-
- -
- - {/* Content */} -
- {/* Schema Validation Status */} -
- {mdxContent.schemaValid ? ( -
- -
-

- Schema-compliant MDX -

-

- All validation checks passed -

-
-
- ) : ( -
-
- -
-

- Schema validation errors -

- -
-
- {showValidationDetails && - mdxContent.validationErrors && - mdxContent.validationErrors.length > 0 && ( -
    - {mdxContent.validationErrors.map((error, index) => ( -
  • - • {error} -
  • - ))} -
- )} -
- )} -
- - {/* Frontmatter Preview */} -
-

- - Frontmatter Preview -

-
-
- - Author: - {mdxContent.frontmatter.author} -
-
- - Date: - {mdxContent.frontmatter.date} -
- {mdxContent.frontmatter.location && ( -
- - Location: - {mdxContent.frontmatter.location} -
- )} -
- - Type: - {mdxContent.frontmatter.storyType} -
-
- {mdxContent.frontmatter.tags && - mdxContent.frontmatter.tags.length > 0 && ( -
- {mdxContent.frontmatter.tags.map((tag, index) => ( - - {tag} - - ))} -
- )} -
- - {/* MDX Source Code Preview */} -
-
-

- MDX Source -

-
-
- - {mdxContent.mdxSource} - -
-
- - {/* Metadata */} -
-

- Generated at:{" "} - {new Date(mdxContent.generatedAt).toLocaleString()} -

-

Slug: {mdxContent.slug}

-
-
- - {/* Actions */} -
- - -
-
-
-
- - {/* Validation Warning Confirmation */} - setShowValidationConfirm(false)} - onConfirm={handleValidationConfirm} - title="Commit with validation errors?" - description="Schema validation has errors. The MDX will be committed but not merged until validation passes." - confirmLabel="Commit Anyway" - variant="warning" - /> -
- ); -} diff --git a/apps/web/src/components/catalyst-ui/fieldset.tsx b/apps/web/src/components/catalyst-ui/fieldset.tsx index a1f4120d..f77366c4 100644 --- a/apps/web/src/components/catalyst-ui/fieldset.tsx +++ b/apps/web/src/components/catalyst-ui/fieldset.tsx @@ -2,49 +2,7 @@ import * as Headless from "@headlessui/react"; import clsx from "clsx"; import type React from "react"; -export function Fieldset({ - className, - ...props -}: { className?: string } & Omit) { - return ( - *+[data-slot=control]]:mt-6" - )} - /> - ); -} - -export function Legend({ - className, - ...props -}: { className?: string } & Omit) { - return ( - - ); -} -export function FieldGroup({ - className, - ...props -}: React.ComponentPropsWithoutRef<"div">) { - return ( -
- ); -} export function Field({ className, diff --git a/apps/web/src/components/catalyst-ui/popover.tsx b/apps/web/src/components/catalyst-ui/popover.tsx index 899cd4b1..97625fc7 100644 --- a/apps/web/src/components/catalyst-ui/popover.tsx +++ b/apps/web/src/components/catalyst-ui/popover.tsx @@ -97,33 +97,4 @@ export function PopoverGroup({ ); } -/** - * PopoverBackdrop adds a backdrop when popover is open. - */ -export function PopoverBackdrop({ className }: { className?: string }) { - return ( - - ); -} -/** - * CloseButton closes the popover when clicked. - */ -export function PopoverClose({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { - return ( - {children} - ); -} diff --git a/apps/web/src/components/contact/contact-form.tsx b/apps/web/src/components/contact/contact-form.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/web/src/components/content/search.tsx b/apps/web/src/components/content/search.tsx deleted file mode 100644 index 382b2fd1..00000000 --- a/apps/web/src/components/content/search.tsx +++ /dev/null @@ -1,194 +0,0 @@ -"use client"; - -import { useEffect, useState, useCallback } from "react"; -import Link from "next/link"; -import { Search, X } from "lucide-react"; - -interface SearchResult { - url: string; - excerpt: string; - meta: { - title?: string; - category?: string; - }; -} - -interface PagefindUI { - search: (query: string) => Promise<{ - results: Array<{ - id: string; - data: () => Promise; - }>; - }>; -} - -interface ContentSearchProps { - placeholder?: string; - className?: string; - /** Callback when user submits search (Enter key or explicit action) */ - onSearchSubmit?: (query: string) => void; -} - -export function ContentSearch({ - placeholder = "Search articles...", - className = "", - onSearchSubmit, -}: ContentSearchProps) { - const [query, setQuery] = useState(""); - const [results, setResults] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [isOpen, setIsOpen] = useState(false); - const [pagefind, setPagefind] = useState(null); - - // Load Pagefind on mount (only in browser, after build) - useEffect(() => { - async function loadPagefind() { - if (typeof window === "undefined") return; - - try { - // Use dynamic path to prevent static analysis by bundler - const pagefindPath = ["/pagefind", "pagefind.js"].join("/"); - const pf = await import(/* webpackIgnore: true */ pagefindPath); - await pf.init(); - setPagefind(pf); - } catch (_error) { - // Pagefind not available - likely in development mode before build - console.warn( - "Pagefind not loaded. Run 'pnpm run build' to generate search index." - ); - } - } - loadPagefind(); - }, []); - - // Search function - const performSearch = useCallback( - async (searchQuery: string) => { - if (!pagefind || !searchQuery.trim()) { - setResults([]); - return; - } - - setIsLoading(true); - try { - const search = await pagefind.search(searchQuery); - const searchResults = await Promise.all( - search.results.slice(0, 10).map((result) => result.data()) - ); - setResults(searchResults); - setIsOpen(true); - } catch (error) { - console.error("Search error:", error); - setResults([]); - } finally { - setIsLoading(false); - } - }, - [pagefind] - ); - - // Debounced search - useEffect(() => { - const timer = setTimeout(() => { - if (query) { - performSearch(query); - } else { - setResults([]); - setIsOpen(false); - } - }, 300); - - return () => clearTimeout(timer); - }, [query, performSearch]); - - // Close on escape - useEffect(() => { - function handleKeyDown(e: KeyboardEvent) { - if (e.key === "Escape") { - setIsOpen(false); - setQuery(""); - } - } - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, []); - - return ( -
- {/* Search input */} -
- - setQuery(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter" && query.trim() && onSearchSubmit) { - e.preventDefault(); - onSearchSubmit(query.trim()); - } - }} - placeholder={placeholder} - className="focus:border-ocean-blue focus:ring-ocean-blue border-hairline bg-surface text-body w-full rounded-lg border py-2 pr-10 pl-10 text-sm focus:ring-1 focus:outline-none" - aria-label="Search articles" - /> - {query && ( - - )} -
- - {/* Search results dropdown */} - {isOpen && (query || results.length > 0) && ( -
- {isLoading ? ( -
- Searching... -
- ) : results.length > 0 ? ( -
    - {results.map((result, index) => ( -
  • - { - setIsOpen(false); - setQuery(""); - }} - className="hover:bg-surface-alt block px-4 py-3" - > -
    - {result.meta.title || "Untitled"} -
    - {result.meta.category && ( - - {result.meta.category} - - )} -

    - -

  • - ))} -
- ) : query ? ( -
- No results found for "{query}" -
- ) : null} -
- )} -
- ); -} diff --git a/apps/web/src/components/landing/hero-section-atmospheric.tsx b/apps/web/src/components/landing/hero-section-atmospheric.tsx deleted file mode 100644 index 8563125b..00000000 --- a/apps/web/src/components/landing/hero-section-atmospheric.tsx +++ /dev/null @@ -1,192 +0,0 @@ -"use client"; - -import { useState } from "react"; -import Image from "next/image"; -import { useRouter } from "next/navigation"; -import { motion, useReducedMotion } from "framer-motion"; -import { ArrowRight, MapPin, Users } from "lucide-react"; -import Link from "next/link"; -import { UnifiedSearch } from "@/components/search"; -import { ScrollIndicator } from "@/components/ui/scroll-indicator"; - -interface HeroSectionAtmosphericProps { - className?: string; -} - -export function HeroSectionAtmospheric({ - className, -}: HeroSectionAtmosphericProps) { - const router = useRouter(); - const [searchFocused, setSearchFocused] = useState(false); - const shouldReduceMotion = useReducedMotion(); - - // --- Interaction Handlers --- - const handleScrollDown = () => { - const sections = document.querySelectorAll("section"); - const heritageSection = sections[1]; - if (heritageSection) { - const rect = heritageSection.getBoundingClientRect(); - window.scrollTo({ - top: window.scrollY + rect.top - 60, - behavior: "smooth", - }); - } else { - window.scrollTo({ - top: window.innerHeight, - behavior: "smooth", - }); - } - }; - - // --- Animation Variants (entrance only, no continuous loops) --- - const containerVariants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { - staggerChildren: 0.15, - delayChildren: 0.2, - }, - }, - } as const; - - const itemVariants = { - hidden: { y: 20, opacity: 0 }, - visible: { - y: 0, - opacity: 1, - transition: { duration: 0.4, ease: "easeOut" as const }, - }, - } as const; - - return ( -
- {/* --- 1. Background Layer (Static) --- */} -
-
- Brava Island coastline at dawn -
-
- - {/* --- 2. Gradient Overlay --- */} -
- - {/* --- 3. Main Content Container --- */} -
- - {/* Context Badges - Trust & Location */} - - - - - - {/* Headlines - capped at text-6xl */} - -

- Discover the
- - Soul of Brava - -

-

- “Nos terra, nos gente, nos memoria.” -

-
- - {/* Description */} - -

- A living archive connecting the people of Djabraba and the global - diaspora. We preserve Brava's oral histories and celebrate the - resilience of our island culture. -

-
- - {/* Dual CTAs - CSS-only hover effects */} - - {/* Primary CTA - Share Your Story */} - - Share Your Story - - - {/* Search Bar - Simplified focus state */} - -
- - router.push(`/directory?q=${encodeURIComponent(query)}`) - } - /> -
-
-
-
- - {/* --- 4. Scroll Indicator --- */} - -
- ); -} - -export default HeroSectionAtmospheric; diff --git a/apps/web/src/components/landing/map-teaser-section.tsx b/apps/web/src/components/landing/map-teaser-section.tsx index 39084ead..431dce25 100644 --- a/apps/web/src/components/landing/map-teaser-section.tsx +++ b/apps/web/src/components/landing/map-teaser-section.tsx @@ -56,7 +56,7 @@ function MapMarker({
{/* Label (Floating below) */} -
+
{label}
@@ -199,7 +199,7 @@ export function MapTeaserSection() { className="order-1 w-full lg:order-2 lg:w-1/2" > {/* Map Container */} -
+
-
+
-
+
3D
diff --git a/apps/web/src/components/newsletter/homepage-newsletter-section.tsx b/apps/web/src/components/newsletter/homepage-newsletter-section.tsx deleted file mode 100644 index e679e484..00000000 --- a/apps/web/src/components/newsletter/homepage-newsletter-section.tsx +++ /dev/null @@ -1,251 +0,0 @@ -"use client"; - -import { useState, useRef } from "react"; -import clsx from "clsx"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { - newsletterSchema, - type NewsletterInput, -} from "@/lib/validation/newsletter-schema"; -import { subscribeToNewsletter } from "@/app/actions/newsletter"; -import { - hasSubmittedEmail, - recordSubmittedEmail, -} from "@/lib/newsletter-submission-cache"; - -/** - * Homepage Newsletter Subscription Section - * - * Features: - * - React Hook Form with Zod validation - * - Server Action integration - * - Inline success/error messages with ARIA live regions - * - Hidden honeypot field for spam prevention - * - Ocean Blue gradient background (Nos Ilha brand) - * - Mobile-first responsive design - * - WCAG 2.1 Level AA accessible - * - * @see frontend/src/app/actions/newsletter.ts - Server Action - * @see frontend/src/lib/validation/newsletter-schema.ts - Validation schema - */ -export default function HomepageNewsletterSection() { - const [responseMessage, setResponseMessage] = useState(null); - const [messageVariant, setMessageVariant] = useState< - "success" | "error" | "info" - >("success"); - const [isProcessing, setIsProcessing] = useState(false); - const emailInputRef = useRef(null); - - const { - register, - handleSubmit, - formState: { errors }, - reset, - } = useForm({ - resolver: zodResolver(newsletterSchema), - }); - - const onSubmit = async (data: NewsletterInput) => { - try { - // Clear any previous messages when form is submitted - setResponseMessage(null); - setIsProcessing(true); - - const normalizedEmail = data.email.trim().toLowerCase(); - - // Security: Show same success message to prevent email enumeration attacks - // (OWASP recommendation: consistent messages for existent/non-existent accounts) - if (hasSubmittedEmail(normalizedEmail)) { - setMessageVariant("success"); - setResponseMessage("Thank you for subscribing!"); - reset(); - emailInputRef.current?.focus(); - return; - } - - // Create FormData for Server Action - const formData = new FormData(); - formData.append("email", data.email); - if (data.website) { - formData.append("website", data.website); - } - - // Call Server Action - const response = await subscribeToNewsletter(formData); - - if (response.success) { - recordSubmittedEmail(normalizedEmail); - // Always show success message (server no longer returns duplicate flag) - setMessageVariant("success"); - setResponseMessage(response.message); - reset(); - emailInputRef.current?.focus(); - } else { - setMessageVariant("error"); - setResponseMessage(response.message); - } - } catch (_error) { - setMessageVariant("error"); - setResponseMessage( - "An unexpected error occurred. Please try again later." - ); - } finally { - setIsProcessing(false); - } - }; - - /** - * Handle validation errors by clearing any existing messages. - * This prevents message persistence when validation fails after showing - * an "already subscribed" message. - */ - const onInvalid = () => { - // Clear the response message when validation fails - setResponseMessage(null); - }; - - const messageClassName = clsx( - "mt-4 rounded-md px-4 py-3 text-center text-base font-semibold", - { - "bg-white/10 text-white": messageVariant === "success", - "bg-sobrado-ochre/20 text-sobrado-ochre": messageVariant === "info", - "bg-white text-bougainvillea-pink": messageVariant === "error", - } - ); - - return ( -
-
-
- {/* Ocean-inspired radial gradient overlay */} - -
-
- ); -} diff --git a/apps/web/src/components/ui/page-transition.tsx b/apps/web/src/components/ui/page-transition.tsx deleted file mode 100644 index 3d1eef50..00000000 --- a/apps/web/src/components/ui/page-transition.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; - -import { motion } from "framer-motion"; -import { usePathname } from "next/navigation"; -import { useMediaQuery } from "@/lib/hooks/use-media-query"; - -interface PageTransitionProps { - children: React.ReactNode; - className?: string; -} - -export function PageTransition({ children, className }: PageTransitionProps) { - const pathname = usePathname(); - const prefersReducedMotion = useMediaQuery( - "(prefers-reduced-motion: reduce)" - ); - - // If reduced motion is preferred, return children without animation - if (prefersReducedMotion) { - return
{children}
; - } - - return ( - - {children} - - ); -} - -export function AnimatedSection({ - children, - className, - delay = 0, -}: PageTransitionProps & { delay?: number }) { - const prefersReducedMotion = useMediaQuery( - "(prefers-reduced-motion: reduce)" - ); - - if (prefersReducedMotion) { - return
{children}
; - } - - return ( - - {children} - - ); -} diff --git a/apps/web/src/components/ui/video-hero-section.tsx b/apps/web/src/components/ui/video-hero-section.tsx deleted file mode 100644 index cae81959..00000000 --- a/apps/web/src/components/ui/video-hero-section.tsx +++ /dev/null @@ -1,178 +0,0 @@ -"use client"; - -import { motion, useInView } from "framer-motion"; -import { useRef, useEffect, useState } from "react"; -import { useMediaQuery } from "@/lib/hooks/use-media-query"; -import { ScrollIndicator } from "@/components/ui/scroll-indicator"; - -interface VideoHeroSectionProps { - videoSrc: string; - posterSrc?: string; - title: string; - subtitle?: string; - overlayContent?: { - text: string; - delay: number; - }[]; - className?: string; -} - -export function VideoHeroSection({ - videoSrc, - posterSrc, - title, - subtitle, - overlayContent, - className = "", -}: VideoHeroSectionProps) { - const sectionRef = useRef(null); - const videoRef = useRef(null); - const isInView = useInView(sectionRef, { - once: false, - margin: "-10% 0px -10% 0px", - }); - - const [isVideoReady, setIsVideoReady] = useState(false); - const reducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)"); - - // Handle video play/pause based on intersection - useEffect(() => { - if (videoRef.current && isVideoReady) { - if (isInView && !reducedMotion) { - videoRef.current.play().catch(console.error); - } else { - videoRef.current.pause(); - } - } - }, [isInView, isVideoReady, reducedMotion]); - - // Handle smooth scroll to next section - const handleScrollToNext = () => { - const nextSection = sectionRef.current?.nextElementSibling; - if (nextSection) { - nextSection.scrollIntoView({ - behavior: "smooth", - block: "start", - }); - } - }; - - const containerVariants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { - duration: 0.6, - staggerChildren: 0.2, - }, - }, - }; - - const textVariants = { - hidden: { - opacity: 0, - y: 30, - scale: 0.95, - }, - visible: { - opacity: 1, - y: 0, - scale: 1, - transition: { - type: "spring" as const, - damping: 20, - stiffness: 100, - }, - }, - }; - - const overlayVariants = { - hidden: { opacity: 0, y: 20 }, - visible: (delay: number) => ({ - opacity: 1, - y: 0, - transition: { - delay: delay, - duration: 0.8, - ease: "easeOut" as const, - }, - }), - }; - - return ( - - {/* Video Background */} -
-