{"id":49592497,"url":"https://github.com/siyabuilds/shukuma-web","last_synced_at":"2026-05-04T01:38:51.896Z","repository":{"id":326447255,"uuid":"1104735051","full_name":"siyabuilds/shukuma-web","owner":"siyabuilds","description":"Full-stack social fitness app with daily exercises, challenges, friend features, streaks, and DO Spaces media.","archived":false,"fork":false,"pushed_at":"2025-11-29T13:44:01.000Z","size":281,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-11-30T11:57:22.126Z","etag":null,"topics":["nextjs","typescript"],"latest_commit_sha":null,"homepage":"https://shukuma.samson.codes","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/siyabuilds.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-11-26T16:04:23.000Z","updated_at":"2025-11-29T13:44:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/siyabuilds/shukuma-web","commit_stats":null,"previous_names":["siyabuilds/shukuma-web"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/siyabuilds/shukuma-web","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/siyabuilds%2Fshukuma-web","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/siyabuilds%2Fshukuma-web/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/siyabuilds%2Fshukuma-web/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/siyabuilds%2Fshukuma-web/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/siyabuilds","download_url":"https://codeload.github.com/siyabuilds/shukuma-web/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/siyabuilds%2Fshukuma-web/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32591603,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T22:12:39.696Z","status":"ssl_error","status_checked_at":"2026-05-03T22:09:10.534Z","response_time":103,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["nextjs","typescript"],"created_at":"2026-05-04T01:38:51.286Z","updated_at":"2026-05-04T01:38:51.885Z","avatar_url":"https://github.com/siyabuilds.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Shukuma Web\n\n\u003cdiv align=\"center\"\u003e\n\n![Next.js](https://img.shields.io/badge/Next.js-000000?style=for-the-badge\u0026logo=nextdotjs\u0026logoColor=white)\n![React](https://img.shields.io/badge/React_19-61DAFB?style=for-the-badge\u0026logo=react\u0026logoColor=black)\n![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge\u0026logo=typescript\u0026logoColor=white)\n![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS_4-06B6D4?style=for-the-badge\u0026logo=tailwindcss\u0026logoColor=white)\n![Framer Motion](https://img.shields.io/badge/Framer_Motion-0055FF?style=for-the-badge\u0026logo=framer\u0026logoColor=white)\n![Font Awesome](https://img.shields.io/badge/Font_Awesome-528DD7?style=for-the-badge\u0026logo=fontawesome\u0026logoColor=white)\n\n\u003c/div\u003e\n\nThe web frontend for Shukuma — a fitness companion app helping users track exercises, build streaks, complete daily challenges, and connect with a community. Built with Next.js 16 using the App Router and React 19.\n\n---\n\n## Getting Started\n\n### Prerequisites\n\n- Node.js 18+\n- npm or yarn\n- Backend API running (see [backend README](https://github.com/siyabuilds/shukuma_backend/blob/main/README.md))\n\n### Installation\n\n```bash\n# Install dependencies\nnpm install\n\n# Create .env.local with backend URL\necho \"BACKEND_URL=http://localhost:3000\" \u003e .env.local\n\n# Start development server (runs on port 4200)\nnpm run dev\n```\n\nOpen [http://localhost:4200](http://localhost:4200) in your browser.\n\n### Environment Variables\n\n| Variable      | Description                    | Default                 |\n| ------------- | ------------------------------ | ----------------------- |\n| `BACKEND_URL` | URL of the Shukuma backend API | `http://localhost:3000` |\n\n---\n\n## File Structure\n\n```\nweb/\n├── app/                          # Next.js App Router pages\n│   ├── layout.tsx                # Root layout with ThemeProvider\n│   ├── page.tsx                  # Home/dashboard page\n│   ├── globals.css               # CSS variables \u0026 theme tokens\n│   ├── api/                      # API route handlers (proxy to backend)\n│   │   ├── login/route.ts\n│   │   ├── exercises/route.ts\n│   │   └── ...                   # One folder per endpoint\n│   ├── exercises/                # Exercise listing \u0026 details\n│   ├── daily/                    # Daily exercise card\n│   ├── progress/                 # Progress tracking \u0026 charts\n│   ├── journal/                  # Personal journal entries\n│   ├── white-noise/              # Ambient audio player\n│   ├── community/                # Social features \u0026 challenges\n│   ├── login/                    # Authentication\n│   └── register/\n├── components/                   # Reusable UI components\n│   ├── Navbar.tsx                # Navigation with auth state\n│   ├── Footer.tsx                # Footer with theme toggle\n│   ├── theme.tsx                 # ThemeToggle button\n│   ├── ExerciseFlipCard.tsx      # Interactive exercise cards\n│   ├── DailyChallengeCard.tsx    # Daily challenge display\n│   ├── ProgressCharts.tsx        # SVG-based charts\n│   ├── StreakBadges.tsx          # Badge display \u0026 progress\n│   ├── AudioTrackItem.tsx        # White noise player\n│   └── WorkoutChallengeCard.tsx  # Friend challenge cards\n├── contexts/\n│   └── ThemeContext.tsx          # React Context for theming\n├── hooks/\n│   └── useTheme.ts               # Theme state management hook\n├── config/\n│   └── colors.ts                 # Color palette constants\n└── utils/\n    └── swal.ts                   # Theme-aware SweetAlert2 helpers\n```\n\n---\n\n## Design Patterns \u0026 Key Approaches\n\n### 1. CSS Variables for Theming (Token System)\n\nInstead of using Tailwind's built-in dark mode classes, we use CSS custom properties (tokens) that switch based on a `data-theme` attribute. This gives us more control and eliminates class duplication.\n\n**How it works:**\n\n```css\n/* globals.css */\n:root,\n[data-theme=\"light\"] {\n  --background: #f8f9fa;\n  --foreground: #212529;\n  --primary: #ff6b35;\n}\n\n[data-theme=\"dark\"] {\n  --background: #1e1e1e;\n  --foreground: #eaeaea;\n  --primary: #ff6b35; /* Primary stays consistent */\n}\n```\n\nComponents simply use `bg-background`, `text-foreground`, `text-primary` — Tailwind maps these to our CSS variables via the `@theme inline` directive. When the theme changes, everything updates automatically.\n\n**Why this approach?**\n\n- Single class per element (no `dark:bg-gray-900` everywhere)\n- Smooth 0.3s transitions on all theme-aware properties\n- Third-party libraries (like SweetAlert2) can read the same tokens\n\n---\n\n### 2. Theme Hook \u0026 Context Pattern\n\nThe theming system uses a two-layer architecture: a custom hook handles the logic, and a Context makes it available app-wide.\n\n**`hooks/useTheme.ts`** — The Brain\n\n```typescript\nexport function useTheme() {\n  const [theme, setTheme] = useState\u003cTheme\u003e(\"light\");\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() =\u003e {\n    // Check localStorage first, then system preference\n    const savedTheme = localStorage.getItem(\"theme\") as Theme | null;\n    const prefersDark = window.matchMedia(\n      \"(prefers-color-scheme: dark)\"\n    ).matches;\n    const initialTheme = savedTheme || (prefersDark ? \"dark\" : \"light\");\n\n    setTheme(initialTheme);\n    document.documentElement.setAttribute(\"data-theme\", initialTheme);\n    setMounted(true);\n  }, []);\n\n  const toggleTheme = () =\u003e {\n    /* ... */\n  };\n  return { theme, toggleTheme, setTheme, mounted };\n}\n```\n\n**`contexts/ThemeContext.tsx`** — The Distributor\n\n```typescript\nexport function ThemeProvider({ children }: { children: ReactNode }) {\n  const themeData = useThemeHook();\n  return (\n    \u003cThemeContext.Provider value={themeData}\u003e{children}\u003c/ThemeContext.Provider\u003e\n  );\n}\n```\n\n**Why separate hook from context?**\n\n- The hook is testable in isolation\n- Context just distributes — single responsibility\n- `mounted` flag prevents hydration mismatch (see next section)\n\n---\n\n### 3. Hydration Flash Prevention\n\nServer-rendered React doesn't know the user's theme preference, so there's a brief \"flash\" of wrong colors on load. We prevent this with an inline script in `layout.tsx`:\n\n```tsx\n\u003cscript\n  dangerouslySetInnerHTML={{\n    __html: `\n    (function() {\n      try {\n        var theme = localStorage.getItem('theme');\n        if (!theme) {\n          theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n        }\n        document.documentElement.setAttribute('data-theme', theme);\n      } catch (e) {}\n    })();\n  `,\n  }}\n/\u003e\n```\n\nThis runs **before React hydrates**, so the correct theme is applied instantly. The `suppressHydrationWarning` on `\u003chtml\u003e` tells React to expect this mismatch.\n\nAdditionally, theme-dependent components check `mounted` before rendering interactive elements:\n\n```tsx\nif (!mounted) {\n  return \u003cdiv className=\"w-12 h-12 rounded-full bg-neutral\" /\u003e; // Skeleton\n}\n```\n\n---\n\n### 4. API Proxy Routes\n\nThe frontend doesn't call the backend directly from the browser. Instead, Next.js API routes act as a proxy:\n\n```\nBrowser → /api/exercises (Next.js) → BACKEND_URL/api/exercises (Express)\n```\n\n**Why proxy?**\n\n- **Security**: `BACKEND_URL` stays server-side only\n- **CORS avoidance**: Same-origin requests from browser to Next.js\n- **Flexibility**: Can add caching, logging, or transforms later\n\n**Pattern used in every route:**\n\n```typescript\n// app/api/exercises/route.ts\nexport async function GET(request: Request) {\n  const backendUrl = process.env.BACKEND_URL || \"http://localhost:3000\";\n  const response = await fetch(`${backendUrl}/api/exercises`);\n  const data = await response.json();\n  return NextResponse.json(data, { status: response.status });\n}\n```\n\nFor authenticated routes, the `Authorization` header is forwarded:\n\n```typescript\nconst authHeader = request.headers.get(\"Authorization\");\nconst response = await fetch(`${backendUrl}/api/progress`, {\n  headers: { Authorization: authHeader || \"\" },\n});\n```\n\n---\n\n### 5. Theme-Aware SweetAlert2 Dialogs\n\nSweetAlert2 doesn't automatically inherit CSS variables, so `utils/swal.ts` provides wrapper functions that read the current theme and apply matching colors:\n\n```typescript\nconst getThemedColors = () =\u003e {\n  const isDark = document.documentElement.getAttribute(\"data-theme\") === \"dark\";\n  return {\n    background: isDark ? \"#1a1a1a\" : \"#ffffff\",\n    color: isDark ? \"#e5e5e5\" : \"#171717\",\n    confirmButtonColor: isDark ? \"#3b82f6\" : \"#2563eb\",\n  };\n};\n\nexport const showAlert = (title, text, icon) =\u003e {\n  const colors = getThemedColors();\n  return Swal.fire({ title, text, icon, ...colors });\n};\n```\n\nComponents import `showAlert` instead of `Swal.fire` directly, ensuring consistent theming everywhere.\n\n---\n\n### 6. Client-Side Authentication State\n\nAuth tokens are stored in `localStorage` and checked on the client side. The `Navbar` component demonstrates this pattern:\n\n```typescript\nuseEffect(() =\u003e {\n  const token = localStorage.getItem(\"token\");\n  setIsLoggedIn(!!token);\n}, []);\n\nconst handleLogout = () =\u003e {\n  localStorage.removeItem(\"token\");\n  setIsLoggedIn(false);\n  window.location.href = \"/\";\n};\n```\n\n**Why client-side?**\n\n- Simple SPAs can manage auth without server middleware\n- Token is sent with each API request via `Authorization` header\n- Backend validates and returns `401` if expired\n\n**Caveat**: Protected pages should redirect if no token exists:\n\n```typescript\nif (!token) {\n  router.push(\"/login\");\n  return;\n}\n```\n\n---\n\n### 7. Exercise Type Color Mapping\n\nExercises have types (`core`, `lowerbody`, `cardio`, `upperbody`), each mapped to a semantic color and icon. This pattern repeats across components:\n\n```typescript\nconst getTypeIcon = (type: string) =\u003e {\n  switch (type) {\n    case \"core\":\n      return \"fa-circle-notch\";\n    case \"lowerbody\":\n      return \"fa-shoe-prints\";\n    case \"cardio\":\n      return \"fa-heartbeat\";\n    case \"upperbody\":\n      return \"fa-dumbbell\";\n    default:\n      return \"fa-dumbbell\";\n  }\n};\n\nconst getTypeColor = (type: string) =\u003e {\n  switch (type) {\n    case \"core\":\n      return \"text-primary\"; // Orange\n    case \"lowerbody\":\n      return \"text-secondary\"; // Blue\n    case \"cardio\":\n      return \"text-warning\"; // Yellow-orange\n    case \"upperbody\":\n      return \"text-success\"; // Green\n    default:\n      return \"text-primary\";\n  }\n};\n```\n\nThis visual language helps users quickly identify exercise categories across cards, filters, and progress charts.\n\n---\n\n### 8. SVG-Based Progress Charts\n\nInstead of a charting library, `ProgressCharts.tsx` renders SVG directly for full styling control and tiny bundle size:\n\n```tsx\n\u003csvg viewBox=\"0 0 100 100\" preserveAspectRatio=\"none\"\u003e\n  {data.map((item, index) =\u003e {\n    const height = (item.count / maxCount) * 80;\n    return (\n      \u003crect\n        x={`${index * barWidth}%`}\n        y={`${100 - height}%`}\n        width={`${barWidth * 0.8}%`}\n        height={`${height}%`}\n        className=\"fill-primary hover:opacity-100\"\n      /\u003e\n    );\n  })}\n\u003c/svg\u003e\n```\n\n**Advantages:**\n\n- No external dependencies\n- Theme colors via CSS classes\n- Responsive by default (`viewBox` scales)\n\n---\n\n### 9. Audio Player with Shared State\n\nThe white noise page needs to ensure only one track plays at a time. Parent state manages which track is active:\n\n```tsx\n// Parent: white-noise/page.tsx\nconst [currentlyPlayingId, setCurrentlyPlayingId] = useState\u003cstring | null\u003e(\n  null\n);\n\n{\n  tracks.map((track) =\u003e (\n    \u003cAudioTrackItem\n      isCurrentlyPlaying={currentlyPlayingId === track._id}\n      onPlay={(id) =\u003e setCurrentlyPlayingId(id)}\n      onPause={() =\u003e setCurrentlyPlayingId(null)}\n    /\u003e\n  ));\n}\n```\n\nThe `AudioTrackItem` component handles its own playback but defers to parent for coordination. When a new track starts, others automatically pause.\n\n---\n\n### 10. Animated Mobile Navigation\n\nThe navbar uses Framer Motion for a smooth mobile menu with staggered link animations:\n\n```tsx\n\u003cAnimatePresence\u003e\n  {isOpen \u0026\u0026 (\n    \u003cmotion.div\n      initial={{ opacity: 0, height: 0 }}\n      animate={{ opacity: 1, height: \"auto\" }}\n      exit={{ opacity: 0, height: 0 }}\n    \u003e\n      {navLinks.map((link, index) =\u003e (\n        \u003cmotion.div\n          initial={{ opacity: 0, x: -20 }}\n          animate={{ opacity: 1, x: 0 }}\n          transition={{ delay: index * 0.1 }}\n        \u003e\n          \u003cLink href={link.href}\u003e{link.name}\u003c/Link\u003e\n        \u003c/motion.div\u003e\n      ))}\n    \u003c/motion.div\u003e\n  )}\n\u003c/AnimatePresence\u003e\n```\n\n`AnimatePresence` enables exit animations (normally components just disappear). The staggered `delay` creates a cascading reveal effect.\n\n---\n\n## Pages Overview\n\n| Route             | Description                                   |\n| ----------------- | --------------------------------------------- |\n| `/`               | Dashboard (logged in) or landing page         |\n| `/login`          | User authentication                           |\n| `/register`       | Account creation                              |\n| `/exercises`      | Browse, filter, and search all exercises      |\n| `/exercises/[id]` | Single exercise details with complete button  |\n| `/daily`          | Today's assigned exercise card                |\n| `/progress`       | Charts, streak badges, and completion history |\n| `/journal`        | Personal fitness journal with mood tracking   |\n| `/white-noise`    | Ambient audio tracks for focus/relaxation     |\n| `/community`      | Social feed, friend challenges, leaderboard   |\n\n---\n\n## Scripts\n\n```bash\nnpm run dev      # Start dev server on port 4200\nnpm run build    # Create production build\nnpm run start    # Start production server\nnpm run lint     # Run ESLint\n```\n\n---\n\n## Learn More\n\n- [Next.js Documentation](https://nextjs.org/docs) — App Router, Server Components\n- [Tailwind CSS v4](https://tailwindcss.com/docs) — Utility-first styling\n- [Framer Motion](https://www.framer.com/motion/) — Animation library\n- [SweetAlert2](https://sweetalert2.github.io/) — Beautiful alert dialogs\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsiyabuilds%2Fshukuma-web","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsiyabuilds%2Fshukuma-web","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsiyabuilds%2Fshukuma-web/lists"}