Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Credits Page #193 #203

Merged
merged 18 commits into from
Jan 26, 2025
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
25 changes: 6 additions & 19 deletions app/(default)/Credits/addcredit/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import {useRouter} from 'next/navigation';

export default function AddCreditPage() {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [linkedinUrl, setLinkedinUrl] = useState('');
const [githubUrl, setGithubUrl] = useState('');
const [image,setImage] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(null);
Expand All @@ -15,7 +14,7 @@ export default function AddCreditPage() {

const handleSubmit = async(e) => {
e.preventDefault();
if(!name || !description || !linkedinUrl || !image){
if(!name || !githubUrl || !image){
setError('Please fill in all fields');
return;
}
Expand All @@ -26,8 +25,7 @@ export default function AddCreditPage() {
try {
const formData = new FormData();
formData.append('name' , name);
formData.append('description' , description);
formData.append('linkedinUrl' , linkedinUrl);
formData.append('githubUrl' , githubUrl);
formData.append('image' , image);

const response = await fetch('/api/credits/new' , {
Expand Down Expand Up @@ -70,25 +68,14 @@ export default function AddCreditPage() {
/>
</div>

{/* Description */}
<div className="mb-4">
<label className="block text-slate-100 mb-2">Description</label>
<textarea
className="w-full p-2 rounded bg-gray-700 text-white"
rows="4"
value={description}
onChange={(e) => setDescription(e.target.value)}
></textarea>
</div>

{/* LinkedIn URL */}
<div className="mb-4">
<label className="block text-slate-100 mb-2">LinkedIn URL</label>
<label className="block text-slate-100 mb-2">GitHub URL</label>
<input
type="url"
className="w-full p-2 rounded bg-gray-700 text-white"
value={linkedinUrl}
onChange={(e) => setLinkedinUrl(e.target.value)}
value={githubUrl}
onChange={(e) => setGithubUrl(e.target.value)}
/>
</div>

Expand Down
68 changes: 29 additions & 39 deletions app/(default)/Credits/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,10 @@ export default function PinPage() {
const handleFormSubmit = async (e) => {
e.preventDefault();

const { name, description, linkedinUrl } = e.target.elements;
const { name, githubUrl } = e.target.elements;
const updatedCredit = {
name: name.value,
description: description.value,
linkedinUrl: linkedinUrl.value,
githubUrl: githubUrl.value,
};

try {
Expand Down Expand Up @@ -123,11 +122,10 @@ export default function PinPage() {
setShowEditIcons((prev) => !prev);
};


return (
<div className="w-full flex flex-col items-center justify-center">
<h1 className="text-3xl sm:text-4xl font-bold text-slate-100 mt-14 mb-6">
Website Contributors
Website <span className="text-green-500">Contributors</span>
</h1>

{isLoading && <p className="text-slate-100">Loading...</p>}
Expand All @@ -153,20 +151,13 @@ export default function PinPage() {
className="w-full p-2 rounded bg-gray-700 text-white"
/>
</div>

<div className="mb-4">
<label className="block mb-2 font-medium">Description</label>
<textarea
name="description"
defaultValue={selectedCredit.description}
className="w-full p-2 rounded bg-gray-700 text-white"
></textarea>
</div>
<div className="mb-4">
<label className="block mb-2 font-medium">LinkedIn URL</label>
<label className="block mb-2 font-medium">GitHub URL</label>
<input
type="url"
name="linkedinUrl"
defaultValue={selectedCredit.linkedinUrl}
defaultValue={selectedCredit.githubUrl}
className="w-full p-2 rounded bg-gray-700 text-white"
/>
</div>
Expand All @@ -190,16 +181,18 @@ export default function PinPage() {

{/* Grid container */}

<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-[10px]">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mt-[10px] items-start">
{!isLoading &&
!error &&
contributors.map((contributor) => (
<div className="relative" key={contributor._id}>
[...contributors]
.sort((a,b) => a.name.localeCompare(b.name))
.map((contributor) => (
<div
className="relative flex flex-col items-center"
key={contributor._id}
>
{/* PinContainer */}
<PinContainer
title="Visit Linkedin"
href={contributor.linkedinUrl}
>
<PinContainer title="Visit GitHub" href={contributor.githubUrl}>
<div className="flex flex-col p-4 tracking-tight text-slate-100/50 w-[20rem] h-[20rem] relative bg-gray-900 rounded-md">
{/* Image wrapper */}
<div className="relative h-full w-full">
Expand All @@ -215,9 +208,6 @@ export default function PinPage() {
<h3 className="font-bold text-base text-slate-100">
{contributor.name}
</h3>
<p className="text-base text-slate-300 mt-2">
{contributor.description}
</p>
</div>
</div>
</div>
Expand Down Expand Up @@ -255,21 +245,21 @@ export default function PinPage() {
</div>

{isAdmin && (
<div className="w-full flex justify-center mt-10">
<button
className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-500 transition"
onClick={() => router.push("/Credits/addcredit")}
>
Add Contributor
</button>
<div className="w-full flex justify-center mt-10">
<button
className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-500 transition"
onClick={() => router.push("/Credits/addcredit")}
>
Add Contributor
</button>

<button
className="bg-white text-green-600 px-4 py-2 rounded hover:bg-gray-500 transition ml-4"
onClick={toggleEditIcons}
>
Edit
</button>
</div>
<button
className="bg-white text-green-600 px-4 py-2 rounded hover:bg-gray-500 transition ml-4"
onClick={toggleEditIcons}
>
Edit
</button>
</div>
)}
</div>
);
Expand Down
8 changes: 3 additions & 5 deletions app/(default)/api/credits/new/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,10 @@ export async function POST(request: NextRequest) {

const formData = await request.formData();
const name = formData.get('name') as string;
const description = formData.get('description') as string;
const linkedinUrl = formData.get('linkedinUrl') as string;
const githubUrl = formData.get('githubUrl') as string;
const file = formData.get('image') as File;

if(!name || !description || !file || !linkedinUrl) {
if(!name || !file || !githubUrl) {
return NextResponse.json({error: 'Missing required fields'}, {status: 400});
}

Expand All @@ -47,8 +46,7 @@ export async function POST(request: NextRequest) {

const newCredit = new Credit({
name,
description,
linkedinUrl,
githubUrl,
imageUrl: secure_url,
publicId: public_id,
});
Expand Down
80 changes: 80 additions & 0 deletions app/(default)/api/credits/newCollaborator/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { NextRequest, NextResponse } from "next/server";
import connectMongoDB from "@/lib/dbConnect";
import cloudinary from 'cloudinary';
import Credit from "@/models/Credit";

cloudinary.v2.config({
cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
api_key: process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY,
api_secret: process.env.NEXT_PUBLIC_CLOUDINARY_API_SECRET,
});

function uploadToCloudinary(fileBuffer: Buffer): Promise<{ secure_url: string; public_id: string }> {
return new Promise((resolve, reject) => {
const uploadStream = cloudinary.v2.uploader.upload_stream(
{ folder: "credits" },
(error, result) => {
if (error) return reject(error);
if (!result) return reject(new Error("Cloudinary upload failed"));
resolve({
secure_url: result.secure_url,
public_id: result.public_id,
});
}
);
uploadStream.end(fileBuffer); // Write buffer data and end the stream
});
}

export async function POST(request: NextRequest) {
try{
await connectMongoDB();

const repoUrl = "https://api.github.com/repos/pbdsce/PB_Website/contributors";

const contributorsResponse = await fetch(repoUrl);
if(!contributorsResponse.ok) {
return NextResponse.json({error: 'Failed to fetch contributors'}, {status: contributorsResponse.status})
}

const contributors = await contributorsResponse.json();

const newCredits = [];

for(const contributor of contributors) {
const { id , name , login , avatar_url , html_url } = contributor;
const contributorName = name || login;

const existingCredit = await Credit.findOne({name: login});
if(existingCredit) {
continue;
}

const avatarResponse = await fetch(avatar_url);
if(!avatarResponse.ok) {
throw new Error(`Failed to fetch avatar for ${contributorName}`);
}

const avatarBuffer = Buffer.from(await avatarResponse.arrayBuffer());

const { secure_url , public_id } = await uploadToCloudinary(avatarBuffer);

const newCredit = new Credit({
userId: id,
name: contributorName,
githubUrl: html_url,
imageUrl: secure_url,
publicId: public_id,
});

await newCredit.save();

newCredits.push(newCredit);
}

return NextResponse.json({success: true, newCredits}, {status: 201});
} catch (error) {
console.log("Error creating credits:", error);
return NextResponse.json({error: 'Failed to create credits'}, {status: 500});
}
}
47 changes: 41 additions & 6 deletions app/(default)/api/hustle/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,30 @@ export async function POST() {
process.env.VJUDGE_CONTEST_API ||
"https://vjudge.net/contest/data?draw=2&start=0&length=20&sortDir=desc&sortCol=0&category=mine&running=3&title=&owner=Pbhustle&_=1733642420751";

const { data } = await axios.get(API_URL);
// Add headers to mimic a browser request
const headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.9',
'Referer': 'https://vjudge.net/',
'Origin': 'https://vjudge.net',
'Cookie': process.env.VJUDGE_COOKIE || 'JSESSlONID=7YP9VCUCK4ZTTTNQO0U0OQFW6ZDHMJXV; _ga=GA1.1.533089565.1728634731; Jax.Q=04yash|PMVRLFQGIPE5GOTC1SGE6B4W785LQJ; __gads=ID=99f1126d6fe46914:T=1728634731:RT=1737718428:S=ALNI_Mb_KPRQ0uquyzQSN44fpJKwUo53GA; __gpi=UID=00000f3e7503e3cd:T=1728634731:RT=1737718428:S=ALNI_Mb0ueGqLg2jRi_frTwTAZsUQqg_wQ; __eoi=ID=9732350f6625bc1b:T=1728634731:RT=1737718428:S=AA-AfjZnX0ZhSiL1BHBPEMWVFpeh; cf_clearance=rHe4M.4i47txtgYJQPXs0a.LnH.QYSRELQUQKvBeHv0-1737728607-1.2.1.1-QS6DaWVn4ZlR0CDzelDl644mlMOCZU1ty_NqdHf68O5PjZEOniNFWXUEEk3GrVQj0CwVlOScM6DpclcDaZKTv39_vxVd.tdzDDdfI3hBk.BKBOopC_pgojfmWDpHxvMqGBJbqfcoq36wQJQchRX4B0.IUalcf8OnqtyKwFa6mOj1a1oNoBst3jz_nVLMzWjzzQsxtJmhLlSGveAigCmovzVeHuJsXGSGifWipcLBKO2U_QCnzDVWlZ5N0Rqs7qlOPhzT9xmvceNQGlIZLoINYpq5_WyfCbumeC6XsX0i.H4; FCNEC=%5B%5B%22AKsRol-aSOdWtkNjxLPUwAZiyc5kmxDI81NA-AWYxiD_dMfHxJZ0hX5MBVRm9H0Pb0bLbRu7vmWOG3ZJAKwynbFz-3CLj98y_Ps-u7uC3PX4myF02jFz23muu0K9r3Xot0YQRKs-gKcJoQetbuaLAVrOLTIdtXKr4w%3D%3D%22%5D%5D; JSESSIONID=E55418E82CB952060BE04F7A459FD1FF; _ga_374JLX1715=GS1.1.1737728605.51.1.1737729138.60.0.0',
};

const { data } = await axios.get(API_URL, { headers });

if (!data || !data.data || !data.data[0]) {
console.error("Invalid response format from VJudge");
return NextResponse.json({
error: "Invalid response from VJudge",
details: "No contest data available"
}, { status: 400 });
}

const ccode = data.data[0][0];
const { data: rankData } = await axios.get(
`https://vjudge.net/contest/rank/single/${ccode}`
`https://vjudge.net/contest/rank/single/${ccode}`,
{ headers }
);

const leaderboardDoc = await LeaderboardModel.findOne({
Expand Down Expand Up @@ -113,8 +133,19 @@ export async function POST() {
message: "Leaderboard updated successfully",
rankings: leaderboardRankings,
});
} catch (error) {
return NextResponse.json({ error: "Failed to update leaderboard" });
} catch (error: any) {
if (axios.isAxiosError(error)) {
console.error("API Error:", error.response?.data || error.message);
} else {
console.error("API Error:", error);
}
return NextResponse.json({
error: "Failed to update leaderboard",
details: error.response?.data || error.message,
status: error.response?.status || 500
}, {
status: error.response?.status || 500
});
}
}

Expand All @@ -133,7 +164,11 @@ export async function GET() {
leaderboard: leaderboardDoc,
},
});
} catch (error) {
return NextResponse.json({ error: "Failed to fetch hustle data" });
} catch (error: any) {
console.error("Database error:", error);
return NextResponse.json({
error: "Failed to fetch hustle data",
details: error.message
}, { status: 500 });
}
}
2 changes: 0 additions & 2 deletions app/(default)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import SparklesText from "@/components/magicui/sparkles-text";
import Achievements from '@/components/achievements';
import Founder from "@/components/founder";
import Share from "@/components/share";
import CreditComp from "@/components/CreditComp";

export default function Home() {
return (
Expand Down Expand Up @@ -52,7 +51,6 @@ export default function Home() {
<Founder />
<Achievements />
<Share />
<CreditComp />
</>
);
}
Loading
Loading