Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/(public)/(auth)/reset-password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ export const metadata: Metadata = {

export default async function ResetPassword() {
return (
<div className="min-h-screen flex grid-bg relative">
<div className="min-h-screen flex relative">
{/* Left Side - Visual / Branding */}
<div className="hidden lg:flex lg:w-1/2 relative flex-col justify-between p-12 md:p-16 xl:p-24 border-r border-gray-200 bg-blue-600 overflow-hidden">
{/* Background elements */}
<div className="absolute inset-0 grid-bg opacity-30" />
<div className="absolute inset-0 opacity-30" />

<div className="relative z-10">
<Link
Expand Down Expand Up @@ -119,7 +119,7 @@ export default async function ResetPassword() {

{/* Right Side - Form */}
<div className="w-full lg:w-1/2 flex flex-col justify-center items-center p-8 sm:p-12 xl:p-20 relative">
<div className="absolute inset-0 grid-bg opacity-20 lg:hidden" />
<div className="absolute inset-0 opacity-20 lg:hidden" />

<div className="w-full max-w-sm relative z-10">
<Link
Expand Down
45 changes: 20 additions & 25 deletions app/(public)/flex/page.tsx → app/(public)/(guest)/flex/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import Footer from "@/app/components/layout/Footer";
import CTA from "@/app/components/common/ui/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";
Expand All @@ -10,9 +8,9 @@ import { prisma } from "@/app/lib/prisma";
import Link from "next/link";

export const metadata: Metadata = {
title: "Flexes - Devpulse",
title: "Flex - Devpulse",
description:
"Flex your coding projects and share your achievements with the Devpulse community. See what others are working on and get inspired!",
"Flex your project to the Devpulse community. Discover what other builders are shipping and get inspired.",
alternates: {
canonical: "https://devpulse.hallofcodes.org/flex",
},
Expand All @@ -27,9 +25,9 @@ export const metadata: Metadata = {
"coding inspiration",
],
openGraph: {
title: "Flexes - Devpulse",
title: "Flex - Devpulse",
description:
"Flex your coding projects and share your achievements with the Devpulse community. See what others are working on and get inspired!",
"Flex your project to the Devpulse community. Discover what other builders are shipping and get inspired.",
url: "https://devpulse.hallofcodes.org/flex",
siteName: "Devpulse",
images: [
Expand All @@ -45,9 +43,9 @@ export const metadata: Metadata = {
},
twitter: {
card: "summary_large_image",
title: "Flexes - Devpulse",
title: "Flex - Devpulse",
description:
"Flex your coding projects and share your achievements with the Devpulse community. See what others are working on and get inspired!",
"Flex your project to the Devpulse community. Discover what other builders are shipping and get inspired.",
images: [
{
url: "https://devpulse.hallofcodes.org/images/devpulse.cover.png",
Expand All @@ -57,28 +55,25 @@ export const metadata: Metadata = {
},
};

export default async function Flexs() {
export default async function FlexPage() {
const now = new Date();
const flexes = await prisma.userFlex.findMany({
where: { expires_at: { gt: new Date() } },
where: { expires_at: { gt: now } },
orderBy: { created_at: "desc" },
});

return (
<div className="min-h-screen grid-bg relative">
<div className="max-w-5xl mx-auto p-6 md:p-10 relative z-10">
<BackButton href="/" />

<div className="flex justify-center items-center gap-3 mb-8">
<Image src="/apple-touch-icon.png" alt="Devpulse Logo" width={36} height={36} />
<h1 className="text-3xl font-bold text-gray-900">Devpulse Flexes</h1>
</div>
<>
<div className="px-6 py-8 flex flex-col gap-8">
<h1 className="text-2xl sm:text-4xl font-extrabold text-gray-900 tracking-tight">
Flex
</h1>

{flexes.length === 0 && (
<div className="max-w-5xl mx-auto p-6 md:p-10 relative z-10">
<h2 className="text-2xl font-bold mb-4">No Flexes Yet</h2>
<div className="max-x-7xl mx-auto p-6 md:p-10 relative z-10">
<h2 className="text-2xl font-bold mb-4">No Project Flexes Yet</h2>
<p className="text-gray-500 mb-6">
Please come back later to see the latest flexes from our
community.
Come back later to see what the community is building.
</p>
</div>
)}
Expand Down Expand Up @@ -111,12 +106,12 @@ export default async function Flexs() {
Open Source:
</span>
<Link
href={flex.project_url}
href={flex.open_source_url}
target="_blank"
rel="noopener noreferrer"
className="text-sm block underline hover:text-green-300 transition mb-2 truncate"
>
{flex.is_open_source}
{flex.open_source_url}
</Link>
</>
)}
Expand All @@ -143,6 +138,6 @@ export default async function Flexs() {

<CTA />
<Footer />
</div>
</>
);
}
61 changes: 11 additions & 50 deletions app/(public)/join/page.tsx → app/(public)/(guest)/join/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Metadata } from "next/types";
import { prisma } from "@/app/lib/prisma";
import { getCurrentUser } from "@/app/lib/auth/user";
import JoinButton from "../../components/JoinButton";
import JoinButton from "../../../components/JoinButton";
import Footer from "@/app/components/layout/Footer";
import Image from "next/image";
import Link from "next/link";
Expand All @@ -12,6 +12,7 @@ import {
faRankingStar,
faUsers,
} from "@fortawesome/free-solid-svg-icons";
import { notFound } from "next/navigation";

type Props = {
searchParams: Promise<{ id?: string }>;
Expand Down Expand Up @@ -81,59 +82,14 @@ export default async function JoinPage({ searchParams }: Props) {
const { id } = await searchParams;
const code = (id || "").trim();

if (!code) {
return (
<div className="min-h-screen flex items-center justify-center grid-bg">
<div className="glass-card p-10 text-center max-w-md mx-auto">
<div className="w-16 h-16 rounded-full bg-blue-50 border border-blue-500/20 flex items-center justify-center mx-auto mb-5">
<FontAwesomeIcon
icon={faCircleInfo}
className="w-8 h-8 text-blue-600"
/>
</div>
<h1 className="text-xl font-bold text-gray-900 mb-2">
Join a Leaderboard
</h1>
<p className="text-gray-500 text-sm mb-6">
Open an invite link like{" "}
<span className="font-mono">/join?id=XXXXXXXX</span>.
</p>
<Link href="/" className="btn-primary inline-block px-6 py-3 text-sm">
Go to Devpulse
</Link>
</div>
</div>
);
}
if (!code) return notFound();

const [leaderboard, { user }] = await Promise.all([
getLeaderboard(code),
getCurrentUser(),
]);

if (!leaderboard) {
return (
<div className="min-h-screen flex items-center justify-center grid-bg">
<div className="glass-card p-10 text-center max-w-md mx-auto">
<div className="w-16 h-16 rounded-full bg-red-500/10 border border-red-500/20 flex items-center justify-center mx-auto mb-5">
<FontAwesomeIcon
icon={faCircleXmark}
className="w-8 h-8 text-red-600"
/>
</div>
<h1 className="text-xl font-bold text-gray-900 mb-2">
Invite Not Found
</h1>
<p className="text-gray-500 text-sm mb-6">
This invite link is invalid or has expired.
</p>
<Link href="/" className="btn-primary inline-block px-6 py-3 text-sm">
Go to Devpulse
</Link>
</div>
</div>
);
}
if (!leaderboard) return notFound();

const memberCount = await getMemberCount(leaderboard.id);

Expand All @@ -152,15 +108,20 @@ export default async function JoinPage({ searchParams }: Props) {
}

return (
<div className="min-h-screen grid-bg relative overflow-hidden">
<div className="min-h-screen relative overflow-hidden">
<div className="glow-orb w-[500px] h-[500px] bg-blue-600/20 top-[-200px] left-1/2 -translate-x-1/2 absolute" />
<div className="glow-orb w-[300px] h-[300px] bg-purple-600/15 bottom-[-100px] right-[-50px] absolute" />

<div className="relative z-10 flex flex-col items-center justify-center min-h-screen px-4 py-16">
<div className="glass-card max-w-lg w-full p-8 md:p-10 text-center">
<div className="flex justify-center mb-6">
<div className="w-16 h-16 rounded-2xl bg-blue-50 border border-blue-500/20 flex items-center justify-center shadow-[0_0_30px_rgba(99,102,241,0.15)]">
<Image src="/apple-touch-icon.png" alt="Devpulse" width={36} height={36} />
<Image
src="/apple-touch-icon.png"
alt="Devpulse"
width={36}
height={36}
/>
</div>
</div>

Expand Down
15 changes: 15 additions & 0 deletions app/(public)/(guest)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Nav from "@/app/components/layout/Nav";

export default function GuestLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen overflow-hidden relative">
<Nav />

<div className="mt-30 max-w-7xl mx-auto">{children}</div>
</div>
);
}
102 changes: 102 additions & 0 deletions app/(public)/(guest)/leaderboard/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import LeaderboardTable, {
NonNullableMember,
} from "../../../../components/leaderboard/LeaderboardTable";
import Footer from "@/app/components/layout/Footer";
import CTA from "@/app/components/common/ui/CTA";
import { notFound } from "next/navigation";
import InviteFriendsButton from "@/app/components/leaderboard/InviteFriendsButton";
import InternalServerError from "@/app/internal-server-error";
import { prisma } from "@/app/lib/prisma";
import LeaderboardStats from "@/app/components/leaderboard/LeaderboardStats";
import { getCurrentUser } from "@/app/lib/auth/user";

export async function generateStaticParams() {
const leaderboards = await prisma.leaderboard.findMany({
select: { slug: true },
});

return leaderboards.map((item) => ({ slug: item.slug }));
}

export default async function LeaderboardPage(props: {
params: Promise<{ slug: string }>;
}) {
const { user } = await getCurrentUser();
const { slug } = await props.params;

const leaderboard = await prisma.leaderboard.findUnique({
where: { slug },
});

if (!leaderboard) return notFound();

let members: NonNullableMember[] = [];
try {
const rows = await prisma.leaderboardMember.findMany({
where: { leaderboard_id: leaderboard.id },
include: {
user: {
select: {
id: true,
email: true,
role: true,
user_stats: {
select: {
total_seconds: true,
languages: true,
operating_systems: true,
editors: true,
},
},
},
},
},
});

members = rows
.filter((r) => r.user.email)
.map((r) => ({
user_id: r.user.id,
role: r.role,
email: r.user.email!,
total_seconds: Number(r.user.user_stats?.total_seconds ?? 0),
languages: (r.user.user_stats?.languages as { name: string }[]) ?? [],
operating_systems:
(r.user.user_stats?.operating_systems as { name: string }[]) ?? [],
editors: (r.user.user_stats?.editors as { name: string }[]) ?? [],
}));
} catch {
return InternalServerError();
}

return (
<>
<div className="px-6 py-8 flex flex-col gap-8">
<div className="flex flex-col gap-4">
<div>
<h1 className="text-2xl sm:text-4xl font-extrabold text-gray-900 tracking-tight">
{leaderboard.name}
</h1>
<p className="text-gray-500 mt-1 text-sm sm:text-base font-medium">
Join {leaderboard.name} to track your coding metrics, compete with
fellow developers, and showcase your engineering skills.
</p>
</div>

{user && (
<InviteFriendsButton
joinCode={leaderboard.join_code}
leaderboardName={leaderboard.name}
/>
)}
</div>

<LeaderboardStats members={members} />
<LeaderboardTable members={members} />
</div>

<CTA />
<Footer />
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,14 @@ export default async function Leaderboards() {
});

return (
<div className="min-h-screen grid-bg relative">
<div className="max-w-5xl mx-auto p-6 md:p-10 relative z-10">
<BackButton href="/leaderboards" />

<div className="flex justify-center items-center gap-3 mb-8">
<Image src="/apple-touch-icon.png" alt="Devpulse Logo" width={36} height={36} />
<h1 className="text-3xl font-bold text-gray-900">
Devpulse Leaderboards
</h1>
</div>
<>
<div className="px-6 py-8 flex flex-col gap-8">
<h1 className="text-2xl sm:text-4xl font-extrabold text-gray-900 tracking-tight">
Leaderboards
</h1>

{leaderboards.length === 0 && (
<div className="max-w-5xl mx-auto p-6 md:p-10 relative z-10">
<div className="max-x-7xl mx-auto p-6 md:p-10 relative z-10">
<h2 className="text-2xl font-bold mb-4">No Leaderboards Yet</h2>
<p className="text-gray-500 mb-6">
Please come back later to see the leaderboards from our community.
Expand All @@ -90,7 +85,7 @@ export default async function Leaderboards() {
key={board.id}
href={`/leaderboard/${board.slug}`}
className="stat-card flex justify-between items-center px-6 py-4 group bg-gray-100 hover:bg-gray-100 transition-all border border-gray-200 rounded-xl rounded-tl-sm hover:border-blue-500/30"

data-aos-delay={(i * 50).toString()}
>
<div className="flex items-center gap-3">
Expand All @@ -111,6 +106,6 @@ export default async function Leaderboards() {

<CTA />
<Footer />
</div>
</>
);
}
Loading