diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9f366c4..e91e9d5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,7 @@ name: Build Next.js App on: push: branches: ["master"] - pull_request: + pull_request_target: branches: ["master"] workflow_dispatch: @@ -26,6 +26,7 @@ jobs: run: npm run lint - name: Generate types from remote + if: ${{ secrets.SUPABASE_PROJECT_ID != '' && secrets.SUPABASE_ACCESS_TOKEN != '' }} env: SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} run: | @@ -41,6 +42,7 @@ jobs: # npx supabase db push --project-id ${{ secrets.SUPABASE_PROJECT_ID }} - name: Create .env file + if: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL != '' && secrets.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 @@ -48,4 +50,5 @@ jobs: echo "NEXT_PUBLIC_NORTON_SAFEWEB_SITE_VERIFICATION=${{ secrets.NEXT_PUBLIC_NORTON_SAFEWEB_SITE_VERIFICATION }}" >> .env - name: Build project + if: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL != '' && secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY != '' }} run: npm run build 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..54ae85a 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 { useCallback, 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, @@ -78,25 +59,48 @@ export default function Stats({ best_day: { date: "", total_seconds: 0 }, }); - const fetchStats = () => { - fetch("/api/wakatime/sync") - .then((res) => res.json()) - .then((data) => { - if (data.success) { - setStats(data.data); - } else { - toast.error( - data.error || "Failed to fetch stats. Please try syncing again.", - ); - } - setSyncing(false); - }); - }; + const fetchStats = useCallback(async () => { + setSyncing(true); - useEffect(() => { - fetchStats(); + const cached = sessionStorage.getItem("wakatimeStats"); + const cacheTime = Number(sessionStorage.getItem("wakatimeStatsTime")); + const now = Date.now(); + + if (cached && cacheTime && now - cacheTime < 1000 * 60 * 5) { + setStats(JSON.parse(cached)); + setSyncing(false); + return; + } + + try { + const res = await fetch("/api/wakatime/sync"); + const data = await res.json(); + + if (data.success) { + setStats(data.data); + + sessionStorage.setItem("wakatimeStats", JSON.stringify(data.data)); + sessionStorage.setItem("wakatimeStatsTime", now.toString()); + } else { + toast.error( + data.error || "Failed to fetch stats. Please try syncing again.", + ); + } + } catch (err) { + toast.error("Network error. Please try again."); + } + + setSyncing(false); }, []); + useEffect(() => { + const run = async () => { + await fetchStats(); + }; + + run(); + }, [fetchStats]); + useEffect(() => { if (!syncing) { setTimeout(() => { @@ -264,103 +268,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 - -
-
-
+