diff --git a/.env.example b/.env.example index 9dd84a7..53de38e 100644 --- a/.env.example +++ b/.env.example @@ -7,11 +7,6 @@ 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= diff --git a/.gitignore b/.gitignore index 5b60097..28e5999 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,3 @@ supabase/* yarn.lock pnpm-lock.yaml - -# Sentry Config File -.env.sentry-build-plugin diff --git a/app/(public)/(auth)/forgot-password/page.tsx b/app/(public)/(auth)/forgot-password/page.tsx new file mode 100644 index 0000000..3608849 --- /dev/null +++ b/app/(public)/(auth)/forgot-password/page.tsx @@ -0,0 +1,55 @@ +import ForgotPassword from "@/app/components/auth/ForgotPassword"; +import { Metadata } from "next/types"; +import { Suspense } from "react"; + +export const metadata: Metadata = { + title: "Forgot Password - Devpulse", + description: + "Lost access to your Devpulse account? Enter your email address to receive a password reset link and get back to tracking your coding activity and competing on leaderboards.", + alternates: { + canonical: "https://devpulse.hallofcodes.org/forgot-password", + }, + openGraph: { + title: "Forgot Password - Devpulse", + description: + "Lost access to your Devpulse account? Enter your email address to receive a password reset link and get back to tracking your coding activity and competing on leaderboards.", + url: "https://devpulse.hallofcodes.org/forgot-password", + siteName: "Devpulse", + images: [ + { + url: "https://devpulse.hallofcodes.org/images/devpulse.cover.png", + width: 1200, + height: 630, + alt: "Devpulse Cover Image", + }, + ], + locale: "en_US", + type: "website", + }, + twitter: { + card: "summary_large_image", + title: "Forgot Password - Devpulse", + description: + "Lost access to your Devpulse account? Enter your email address to receive a password reset link and get back to tracking your coding activity and competing on leaderboards.", + images: [ + { + url: "https://devpulse.hallofcodes.org/images/devpulse.cover.png", + alt: "Devpulse Cover Image", + }, + ], + }, +}; + +export default function ForgotPasswordPage() { + return ( + + Loading... + + } + > + + + ); +} diff --git a/app/(public)/(auth)/login/page.tsx b/app/(public)/(auth)/login/page.tsx index 8de1047..947e639 100644 --- a/app/(public)/(auth)/login/page.tsx +++ b/app/(public)/(auth)/login/page.tsx @@ -1,7 +1,6 @@ -import Link from "next/link"; -import Image from "next/image"; -import LoginForm from "@/app/components/auth/LoginForm"; import { Metadata } from "next/types"; +import { Suspense } from "react"; +import Login from "@/app/components/auth/Login"; export const metadata: Metadata = { title: "Login - Devpulse", @@ -54,122 +53,16 @@ export const metadata: Metadata = { }, }; -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; - +export default async function LoginPage() { return ( -
- {/* 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 - -

+ + Loading...
-
-
+ } + > + + ); } diff --git a/app/(public)/(auth)/reset-password/page.tsx b/app/(public)/(auth)/reset-password/page.tsx new file mode 100644 index 0000000..e64fadd --- /dev/null +++ b/app/(public)/(auth)/reset-password/page.tsx @@ -0,0 +1,164 @@ +import Link from "next/link"; +import Image from "next/image"; +import { Metadata } from "next/types"; +import ResetPasswordForm from "@/app/components/auth/form/ResetPasswordForm"; +import { Suspense } from "react"; + +// doesnt need description or keywords since this page is only accessible via a link in the email sent to the user, +// and we dont want it indexed by search engines +export const metadata: Metadata = { + title: "Reset Password - Devpulse", + description: "", + alternates: { + canonical: "https://devpulse.hallofcodes.org/reset-password", + }, + robots: { + index: false, + follow: false, + }, + openGraph: { + title: "Reset Password - Devpulse", + description: "", + url: "https://devpulse.hallofcodes.org/reset-password", + siteName: "Devpulse", + images: [ + { + url: "https://devpulse.hallofcodes.org/images/devpulse.cover.png", + width: 1200, + height: 630, + alt: "Devpulse Cover Image", + }, + ], + locale: "en_US", + type: "website", + }, + twitter: { + card: "summary_large_image", + title: "Reset Password - Devpulse", + description: "", + images: [ + { + url: "https://devpulse.hallofcodes.org/images/devpulse.cover.png", + alt: "Devpulse Cover Image", + }, + ], + }, +}; + +export default async function ResetPassword() { + return ( +
+ {/* Left Side - Visual / Branding */} +
+ {/* Background elements */} +
+ +
+ + Devpulse Logo + + Devpulse + + +
+ +
+

+ Change your password and get back to tracking your coding activity! +

+

+ Your Devpulse dashboard is waiting for you. Enter a new password to + regain access and continue your coding journey. +

+ +
+
+
+
+
+ + setup.ts + +
+
+
+ const + dev + = + getAccount + ( + this + ); +
+
+ dev + . + setNewPassword + ( + + "your-new-password" + + ); +
+
+ + {"// And just like that, you're back in the game. 🎉"} + +
+
+
+
+ +
+ © {new Date().getFullYear()} Devpulse. All rights reserved. +
+
+ + {/* Right Side - Form */} +
+
+ +
+ + Devpulse Logo +

Devpulse

+ + +
+

+ Reset your password +

+

+ New password, who dis? Enter a new password to regain access to + your account and get back to tracking your coding stats! +

+
+ + Loading...
+ } + > + + + +

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

+
+
+
+ ); +} diff --git a/app/(public)/(auth)/signup/page.tsx b/app/(public)/(auth)/signup/page.tsx index 30f7b72..a95feef 100644 --- a/app/(public)/(auth)/signup/page.tsx +++ b/app/(public)/(auth)/signup/page.tsx @@ -1,7 +1,6 @@ -import Link from "next/link"; -import Image from "next/image"; -import SignupForm from "@/app/components/auth/SignupForm"; import { Metadata } from "next/types"; +import Signup from "@/app/components/auth/Signup"; +import { Suspense } from "react"; export const metadata: Metadata = { title: "Sign Up - Devpulse", @@ -41,124 +40,16 @@ export const metadata: Metadata = { }, }; -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; - +export default async function SignupPage() { return ( -
- {/* 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 - -

+ + Loading...
-
-
+ } + > + + ); } diff --git a/app/(public)/flex/page.tsx b/app/(public)/flex/page.tsx index 98874fa..1a0585c 100644 --- a/app/(public)/flex/page.tsx +++ b/app/(public)/flex/page.tsx @@ -1,4 +1,3 @@ -import { createClient } from "../../lib/supabase/server"; import Footer from "@/app/components/layout/Footer"; import CTA from "@/app/components/common/ui/CTA"; import BackButton from "@/app/components/leaderboard/BackButton"; @@ -7,6 +6,7 @@ import { timeAgo } from "@/app/utils/time"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faExternalLink } from "@fortawesome/free-solid-svg-icons"; import { Metadata } from "next/types"; +import { createPublicClient } from "@/app/lib/supabase/public"; export const metadata: Metadata = { title: "Flexes - Devpulse", @@ -57,18 +57,15 @@ export const metadata: Metadata = { }; export default async function Flexs() { - const supabase = await createClient(); + const supabase = createPublicClient(); + const { data, error } = await supabase + .from("user_flexes") + .select("*") + .order("created_at", { ascending: false }); - 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; + if (error) { + console.error("Error fetching flexes:", error); + } return (
@@ -80,6 +77,15 @@ export default async function Flexs() {

Devpulse Flexes

+ {error && ( +
+

Error Loading Flexes

+

+ There was an error fetching the flexes. Please try again later. +

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

No Flexes Yet

@@ -146,7 +152,7 @@ export default async function Flexs() { )}
- {!user && } +
); diff --git a/app/(public)/leaderboard/[slug]/page.tsx b/app/(public)/leaderboard/[slug]/page.tsx index aeefbac..5bbcb42 100644 --- a/app/(public)/leaderboard/[slug]/page.tsx +++ b/app/(public)/leaderboard/[slug]/page.tsx @@ -1,69 +1,109 @@ -import { createClient } from "../../../lib/supabase/server"; import LeaderboardTable, { NonNullableMember, -} from "../../../components/LeaderboardTable"; -import LeaderboardHeader from "@/app/components/leaderboard/Header"; +} from "../../../components/leaderboard/LeaderboardTable"; import Footer from "@/app/components/layout/Footer"; import CTA from "@/app/components/common/ui/CTA"; -import { getUserWithProfile } from "@/app/lib/supabase/help/user"; +import { notFound } from "next/navigation"; +import Banner from "@/app/components/leaderboard/Banner"; +import BackButton from "@/app/components/leaderboard/BackButton"; +import Image from "next/image"; +import InviteFriendsButton from "@/app/components/leaderboard/InviteFriendsButton"; +import { createPublicClient } from "@/app/lib/supabase/public"; +import InternalServerError from "@/app/internal-server-error"; + +export async function generateStaticParams() { + const supabase = createPublicClient(); + + const { data } = await supabase.from("leaderboards").select("slug"); + + return (data || []).map((item) => ({ + slug: item.slug, + })); +} export default async function LeaderboardPage(props: { params: Promise<{ slug: string }>; }) { - const [{ user }, { slug }, supabase] = await Promise.all([ - getUserWithProfile(), - props.params, - createClient(), - ]); + const { slug } = await props.params; + const supabase = createPublicClient(); - const { data: leaderboard } = await supabase + const { data: leaderboard, error: leaderboardError } = await supabase .from("leaderboards") .select("*") .eq("slug", slug) .single(); - - if (!leaderboard) { - return ( -
-
-

Leaderboard not found

-

{slug}

-
-
- ); + if (!leaderboard) return notFound(); + if (leaderboardError) { + console.error("Error fetching leaderboard:", leaderboardError); + return InternalServerError(); } - const { data: members, error } = await supabase + const { data: members, error: membersError } = 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.

-
-
- ); + if (membersError) { + console.error("Error fetching members:", membersError); + return InternalServerError(); } return (
- -
- + + +
+
+ +
+
+ +
+
+
+
+ Devpulse Logo +
+
+ +
+

+ {leaderboard.name} +

+

+ {leaderboard.description && + leaderboard.description?.length > 0 + ? leaderboard.description + : `Join ${leaderboard.name} to track your coding metrics, compete with fellow developers, and showcase your engineering skills.`} +

+
+
+ +
+ +
+
+
+ +
+
- {!user && } +
); diff --git a/app/(public)/leaderboard/page.tsx b/app/(public)/leaderboard/page.tsx index 5b1d6d2..a0c0cf6 100644 --- a/app/(public)/leaderboard/page.tsx +++ b/app/(public)/leaderboard/page.tsx @@ -1,5 +1,4 @@ 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/common/ui/CTA"; import Image from "next/image"; @@ -7,6 +6,7 @@ import { Metadata } from "next/types"; import { getUserWithProfile } from "@/app/lib/supabase/help/user"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; +import { createPublicClient } from "@/app/lib/supabase/public"; export const metadata: Metadata = { title: "Leaderboards - Devpulse", @@ -58,36 +58,20 @@ export const metadata: Metadata = { }; 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(), - ]); + const supabase = createPublicClient(); + const { data, error } = await supabase + .from("leaderboards") + .select("id, name, slug") + .order("created_at", { ascending: false }); if (error) { - return ( -
-

Failed to load leaderboards.

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

No leaderboards found.

-
- ); + console.error("Error fetching leaderboards:", error); } return (
- +
Devpulse Logo @@ -96,33 +80,59 @@ export default async function Leaderboards() {
- ); diff --git a/app/api/auth/callback/route.ts b/app/api/auth/callback/route.ts index 8f83d09..f245a1b 100644 --- a/app/api/auth/callback/route.ts +++ b/app/api/auth/callback/route.ts @@ -9,7 +9,7 @@ export async function GET(req: NextRequest) { const redirectTo = redirectParam && redirectParam.startsWith("/") && !redirectParam.startsWith("//") ? redirectParam - : "/dashboard"; + : "/d"; const response = NextResponse.redirect(`${origin}${redirectTo}`); response.cookies.set("devpulse_redirect", "", { path: "/", maxAge: 0 }); diff --git a/app/api/sentry-example-api/route.ts b/app/api/sentry-example-api/route.ts deleted file mode 100644 index 5ffa746..0000000 --- a/app/api/sentry-example-api/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const dynamic = "force-dynamic"; - -class SentryExampleAPIError extends Error { - constructor(message: string | undefined) { - super(message); - this.name = "SentryExampleAPIError"; - } -} - -// A faulty API route to test Sentry's error monitoring -export function GET() { - throw new SentryExampleAPIError( - "This error is raised on the backend called by the example page.", - ); -} diff --git a/app/components/LeaderboardTable.tsx b/app/components/LeaderboardTable.tsx deleted file mode 100644 index 13cf530..0000000 --- a/app/components/LeaderboardTable.tsx +++ /dev/null @@ -1,329 +0,0 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Database } from "../supabase-types"; -import { BADGE_LEGEND_HOURS, getBadgeInfoFromHours } from "@/app/utils/badge"; - -type LeaderboardMembersRow = - Database["public"]["Views"]["leaderboard_members_view"]["Row"]; - -// Supabase-generated types for views are always nullable -// due to the nature of views and -// conservative type generation. -export type NonNullableMember = Omit< - LeaderboardMembersRow, - | "user_id" - | "role" - | "email" - | "total_seconds" - | "languages" - | "operating_systems" - | "editors" -> & { - user_id: string; - role: string; - email: string; - total_seconds: number; - languages: { name: string }[]; - operating_systems: { name: string }[]; - editors: { name: string }[]; -}; - -function LeaderboardPodium({ topUsers }: { topUsers: { user_id: string; rank: number; email: string | null; hours: number; role: string | null; languages: string[]; os: string; editor: string; }[] }) { - if (topUsers.length === 0) return null; - - return ( -
- {topUsers.map((user, idx) => { - const rankColor = - idx === 0 - ? "text-yellow-400 drop-shadow-[0_0_8px_rgba(250,204,21,0.4)]" - : idx === 1 - ? "text-gray-300 drop-shadow-[0_0_8px_rgba(209,213,219,0.4)]" - : "text-amber-600 drop-shadow-[0_0_8px_rgba(217,119,6,0.4)]"; - - const badgeInfo = getBadgeInfoFromHours(user.hours); - const initial = (user.email?.[0] || "?").toUpperCase(); - - return ( -
- {/* Minimal Background Glow based on rank */} -
- -
-
-
- {initial} -
-
-
- {user.email?.split("@")[0] || "Unknown"} -
-
- {badgeInfo.icon && ( - - )} - {badgeInfo.label} -
-
-
- -
- - {idx === 0 ? "01" : idx === 1 ? "02" : "03"} - -
-
- - {/* Stats Row */} -
-
- - Hours - - - {user.hours} - -
-
- - Language - - - {user.languages?.[0] || "N/A"} - -
-
- - Editor - - - {user.editor || "N/A"} - -
-
-
- ); - })} -
- ); -} - -import LeaderboardStats from "./leaderboard/LeaderboardStats"; - -export default function LeaderboardTable({ - members, - ownerId, -}: { - members: NonNullableMember[]; - ownerId?: string; -}) { - const ranked = members - .sort((a, b) => (b.total_seconds || 0) - (a.total_seconds || 0)) - .map((member, index) => ({ - user_id: member.user_id, - rank: index + 1, - email: member.email, - hours: Math.round((member.total_seconds || 0) / 3600), - role: member.role, - languages: (member.languages || []).slice(0, 3).map((l) => l.name), - os: member.operating_systems?.[0]?.name || "N/A", - editor: member.editors?.[0]?.name || "N/A", - })); - - const maxHours = ranked[0]?.hours || 1; - const formatRank = (rank: number) => rank.toString().padStart(2, "0"); - - const getRankColor = (rank: number) => { - if (rank === 1) - return "text-yellow-400 font-bold drop-shadow-[0_0_8px_rgba(250,204,21,0.4)]"; - if (rank === 2) return "text-gray-300 font-bold"; - if (rank === 3) return "text-amber-600 font-bold"; - return "text-gray-600 font-medium"; - }; - - return ( -
- - - - -
- - - {ranked.length === 0 ? ( -
-

- No tracking data available yet. -

-
- ) : ( -
- {/* Header Row (Desktop) */} -
-
Rank
-
Developer
-
Language
-
Editor
-
Hours
-
- - {/* List Body */} -
- {ranked.slice(3).map((user) => { - const isCurrentUser = user.user_id === ownerId; - const pct = Math.max(2, (user.hours / maxHours) * 100); - const badgeInfo = getBadgeInfoFromHours(user.hours); - - return ( -
- {/* Background Progress Bar */} -
- {isCurrentUser && ( -
- )} - - {/* MOBILE TOP ROW / DESKTOP LEFT FLEX */} -
- {/* Rank */} -
- - {formatRank(user.rank)} - -
- - {/* Profile + Badges */} -
-
- {user.email?.charAt(0) || "?"} -
-
-
-

- {user.email?.split("@")[0] || "Unknown"} -

- {isCurrentUser && ( - - You - - )} -
-
-
- {badgeInfo.icon && ( - - )} - {badgeInfo.label} -
-
-
-
- - {/* Mobile Score */} -
-

- {user.hours} -

- - hrs - -
-
- - {/* MOBILE BOTTOM STACK / DESKTOP RIGHT ROW */} -
- {/* Language */} -
- {user.languages.length > 0 ? ( - user.languages.map((lang, i) => ( - - {lang} - - )) - ) : ( - - No stack tracked - - )} -
- - {/* Editor */} -
- {user.editor !== "N/A" && ( - - {user.editor} - - )} - {user.editor !== "N/A" && user.os !== "N/A" && ( - - )} - {user.os !== "N/A" && ( - - {user.os} - - )} -
- - {/* Score (Desktop) */} -
-

- {user.hours} -

- - hrs - -
-
-
- ); - })} -
-
- )} -
- - {/* Rankings Legend */} -
-
-

- Rankings Legend -

-
- {BADGE_LEGEND_HOURS.map((hours) => { - const b = getBadgeInfoFromHours(hours); - return ( -
-
- {b.icon && } - {b.label} -
- - {hours === 0 ? "0 hrs" : `${hours}+`} - -
- ); - })} -
-
- -
-
- ); -} \ No newline at end of file diff --git a/app/components/auth/ForgotPassword.tsx b/app/components/auth/ForgotPassword.tsx new file mode 100644 index 0000000..4278b6b --- /dev/null +++ b/app/components/auth/ForgotPassword.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import ForgotPasswordForm from "./form/ForgotPasswordForm"; +import Link from "next/link"; +import Image from "next/image"; + +export default function ForgotPassword() { + const searchParams = useSearchParams(); + + const redirectParam = searchParams.get("redirect"); + + const redirectTo = + redirectParam && + redirectParam.startsWith("/") && + !redirectParam.startsWith("//") + ? redirectParam + : undefined; + + return ( +
+ {/* Left Side - Visual / Branding */} +
+ {/* Background elements */} +
+ +
+ + Devpulse Logo + + Devpulse + + +
+ +
+

+ Loss of access? No problem! +

+

+ We got you covered. All you need to do is enter your email address + and we will send you a password reset link to get you back on track + with monitoring your coding activity and competing on leaderboards. +

+ +
+
+
+
+
+ + setup.ts + +
+
+
+ const + auth + = + new + SupabaseAuth + (); +
+
+ auth + . + sendPasswordResetEmail + ( + email + ); +
+
+ + {"// Check your inbox. "} + +
+
+
+
+ +
+ © {new Date().getFullYear()} Devpulse. All rights reserved. +
+
+ + {/* Right Side - Form */} +
+
+ +
+ + Devpulse Logo +

Devpulse

+ + +
+

+ Forgot your password? +

+

+ No worries! Just enter your email address and we'll send you + an email. +

+
+ + + +

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

+
+
+
+ ); +} diff --git a/app/components/auth/Login.tsx b/app/components/auth/Login.tsx new file mode 100644 index 0000000..cd65644 --- /dev/null +++ b/app/components/auth/Login.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import Link from "next/link"; +import Image from "next/image"; +import LoginForm from "./form/LoginForm"; + +export default function Login() { + const searchParams = useSearchParams(); + + const redirectParam = searchParams.get("redirect"); + + const redirectTo = + redirectParam && + redirectParam.startsWith("/") && + !redirectParam.startsWith("//") + ? redirectParam + : undefined; + + return ( +
+ {/* 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. +

+
+ + + +
+ + Forgot your password? + +
+
+
+
+ ); +} diff --git a/app/components/auth/Signup.tsx b/app/components/auth/Signup.tsx new file mode 100644 index 0000000..6a172a6 --- /dev/null +++ b/app/components/auth/Signup.tsx @@ -0,0 +1,129 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; +import SignupForm from "./form/SignupForm"; + +export default function Signup() { + const searchParams = useSearchParams(); + + const redirectParam = searchParams.get("redirect"); + + const redirectTo = + redirectParam && + redirectParam.startsWith("/") && + !redirectParam.startsWith("//") + ? redirectParam + : undefined; + + return ( +
+ {/* 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/components/auth/form/ForgotPasswordForm.tsx b/app/components/auth/form/ForgotPasswordForm.tsx new file mode 100644 index 0000000..ab12ea6 --- /dev/null +++ b/app/components/auth/form/ForgotPasswordForm.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { useRef, useState } from "react"; +import { createClient } from "@/app/lib/supabase/client"; +import { toast } from "react-toastify"; +import HCaptcha from "@hcaptcha/react-hcaptcha"; + +export default function ForgotPasswordForm() { + const supabase = createClient(); + const [email, setEmail] = useState(""); + const [loading, setLoading] = useState(false); + const captcha = useRef(null); + const [showCaptcha, setShowCaptcha] = useState(false); + + const handleLogin = async (e: React.SyntheticEvent) => { + e.preventDefault(); + setShowCaptcha(true); + }; + + const handleCaptchaVerify = async (token: string) => { + setShowCaptcha(false); + setLoading(true); + + const sendReset = new Promise(async (resolve, reject) => { + try { + const { data, error } = await supabase.auth.resetPasswordForEmail( + email, + { + captchaToken: token, + redirectTo: `${window.location.origin}/reset-password`, + }, + ); + + if (error) return reject(error); + + resolve(data); + } catch (error) { + reject(error); + } + }); + + toast.promise(sendReset, { + pending: "Hold tight...", + success: "Reset instructions sent! Check your email.", + error: { + render({ data }) { + const err = data as Error; + return ( + err?.message || + "Failed to send reset instructions. Please try again." + ); + }, + }, + }); + + sendReset.finally(() => { + if (captcha.current) captcha.current.resetCaptcha(); + setLoading(false); + }); + }; + + return ( + <> +
+ setEmail(e.target.value)} + required + /> + + +
+ + {showCaptcha && ( +
+
+

+ Verify you are human +

+ + + + +
+
+ )} + + ); +} diff --git a/app/components/auth/LoginForm.tsx b/app/components/auth/form/LoginForm.tsx similarity index 85% rename from app/components/auth/LoginForm.tsx rename to app/components/auth/form/LoginForm.tsx index 2e39933..309c116 100644 --- a/app/components/auth/LoginForm.tsx +++ b/app/components/auth/form/LoginForm.tsx @@ -1,12 +1,13 @@ "use client"; import { useRef, useState } from "react"; -import { createClient } from "../../lib/supabase/client"; +import { createClient } from "@/app/lib/supabase/client"; import { toast } from "react-toastify"; import { useRouter } from "next/navigation"; import { useSearchParams } from "next/navigation"; import HCaptcha from "@hcaptcha/react-hcaptcha"; -import Oauth2 from "./Oauth2"; +import Oauth2 from "../Oauth2"; +import Link from "next/link"; export default function LoginForm() { const supabase = createClient(); @@ -18,7 +19,7 @@ export default function LoginForm() { redirectParam.startsWith("/") && !redirectParam.startsWith("//") ? redirectParam - : "/dashboard"; + : "/d"; const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [loading, setLoading] = useState(false); @@ -104,8 +105,24 @@ export default function LoginForm() { Login +

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

+
+ Or continue with +
diff --git a/app/components/auth/form/ResetPasswordForm.tsx b/app/components/auth/form/ResetPasswordForm.tsx new file mode 100644 index 0000000..de93dd8 --- /dev/null +++ b/app/components/auth/form/ResetPasswordForm.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { createClient } from "@/app/lib/supabase/client"; +import { toast } from "react-toastify"; +import { useRouter, useSearchParams } from "next/navigation"; +import HCaptcha from "@hcaptcha/react-hcaptcha"; + +export default function ResetPasswordForm() { + const supabase = createClient(); + const router = useRouter(); + const searchParams = useSearchParams(); + const captcha = useRef(null); + + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [showCaptcha, setShowCaptcha] = useState(false); + const [checking, setChecking] = useState(true); + + useEffect(() => { + let cancelled = false; + + const verifyResetSession = async () => { + const type = searchParams.get("type"); + const code = searchParams.get("code"); + + if (type && type !== "recovery") { + router.replace("/login"); + return; + } + + if (code) { + const { error } = await supabase.auth.exchangeCodeForSession(code); + if (error) { + router.replace("/login"); + return; + } + } + + const { data } = await supabase.auth.getSession(); + if (!data.session) { + router.replace("/login"); + return; + } + + if (!cancelled) setChecking(false); + }; + + verifyResetSession(); + + return () => { + cancelled = true; + }; + }, [router, searchParams, supabase]); + + const handleSubmit = async (e: React.SyntheticEvent) => { + e.preventDefault(); + if (checking) return; + setShowCaptcha(true); + }; + + const handleCaptchaVerify = async (_token: string) => { + setShowCaptcha(false); + setLoading(true); + + const updatePassword = new Promise(async (resolve, reject) => { + try { + if (password !== confirmPassword) { + return reject(new Error("Passwords do not match.")); + } + + const { data, error } = await supabase.auth.updateUser({ password }); + if (error) return reject(error); + + resolve(data); + } catch (error) { + reject(error); + } + }); + + toast.promise(updatePassword, { + pending: "Resetting password...", + success: { + render() { + if (captcha.current) captcha.current.resetCaptcha(); + setLoading(false); + setPassword(""); + setConfirmPassword(""); + return "Password updated! You can log in now."; + }, + }, + error: { + render({ data }) { + if (captcha.current) captcha.current.resetCaptcha(); + setLoading(false); + const err = data as Error; + return err?.message || "Failed to reset password. Please try again."; + }, + }, + }); + + updatePassword.then(() => { + router.replace("/login"); + }); + }; + + return ( + <> +
+ setPassword(e.target.value)} + required + /> + + setConfirmPassword(e.target.value)} + required + /> + + +
+ + {showCaptcha && ( +
+
+

+ Verify you are human +

+ + + + +
+
+ )} + + ); +} diff --git a/app/components/auth/SignupForm.tsx b/app/components/auth/form/SignupForm.tsx similarity index 84% rename from app/components/auth/SignupForm.tsx rename to app/components/auth/form/SignupForm.tsx index c7b4a9c..01eb520 100644 --- a/app/components/auth/SignupForm.tsx +++ b/app/components/auth/form/SignupForm.tsx @@ -1,13 +1,14 @@ "use client"; import { useRef, useState } from "react"; -import { createClient } from "../../lib/supabase/client"; +import { createClient } from "@/app/lib/supabase/client"; import { toast } from "react-toastify"; import { useSearchParams } from "next/navigation"; import HCaptcha from "@hcaptcha/react-hcaptcha"; -import Oauth2 from "./Oauth2"; +import Oauth2 from "../Oauth2"; +import Link from "next/link"; -export default function AuthPage() { +export default function SignupForm() { const supabase = createClient(); const searchParams = useSearchParams(); const redirectParam = searchParams.get("redirect"); @@ -16,7 +17,7 @@ export default function AuthPage() { redirectParam.startsWith("/") && !redirectParam.startsWith("//") ? redirectParam - : "/dashboard"; + : "/d"; const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); @@ -111,6 +112,26 @@ export default function AuthPage() { required /> +

+ By signing up, you agree to our{" "} + + Terms of Service + {" "} + and{" "} + + Privacy Policy + + . +

+ - )}

{leaderboard.description && leaderboard.description?.length > 0 @@ -128,46 +61,6 @@ export default function LeaderboardHeader({

- - {open && ( -
-
-

- Edit Leaderboard -

- - setName(e.target.value)} - placeholder="Leaderboard name" - /> - -