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
53 changes: 53 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Build CI

on:
pull_request:
branches: ["master"]
push:
branches: ["master"]
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest

services:
mysql:
image: mysql:8
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: devpulse
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h localhost"
--health-interval=10s
--health-timeout=5s
--health-retries=5

env:
DATABASE_URL: "mysql://root:root@localhost:3306/devpulse"

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: "26"

- name: Install dependencies
run: npm ci

- name: Generate Prisma client
run: npx prisma generate

- name: Run migrations
run: npx prisma migrate deploy

- name: Typecheck
run: npx tsc --noEmit

- name: Build
run: npm run build
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { redirect } from "next/navigation";

export const metadata: Metadata = {
title: "Verify Email - Devpulse",
description: "Verify your email address to activate your Devpulse account.",
};

export default async function VerifyEmailPage() {
Expand Down
33 changes: 33 additions & 0 deletions app/(auth)/verify-wakatime/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Metadata } from "next/types";
import { Suspense } from "react";
import { auth } from "@/app/lib/auth";
import { redirect } from "next/navigation";
import VerifyWakatime from "@/app/components/auth/VerifyWakatime";

export const metadata: Metadata = {
title: "Verify Wakatime - Devpulse",
};

export default async function VerifyWakatimePage() {
const session = await auth();

if (!session) {
return redirect("/login");
}

if (session.user.wakatime_api_key) {
return redirect("/");
}

return (
<Suspense
fallback={
<div className="min-h-screen flex items-center justify-center">
Loading...
</div>
}
>
<VerifyWakatime />
</Suspense>
);
}
4 changes: 2 additions & 2 deletions app/(public)/(auth)/reset-password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const metadata: Metadata = {

export default async function ResetPassword() {
return (
<div className="min-h-screen flex relative">
<div className="min-h-screen flex grid-bg 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 */}
Expand Down Expand Up @@ -88,7 +88,7 @@ export default async function ResetPassword() {
<span className="text-purple-600 mr-2">const</span>
<span className="text-blue-600">dev</span>
<span className="text-gray-700 mx-2">=</span>
<span className="text-indigo-600">getAccount</span>
<span className="text-blue-600">getAccount</span>
<span className="text-gray-700">(</span>
<span className="text-yellow-700">this</span>
<span className="text-gray-700">);</span>
Expand Down
27 changes: 14 additions & 13 deletions app/(public)/flex/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExternalLink } from "@fortawesome/free-solid-svg-icons";
import { Metadata } from "next/types";
import { prisma } from "@/app/lib/prisma";
import Link from "next/link";

export const metadata: Metadata = {
title: "Flexes - Devpulse",
Expand Down Expand Up @@ -58,8 +59,8 @@ export const metadata: Metadata = {

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

return (
Expand Down Expand Up @@ -91,40 +92,40 @@ export default async function Flexs() {
>
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold mb-2">
{flex.projectName}
{flex.project_name}
</h3>
<span className="text-sm">
{timeAgo(flex.createdAt.toISOString())}
{timeAgo(flex.created_at.toISOString())}
</span>
</div>
<div className="text-sm text-gray-500 mb-4">
{flex.projectTime}
{flex.project_time}
</div>
<span className="font-bold text-xs text-gray-500">
Description:
</span>
<p className="text-gray-500 mb-2">{flex.projectDescription}</p>
{flex.isOpenSource && (
<p className="text-gray-500 mb-2">{flex.project_description}</p>
{flex.is_open_source && (
<>
<span className="font-bold text-xs text-gray-500">
Open Source:
</span>
<a
href={flex.openSourceUrl}
<Link
href={flex.project_url}
target="_blank"
rel="noopener noreferrer"
className="text-sm block underline hover:text-green-300 transition mb-2 truncate"
>
{flex.openSourceUrl}
</a>
{flex.is_open_source}
</Link>
</>
)}
<div className="flex items-center justify-between mt-4">
<p className="text-sm text-gray-500">
Posted by {flex.userEmail.split("@")[0]}
Posted by {flex.user_email.split("@")[0]}
</p>
<a
href={flex.projectUrl}
href={flex.project_url}
target="_blank"
rel="noopener noreferrer"
>
Expand Down
2 changes: 1 addition & 1 deletion app/(public)/join/[code]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { code } = await params;

const leaderboard = await prisma.leaderboard.findUnique({
where: { joinCode: code },
where: { join_code: code },
select: { name: true, description: true },
});

Expand Down
30 changes: 16 additions & 14 deletions app/(public)/join/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,22 @@ type Props = {

async function getLeaderboard(code: string) {
return prisma.leaderboard.findUnique({
where: { joinCode: code },
where: { join_code: code },
select: {
id: true,
name: true,
description: true,
slug: true,
ownerId: true,
createdAt: true,
owner_id: true,
created_at: true,
},
});
}

async function getMemberCount(leaderboardId: string) {
return prisma.leaderboardMember.count({ where: { leaderboardId } });
return prisma.leaderboardMember.count({
where: { leaderboard_id: leaderboardId },
});
}

export async function generateMetadata({
Expand Down Expand Up @@ -83,10 +85,10 @@ export default async function JoinPage({ searchParams }: Props) {
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-indigo-50 border border-indigo-500/20 flex items-center justify-center mx-auto mb-5">
<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-indigo-600"
className="w-8 h-8 text-blue-600"
/>
</div>
<h1 className="text-xl font-bold text-gray-900 mb-2">
Expand Down Expand Up @@ -139,9 +141,9 @@ export default async function JoinPage({ searchParams }: Props) {
if (user) {
const membership = await prisma.leaderboardMember.findUnique({
where: {
leaderboardId_userId: {
leaderboardId: leaderboard.id,
userId: user.id,
leaderboard_id_user_id: {
leaderboard_id: leaderboard.id,
user_id: user.id,
},
},
select: { id: true },
Expand All @@ -151,18 +153,18 @@ export default async function JoinPage({ searchParams }: Props) {

return (
<div className="min-h-screen grid-bg relative overflow-hidden">
<div className="glow-orb w-[500px] h-[500px] bg-indigo-600/20 top-[-200px] left-1/2 -translate-x-1/2 absolute" />
<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-indigo-50 border border-indigo-500/20 flex items-center justify-center shadow-[0_0_30px_rgba(99,102,241,0.15)]">
<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="/logo.svg" alt="Devpulse" width={36} height={36} />
</div>
</div>

<p className="text-xs uppercase tracking-[0.2em] text-indigo-600 font-semibold mb-3">
<p className="text-xs uppercase tracking-[0.2em] text-blue-600 font-semibold mb-3">
{alreadyMember
? "You’re already a member of"
: "You’ve been invited to"}
Expand All @@ -182,7 +184,7 @@ export default async function JoinPage({ searchParams }: Props) {
<div className="flex items-center gap-2 text-gray-500 text-sm">
<FontAwesomeIcon
icon={faUsers}
className="w-4 h-4 text-indigo-600"
className="w-4 h-4 text-blue-600"
/>
<span>
{memberCount} {memberCount === 1 ? "member" : "members"}
Expand All @@ -209,7 +211,7 @@ export default async function JoinPage({ searchParams }: Props) {
Powered by{" "}
<Link
href="/"
className="text-indigo-600/70 hover:text-indigo-600 transition-colors"
className="text-blue-600/70 hover:text-blue-600 transition-colors"
>
Devpulse
</Link>{" "}
Expand Down
20 changes: 10 additions & 10 deletions app/(public)/leaderboard/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,18 @@ export default async function LeaderboardPage(props: {
let members: NonNullableMember[] = [];
try {
const rows = await prisma.leaderboardMember.findMany({
where: { leaderboardId: leaderboard.id },
where: { leaderboard_id: leaderboard.id },
include: {
user: {
select: {
id: true,
email: true,
role: true,
userStats: {
user_stats: {
select: {
totalSeconds: true,
total_seconds: true,
languages: true,
operatingSystems: true,
operating_systems: true,
editors: true,
},
},
Expand All @@ -56,14 +56,14 @@ export default async function LeaderboardPage(props: {
members = rows
.filter((r) => r.user.email)
.map((r) => ({
user_id: r.userId,
user_id: r.user.id,
role: r.role,
email: r.user.email!,
total_seconds: Number(r.user.userStats?.totalSeconds ?? 0),
languages: (r.user.userStats?.languages as { name: string }[]) ?? [],
total_seconds: Number(r.user.user_stats?.total_seconds ?? 0),
languages: (r.user.user_stats?.languages as { name: string }[]) ?? [],
operating_systems:
(r.user.userStats?.operatingSystems as { name: string }[]) ?? [],
editors: (r.user.userStats?.editors as { name: string }[]) ?? [],
(r.user.user_stats?.operating_systems as { name: string }[]) ?? [],
editors: (r.user.user_stats?.editors as { name: string }[]) ?? [],
}));
} catch {
return InternalServerError();
Expand Down Expand Up @@ -112,7 +112,7 @@ export default async function LeaderboardPage(props: {

<div className="mb-2 sm:mb-3 shrink-0 scale-90 sm:scale-95 origin-bottom-right">
<InviteFriendsButton
joinCode={leaderboard.joinCode}
joinCode={leaderboard.join_code}
leaderboardName={leaderboard.name}
/>
</div>
Expand Down
10 changes: 5 additions & 5 deletions app/(public)/leaderboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const metadata: Metadata = {
export default async function Leaderboards() {
const leaderboards = await prisma.leaderboard.findMany({
select: { id: true, name: true, slug: true },
orderBy: { createdAt: "desc" },
orderBy: { created_at: "desc" },
});

return (
Expand Down Expand Up @@ -89,17 +89,17 @@ export default async function Leaderboards() {
<a
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-indigo-500/30"
data-aos="fade-up"
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">
<div className="w-2 h-2 rounded-full bg-indigo-500 group-hover:shadow-[0_0_10px_rgba(99,102,241,0.8)] transition-all" />
<div className="w-2 h-2 rounded-full bg-blue-500 group-hover:shadow-[0_0_10px_rgba(99,102,241,0.8)] transition-all" />
<span className="text-gray-700 font-semibold group-hover:text-gray-900 transition">
{board.name}
</span>
</div>
<span className="text-gray-500 text-sm group-hover:text-indigo-600 transition flex items-center gap-2">
<span className="text-gray-500 text-sm group-hover:text-blue-600 transition flex items-center gap-2">
View{" "}
<FontAwesomeIcon icon={faArrowRight} className="w-4 h-4" />
</span>
Expand Down
12 changes: 6 additions & 6 deletions app/api/admin/stats/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,22 @@ export async function GET() {
await Promise.all([
prisma.userStats.findMany({
select: {
userId: true,
totalSeconds: true,
user_id: true,
total_seconds: true,
categories: true,
user: { select: { email: true } },
},
}),
prisma.conversation.count(),
prisma.message.count({ where: { expiresAt: { gt: new Date() } } }),
prisma.message.count({ where: { expires_at: { gt: new Date() } } }),
prisma.leaderboard.count(),
prisma.userFlex.count({ where: { expiresAt: { gt: new Date() } } }),
prisma.userFlex.count({ where: { expires_at: { gt: new Date() } } }),
]);

const users = topUserStats.map((row) => ({
user_id: row.userId,
user_id: row.user_id,
email: row.user.email,
total_seconds: Number(row.totalSeconds),
total_seconds: Number(row.total_seconds),
categories: row.categories,
}));

Expand Down
Loading