Skip to content
Open
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
189 changes: 189 additions & 0 deletions scripts/generate-unclaimed-colleges-csv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
#!/usr/bin/env tsx

/**
* Unclaimed Colleges CSV Generator Script
*
* This script generates a CSV file containing all unclaimed college/university profiles
* (schools with no associated coaches) along with their claim links.
*
* Usage:
* npx tsx scripts/generate-unclaimed-colleges-csv.ts [--base-url=https://evalgaming.com] [--output=unclaimed-colleges.csv]
*
* Options:
* --base-url Base URL for generating profile and claim links (default: https://evalgaming.com)
* --output Output file path (default: unclaimed-colleges-{date}.csv)
*/

// Load environment variables from .env file
import dotenv from "dotenv";
dotenv.config();

import { writeFileSync } from "fs";
import { db } from "../src/server/db";

// Colors for console output
const colors = {
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
reset: "\x1b[0m",
cyan: "\x1b[36m",
magenta: "\x1b[35m",
};

const log = {
info: (msg: string) => console.log(`${colors.blue}ℹ️ ${msg}${colors.reset}`),
success: (msg: string) =>
console.log(`${colors.green}✅ ${msg}${colors.reset}`),
warning: (msg: string) =>
console.log(`${colors.yellow}⚠️ ${msg}${colors.reset}`),
error: (msg: string) => console.log(`${colors.red}❌ ${msg}${colors.reset}`),
step: (msg: string) => console.log(`${colors.cyan}🔄 ${msg}${colors.reset}`),
data: (msg: string) =>
console.log(`${colors.magenta}📄 ${msg}${colors.reset}`),
};

// Parse command line arguments
function parseArgs(): { baseUrl: string; output: string } {
const args = process.argv.slice(2);
let baseUrl = "https://evalgaming.com";
let output = `unclaimed-colleges-${new Date().toISOString().split("T")[0]}.csv`;

for (const arg of args) {
if (arg.startsWith("--base-url=")) {
baseUrl = arg.split("=")[1] ?? baseUrl;
} else if (arg.startsWith("--output=")) {
output = arg.split("=")[1] ?? output;
}
}

return { baseUrl, output };
}

// Escape CSV field values
function escapeCSVField(value: string | null | undefined): string {
if (!value) return "";
// If the value contains comma, newline, or quote, wrap in quotes and escape internal quotes
if (value.includes(",") || value.includes("\n") || value.includes('"')) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
}

async function main() {
const { baseUrl, output } = parseArgs();

log.info("Unclaimed Colleges CSV Generator");
log.info("================================");
log.data(`Base URL: ${baseUrl}`);
log.data(`Output file: ${output}`);
console.log("");

try {
log.step("Connecting to database...");

// Fetch all unclaimed colleges/universities
log.step("Fetching unclaimed college profiles...");
const schools = await db.school.findMany({
where: {
type: {
in: ["COLLEGE", "UNIVERSITY"],
},
coaches: {
none: {},
},
},
select: {
id: true,
name: true,
type: true,
location: true,
state: true,
region: true,
country: true,
website: true,
},
orderBy: [{ state: "asc" }, { name: "asc" }],
});

if (schools.length === 0) {
log.warning("No unclaimed colleges found!");
await db.$disconnect();
return;
}

log.success(`Found ${schools.length} unclaimed college profiles`);

// Generate CSV content
log.step("Generating CSV content...");

const headers = [
"School Name",
"School Type",
"Location",
"State",
"Region",
"Country",
"Website",
"Profile URL",
"Claim Link",
];

const rows = schools.map((school) => {
const profileUrl = `${baseUrl}/profiles/school/${school.id}`;
const claimLink = `${baseUrl}/onboarding/coach?schoolId=${school.id}&schoolName=${encodeURIComponent(school.name)}`;

return [
escapeCSVField(school.name),
school.type,
escapeCSVField(school.location),
escapeCSVField(school.state),
escapeCSVField(school.region),
escapeCSVField(school.country ?? "USA"),
escapeCSVField(school.website),
profileUrl,
claimLink,
].join(",");
});

const csvContent = [headers.join(","), ...rows].join("\n");

// Write CSV file
log.step(`Writing CSV file to ${output}...`);
writeFileSync(output, csvContent, "utf-8");

log.success(`CSV file generated successfully!`);
console.log("");
log.info("Summary:");
log.data(` - Total unclaimed colleges: ${schools.length}`);
log.data(` - Output file: ${output}`);
log.data(` - File size: ${(csvContent.length / 1024).toFixed(2)} KB`);

// Print some sample data
console.log("");
log.info("Sample entries (first 5):");
schools.slice(0, 5).forEach((school, i) => {
log.data(
` ${i + 1}. ${school.name} (${school.state || "Unknown State"})`,
);
});

// Cleanup
await db.$disconnect();

log.success("Script completed successfully!");
} catch (error) {
log.error(`Error generating CSV: ${error instanceof Error ? error.message : "Unknown error"}`);
console.error(error);
await db.$disconnect();
process.exit(1);
}
}

// Run the script
main().catch((error) => {
log.error(`Unhandled error: ${error instanceof Error ? error.message : "Unknown error"}`);
console.error(error);
process.exit(1);
});
138 changes: 138 additions & 0 deletions src/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { useState } from "react";
import {
Card,
CardContent,
Expand All @@ -23,8 +24,12 @@ import {
ClipboardList,
Crown,
AlertCircle,
Download,
School,
Loader2,
} from "lucide-react";
import { api } from "@/trpc/react";
import { toast } from "sonner";

const adminTools = [
{
Expand Down Expand Up @@ -80,6 +85,8 @@ const adminTools = [
];

export default function AdminDashboard() {
const [isDownloadingCSV, setIsDownloadingCSV] = useState(false);

// Fetch pending counts
const { data: pendingSchoolRequests } =
api.schoolAssociationRequests.getPendingCount.useQuery();
Expand All @@ -88,11 +95,91 @@ export default function AdminDashboard() {
const { data: pendingLeagueSchoolCreationRequests } =
api.leagueSchoolCreationRequests.getPendingCount.useQuery();

// Fetch unclaimed colleges count
const { data: unclaimedCollegesData } =
api.adminDirectory.getUnclaimedColleges.useQuery({
limit: 1,
offset: 0,
});

// CSV generation query (only fetch when download is triggered)
const csvQuery = api.adminDirectory.getUnclaimedCollegesCSV.useQuery(
{
baseUrl:
typeof window !== "undefined" ? window.location.origin : "https://evalgaming.com",
},
{
enabled: false, // Only fetch manually
},
);

const totalPending =
(pendingSchoolRequests ?? 0) +
(pendingLeagueRequests ?? 0) +
(pendingLeagueSchoolCreationRequests ?? 0);

const handleDownloadCSV = async () => {
setIsDownloadingCSV(true);
try {
const result = await csvQuery.refetch();

if (!result.data?.data || result.data.data.length === 0) {
toast.error("No unclaimed colleges found");
return;
}

// Generate CSV content
const headers = [
"School Name",
"School Type",
"Location",
"State",
"Region",
"Country",
"Website",
"Profile URL",
"Claim Link",
];

const rows = result.data.data.map((school) => [
`"${school.schoolName.replace(/"/g, '""')}"`,
school.schoolType,
`"${school.location.replace(/"/g, '""')}"`,
school.state,
school.region,
school.country,
school.website,
school.profileUrl,
school.claimLink,
]);

const csvContent = [headers.join(","), ...rows.map((row) => row.join(","))].join(
"\n",
);

// Create and download the file
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.setAttribute(
"download",
`unclaimed-colleges-${new Date().toISOString().split("T")[0]}.csv`,
);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);

toast.success(`Downloaded ${result.data.data.length} unclaimed college profiles`);
} catch (error) {
console.error("Error downloading CSV:", error);
toast.error("Failed to download CSV");
} finally {
setIsDownloadingCSV(false);
}
};

return (
<div className="space-y-8">
<div className="flex items-center space-x-2">
Expand Down Expand Up @@ -236,6 +323,57 @@ export default function AdminDashboard() {
</CardContent>
</Card>

{/* Unclaimed Colleges Section */}
<Card className="border-green-600/50 bg-gradient-to-r from-green-900/20 to-emerald-900/20">
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<School className="h-6 w-6 text-green-400" />
<CardTitle className="text-white">
Coach Recruitment Tools
</CardTitle>
</div>
{unclaimedCollegesData && (
<Badge
variant="outline"
className="border-green-500 bg-green-500/20 text-green-400"
>
{unclaimedCollegesData.total} Unclaimed
</Badge>
)}
</div>
<CardDescription className="text-gray-300">
Download CSV of unclaimed college profiles with claim links for coach outreach
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="rounded-lg border border-green-500/20 bg-green-900/20 p-4">
<p className="text-sm text-gray-300">
Generate a CSV file containing all college and university profiles without associated coaches.
Each row includes the school name, location, profile URL, and a unique claim link that coaches
can use to sign up and request association with their school.
</p>
</div>
<Button
onClick={handleDownloadCSV}
disabled={isDownloadingCSV}
className="w-full bg-gradient-to-r from-green-500 to-emerald-600 text-white hover:from-green-600 hover:to-emerald-700"
>
{isDownloadingCSV ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating CSV...
</>
) : (
<>
<Download className="mr-2 h-4 w-4" />
Download Unclaimed Colleges CSV
</>
)}
</Button>
</CardContent>
</Card>

<Card className="border-gray-700 bg-gray-800">
<CardHeader>
<div className="flex items-center space-x-2">
Expand Down
29 changes: 29 additions & 0 deletions src/app/onboarding/coach/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "Claim School Profile - EVAL Gaming",
description:
"Claim your school's esports profile on EVAL Gaming. Sign up as a coach to manage tryouts, recruit players, and build your competitive gaming program.",
keywords: [
"claim school",
"esports coach",
"college esports",
"school profile",
"coach registration",
"EVAL Gaming",
],
openGraph: {
title: "Claim School Profile - EVAL Gaming",
description:
"Claim your school's esports profile on EVAL Gaming. Sign up as a coach to manage tryouts, recruit players, and build your competitive gaming program.",
type: "website",
},
};

export default function CoachOnboardingLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}
Loading