feat: add icon support for sports with file upload and active seasons display

This commit is contained in:
Chris Parsons 2025-10-13 10:30:47 -07:00
parent 6814a82b76
commit c343521706
10 changed files with 1176 additions and 2 deletions

View file

@ -6,6 +6,10 @@ export type Sport = typeof schema.sports.$inferSelect;
export type NewSport = typeof schema.sports.$inferInsert;
export type SportType = "team" | "individual";
export type SportWithActiveSeasons = Sport & {
activeSeasons: Array<{ id: string; name: string; year: number }>;
};
export async function createSport(data: NewSport): Promise<Sport> {
const db = database();
const [sport] = await db
@ -36,6 +40,32 @@ export async function findAllSports(): Promise<Sport[]> {
});
}
export async function findAllSportsWithActiveSeasons(): Promise<SportWithActiveSeasons[]> {
const db = database();
const sports = await db.query.sports.findMany({
orderBy: (sports, { asc }) => [asc(sports.name)],
with: {
sportsSeasons: {
where: (sportsSeasons, { eq, or }) =>
or(
eq(sportsSeasons.status, "active"),
eq(sportsSeasons.status, "upcoming")
),
orderBy: (sportsSeasons, { desc }) => [desc(sportsSeasons.year)],
},
},
});
return sports.map((sport) => ({
...sport,
activeSeasons: sport.sportsSeasons.map((season) => ({
id: season.id,
name: season.name,
year: season.year,
})),
}));
}
export async function findSportsByType(type: SportType): Promise<Sport[]> {
const db = database();
return await db.query.sports.findMany({

View file

@ -37,6 +37,21 @@ export async function findSportsSeasonsBySportId(sportId: string): Promise<Sport
});
}
export async function findActiveSportsSeasonsBySportId(sportId: string): Promise<SportsSeason[]> {
const db = database();
return await db.query.sportsSeasons.findMany({
where: (sportsSeasons, { eq, and }) =>
and(
eq(sportsSeasons.sportId, sportId),
eq(sportsSeasons.status, "active")
),
orderBy: (sportsSeasons, { desc }) => [desc(sportsSeasons.year)],
with: {
sport: true,
},
});
}
export async function findSportsSeasonsByYear(year: number): Promise<SportsSeason[]> {
const db = database();
return await db.query.sportsSeasons.findMany({

View file

@ -1,6 +1,8 @@
import { Form, Link, redirect } from "react-router";
import type { Route } from "./+types/admin.sports.$id";
import { findSportById, updateSport } from "~/models/sport";
import { writeFile, mkdir } from "node:fs/promises";
import { join } from "node:path";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
@ -36,6 +38,8 @@ export async function action({ request, params }: Route.ActionArgs) {
const type = formData.get("type");
const slug = formData.get("slug");
const description = formData.get("description");
const iconFile = formData.get("iconFile");
const keepExistingIcon = formData.get("keepExistingIcon");
// Validation
if (typeof name !== "string" || !name.trim()) {
@ -55,12 +59,49 @@ export async function action({ request, params }: Route.ActionArgs) {
return { error: "Slug must contain only lowercase letters, numbers, and hyphens" };
}
// Get existing sport to preserve icon if needed
const existingSport = await findSportById(params.id);
let iconUrl: string | null | undefined = existingSport?.iconUrl;
// Handle file upload
if (iconFile instanceof File && iconFile.size > 0) {
const fileExtension = iconFile.name.split(".").pop()?.toLowerCase();
// Validate file type
if (!fileExtension || !["svg", "png", "jpg", "jpeg"].includes(fileExtension)) {
return { error: "Icon must be an SVG, PNG, or JPG file" };
}
// Create filename using slug
const filename = `${slug.trim()}.${fileExtension}`;
iconUrl = filename;
try {
// Ensure directory exists
const iconsDir = join(process.cwd(), "public", "sports-icons");
await mkdir(iconsDir, { recursive: true });
// Save file
const filepath = join(iconsDir, filename);
const bytes = await iconFile.arrayBuffer();
const buffer = Buffer.from(bytes);
await writeFile(filepath, buffer);
} catch (error) {
console.error("Error saving icon file:", error);
return { error: "Failed to save icon file" };
}
} else if (keepExistingIcon !== "true") {
// If no file uploaded and not keeping existing, set to null
iconUrl = null;
}
try {
await updateSport(params.id, {
name: name.trim(),
type,
slug: slug.trim(),
description: typeof description === "string" ? description.trim() : null,
iconUrl,
});
return redirect("/admin/sports");
@ -147,6 +188,36 @@ export default function EditSport({ loaderData, actionData }: Route.ComponentPro
/>
</div>
<div className="space-y-2">
<Label htmlFor="iconFile">Icon File (Optional)</Label>
{sport.iconUrl && (
<div className="mb-2 p-3 bg-muted rounded-md">
<p className="text-sm font-medium mb-1">Current icon:</p>
<div className="flex items-center gap-3">
<img
src={`/sports-icons/${sport.iconUrl}`}
alt={sport.name}
className="w-12 h-12 object-contain"
onError={(e) => {
e.currentTarget.style.display = 'none';
}}
/>
<code className="text-xs bg-background px-2 py-1 rounded">{sport.iconUrl}</code>
</div>
</div>
)}
<Input
id="iconFile"
name="iconFile"
type="file"
accept=".svg,.png,.jpg,.jpeg"
/>
<input type="hidden" name="keepExistingIcon" value={sport.iconUrl ? "true" : "false"} />
<p className="text-sm text-muted-foreground">
Upload a new SVG, PNG, or JPG file to replace the current icon. Leave empty to keep the existing icon.
</p>
</div>
{actionData?.error && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
{actionData.error}

View file

@ -1,6 +1,8 @@
import { Form, Link, redirect } from "react-router";
import type { Route } from "./+types/admin.sports.new";
import { createSport } from "~/models/sport";
import { writeFile, mkdir } from "node:fs/promises";
import { join } from "node:path";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
@ -26,6 +28,7 @@ export async function action({ request }: Route.ActionArgs) {
const type = formData.get("type");
const slug = formData.get("slug");
const description = formData.get("description");
const iconFile = formData.get("iconFile");
// Validation
if (typeof name !== "string" || !name.trim()) {
@ -45,12 +48,44 @@ export async function action({ request }: Route.ActionArgs) {
return { error: "Slug must contain only lowercase letters, numbers, and hyphens" };
}
let iconUrl: string | null = null;
// Handle file upload
if (iconFile instanceof File && iconFile.size > 0) {
const fileExtension = iconFile.name.split(".").pop()?.toLowerCase();
// Validate file type
if (!fileExtension || !["svg", "png", "jpg", "jpeg"].includes(fileExtension)) {
return { error: "Icon must be an SVG, PNG, or JPG file" };
}
// Create filename using slug
const filename = `${slug.trim()}.${fileExtension}`;
iconUrl = filename;
try {
// Ensure directory exists
const iconsDir = join(process.cwd(), "public", "sports-icons");
await mkdir(iconsDir, { recursive: true });
// Save file
const filepath = join(iconsDir, filename);
const bytes = await iconFile.arrayBuffer();
const buffer = Buffer.from(bytes);
await writeFile(filepath, buffer);
} catch (error) {
console.error("Error saving icon file:", error);
return { error: "Failed to save icon file" };
}
}
try {
await createSport({
name: name.trim(),
type,
slug: slug.trim(),
description: typeof description === "string" ? description.trim() : null,
iconUrl,
});
return redirect("/admin/sports");
@ -132,6 +167,19 @@ export default function NewSport({ actionData }: Route.ComponentProps) {
/>
</div>
<div className="space-y-2">
<Label htmlFor="iconFile">Icon File (Optional)</Label>
<Input
id="iconFile"
name="iconFile"
type="file"
accept=".svg,.png,.jpg,.jpeg"
/>
<p className="text-sm text-muted-foreground">
Upload an SVG, PNG, or JPG file. The file will be saved as <code className="bg-muted px-1 py-0.5 rounded">{`{slug}.{ext}`}</code>
</p>
</div>
{actionData?.error && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
{actionData.error}

View file

@ -1,6 +1,6 @@
import { Link } from "react-router";
import type { Route } from "./+types/admin.sports";
import { findAllSports } from "~/models/sport";
import { findAllSportsWithActiveSeasons } from "~/models/sport";
import {
Card,
CardContent,
@ -21,7 +21,7 @@ import {
import { Badge } from "~/components/ui/badge";
export async function loader() {
const sports = await findAllSports();
const sports = await findAllSportsWithActiveSeasons();
return { sports };
}
@ -74,6 +74,7 @@ export default function AdminSports({ loaderData }: Route.ComponentProps) {
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>Slug</TableHead>
<TableHead>Current Seasons</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
@ -89,6 +90,25 @@ export default function AdminSports({ loaderData }: Route.ComponentProps) {
<TableCell className="text-muted-foreground">
{sport.slug}
</TableCell>
<TableCell>
{sport.activeSeasons.length > 0 ? (
<div className="flex flex-col gap-1">
{sport.activeSeasons.map((season) => (
<Link
key={season.id}
to={`/admin/sports-seasons/${season.id}`}
className="text-sm text-primary hover:underline"
>
{season.name} ({season.year})
</Link>
))}
</div>
) : (
<span className="text-sm text-muted-foreground">
No current seasons
</span>
)}
</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" asChild>
<Link to={`/admin/sports/${sport.id}`}>Edit</Link>

View file

@ -98,6 +98,7 @@ export const sports = pgTable("sports", {
type: sportTypeEnum("type").notNull(),
slug: varchar("slug", { length: 255 }).notNull().unique(),
description: text("description"),
iconUrl: varchar("icon_url", { length: 255 }), // Filename of icon in /public/sports-icons/
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

View file

@ -0,0 +1 @@
ALTER TABLE "sports" ADD COLUMN "icon_url" varchar(255);

View file

@ -0,0 +1,947 @@
{
"id": "a87c9793-9d30-4cf6-b1f5-d3f6322128e2",
"prevId": "462e431e-4517-4a02-b4e9-80c82c1b2c9f",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.commissioners": {
"name": "commissioners",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"league_id": {
"name": "league_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"commissioners_league_id_leagues_id_fk": {
"name": "commissioners_league_id_leagues_id_fk",
"tableFrom": "commissioners",
"tableTo": "leagues",
"columnsFrom": [
"league_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.leagues": {
"name": "leagues",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"created_by": {
"name": "created_by",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"current_season_id": {
"name": "current_season_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.participant_results": {
"name": "participant_results",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"participant_id": {
"name": "participant_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"sports_season_id": {
"name": "sports_season_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"final_position": {
"name": "final_position",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"points_awarded": {
"name": "points_awarded",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"qualifying_points": {
"name": "qualifying_points",
"type": "numeric(10, 2)",
"primaryKey": false,
"notNull": false
},
"notes": {
"name": "notes",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"participant_results_participant_id_participants_id_fk": {
"name": "participant_results_participant_id_participants_id_fk",
"tableFrom": "participant_results",
"tableTo": "participants",
"columnsFrom": [
"participant_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"participant_results_sports_season_id_sports_seasons_id_fk": {
"name": "participant_results_sports_season_id_sports_seasons_id_fk",
"tableFrom": "participant_results",
"tableTo": "sports_seasons",
"columnsFrom": [
"sports_season_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.participants": {
"name": "participants",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"sports_season_id": {
"name": "sports_season_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"short_name": {
"name": "short_name",
"type": "varchar(100)",
"primaryKey": false,
"notNull": false
},
"external_id": {
"name": "external_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"participants_sports_season_id_sports_seasons_id_fk": {
"name": "participants_sports_season_id_sports_seasons_id_fk",
"tableFrom": "participants",
"tableTo": "sports_seasons",
"columnsFrom": [
"sports_season_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.season_sports": {
"name": "season_sports",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"season_id": {
"name": "season_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"sports_season_id": {
"name": "sports_season_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"is_required": {
"name": "is_required",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"season_sports_season_id_seasons_id_fk": {
"name": "season_sports_season_id_seasons_id_fk",
"tableFrom": "season_sports",
"tableTo": "seasons",
"columnsFrom": [
"season_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"season_sports_sports_season_id_sports_seasons_id_fk": {
"name": "season_sports_sports_season_id_sports_seasons_id_fk",
"tableFrom": "season_sports",
"tableTo": "sports_seasons",
"columnsFrom": [
"sports_season_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.season_template_sports": {
"name": "season_template_sports",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"template_id": {
"name": "template_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"sports_season_id": {
"name": "sports_season_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"is_required": {
"name": "is_required",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"season_template_sports_template_id_season_templates_id_fk": {
"name": "season_template_sports_template_id_season_templates_id_fk",
"tableFrom": "season_template_sports",
"tableTo": "season_templates",
"columnsFrom": [
"template_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"season_template_sports_sports_season_id_sports_seasons_id_fk": {
"name": "season_template_sports_sports_season_id_sports_seasons_id_fk",
"tableFrom": "season_template_sports",
"tableTo": "sports_seasons",
"columnsFrom": [
"sports_season_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.season_templates": {
"name": "season_templates",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"year": {
"name": "year",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"is_active": {
"name": "is_active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.seasons": {
"name": "seasons",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"league_id": {
"name": "league_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"year": {
"name": "year",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "season_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'pre_draft'"
},
"template_id": {
"name": "template_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"flex_spots": {
"name": "flex_spots",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"seasons_league_id_leagues_id_fk": {
"name": "seasons_league_id_leagues_id_fk",
"tableFrom": "seasons",
"tableTo": "leagues",
"columnsFrom": [
"league_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"seasons_template_id_season_templates_id_fk": {
"name": "seasons_template_id_season_templates_id_fk",
"tableFrom": "seasons",
"tableTo": "season_templates",
"columnsFrom": [
"template_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.sports": {
"name": "sports",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"type": {
"name": "type",
"type": "sport_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"icon_url": {
"name": "icon_url",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"sports_slug_unique": {
"name": "sports_slug_unique",
"nullsNotDistinct": false,
"columns": [
"slug"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.sports_seasons": {
"name": "sports_seasons",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"sport_id": {
"name": "sport_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"year": {
"name": "year",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"start_date": {
"name": "start_date",
"type": "date",
"primaryKey": false,
"notNull": false
},
"end_date": {
"name": "end_date",
"type": "date",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "sports_season_status",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'upcoming'"
},
"scoring_type": {
"name": "scoring_type",
"type": "scoring_type",
"typeSchema": "public",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"sports_seasons_sport_id_sports_id_fk": {
"name": "sports_seasons_sport_id_sports_id_fk",
"tableFrom": "sports_seasons",
"tableTo": "sports",
"columnsFrom": [
"sport_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.teams": {
"name": "teams",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"season_id": {
"name": "season_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"owner_id": {
"name": "owner_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"teams_season_id_seasons_id_fk": {
"name": "teams_season_id_seasons_id_fk",
"tableFrom": "teams",
"tableTo": "seasons",
"columnsFrom": [
"season_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"clerk_id": {
"name": "clerk_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"display_name": {
"name": "display_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"first_name": {
"name": "first_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"last_name": {
"name": "last_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"image_url": {
"name": "image_url",
"type": "varchar(512)",
"primaryKey": false,
"notNull": false
},
"is_admin": {
"name": "is_admin",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_clerk_id_unique": {
"name": "users_clerk_id_unique",
"nullsNotDistinct": false,
"columns": [
"clerk_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.scoring_type": {
"name": "scoring_type",
"schema": "public",
"values": [
"playoffs",
"regular_season",
"majors"
]
},
"public.season_status": {
"name": "season_status",
"schema": "public",
"values": [
"pre_draft",
"draft",
"active",
"completed"
]
},
"public.sport_type": {
"name": "sport_type",
"schema": "public",
"values": [
"team",
"individual"
]
},
"public.sports_season_status": {
"name": "sports_season_status",
"schema": "public",
"values": [
"upcoming",
"active",
"completed"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -57,6 +57,13 @@
"when": 1760327817376,
"tag": "0007_pink_nebula",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1760375514773,
"tag": "0008_salty_trauma",
"breakpoints": true
}
]
}

View file

@ -0,0 +1,34 @@
# Sports Icons Directory
This directory contains icon files for sports. Icons are referenced in the database by filename only.
## Usage
Icons are automatically uploaded through the admin panel when creating or editing sports:
1. Go to Admin > Sports > Add/Edit Sport
2. Use the "Icon File" upload field to select an SVG, PNG, or JPG file
3. The file will be automatically saved as `{slug}.{extension}` in this directory
You can also manually place icon files here if needed.
## File Format
- **Preferred**: SVG files for scalability
- **Supported**: PNG files (recommend 512x512px or higher)
- **Naming**: Use the sport slug as the filename (e.g., `nfl.svg`, `golf-mens.svg`)
## Import/Export
When exporting sports data:
- This entire directory should be included in the export
- The export script will copy all files from this directory
When importing sports data:
- Extract the `sports-icons` folder to `/public/sports-icons/`
- The import script will update database references to match the imported files
## Access
Icons are served from `/sports-icons/{filename}` in the application.
Example: `/sports-icons/nfl.svg`