From 4d4e5f726e43e9969e24d607938ff14be721cf4c Mon Sep 17 00:00:00 2001 From: Melvin Jones Repol Date: Thu, 26 Mar 2026 21:05:08 +0800 Subject: [PATCH 1/3] feat(nav): Add ProfileDropdown and header - Client-side profile dropdown with avatar or email initial, name, animated caret, outside-click to close and event propagation handling - Conditional dashboard links (Dashboard, Leaderboards, Flex, Chat), omitted when already on /dashboard - Nav header fetches user profile and shows dropdown for authenticated users or Login/Sign up links for guests - Include Nav and Footer in legal/privacy/terms pages and update home layout spacing and background for consistent header/footer - Minor fixes: replace space-x-3 with gap-3 in LoginForm, tidy imports and formatting in Player and Stats components --- app/components/ProfileDropdown.tsx | 149 +++++++++++ app/components/auth/LoginForm.tsx | 7 +- app/components/chat/Player.tsx | 208 +++++++++++----- app/components/dashboard/Stats.tsx | 121 +-------- app/components/layout/Nav.tsx | 55 +++++ app/legal/contribution-guidelines/page.tsx | 110 +++++---- app/legal/privacy/page.tsx | 271 +++++++++++---------- app/legal/terms/page.tsx | 202 +++++++-------- app/page.tsx | 42 +--- 9 files changed, 660 insertions(+), 505 deletions(-) create mode 100644 app/components/ProfileDropdown.tsx create mode 100644 app/components/layout/Nav.tsx diff --git a/app/components/ProfileDropdown.tsx b/app/components/ProfileDropdown.tsx new file mode 100644 index 0000000..b08ca23 --- /dev/null +++ b/app/components/ProfileDropdown.tsx @@ -0,0 +1,149 @@ +"use client"; + +import { + faArrowRightFromBracket, + faDashboard, + faGear, + faMessage, + faRankingStar, + faStar, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Image from "next/image"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useEffect, useRef, useState } from "react"; + +export default function ProfileDropdown({ + avatar, + name, + email, +}: { + avatar: string | null; + name: string; + email: string; +}) { + const [profileOpen, setProfileOpen] = useState(false); + const profileRef = useRef(null); + const pathname = usePathname(); + const showDashboardLink = !pathname.includes("/dashboard"); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + profileRef.current && + !profileRef.current.contains(event.target as Node) + ) { + setProfileOpen(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + return ( +
setProfileOpen(!profileOpen)} + > + {avatar ? ( + Profile Avatar + ) : ( +
+ {email.charAt(0).toUpperCase()} +
+ )} + + {name} + + + + {/* Dropdown Menu */} + {profileOpen && ( +
e.stopPropagation()} + > +
+

{name}

+

{email}

+
+ {showDashboardLink && ( + <> + setProfileOpen(false)} + > + + Dashboard + + setProfileOpen(false)} + > + + Leaderboards + + setProfileOpen(false)} + > + + Flex + + setProfileOpen(false)} + > + + Chat + + + )} + setProfileOpen(false)} + > + + Settings + + setProfileOpen(false)} + > + + Logout + +
+ )} +
+ ); +} diff --git a/app/components/auth/LoginForm.tsx b/app/components/auth/LoginForm.tsx index 3f2e522..99e6b34 100644 --- a/app/components/auth/LoginForm.tsx +++ b/app/components/auth/LoginForm.tsx @@ -6,11 +6,6 @@ import { toast } from "react-toastify"; import { useRouter } from "next/navigation"; import { useSearchParams } from "next/navigation"; import HCaptcha from "@hcaptcha/react-hcaptcha"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faGoogle } from "@fortawesome/free-brands-svg-icons/faGoogle"; -import { faMicrosoft } from "@fortawesome/free-brands-svg-icons/faMicrosoft"; -import { faGithub } from "@fortawesome/free-brands-svg-icons"; -import Image from "next/image"; export default function LoginForm() { const supabase = createClient(); @@ -122,7 +117,7 @@ export default function LoginForm() { Or continue with -
+
{(onDownload || onClose) && ( @@ -429,9 +491,13 @@ export default function Player({ setShowSettings(false); onDownload(); }} - onMouseEnter={(e) => showHint("Download", e.currentTarget, "below")} + onMouseEnter={(e) => + showHint("Download", e.currentTarget, "below") + } onMouseLeave={hideHint} - onTouchStart={(e) => showHint("Download", e.currentTarget, "below")} + onTouchStart={(e) => + showHint("Download", e.currentTarget, "below") + } className="w-8 h-8 rounded-md text-white/90 hover:text-white transition" aria-label="Download video" > @@ -474,13 +540,20 @@ export default function Player({ setShowSettings(false); void togglePlay(); }} - onMouseEnter={(e) => showHint(playing ? "Pause" : "Play", e.currentTarget)} + onMouseEnter={(e) => + showHint(playing ? "Pause" : "Play", e.currentTarget) + } onMouseLeave={hideHint} - onTouchStart={(e) => showHint(playing ? "Pause" : "Play", e.currentTarget)} + onTouchStart={(e) => + showHint(playing ? "Pause" : "Play", e.currentTarget) + } className="w-8 h-8 rounded-md text-white/90 hover:text-white transition" aria-label={playing ? "Pause" : "Play"} > - + {showUi && showMobileVolume && ( -
- +
+
- {["Auto", ...(detectedQuality ? [detectedQuality] : [])].map((q) => ( - - ))} + {["Auto", ...(detectedQuality ? [detectedQuality] : [])].map( + (q) => ( + + ), + )}
{detectedQuality @@ -735,7 +816,6 @@ export default function Player({
)} -
); } diff --git a/app/components/dashboard/Stats.tsx b/app/components/dashboard/Stats.tsx index a93e3c9..081bb74 100644 --- a/app/components/dashboard/Stats.tsx +++ b/app/components/dashboard/Stats.tsx @@ -1,8 +1,6 @@ "use client"; -import { useEffect, useState, useRef } from "react"; -import Link from "next/link"; - +import { useEffect, useState } from "react"; import AOS from "aos"; import "devicon/devicon.min.css"; import { toast } from "react-toastify"; @@ -15,9 +13,8 @@ import OperatingSystem from "./widgets/OperatingSystem"; import Projects from "./widgets/Projects"; import Machines from "./widgets/Machines"; import Categories from "./widgets/Categories"; - import Dependencies from "./widgets/Dependencies"; -import Image from "next/image"; +import ProfileDropdown from "../ProfileDropdown"; export interface StatsData { total_seconds: number; @@ -47,22 +44,6 @@ export default function Stats({ }: StatsProps) { const [syncing, setSyncing] = useState(false); const [animated, setAnimated] = useState(false); - const [profileOpen, setProfileOpen] = useState(false); - const profileRef = useRef(null); - - // Close profile dropdown when clicking outside - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if ( - profileRef.current && - !profileRef.current.contains(event.target as Node) - ) { - setProfileOpen(false); - } - } - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, []); const [stats, setStats] = useState({ total_seconds: 0, @@ -264,103 +245,7 @@ export default function Stats({ - {/* Profile Section */} -
setProfileOpen(!profileOpen)} - > - {avatar ? ( - Profile Avatar - ) : ( -
- {email.charAt(0).toUpperCase()} -
- )} - - {name} - - - - {/* Dropdown Menu */} - {profileOpen && ( -
e.stopPropagation()} - > -
-

- {name} -

-

{email}

-
- setProfileOpen(false)} - > - - - - - Settings - - setProfileOpen(false)} - > - - - - Logout - -
- )} -
+
diff --git a/app/components/layout/Nav.tsx b/app/components/layout/Nav.tsx new file mode 100644 index 0000000..649fa79 --- /dev/null +++ b/app/components/layout/Nav.tsx @@ -0,0 +1,55 @@ +import { getUserWithProfile } from "@/app/lib/supabase/help/user"; +import Image from "next/image"; +import Link from "next/link"; +import ProfileDropdown from "../ProfileDropdown"; + +export default async function Nav() { + const { user } = await getUserWithProfile(); + + return ( +
+
+ + DevPulse Logo + DevPulse + + + {user ? ( + + ) : ( +
+ + Log in + + + Sign up + +
+ )} +
+
+ ); +} diff --git a/app/legal/contribution-guidelines/page.tsx b/app/legal/contribution-guidelines/page.tsx index aa8e332..8348552 100644 --- a/app/legal/contribution-guidelines/page.tsx +++ b/app/legal/contribution-guidelines/page.tsx @@ -1,3 +1,5 @@ +import Footer from "@/app/components/layout/Footer"; +import Nav from "@/app/components/layout/Nav"; import { Metadata } from "next"; export const metadata: Metadata = { @@ -8,62 +10,70 @@ export const metadata: Metadata = { export default function ContributionGuidelines() { return ( -
-

- Contribution Guidelines -

+
+
); } diff --git a/app/legal/privacy/page.tsx b/app/legal/privacy/page.tsx index cb8afe7..c2f9161 100644 --- a/app/legal/privacy/page.tsx +++ b/app/legal/privacy/page.tsx @@ -1,3 +1,5 @@ +import Footer from "@/app/components/layout/Footer"; +import Nav from "@/app/components/layout/Nav"; import { Metadata } from "next"; export const metadata: Metadata = { @@ -8,137 +10,144 @@ export const metadata: Metadata = { export default function Privacy() { return ( -
-

- Privacy Policy -

- -

- This Privacy Policy explains how DevPulse{" "} - ("we", "our", or "us") collects, uses, and - protects your information when you use - devpulse-waka.vercel.app.{" "} -

- -

- 1. Information We Collect -

- -

a. Automatically Collected Data

-

- Our application is hosted on Vercel. Vercel may automatically collect - certain information such as: -

-
    -
  • IP address
  • -
  • Browser type and version
  • -
  • Device information
  • -
  • Request logs and usage data
  • -
- -

b. Authentication (Supabase)

-

- We use Supabase as our authentication provider. When you sign up or log - in, we collect: -

-
    -
  • Email address
  • -
  • Password
  • -
-

- This information is securely processed and stored by Supabase to create - and manage your account. We do not directly store or have access to your - password in plain text. -

- -

c. WakaTime API Key

-

- When you use DevPulse, you may provide your WakaTime API key. This key - is used solely to fetch your coding activity data from the WakaTime API - and generate statistics. We do not use this key for any other purpose. -

- -

d. GitHub OAuth Data

-

- When you connect your GitHub account via OAuth2, we may access: -

-
    -
  • Your name
  • -
  • Your email address
  • -
-

- This information is used only to identify your account and personalize - your experience. -

- -

e. Sentry

-

- We use Sentry for error monitoring. While only error-related features - are enabled, Sentry may still collect certain information, including - personally identifiable information (PII), stack traces, and request - data. This information is used solely to help diagnose errors and - improve the reliability and performance of the application. -

- -

- 2. How We Use Your Information -

-
    -
  • To authenticate users and manage accounts
  • -
  • To fetch and display your coding statistics
  • -
  • To personalize your experience
  • -
  • To improve the performance and reliability of the app
  • -
- -

- 3. Third-Party Services -

-

- We use third-party services that may collect and process data: -

-
    -
  • Vercel (hosting and infrastructure)
  • -
  • Supabase (authentication and backend services)
  • -
  • Google Search Console (search performance monitoring)
  • -
  • WakaTime API (coding activity data)
  • -
  • GitHub OAuth (authentication and profile data)
  • -
- -

- 4. Data Storage and Security -

-

- We take reasonable measures to protect your information. Authentication - data is handled securely by Supabase. However, no method of transmission - over the internet is 100% secure. -

- -

5. Data Sharing

-

- We do not sell, trade, or rent your personal information. Data is only - shared with third-party services as necessary to operate the - application. -

- -

6. Your Rights

-

- You may choose not to provide certain information (such as your WakaTime - API key or GitHub access), but this may limit functionality. -

- -

- 7. Changes to This Policy -

-

- We may update this Privacy Policy from time to time. Changes will be - posted on this page. -

- -

8. Contact

-

- If you have any questions about this Privacy Policy, please contact us - through the project repository or application interface. -

+
+
); } diff --git a/app/legal/terms/page.tsx b/app/legal/terms/page.tsx index dbed151..5a256f4 100644 --- a/app/legal/terms/page.tsx +++ b/app/legal/terms/page.tsx @@ -1,3 +1,5 @@ +import Footer from "@/app/components/layout/Footer"; +import Nav from "@/app/components/layout/Nav"; import { Metadata } from "next"; export const metadata: Metadata = { @@ -8,113 +10,121 @@ export const metadata: Metadata = { export default function Terms() { return ( -
-

- Terms of Service -

+
+
); } diff --git a/app/page.tsx b/app/page.tsx index 8ae898f..508a580 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,4 @@ import Link from "next/link"; -import Image from "next/image"; import { createClient } from "./lib/supabase/server"; import Footer from "./components/layout/Footer"; import CTA from "./components/layout/CTA"; @@ -11,6 +10,7 @@ import TopLeaderboard, { } from "./components/landing-page/TopLeaderbord"; import ContributeCard from "./components/landing-page/ContributeCard"; import VibeCoders from "./components/landing-page/VibeCoders"; +import Nav from "./components/layout/Nav"; export default async function Home() { const supabase = await createClient(); @@ -115,45 +115,7 @@ export default async function Home() { />
- {/* Header / Nav */} -
-
- - DevPulse Logo - - DevPulse - - - -
- - Log in - - - Sign up - -
-
-
+