feat: add icon support for sports with file upload and active seasons display
This commit is contained in:
parent
6814a82b76
commit
c343521706
10 changed files with 1176 additions and 2 deletions
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
|||
1
drizzle/0008_salty_trauma.sql
Normal file
1
drizzle/0008_salty_trauma.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "sports" ADD COLUMN "icon_url" varchar(255);
|
||||
947
drizzle/meta/0008_snapshot.json
Normal file
947
drizzle/meta/0008_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -57,6 +57,13 @@
|
|||
"when": 1760327817376,
|
||||
"tag": "0007_pink_nebula",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "7",
|
||||
"when": 1760375514773,
|
||||
"tag": "0008_salty_trauma",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
34
public/sports-icons/README.md
Normal file
34
public/sports-icons/README.md
Normal 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`
|
||||
Loading…
Add table
Reference in a new issue