Skip to content
Merged
20 changes: 10 additions & 10 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ datasource db {
url = env("DATABASE_URL")
}

enum Difficulty {
EASY
MEDIUM
HARD
}

model Account {
id String @id @default(cuid())
userId String
Expand Down Expand Up @@ -46,8 +40,8 @@ model User {
email String? @unique
emailVerified DateTime?
image String?
leetcodeUser String?
role String @default("MEMBER")
leetcodeUser String?
accounts Account[]
sessions Session[]
solvedProblems Problem[] @relation("SolvedProblems")
Expand All @@ -73,11 +67,17 @@ model Week {
}

model Problem {
id String @id @default(cuid())
id String @id @default(cuid())
name String
level Difficulty
leetcodeUrl String
weekId String
week Week @relation(fields: [weekId], references: [id], onDelete: Cascade)
solvedBy User[] @relation("SolvedProblems")
week Week @relation(fields: [weekId], references: [id], onDelete: Cascade)
solvedBy User[] @relation("SolvedProblems")
}

enum Difficulty {
EASY
MEDIUM
HARD
}
22 changes: 22 additions & 0 deletions src/app/(pages)/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

import Subtitle from "~/app/_components/subtitle";
import Title from "~/app/_components/title";
import ProfileCard from "~/app/_components/Profile/ProfileCard";
import { auth } from "~/server/auth";
import { api } from "~/trpc/server";

const Profile = async () => {

const session = await auth();
if (!session) {
return <div>You must be logged in to view your profile.</div>;
}
return (
<div>
<Title label="Profile" />
<ProfileCard idUser={session.user.id ?? "NA"} />
</div>
)
}

export default Profile;
109 changes: 109 additions & 0 deletions src/app/_components/Profile/ProfileCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"use client";
import { cn } from "utils/merge";
import { FaUser } from "react-icons/fa6";
import { IoMdSend } from "react-icons/io";
import { useState } from "react";
import { api } from "~/trpc/react";
import { AiOutlineLoading } from "react-icons/ai";
import Link from "next/link";

interface ProfileCardProps {
idUser: string;
name?: string | null;
image?: string | null;
}

export default function ProfileCard({
idUser,
name = "No Name",
image,
}: ProfileCardProps) {
const updateUser = api.profile.update.useMutation();
const handleSend = async() =>{
refetch();
await updateUser.mutateAsync({id:idUser , leetcodeUser: NewLeetcodeUser});
}
const [NewLeetcodeUser, setInputValue] = useState("");
const { data, isLoading, error, refetch, isRefetching } = api.profile.getById.useQuery(idUser);
if (isLoading || error || !data) {
return (
<div className="p-6 w-80 bg-gray-700 rounded-xl text-center text-white">
Loading…
</div>
);
}

const { leetcodeUser } = data;

return (
<div
className={cn(
"w-96 bg-gradient-to-br from-gray-800 to-gray-900",
"p-6 rounded-2xl shadow-2xl text-white flex flex-col gap-4"
)}
>
{/* Encabezado: foto y nombre */}
<div className="flex items-center gap-4">
{image ? (
<img
src={image}
alt={`${name}'s profile`}
className="w-20 h-20 rounded-full border-2 border-accent object-cover"
/>
) : (
<FaUser className="w-20 h-20 text-accent" />
)}
<div>
<h2 className="text-2xl font-bold leading-snug">{name}</h2>
<p className="text-sm text-gray-400">{leetcodeUser ?? "No user"}</p>
</div>
</div>

<hr className="border-gray-600" />

{/* Formulario para cambiar LeetCode user */}
<div className="flex items-center gap-2">
<input
type="text"
placeholder="New LeetCode Handle"
value={NewLeetcodeUser}
onChange={(e) => setInputValue(e.target.value)}
className="flex-1 bg-gray-700 placeholder-gray-500 text-white px-4 py-2 rounded-lg focus:ring-2 focus:ring-accent outline-none transition"
/>
<button
onClick={async () => {
await updateUser.mutateAsync({
id: idUser,
leetcodeUser: NewLeetcodeUser,
});
refetch();
}}
disabled={isRefetching}
className={cn(
"p-3 rounded-full",
isRefetching
? "bg-gray-600 cursor-not-allowed"
: "bg-accent hover:bg-accent-dark transition"
)}
>
{isRefetching ? (
<AiOutlineLoading className="animate-spin text-xl" />
) : (
<IoMdSend className="text-xl text-white" />
)}
</button>
</div>

{/* Botón de cierre de sesión */}
<div className="gap-2">
<Link
href="/api/auth/signout"
className="block w-full bg-red-600 hover:bg-red-700 text-white font-semibold py-2 rounded-lg transition text-center"
>
Sign out
</Link>
</div>

</div>
);
}
87 changes: 87 additions & 0 deletions src/app/_components/nav/UserMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// src/app/_components/nav/UserMenu.tsx
"use client";

import { useState, useEffect } from "react";
import Link from "next/link"; // ← 1) importamos Link
import { useSession, signIn } from "next-auth/react";
import { cn } from "utils/merge";
import ProfileCard from "../Profile/ProfileCard";

export default function UserMenu() {
const { data: session, status } = useSession();
const loading = status === "loading";
const [isOpen, setIsOpen] = useState(false);

useEffect(() => {
const onKey = (e: KeyboardEvent) => e.key === "Escape" && setIsOpen(false);
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);

if (loading) {
return <span className="text-primary-foreground">Loading...</span>;
}

if (!session) {
return (
<Link
href="/api/auth/signin"
className="text-primary-foreground font-code text-sm hover:underline"
>
Sign in
</Link>
);
}

const { user } = session;

return (
<div className="relative">
<button
onClick={() => setIsOpen((v) => !v)}
className={cn(
"flex items-center gap-2 font-code text-sm",
"text-primary-foreground hover:underline"
)}
>
{user.image && (
<img
src={user.image}
alt={user.name ?? ""}
className="w-6 h-6 rounded-full object-cover"
/>
)}
<span>{user.name}</span>
</button>

{isOpen && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
role="dialog"
aria-modal="true"
onClick={() => setIsOpen(false)}
>
<div
className="bg-primary-light text-white rounded-xl p-6 shadow-lg w-120"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={() => setIsOpen(false)}
aria-label="Close profile"
className="absolute top-2 right-3 text-2xl leading-none"
>
&times;
</button>

{/* ProfileCard */}
<ProfileCard
idUser={user.id}
name={user.name}
image={user.image}
/>
</div>
</div>
)}
</div>
);
}
11 changes: 2 additions & 9 deletions src/app/_components/nav/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Image from "next/image"
import { auth } from "~/server/auth";
import logo from "../../../../public/images/Logo.png"
import NavElement from "./navElement";
import UserMenu from "./UserMenu";

const Navbar = async () => {
const session = await auth();
Expand All @@ -18,15 +19,7 @@ const Navbar = async () => {
<NavElement href="/resources" label="Resources" />
</div>
<div className="flex flex-row items-center justify-center gap-4">
<p className="text-primary-foreground font-code font-normal text-sm hover:cursor-pointer hover:underline">
{session && <span> {session.user?.name}</span>}
</p>
<Link
href={session ? "/api/auth/signout" : "/api/auth/signin"}
className="text-primary-foreground font-code font-normal text-sm hover:cursor-pointer hover:underline"
>
{session ? "Sign out" : "Sign in"}
</Link>
<UserMenu />
</div>
</div>
)
Expand Down
14 changes: 12 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,21 @@ export default function RootLayout({
return (
<html lang="en" className={`${GeistSans.variable}`}>
<body className="bg-primary min-h-screen">
<Navbar />
<SessionProvider>
<TRPCReactProvider>
<Navbar />
<div className="px-20 py-10 h-screen">
{children}
</div>
<Footer />
</TRPCReactProvider>
</SessionProvider>

{/* <Navbar />
<div className="h-auto px-20 py-10 ">
<SessionProvider><TRPCReactProvider>{children}</TRPCReactProvider></SessionProvider>
</div>
<Footer />
<Footer /> */}
</body>
</html>
);
Expand Down
2 changes: 2 additions & 0 deletions src/server/api/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { leetcodeRouter } from "~/server/api/routers/leetcode";
import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
import { weekRouter } from "./routers/week";
import { problemRouter } from "./routers/problem";
import { profileRouter } from "./routers/profile";
import { leaderboardRouter } from "./routers/leaderboard";

/**
Expand All @@ -13,6 +14,7 @@ export const appRouter = createTRPCRouter({
leetcode: leetcodeRouter,
week: weekRouter,
problem : problemRouter,
profile : profileRouter,
leaderboard: leaderboardRouter,
});

Expand Down
18 changes: 18 additions & 0 deletions src/server/api/routers/profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
// import { Prisma } from "@prisma/client";

export const profileRouter = createTRPCRouter({
getById: protectedProcedure.input(z.string()).query(async ({ ctx, input }) => {
return await ctx.db.user.findUnique({ where: { id: input }, include: { accounts: true, sessions: false, solvedProblems: false } });
}),
update: protectedProcedure
.input(z.object({
id: z.string(),
leetcodeUser: z.string()
}))
.mutation(async ({ ctx, input }) => {
const { id, ...data } = input;
return await ctx.db.user.update({ where: { id }, data });
}),
});
Loading