From ba24f9d7c059534f33273f1d518a06f25ce3d348 Mon Sep 17 00:00:00 2001 From: Jay Patrick Cano <0x3ef8@gmail.com> Date: Tue, 7 Apr 2026 19:00:41 +0800 Subject: [PATCH 1/8] chore: reset fork to current local snapshot --- .env.example | 18 + .github/workflows/build.yml | 60 + .gitignore | 59 + .jules/bolt.md | 3 + LICENSE | 21 + README.md | 138 + app/(auth)/login/page.tsx | 179 + app/(auth)/signup/page.tsx | 168 + app/(public)/flex/page.tsx | 150 + app/(public)/join/[code]/JoinButton.tsx | 129 + app/(public)/join/[code]/page.tsx | 57 + app/(public)/join/page.tsx | 226 + app/(public)/leaderboard/[slug]/page.tsx | 70 + app/(public)/leaderboard/page.tsx | 126 + app/(user)/dashboard/admin/page.tsx | 22 + app/(user)/dashboard/chat/page.tsx | 16 + app/(user)/dashboard/flex/page.tsx | 16 + app/(user)/dashboard/layout.tsx | 25 + app/(user)/dashboard/leaderboards/page.tsx | 32 + app/(user)/dashboard/page.tsx | 28 + app/(user)/dashboard/settings/page.tsx | 62 + app/(user)/logout/page.tsx | 10 + app/(user)/update-password/page.tsx | 31 + app/api/auth/callback/route.ts | 43 + app/api/sentry-example-api/route.ts | 15 + app/api/wakatime/sync/route.ts | 65 + app/components/AOSWrapper.tsx | 17 + app/components/BoardList.tsx | 311 + app/components/BrowserCheck.tsx | 27 + app/components/Chat.tsx | 775 ++ app/components/DevToolsDetector.tsx | 17 + app/components/Flex.tsx | 487 + app/components/LeaderboardTable.tsx | 329 + app/components/NortonSafeweb.tsx | 11 + app/components/ProfileDropdown.tsx | 141 + app/components/admin/Dashbord.tsx | 172 + .../admin/Widgets/FeatureInsights.tsx | 48 + .../admin/Widgets/RankingInsights.tsx | 91 + app/components/admin/Widgets/TopInsights.tsx | 49 + app/components/admin/Widgets/UserLists.tsx | 51 + app/components/auth/LoginForm.tsx | 180 + app/components/auth/Logout.tsx | 30 + app/components/auth/SignupForm.tsx | 195 + app/components/auth/UpdatePasswordForm.tsx | 123 + app/components/chat/Conversations.tsx | 118 + app/components/chat/MediaViewerModal.tsx | 146 + app/components/chat/Messages.tsx | 725 + app/components/chat/Player.tsx | 856 ++ .../chat/hooks/useActiveConversationStream.ts | 364 + .../chat/hooks/useChatAttachmentInput.ts | 70 + app/components/chat/hooks/useChatBadWords.ts | 7 + app/components/chat/hooks/useChatBadges.ts | 83 + .../chat/hooks/useChatConversationActions.ts | 277 + .../hooks/useChatConversationsRealtime.ts | 436 + .../chat/hooks/useChatInputBehavior.ts | 80 + .../chat/hooks/useChatMessageComposer.ts | 224 + app/components/chat/hooks/useChatPresence.ts | 214 + app/components/chat/hooks/useChatTyping.ts | 159 + .../chat/hooks/useChatUserPicker.ts | 69 + app/components/dashboard/LeaderbordList.tsx | 120 + app/components/dashboard/Navbar.tsx | 558 + app/components/dashboard/Settings/Profile.tsx | 173 + .../dashboard/Settings/ResetPassword.tsx | 113 + .../dashboard/Settings/WakaTimeKey.tsx | 165 + app/components/dashboard/Stats.tsx | 451 + app/components/dashboard/WithKey.tsx | 345 + app/components/dashboard/WithoutKey.tsx | 155 + .../dashboard/widgets/Categories.tsx | 69 + .../dashboard/widgets/CodingActivity.tsx | 101 + .../widgets/CodingConsistencyHeatmap.tsx | 267 + .../dashboard/widgets/Dependencies.tsx | 59 + app/components/dashboard/widgets/Editors.tsx | 90 + .../widgets/LanguageDestribution.tsx | 131 + app/components/dashboard/widgets/Machines.tsx | 62 + .../dashboard/widgets/OperatingSystem.tsx | 100 + app/components/dashboard/widgets/Projects.tsx | 65 + .../dashboard/widgets/StatsCard.tsx | 76 + .../landing-page/ContributeCard.tsx | 72 + app/components/landing-page/Contributors.tsx | 106 + app/components/landing-page/LosserMembers.tsx | 119 + .../landing-page/RecentLeaderboard.tsx | 167 + app/components/landing-page/TopLeaderbord.tsx | 176 + app/components/landing-page/VibeCoders.tsx | 107 + app/components/layout/CTA.tsx | 58 + app/components/layout/Footer.tsx | 62 + app/components/layout/Nav.tsx | 55 + app/components/leaderboard/BackButton.tsx | 21 + app/components/leaderboard/Banner.tsx | 41 + app/components/leaderboard/Header.tsx | 173 + .../leaderboard/InviteFriendsButton.tsx | 32 + .../leaderboard/LeaderboardStats.tsx | 143 + app/global-error.tsx | 27 + app/global.d.ts | 3 + app/globals.css | 251 + app/hooks/useBadWords.ts | 77 + app/layout.tsx | 131 + app/legal/contribution-guidelines/page.tsx | 79 + app/legal/privacy/page.tsx | 153 + app/legal/terms/page.tsx | 130 + app/lib/proxy/auth.ts | 51 + app/lib/proxy/headless-browser-check.ts | 32 + app/lib/proxy/rate-limiter.ts | 42 + app/lib/rate-limit.ts | 23 + app/lib/supabase/client.ts | 9 + app/lib/supabase/help/user.ts | 20 + app/lib/supabase/server.ts | 21 + app/lib/wakatime/repository.ts | 53 + app/lib/wakatime/sync.ts | 317 + app/not-found.tsx | 120 + app/page.tsx | 375 + app/robots.ts | 12 + app/sentry-example-page/page.tsx | 225 + app/sitemap.tsx | 14 + app/sitemaps/leaderboards.ts | 17 + app/sitemaps/static.ts | 14 + app/supabase-types.ts | 524 + app/utils/badge.ts | 86 + app/utils/media.ts | 21 + app/utils/moderation.ts | 43 + app/utils/slug.ts | 11 + app/utils/time.ts | 33 + app/utils/wakatime.ts | 63 + eslint.config.mjs | 18 + instrumentation-client.ts | 16 + instrumentation.ts | 13 + next.config.ts | 54 + package-lock.json | 11492 ++++++++++++++++ package.json | 44 + postcss.config.mjs | 7 + proxy.ts | 34 + public/favicon.ico | Bin 0 -> 4286 bytes public/favicon.png | Bin 0 -> 29207 bytes public/file.svg | 1 + public/globe.svg | 1 + public/icon.svg | 33 + public/images/devpulse.cover.png | Bin 0 -> 213751 bytes public/logo.svg | 33 + public/next.svg | 1 + public/vercel.svg | 1 + public/window.svg | 1 + sentry.edge.config.ts | 15 + sentry.server.config.ts | 14 + .../20260407120000_baseline_fresh_setup.sql | 772 ++ .../20260320234600_add_user_stats.sql | 33 + .../20260320234643_add_top_user_stats.sql | 7 + .../20260320234653_add_profiles.sql | 43 + .../20260320234714_add_leaderboards.sql | 100 + .../20260320234731_add_enforcement_checks.sql | 61 + .../20260320234742_add_user_projects.sql | 23 + .../20260320234758_add_conversations.sql | 96 + ...40532_add_categories_to_top_user_stats.sql | 8 + ...260323033205_add_type_to_conversations.sql | 1 + .../20260323033729_add_global_chat.sql | 18 + ..._add_type_to_conversation_participants.sql | 6 + .../20260323041543_add_trigger_to_type.sql | 18 + ...0323075413_add_attachments_to_messages.sql | 21 + .../20260325044133_add_user_flexes.sql | 88 + ...60325054413_add_expires_at_user_flexes.sql | 11 + .../20260325072343_add_role_to_profiles.sql | 17 + ...0327100000_cascade_conversation_delete.sql | 10 + ...329120000_add_user_dashboard_snapshots.sql | 42 + ...00_add_chat_presence_and_read_tracking.sql | 14 + ...03000_enable_chat_realtime_publication.sql | 33 + ...60330104500_fix_global_chat_membership.sql | 45 + tsconfig.json | 34 + vercel.json | 14 + 166 files changed, 29353 insertions(+) create mode 100644 .env.example create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 .jules/bolt.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/(auth)/login/page.tsx create mode 100644 app/(auth)/signup/page.tsx create mode 100644 app/(public)/flex/page.tsx create mode 100644 app/(public)/join/[code]/JoinButton.tsx create mode 100644 app/(public)/join/[code]/page.tsx create mode 100644 app/(public)/join/page.tsx create mode 100644 app/(public)/leaderboard/[slug]/page.tsx create mode 100644 app/(public)/leaderboard/page.tsx create mode 100644 app/(user)/dashboard/admin/page.tsx create mode 100644 app/(user)/dashboard/chat/page.tsx create mode 100644 app/(user)/dashboard/flex/page.tsx create mode 100644 app/(user)/dashboard/layout.tsx create mode 100644 app/(user)/dashboard/leaderboards/page.tsx create mode 100644 app/(user)/dashboard/page.tsx create mode 100644 app/(user)/dashboard/settings/page.tsx create mode 100644 app/(user)/logout/page.tsx create mode 100644 app/(user)/update-password/page.tsx create mode 100644 app/api/auth/callback/route.ts create mode 100644 app/api/sentry-example-api/route.ts create mode 100644 app/api/wakatime/sync/route.ts create mode 100644 app/components/AOSWrapper.tsx create mode 100644 app/components/BoardList.tsx create mode 100644 app/components/BrowserCheck.tsx create mode 100644 app/components/Chat.tsx create mode 100644 app/components/DevToolsDetector.tsx create mode 100644 app/components/Flex.tsx create mode 100644 app/components/LeaderboardTable.tsx create mode 100644 app/components/NortonSafeweb.tsx create mode 100644 app/components/ProfileDropdown.tsx create mode 100644 app/components/admin/Dashbord.tsx create mode 100644 app/components/admin/Widgets/FeatureInsights.tsx create mode 100644 app/components/admin/Widgets/RankingInsights.tsx create mode 100644 app/components/admin/Widgets/TopInsights.tsx create mode 100644 app/components/admin/Widgets/UserLists.tsx create mode 100644 app/components/auth/LoginForm.tsx create mode 100644 app/components/auth/Logout.tsx create mode 100644 app/components/auth/SignupForm.tsx create mode 100644 app/components/auth/UpdatePasswordForm.tsx create mode 100644 app/components/chat/Conversations.tsx create mode 100644 app/components/chat/MediaViewerModal.tsx create mode 100644 app/components/chat/Messages.tsx create mode 100644 app/components/chat/Player.tsx create mode 100644 app/components/chat/hooks/useActiveConversationStream.ts create mode 100644 app/components/chat/hooks/useChatAttachmentInput.ts create mode 100644 app/components/chat/hooks/useChatBadWords.ts create mode 100644 app/components/chat/hooks/useChatBadges.ts create mode 100644 app/components/chat/hooks/useChatConversationActions.ts create mode 100644 app/components/chat/hooks/useChatConversationsRealtime.ts create mode 100644 app/components/chat/hooks/useChatInputBehavior.ts create mode 100644 app/components/chat/hooks/useChatMessageComposer.ts create mode 100644 app/components/chat/hooks/useChatPresence.ts create mode 100644 app/components/chat/hooks/useChatTyping.ts create mode 100644 app/components/chat/hooks/useChatUserPicker.ts create mode 100644 app/components/dashboard/LeaderbordList.tsx create mode 100644 app/components/dashboard/Navbar.tsx create mode 100644 app/components/dashboard/Settings/Profile.tsx create mode 100644 app/components/dashboard/Settings/ResetPassword.tsx create mode 100644 app/components/dashboard/Settings/WakaTimeKey.tsx create mode 100644 app/components/dashboard/Stats.tsx create mode 100644 app/components/dashboard/WithKey.tsx create mode 100644 app/components/dashboard/WithoutKey.tsx create mode 100644 app/components/dashboard/widgets/Categories.tsx create mode 100644 app/components/dashboard/widgets/CodingActivity.tsx create mode 100644 app/components/dashboard/widgets/CodingConsistencyHeatmap.tsx create mode 100644 app/components/dashboard/widgets/Dependencies.tsx create mode 100644 app/components/dashboard/widgets/Editors.tsx create mode 100644 app/components/dashboard/widgets/LanguageDestribution.tsx create mode 100644 app/components/dashboard/widgets/Machines.tsx create mode 100644 app/components/dashboard/widgets/OperatingSystem.tsx create mode 100644 app/components/dashboard/widgets/Projects.tsx create mode 100644 app/components/dashboard/widgets/StatsCard.tsx create mode 100644 app/components/landing-page/ContributeCard.tsx create mode 100644 app/components/landing-page/Contributors.tsx create mode 100644 app/components/landing-page/LosserMembers.tsx create mode 100644 app/components/landing-page/RecentLeaderboard.tsx create mode 100644 app/components/landing-page/TopLeaderbord.tsx create mode 100644 app/components/landing-page/VibeCoders.tsx create mode 100644 app/components/layout/CTA.tsx create mode 100644 app/components/layout/Footer.tsx create mode 100644 app/components/layout/Nav.tsx create mode 100644 app/components/leaderboard/BackButton.tsx create mode 100644 app/components/leaderboard/Banner.tsx create mode 100644 app/components/leaderboard/Header.tsx create mode 100644 app/components/leaderboard/InviteFriendsButton.tsx create mode 100644 app/components/leaderboard/LeaderboardStats.tsx create mode 100644 app/global-error.tsx create mode 100644 app/global.d.ts create mode 100644 app/globals.css create mode 100644 app/hooks/useBadWords.ts create mode 100644 app/layout.tsx create mode 100644 app/legal/contribution-guidelines/page.tsx create mode 100644 app/legal/privacy/page.tsx create mode 100644 app/legal/terms/page.tsx create mode 100644 app/lib/proxy/auth.ts create mode 100644 app/lib/proxy/headless-browser-check.ts create mode 100644 app/lib/proxy/rate-limiter.ts create mode 100644 app/lib/rate-limit.ts create mode 100644 app/lib/supabase/client.ts create mode 100644 app/lib/supabase/help/user.ts create mode 100644 app/lib/supabase/server.ts create mode 100644 app/lib/wakatime/repository.ts create mode 100644 app/lib/wakatime/sync.ts create mode 100644 app/not-found.tsx create mode 100644 app/page.tsx create mode 100644 app/robots.ts create mode 100644 app/sentry-example-page/page.tsx create mode 100644 app/sitemap.tsx create mode 100644 app/sitemaps/leaderboards.ts create mode 100644 app/sitemaps/static.ts create mode 100644 app/supabase-types.ts create mode 100644 app/utils/badge.ts create mode 100644 app/utils/media.ts create mode 100644 app/utils/moderation.ts create mode 100644 app/utils/slug.ts create mode 100644 app/utils/time.ts create mode 100644 app/utils/wakatime.ts create mode 100644 eslint.config.mjs create mode 100644 instrumentation-client.ts create mode 100644 instrumentation.ts create mode 100644 next.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 proxy.ts create mode 100644 public/favicon.ico create mode 100644 public/favicon.png create mode 100644 public/file.svg create mode 100644 public/globe.svg create mode 100644 public/icon.svg create mode 100644 public/images/devpulse.cover.png create mode 100644 public/logo.svg create mode 100644 public/next.svg create mode 100644 public/vercel.svg create mode 100644 public/window.svg create mode 100644 sentry.edge.config.ts create mode 100644 sentry.server.config.ts create mode 100644 supabase/migrations/20260407120000_baseline_fresh_setup.sql create mode 100644 supabase/migrations_archive/20260320234600_add_user_stats.sql create mode 100644 supabase/migrations_archive/20260320234643_add_top_user_stats.sql create mode 100644 supabase/migrations_archive/20260320234653_add_profiles.sql create mode 100644 supabase/migrations_archive/20260320234714_add_leaderboards.sql create mode 100644 supabase/migrations_archive/20260320234731_add_enforcement_checks.sql create mode 100644 supabase/migrations_archive/20260320234742_add_user_projects.sql create mode 100644 supabase/migrations_archive/20260320234758_add_conversations.sql create mode 100644 supabase/migrations_archive/20260321140532_add_categories_to_top_user_stats.sql create mode 100644 supabase/migrations_archive/20260323033205_add_type_to_conversations.sql create mode 100644 supabase/migrations_archive/20260323033729_add_global_chat.sql create mode 100644 supabase/migrations_archive/20260323041448_add_type_to_conversation_participants.sql create mode 100644 supabase/migrations_archive/20260323041543_add_trigger_to_type.sql create mode 100644 supabase/migrations_archive/20260323075413_add_attachments_to_messages.sql create mode 100644 supabase/migrations_archive/20260325044133_add_user_flexes.sql create mode 100644 supabase/migrations_archive/20260325054413_add_expires_at_user_flexes.sql create mode 100644 supabase/migrations_archive/20260325072343_add_role_to_profiles.sql create mode 100644 supabase/migrations_archive/20260327100000_cascade_conversation_delete.sql create mode 100644 supabase/migrations_archive/20260329120000_add_user_dashboard_snapshots.sql create mode 100644 supabase/migrations_archive/20260329133000_add_chat_presence_and_read_tracking.sql create mode 100644 supabase/migrations_archive/20260330103000_enable_chat_realtime_publication.sql create mode 100644 supabase/migrations_archive/20260330104500_fix_global_chat_membership.sql create mode 100644 tsconfig.json create mode 100644 vercel.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9dd84a7 --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +NODE_ENV=development +NEXT_PUBLIC_NODE_ENV=development + +NEXT_PUBLIC_SUPABASE_BUCKET_NAME= +NEXT_PUBLIC_SUPABASE_URL= +NEXT_PUBLIC_SUPABASE_ANON_KEY= + +NEXT_PUBLIC_HCAPTCHA_SITE_KEY= + +SENTRY_AUTH_TOKEN= +SENTRY_ORG= +SENTRY_PROJECT= +SENTRY_DNS= + +NEXT_PUBLIC_NORTON_SAFEWEB_SITE_VERIFICATION= + +SUPABASE_ACCESS_TOKEN= +SUPABASE_PROJECT_ID=vswabkwgipyweqsabzwv diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..913a7af --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,60 @@ +name: Build Next.js App + +on: + push: + branches: ["master"] + pull_request_target: + branches: ["master"] + workflow_dispatch: + +env: + SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }} + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} + NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install dependencies + run: npm ci + + - name: Run lint + run: npm run lint + + - name: Generate types from remote + if: ${{ env.SUPABASE_PROJECT_ID && env.SUPABASE_ACCESS_TOKEN }} + env: + SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + run: | + npx supabase gen types typescript \ + --project-id ${{ secrets.SUPABASE_PROJECT_ID }} \ + > app/supabase-types.ts + + # this will be suspended for now + # - name: Run migrations + # env: + # SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} + # run: | + # npx supabase db push --project-id ${{ secrets.SUPABASE_PROJECT_ID }} + + - name: Create .env file + if: ${{ env.NEXT_PUBLIC_SUPABASE_URL && env.NEXT_PUBLIC_SUPABASE_ANON_KEY }} + run: | + echo "NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}" >> .env + echo "NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}" >> .env + echo "NEXT_PUBLIC_HCAPTCHA_SITE_KEY=${{ secrets.NEXT_PUBLIC_HCAPTCHA_SITE_KEY }}" >> .env + echo "NEXT_PUBLIC_NORTON_SAFEWEB_SITE_VERIFICATION=${{ secrets.NEXT_PUBLIC_NORTON_SAFEWEB_SITE_VERIFICATION }}" >> .env + + - name: Build project + if: ${{ env.NEXT_PUBLIC_SUPABASE_URL && env.NEXT_PUBLIC_SUPABASE_ANON_KEY }} + run: npm run build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b60097 --- /dev/null +++ b/.gitignore @@ -0,0 +1,59 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* +.env.local +.env.development.local +.env.test.local +.env.production.local +!.env.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# supabase +supabase/* +!supabase/migrations +!supabase/migrations_archive +!supabase/migrations_archive/** + + +yarn.lock +pnpm-lock.yaml + +# Sentry Config File +.env.sentry-build-plugin diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..86c6287 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-04-07 - Filtering Performance Optimization in Chat Component +**Learning:** Extracting inline array filtering (O(N) operations) out of the return statement into a `useMemo` hook reduces UI lag during text entry by preventing re-computation on unrelated state changes (like typing). Early returning the unmodified array when no filter is applied allows child components to bail out of re-rendering. +**Action:** Always scan for unmemoized O(N) array transformations inside JSX props, especially when the parent component has rapidly changing states like text inputs. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cc40a16 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Hall of Codes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..685e640 --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +Screenshot of the floating console extension in action + +# devpulse + +Measure and share your coding productivity with personalized leaderboards. Compare your progress with peers while keeping full control over privacy and leaderboard settings. + +## Getting Started +Install the dependencies: + +npm install +``` + +## Supabase + +First by creating a supabase cloud project: + +- go to [Supabase Dashboard](https://app.supabase.com) +- click `New Project` +- choose: + - Organization → (create one if needed) + - Project Name → e.g. devpulse-waka + - Database Password → choose a secure one + - Region → pick the nearest location +- Click Create new project +- Wait a few moments for the database to be provisioned. + +## Setup Environment + +Copy the .env.example to .env + +```bash +cp .env.example .env + +# Open .env and fill in the values for: +# NEXT_PUBLIC_SUPABASE_URL=your_supabase_url +# NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key +``` + +## Development + +First, run the development server: + +```bash +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +## Database Migrations + +For a brand-new Supabase project, use this flow from the repo root. + +This repository now uses a squashed baseline migration for fresh installs: + +- Active baseline: `supabase/migrations/20260407120000_baseline_fresh_setup.sql` + +- Historical migrations archive: `supabase/migrations_archive/` + +1. Login to Supabase CLI: + +```bash +npx supabase login +``` + +2. Initialize local Supabase config (only if missing): + +```bash +npx supabase init +``` + +3. Link this repo to your cloud project: + +```bash +npx supabase link +``` + +You can select from the project list, or run `npx supabase link --project-ref `. + +4. Push all migrations to the new project: + +```bash +npx supabase db push +``` + +On a fresh project this applies only the single baseline migration. + +5. (Optional) Pull remote schema changes into migrations: + +```bash +npx supabase db pull +``` + +6. Regenerate Supabase TypeScript types after schema changes: + +```bash +npx supabase gen types typescript --project-id --schema public > app/supabase-types.ts +``` + +If you want to re-run the full migration chain on local development: + +```bash +npx supabase db reset +``` + +If you need to inspect migration history, use the files in `supabase/migrations_archive/`. + + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy this Next.js app is to use the [Vercel Platform](https://vercel.com/new/clone?repository-url=https://github.com/mrepol742/devpulse) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. + +## Contribution Guidelines + +Contributions to devpulse are welcome! Please follow these guidelines: + +1. Before modifying any existing design or logic that is already working or in use, **contact us first** to avoid conflicts. +2. You are welcome to contribute new features, bug fixes, or improvements. +3. Check the Issues tab (if available) before starting to avoid duplicate work. +4. Follow the existing code style and conventions. +5. Submit a pull request with a clear description of your changes and the problem it solves. + +> Pull requests that modify existing working features without prior discussion may not be merged. + +Help us keep the codebase ("DevPulse") clean, stable, and maintainable. + +## License +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx new file mode 100644 index 0000000..05cd1c5 --- /dev/null +++ b/app/(auth)/login/page.tsx @@ -0,0 +1,179 @@ +import Link from "next/link"; +import Image from "next/image"; +import LoginForm from "@/app/components/auth/LoginForm"; +import { Metadata } from "next"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faChevronLeft } from "@fortawesome/free-solid-svg-icons"; + +export const metadata: Metadata = { + title: "Login - DevPulse", + description: + "Log in to your DevPulse account to monitor your coding activity and compete on leaderboards.", + keywords: [ + "DevPulse", + "login", + "coding activity tracker", + "developer leaderboards", + "WakaTime integration", + "coding stats", + "programming habits", + "developer competition", + "flex your projects", + "coding streaks", + "productivity insights", + ], + openGraph: { + title: "Login - DevPulse", + description: + "Log in to your DevPulse account to monitor your coding activity and compete on leaderboards.", + url: "https://devpulse-waka.vercel.app/login", + siteName: "DevPulse", + images: [ + { + url: "https://devpulse-waka.vercel.app/images/devpulse.cover.png", + width: 1200, + height: 630, + alt: "DevPulse Cover Image", + }, + ], + locale: "en_US", + type: "website", + }, + twitter: { + card: "summary_large_image", + title: "Login - DevPulse", + description: + "Log in to your DevPulse account to monitor your coding activity and compete on leaderboards.", + images: [ + { + url: "https://devpulse-waka.vercel.app/images/devpulse.cover.png", + alt: "DevPulse Cover Image", + }, + ], + }, +}; + +export default async function Login(props: { + searchParams?: Promise<{ redirect?: string }>; +}) { + const redirectParam = (await props.searchParams)?.redirect; + const redirectTo = + redirectParam && + redirectParam.startsWith("/") && + !redirectParam.startsWith("//") + ? redirectParam + : undefined; + + return ( +
+ + + Back + + + {/* Left Side - Visual / Branding */} +
+ {/* Background elements */} +
+ +
+ + DevPulse Logo + + DevPulse + + +
+ +
+

+ Welcome back to your dashboard. +

+

+ Access your personalized coding metrics, compare your stats, and + keep your productivity streak alive. +

+ +
+
+
+
+
+ + devpulse-auth.ts + +
+
+
+ import + {"{ Metrics }"} + from + + '@devpulse/core' + + ; +
+
+ await + Metrics + . + syncToday + (); +
+
+ + {"// Connection established. Ready to track. ⚡"} + +
+
+
+
+ +
+ © {new Date().getFullYear()} DevPulse. All rights reserved. +
+
+ + {/* Right Side - Form */} +
+
+ +
+
+ DevPulse Logo +

DevPulse

+
+ +
+

Log in

+

+ Enter your credentials to access your account. +

+
+ + + +

+ Don't have an account?{" "} + + Sign up + +

+
+
+
+ ); +} diff --git a/app/(auth)/signup/page.tsx b/app/(auth)/signup/page.tsx new file mode 100644 index 0000000..33c39d6 --- /dev/null +++ b/app/(auth)/signup/page.tsx @@ -0,0 +1,168 @@ +import Link from "next/link"; +import Image from "next/image"; +import SignupForm from "@/app/components/auth/SignupForm"; +import { Metadata } from "next"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faChevronLeft } from "@fortawesome/free-solid-svg-icons"; + +export const metadata: Metadata = { + title: "Sign Up - DevPulse", + description: + "Create a DevPulse account to monitor your coding activity and compete on leaderboards.", + openGraph: { + title: "Sign Up - DevPulse", + description: + "Create a DevPulse account to monitor your coding activity and compete on leaderboards.", + url: "https://devpulse-waka.vercel.app/signup", + siteName: "DevPulse", + images: [ + { + url: "https://devpulse-waka.vercel.app/images/devpulse.cover.png", + width: 1200, + height: 630, + alt: "DevPulse Cover Image", + }, + ], + locale: "en_US", + type: "website", + }, + twitter: { + card: "summary_large_image", + title: "Sign Up - DevPulse", + description: + "Create a DevPulse account to monitor your coding activity and compete on leaderboards.", + images: [ + { + url: "https://devpulse-waka.vercel.app/images/devpulse.cover.png", + alt: "DevPulse Cover Image", + }, + ], + }, +}; + +export default async function Signup(props: { + searchParams?: Promise<{ redirect?: string }>; +}) { + const redirectParam = (await props.searchParams)?.redirect; + const redirectTo = + redirectParam && + redirectParam.startsWith("/") && + !redirectParam.startsWith("//") + ? redirectParam + : undefined; + + return ( +
+ + + Back + + + {/* Left Side - Visual / Branding */} +
+ {/* Background elements */} +
+ +
+ + DevPulse Logo + + DevPulse + + +
+ +
+

+ Start measuring your coding pulse. +

+

+ Join thousands of developers tracking their progress, competing on + leaderboards, and leveling up their skills. +

+ +
+
+
+
+
+ + setup.ts + +
+
+
+ const + dev + = + new + Developer + (); +
+
+ dev + . + connect + ( + 'wakatime' + ); +
+
+ + {"// Your journey begins here. 🚀"} + +
+
+
+
+ +
+ © {new Date().getFullYear()} DevPulse. All rights reserved. +
+
+ + {/* Right Side - Form */} +
+
+ +
+
+ DevPulse Logo +

DevPulse

+
+ +
+

+ Create an account +

+

+ Start tracking your coding stats today. +

+
+ + + +

+ Already have an account?{" "} + + Log in + +

+
+
+
+ ); +} diff --git a/app/(public)/flex/page.tsx b/app/(public)/flex/page.tsx new file mode 100644 index 0000000..ad182b2 --- /dev/null +++ b/app/(public)/flex/page.tsx @@ -0,0 +1,150 @@ +import { createClient } from "../../lib/supabase/server"; +import Footer from "@/app/components/layout/Footer"; +import CTA from "@/app/components/layout/CTA"; +import BackButton from "@/app/components/leaderboard/BackButton"; +import Image from "next/image"; +import { timeAgo } from "@/app/utils/time"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faExternalLink } from "@fortawesome/free-solid-svg-icons"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Flexes - DevPulse", + description: + "Flex your coding projects and share your achievements with the DevPulse community. See what others are working on and get inspired!", + keywords: [ + "DevPulse", + "coding flexes", + "developer projects", + "coding achievements", + "programming flexes", + "open source projects", + "developer community", + "coding inspiration", + ], + openGraph: { + title: "Flexes - DevPulse", + description: + "Flex your coding projects and share your achievements with the DevPulse community. See what others are working on and get inspired!", + url: "https://devpulse-waka.vercel.app/flex", + siteName: "DevPulse", + images: [ + { + url: "https://devpulse-waka.vercel.app/images/devpulse.cover.png", + width: 1200, + height: 630, + alt: "DevPulse Cover Image", + }, + ], + locale: "en_US", + type: "website", + }, + twitter: { + card: "summary_large_image", + title: "Flexes - DevPulse", + description: + "Flex your coding projects and share your achievements with the DevPulse community. See what others are working on and get inspired!", + images: [ + { + url: "https://devpulse-waka.vercel.app/images/devpulse.cover.png", + alt: "DevPulse Cover Image", + }, + ], + }, +}; + +export default async function Flexs() { + const supabase = await createClient(); + + const [userFlexes, userResult] = await Promise.all([ + supabase + .from("user_flexes") + .select("*") + .order("created_at", { ascending: false }), + supabase.auth.getUser(), + ]); + + const { data } = userFlexes; + const { data: user } = userResult; + + return ( +
+
+ + +
+ DevPulse Logo +

DevPulse Flexes

+
+ + {data?.length === 0 && ( +
+

No Flexes Yet

+

+ Please come back later to see the latest flexes from our + community. +

+
+ )} + + {data && data.length > 0 && ( +
+ {data.map((flex) => ( +
+
+

+ {flex.project_name} +

+ {timeAgo(flex.created_at)} +
+
+ {flex.project_time} +
+ + Description: + +

{flex.project_description}

+ {flex.is_open_source && ( + <> + + Open Source: + + + {flex.open_source_url} + + + )} +
+

+ Posted by {flex.user_email.split("@")[0]} +

+ + + +
+
+ ))} +
+ )} +
+ + {!user && } +
+
+ ); +} diff --git a/app/(public)/join/[code]/JoinButton.tsx b/app/(public)/join/[code]/JoinButton.tsx new file mode 100644 index 0000000..9882448 --- /dev/null +++ b/app/(public)/join/[code]/JoinButton.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { useState } from "react"; +import { createClient } from "../../../lib/supabase/client"; +import { useRouter } from "next/navigation"; +import { toast } from "react-toastify"; +import Link from "next/link"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faArrowRightToBracket, + faCheck, + faSpinner, + faUserPlus, +} from "@fortawesome/free-solid-svg-icons"; + +export default function JoinButton({ + code, + leaderboardSlug, + isLoggedIn, + alreadyMember, +}: { + code: string; + leaderboardSlug: string; + isLoggedIn: boolean; + alreadyMember: boolean; +}) { + const router = useRouter(); + const [joining, setJoining] = useState(false); + + if (alreadyMember) { + return ( + + + View + View Leaderboard + + ); + } + + if (!isLoggedIn) { + return ( +
+ + + Log In to Join + +

+ Don't have an account?{" "} + + Sign up free + +

+
+ ); + } + + const handleJoin = async () => { + setJoining(true); + const supabase = createClient(); + + const joinPromise = (async () => { + const { data: userData } = await supabase.auth.getUser(); + const user = userData.user; + if (!user) throw new Error("Not authenticated"); + + const { data: board } = await supabase + .from("leaderboards") + .select("id") + .eq("join_code", code) + .single(); + + if (!board) throw new Error("Invalid invite code"); + + const { error } = await supabase.from("leaderboard_members").insert({ + leaderboard_id: board.id, + user_id: user.id, + }); + + if (error) throw error; + return board; + })(); + + try { + await toast.promise(joinPromise, { + pending: "Joining leaderboard...", + success: "You're in! Welcome to the leaderboard.", + error: { + render({ data }) { + const err = data as Error; + if (err?.code === "23505") { + return "You are already a member of this leaderboard."; + } + return err?.message || "Failed to join. Please try again."; + }, + }, + }); + + router.push(`/leaderboard/${leaderboardSlug}`); + } finally { + setJoining(false); + } + }; + + return ( + + ); +} diff --git a/app/(public)/join/[code]/page.tsx b/app/(public)/join/[code]/page.tsx new file mode 100644 index 0000000..d956f8d --- /dev/null +++ b/app/(public)/join/[code]/page.tsx @@ -0,0 +1,57 @@ +import { Metadata } from "next"; +import { createClient } from "../../../lib/supabase/server"; +import { redirect } from "next/navigation"; + +type Props = { + params: Promise<{ code: string }>; +}; + +async function getLeaderboard(code: string) { + const supabase = await createClient(); + const { data } = await supabase + .from("leaderboards") + .select("id, name, description, slug, owner_id, created_at") + .eq("join_code", code) + .single(); + return data; +} + +export async function generateMetadata({ params }: Props): Promise { + const { code } = await params; + const leaderboard = await getLeaderboard(code); + + if (!leaderboard) { + return { + title: "Invite Not Found - DevPulse", + description: "This invite link is invalid or has expired.", + }; + } + + const title = `You're invited to join ${leaderboard.name}!`; + const description = + leaderboard?.description && leaderboard.description.length > 0 + ? leaderboard.description + : `Join the ${leaderboard.name} leaderboard on DevPulse and compete with other developers. Track your coding activity and climb the ranks!`; + + return { + title: `${title} - DevPulse`, + description, + openGraph: { + title, + description, + type: "website", + siteName: "DevPulse", + url: `/join?id=${encodeURIComponent(code)}`, + }, + twitter: { + card: "summary_large_image", + title, + description, + }, + }; +} + +export default async function JoinPage({ params }: Props) { + const { code } = await params; + redirect(`/join?id=${encodeURIComponent(code)}`); +} diff --git a/app/(public)/join/page.tsx b/app/(public)/join/page.tsx new file mode 100644 index 0000000..25e8498 --- /dev/null +++ b/app/(public)/join/page.tsx @@ -0,0 +1,226 @@ +import { Metadata } from "next"; +import { createClient } from "../../lib/supabase/server"; +import JoinButton from "./[code]/JoinButton"; +import Footer from "@/app/components/layout/Footer"; +import Image from "next/image"; +import Link from "next/link"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faCircleInfo, + faCircleXmark, + faRankingStar, + faUsers, +} from "@fortawesome/free-solid-svg-icons"; + +type Props = { + searchParams: Promise<{ id?: string }>; +}; + +async function getLeaderboard(code: string) { + const supabase = await createClient(); + const { data } = await supabase + .from("leaderboards") + .select("id, name, description, slug, owner_id, created_at") + .eq("join_code", code) + .single(); + return data; +} + +async function getMemberCount(leaderboardId: string) { + const supabase = await createClient(); + const { count } = await supabase + .from("leaderboard_members_view") + .select("*", { count: "exact", head: true }) + .eq("leaderboard_id", leaderboardId); + return count ?? 0; +} + +export async function generateMetadata({ + searchParams, +}: Props): Promise { + const { id } = await searchParams; + const code = (id || "").trim(); + + if (!code) { + return { + title: "Join - DevPulse", + description: "Open an invite link to join a DevPulse leaderboard.", + }; + } + + const leaderboard = await getLeaderboard(code); + if (!leaderboard) { + return { + title: "Invite Not Found - DevPulse", + description: "This invite link is invalid or has expired.", + }; + } + + const title = `You're invited to join ${leaderboard.name}!`; + const description = + leaderboard.description && leaderboard.description?.length > 0 + ? leaderboard.description + : `Join the ${leaderboard.name} leaderboard on DevPulse and compete with other developers. Track your coding activity and climb the ranks!`; + + return { + title: `${title} - DevPulse`, + description, + openGraph: { + title, + description, + type: "website", + siteName: "DevPulse", + url: `https://devpulse-waka.vercel.app/join?id=${encodeURIComponent(code)}`, + }, + twitter: { + card: "summary_large_image", + title, + description, + }, + }; +} + +export default async function JoinPage({ searchParams }: Props) { + const { id } = await searchParams; + const code = (id || "").trim(); + + if (!code) { + return ( +
+
+
+ +
+

+ Join a Leaderboard +

+

+ Open an invite link like{" "} + /join?id=XXXXXXXX. +

+ + Go to DevPulse + +
+
+ ); + } + + const leaderboard = await getLeaderboard(code); + + if (!leaderboard) { + return ( +
+
+
+ +
+

+ Invite Not Found +

+

+ This invite link is invalid or has expired. +

+ + Go to DevPulse + +
+
+ ); + } + + const memberCount = await getMemberCount(leaderboard.id); + + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + + let alreadyMember = false; + if (user) { + const { data: membership } = await supabase + .from("leaderboard_members") + .select("id") + .eq("leaderboard_id", leaderboard.id) + .eq("user_id", user.id) + .single(); + alreadyMember = !!membership; + } + + return ( +
+
+
+ +
+
+
+
+ DevPulse +
+
+ +

+ {alreadyMember + ? "You\u2019re already a member of" + : "You\u2019ve been invited to"} +

+ +

+ {leaderboard.name} +

+ + {leaderboard.description && leaderboard.description.length > 0 && ( +

+ {leaderboard.description} +

+ )} + +
+
+ + + {memberCount} {memberCount === 1 ? "member" : "members"} + +
+
+ + Leaderboard +
+
+ + + + {!user && ( +

+ Powered by{" "} + + DevPulse + {" "} + — Track your coding activity & compete +

+ )} +
+
+ + {!user &&
} +
+ ); +} diff --git a/app/(public)/leaderboard/[slug]/page.tsx b/app/(public)/leaderboard/[slug]/page.tsx new file mode 100644 index 0000000..da7fc21 --- /dev/null +++ b/app/(public)/leaderboard/[slug]/page.tsx @@ -0,0 +1,70 @@ +import { createClient } from "../../../lib/supabase/server"; +import LeaderboardTable, { + NonNullableMember, +} from "../../../components/LeaderboardTable"; +import LeaderboardHeader from "@/app/components/leaderboard/Header"; +import Footer from "@/app/components/layout/Footer"; +import CTA from "@/app/components/layout/CTA"; +import { getUserWithProfile } from "@/app/lib/supabase/help/user"; + +export default async function LeaderboardPage(props: { + params: Promise<{ slug: string }>; +}) { + const [{ user }, { slug }, supabase] = await Promise.all([ + getUserWithProfile(), + props.params, + createClient(), + ]); + + const { data: leaderboard } = await supabase + .from("leaderboards") + .select("*") + .eq("slug", slug) + .single(); + + if (!leaderboard) { + return ( +
+
+

Leaderboard not found

+

{slug}

+
+
+ ); + } + + const { data: members, error } = await supabase + .from("leaderboard_members_view") + .select("*") + .eq("leaderboard_id", leaderboard.id); + + const isOwner = user?.id === leaderboard.owner_id; + + if (error) { + console.error("Error fetching members:", error); + return ( +
+
+

Error loading members.

+
+
+ ); + } + + return ( +
+
+ +
+ +
+
+ + {!user && } +
+
+ ); +} diff --git a/app/(public)/leaderboard/page.tsx b/app/(public)/leaderboard/page.tsx new file mode 100644 index 0000000..0bb0f99 --- /dev/null +++ b/app/(public)/leaderboard/page.tsx @@ -0,0 +1,126 @@ +import BackButton from "@/app/components/leaderboard/BackButton"; +import { createClient } from "../../lib/supabase/server"; +import Footer from "@/app/components/layout/Footer"; +import CTA from "@/app/components/layout/CTA"; +import Image from "next/image"; +import { Metadata } from "next"; +import { getUserWithProfile } from "@/app/lib/supabase/help/user"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; + +export const metadata: Metadata = { + title: "Leaderboards - DevPulse", + description: + "Explore the DevPulse leaderboards and see how you rank against other developers. Check out the top coders and get inspired to climb the ranks!", + keywords: [ + "DevPulse", + "developer leaderboards", + "coding rankings", + "programming competition", + "developer stats", + "coding achievements", + "programming leaderboards", + "developer community", + "coding inspiration", + ], + openGraph: { + title: "Leaderboards - DevPulse", + description: + "Explore the DevPulse leaderboards and see how you rank against other developers. Check out the top coders and get inspired to climb the ranks!", + url: "https://devpulse-waka.vercel.app/leaderboard", + siteName: "DevPulse", + images: [ + { + url: "https://devpulse-waka.vercel.app/images/devpulse.cover.png", + width: 1200, + height: 630, + alt: "DevPulse Cover Image", + }, + ], + locale: "en_US", + type: "website", + }, + twitter: { + card: "summary_large_image", + title: "Leaderboards - DevPulse", + description: + "Explore the DevPulse leaderboards and see how you rank against other developers. Check out the top coders and get inspired to climb the ranks!", + images: [ + { + url: "https://devpulse-waka.vercel.app/images/devpulse.cover.png", + alt: "DevPulse Cover Image", + }, + ], + }, +}; + +export default async function Leaderboards() { + const supabase = await createClient(); + + const [{ data, error }, { user }] = await Promise.all([ + supabase + .from("leaderboards") + .select("id, name, slug") + .order("created_at", { ascending: false }), + getUserWithProfile(), + ]); + + if (error) { + return ( +
+

Failed to load leaderboards.

+
+ ); + } + + if (!data || data.length === 0) { + return ( +
+

No leaderboards found.

+
+ ); + } + + return ( +
+
+ + +
+ DevPulse Logo +

+ DevPulse Leaderboards +

+
+ +
+ {data.map( + (board: { id: string; name: string; slug: string }, i: number) => ( + + +
+ + {!user && } +
+
+ ); +} diff --git a/app/(user)/dashboard/admin/page.tsx b/app/(user)/dashboard/admin/page.tsx new file mode 100644 index 0000000..5b2ea59 --- /dev/null +++ b/app/(user)/dashboard/admin/page.tsx @@ -0,0 +1,22 @@ +import Dashboard from "@/app/components/admin/Dashbord"; +import { getUserWithProfile } from "@/app/lib/supabase/help/user"; +import { Metadata } from "next"; +import { redirect } from "next/navigation"; + +export const metadata: Metadata = { + title: "Admin Panel - DevPulse", +}; + +export default async function AdminPage() { + const { user, profile } = await getUserWithProfile(); + + if (!user) { + redirect("/login?from=/dashboard/admin"); + } + + if (!profile || profile.role !== "admin") { + redirect("/dashbord"); + } + + return ; +} diff --git a/app/(user)/dashboard/chat/page.tsx b/app/(user)/dashboard/chat/page.tsx new file mode 100644 index 0000000..73e0bdc --- /dev/null +++ b/app/(user)/dashboard/chat/page.tsx @@ -0,0 +1,16 @@ +import Chat from "@/app/components/Chat"; +import { getUserWithProfile } from "@/app/lib/supabase/help/user"; +import { Metadata } from "next"; +import { redirect } from "next/navigation"; + +export const metadata: Metadata = { + title: "Chat - DevPulse", +}; + +export default async function ChatPage() { + const { user } = await getUserWithProfile(); + + if (!user) return redirect("/login?from=/dashboard/chat"); + + return ; +} diff --git a/app/(user)/dashboard/flex/page.tsx b/app/(user)/dashboard/flex/page.tsx new file mode 100644 index 0000000..5a4b1c0 --- /dev/null +++ b/app/(user)/dashboard/flex/page.tsx @@ -0,0 +1,16 @@ +import Flex from "@/app/components/Flex"; +import { getUserWithProfile } from "@/app/lib/supabase/help/user"; +import { Metadata } from "next"; +import { redirect } from "next/navigation"; + +export const metadata: Metadata = { + title: "Flexes - DevPulse", +}; + +export default async function FlexPage() { + const { user } = await getUserWithProfile(); + + if (!user) return redirect("/login?from=/dashboard/flex"); + + return ; +} diff --git a/app/(user)/dashboard/layout.tsx b/app/(user)/dashboard/layout.tsx new file mode 100644 index 0000000..74e78ca --- /dev/null +++ b/app/(user)/dashboard/layout.tsx @@ -0,0 +1,25 @@ +import { redirect } from "next/navigation"; +import DashboardLayout from "@/app/components/dashboard/Navbar"; +import { getUserWithProfile } from "@/app/lib/supabase/help/user"; + +export default async function Layout({ + children, +}: { + children: React.ReactNode; +}) { + const { user, profile } = await getUserWithProfile(); + if (!user) redirect("/login"); + + const email = profile?.email || user.email!; + const name = user?.user_metadata?.name || email.split("@")[0]; + + return ( + + {children} + + ); +} diff --git a/app/(user)/dashboard/leaderboards/page.tsx b/app/(user)/dashboard/leaderboards/page.tsx new file mode 100644 index 0000000..ab85c39 --- /dev/null +++ b/app/(user)/dashboard/leaderboards/page.tsx @@ -0,0 +1,32 @@ +import DashboardWithKey from "../../../components/dashboard/WithKey"; +import LeaderboardsList from "@/app/components/dashboard/LeaderbordList"; +import { getUserWithProfile } from "@/app/lib/supabase/help/user"; +import { Metadata } from "next"; +import { redirect } from "next/navigation"; + +export const metadata: Metadata = { + title: "Leaderboards - DevPulse", +}; + +export default async function LeaderboardsPage() { + const { user } = await getUserWithProfile(); + if (!user) return redirect("/login?from=/dashboard/settings"); + + return ( +
+
+
+

Leaderboards

+

+ Create, join, and manage your coding servers +

+
+ +
+ +
+ +
+
+ ); +} diff --git a/app/(user)/dashboard/page.tsx b/app/(user)/dashboard/page.tsx new file mode 100644 index 0000000..a2aece8 --- /dev/null +++ b/app/(user)/dashboard/page.tsx @@ -0,0 +1,28 @@ +import { redirect } from "next/navigation"; +import DashboardWithoutKey from "../../components/dashboard/WithoutKey"; +import Stats from "@/app/components/dashboard/Stats"; +import { Metadata } from "next"; +import { getUserWithProfile } from "@/app/lib/supabase/help/user"; + +export const metadata: Metadata = { + title: "Dashboard - DevPulse", +}; + +export default async function Dashboard() { + const { user, profile } = await getUserWithProfile(); + if (!user) redirect("/login"); + + if (!profile?.wakatime_api_key) { + return ; + } + + const email = profile?.email || user.email!; + const name = user?.user_metadata?.name || email.split("@")[0]; + const prefferedAvatar = + user?.user_metadata?.avatar_url || + user?.user_metadata?.picture || + user?.user_metadata?.avatar || + null; + + return ; +} diff --git a/app/(user)/dashboard/settings/page.tsx b/app/(user)/dashboard/settings/page.tsx new file mode 100644 index 0000000..fbf2fb7 --- /dev/null +++ b/app/(user)/dashboard/settings/page.tsx @@ -0,0 +1,62 @@ +import { Metadata } from "next"; +import UserProfile from "@/app/components/dashboard/Settings/Profile"; +import ResetPassword from "@/app/components/dashboard/Settings/ResetPassword"; +import WakaTimeKey from "@/app/components/dashboard/Settings/WakaTimeKey"; +import { getUserWithProfile } from "@/app/lib/supabase/help/user"; +import { redirect } from "next/navigation"; + +export const metadata: Metadata = { + title: "Settings - DevPulse", +}; + +export default async function SettingsPage() { + const { user, profile } = await getUserWithProfile(); + if (!user) return redirect("/login?from=/dashboard/settings"); + + const hasWakaKey = Boolean(profile?.wakatime_api_key); + const maskedWakaKey = profile?.wakatime_api_key + ? `${profile.wakatime_api_key.slice(0, 8)}...${profile.wakatime_api_key.slice(-4)}` + : null; + + return ( +
+
+
+
+

+ Account Settings +

+

+ Manage profile details, WakaTime connection, and account security. +

+
+ +
+ + {hasWakaKey ? "WakaTime Connected" : "WakaTime Not Connected"} + +
+
+
+ + {user && ( +
+
+ + +
+ +
+ +
+
+ )} +
+ ); +} diff --git a/app/(user)/logout/page.tsx b/app/(user)/logout/page.tsx new file mode 100644 index 0000000..be48cec --- /dev/null +++ b/app/(user)/logout/page.tsx @@ -0,0 +1,10 @@ +import LogoutForm from "@/app/components/auth/Logout"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Logout - DevPulse", +}; + +export default async function Logout() { + return ; +} diff --git a/app/(user)/update-password/page.tsx b/app/(user)/update-password/page.tsx new file mode 100644 index 0000000..e63c719 --- /dev/null +++ b/app/(user)/update-password/page.tsx @@ -0,0 +1,31 @@ +import Image from "next/image"; +import UpdatePasswordForm from "@/app/components/auth/UpdatePasswordForm"; +import { Metadata } from "next"; +import Footer from "@/app/components/layout/Footer"; + +export const metadata: Metadata = { + title: "Update Password - DevPulse", +}; + +export default async function UpdatePassword() { + return ( + <> +
+ +
+
+ DevPulse Logo +

DevPulse

+
+

+ Update your password to keep your account secure. Enter a strong new +

+ + +
+
+ +