feat: implement season requests management with CRUD operations and enhance UI components

This commit is contained in:
maxDorninger
2025-05-24 14:24:00 +02:00
parent f2b7f0f370
commit d2f0b8f22d
15 changed files with 253 additions and 71 deletions

View File

@@ -3,6 +3,7 @@ from uuid import UUID
from sqlalchemy import ForeignKey, PrimaryKeyConstraint, UniqueConstraint from sqlalchemy import ForeignKey, PrimaryKeyConstraint, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from auth.db import User
from backend.src.database import Base from backend.src.database import Base
from torrent.models import Quality from torrent.models import Quality
@@ -36,6 +37,7 @@ class Season(Base):
episodes: Mapped[list["Episode"]] = relationship(back_populates="season", cascade="all, delete") episodes: Mapped[list["Episode"]] = relationship(back_populates="season", cascade="all, delete")
season_files = relationship("SeasonFile", back_populates="season", cascade="all, delete") season_files = relationship("SeasonFile", back_populates="season", cascade="all, delete")
season_requests = relationship("SeasonRequest", back_populates="season", cascade="all, delete")
class Episode(Base): class Episode(Base):
@@ -74,6 +76,10 @@ class SeasonRequest(Base):
season_id: Mapped[UUID] = mapped_column(ForeignKey(column="season.id", ondelete="CASCADE"), ) season_id: Mapped[UUID] = mapped_column(ForeignKey(column="season.id", ondelete="CASCADE"), )
wanted_quality: Mapped[Quality] wanted_quality: Mapped[Quality]
min_quality: Mapped[Quality] min_quality: Mapped[Quality]
requested_by: Mapped[UUID | None] = mapped_column(ForeignKey(column="user.id", ondelete="SET NULL"), ) requested_by_id: Mapped[UUID | None] = mapped_column(ForeignKey(column="user.id", ondelete="SET NULL"), )
authorized: Mapped[bool] = mapped_column(default=False) authorized: Mapped[bool] = mapped_column(default=False)
authorized_by: Mapped[UUID | None] = mapped_column(ForeignKey(column="user.id", ondelete="SET NULL"), ) authorized_by_id: Mapped[UUID | None] = mapped_column(ForeignKey(column="user.id", ondelete="SET NULL"), )
requested_by: Mapped["User|None"] = relationship(foreign_keys=[requested_by_id], uselist=False)
authorized_by: Mapped["User|None"] = relationship(foreign_keys=[authorized_by_id], uselist=False)
season = relationship("Season", back_populates="season_requests", uselist=False)

View File

@@ -1,6 +1,4 @@
import pprint from sqlalchemy import select, delete
from sqlalchemy import select, delete, update
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session, joinedload from sqlalchemy.orm import Session, joinedload
@@ -121,12 +119,13 @@ def add_season_to_requested_list(season_request: SeasonRequestSchema, db: Sessio
db.commit() db.commit()
def remove_season_from_requested_list(season_request: SeasonRequestSchema, db: Session) -> None: def delete_season_request(season_request_id: SeasonRequestId, db: Session) -> None:
""" """
Removes a Season from the SeasonRequest table, which removes it from the 'requested' list. Removes a Season from the SeasonRequest table, which removes it from the 'requested' list.
""" """
db.delete(SeasonRequest(**season_request.model_dump())) stmt = delete(SeasonRequest).where(SeasonRequest.id == season_request_id)
db.execute(stmt)
db.commit() db.commit()
@@ -138,10 +137,16 @@ def get_season_by_number(db: Session, season_number: int, show_id: ShowId) -> Se
def get_season_requests(db: Session) -> list[RichSeasonRequestSchema]: def get_season_requests(db: Session) -> list[RichSeasonRequestSchema]:
stmt = select(SeasonRequest).join(Season, Season.id == SeasonRequest.season_id).join(Show, stmt = select(SeasonRequest).options(joinedload(SeasonRequest.requested_by),
Season.show_id == Show.id) joinedload(SeasonRequest.authorized_by),
result = db.execute(stmt).scalars().all() joinedload(SeasonRequest.season).joinedload(Season.show))
return [RichSeasonRequestSchema.model_validate(season) for season in result] result = db.execute(stmt).scalars().unique().all()
return [RichSeasonRequestSchema(min_quality=x.min_quality,
wanted_quality=x.wanted_quality, show=x.season.show, season=x.season,
requested_by=x.requested_by, authorized_by=x.authorized_by, authorized=x.authorized,
id=x.id)
for x in result]
def add_season_file(db: Session, season_file: SeasonFileSchema) -> SeasonFileSchema: def add_season_file(db: Session, season_file: SeasonFileSchema) -> SeasonFileSchema:
db.add(SeasonFile(**season_file.model_dump())) db.add(SeasonFile(**season_file.model_dump()))
@@ -209,5 +214,3 @@ def update_season_request(db: Session, season_request: SeasonRequestSchema) -> N
db.delete(db.get(SeasonRequest, season_request.id)) db.delete(db.get(SeasonRequest, season_request.id))
db.add(SeasonRequest(**season_request.model_dump())) db.add(SeasonRequest(**season_request.model_dump()))
db.commit() db.commit()

View File

@@ -6,6 +6,7 @@ from fastapi.responses import JSONResponse
import tv.repository import tv.repository
import tv.service import tv.service
from auth.db import User from auth.db import User
from auth.schemas import UserRead
from auth.users import current_active_user, current_superuser from auth.users import current_active_user, current_superuser
from backend.src.database import DbSessionDependency from backend.src.database import DbSessionDependency
from indexer.schemas import PublicIndexerQueryResult, IndexerQueryResultId from indexer.schemas import PublicIndexerQueryResult, IndexerQueryResultId
@@ -13,7 +14,7 @@ from metadataProvider.schemas import MetaDataProviderShowSearchResult
from torrent.schemas import Torrent from torrent.schemas import Torrent
from tv.exceptions import MediaAlreadyExists from tv.exceptions import MediaAlreadyExists
from tv.schemas import Show, SeasonRequest, ShowId, RichShowTorrent, PublicShow, PublicSeasonFile, SeasonNumber, \ from tv.schemas import Show, SeasonRequest, ShowId, RichShowTorrent, PublicShow, PublicSeasonFile, SeasonNumber, \
CreateSeasonRequest, SeasonRequestId, UpdateSeasonRequest, RichSeasonRequest, SeasonId CreateSeasonRequest, SeasonRequestId, UpdateSeasonRequest, RichSeasonRequest
router = APIRouter() router = APIRouter()
@@ -82,50 +83,51 @@ def get_season_files(db: DbSessionDependency, season_number: SeasonNumber, show_
# MANAGE REQUESTS # MANAGE REQUESTS
# -------------------------------- # --------------------------------
@router.post("/seasons/requests", status_code=status.HTTP_200_OK, response_model=RichSeasonRequest) @router.post("/seasons/requests", status_code=status.HTTP_204_NO_CONTENT)
def request_a_season(db: DbSessionDependency, user: Annotated[User, Depends(current_active_user)], def request_a_season(db: DbSessionDependency, user: Annotated[User, Depends(current_active_user)],
season_request: CreateSeasonRequest): season_request: CreateSeasonRequest):
""" """
adds request flag to a season adds request flag to a season
""" """
request: SeasonRequest = SeasonRequest.model_validate(season_request) request: SeasonRequest = SeasonRequest.model_validate(season_request)
request.requested_by = user.id request.requested_by = UserRead.model_validate(user)
tv.service.request_season(db=db, season_request=request) tv.service.request_season(db=db, season_request=request)
return request return
@router.get("/seasons/requests", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)]) @router.get("/seasons/requests", status_code=status.HTTP_200_OK, dependencies=[Depends(current_active_user)],
def get_requested_seasons(db: DbSessionDependency) -> list[SeasonRequest]: response_model=list[RichSeasonRequest])
def get_season_requests(db: DbSessionDependency) -> list[RichSeasonRequest]:
return tv.service.get_all_season_requests(db=db) return tv.service.get_all_season_requests(db=db)
@router.patch("/seasons/requests/{season_request_id}", status_code=status.HTTP_200_OK, response_model=SeasonRequest) @router.patch("/seasons/requests/{season_request_id}", status_code=status.HTTP_204_NO_CONTENT)
def authorize_request(db: DbSessionDependency, user: Annotated[User, Depends(current_superuser)], def authorize_request(db: DbSessionDependency, user: Annotated[User, Depends(current_superuser)],
season_request_id: SeasonRequestId, authorized_status: bool = False): season_request_id: SeasonRequestId, authorized_status: bool = False):
""" """
updates the request flag to true updates the request flag to true
""" """
season_request: SeasonRequest = tv.repository.get_season_request(db=db, season_request_id=season_request_id) season_request: SeasonRequest = tv.repository.get_season_request(db=db, season_request_id=season_request_id)
season_request.authorized_by = user.id season_request.authorized_by = UserRead.model_validate(user)
season_request.authorized = authorized_status season_request.authorized = authorized_status
tv.service.update_season_request(db=db, season_request=season_request) tv.service.update_season_request(db=db, season_request=season_request)
return season_request return
@router.put("/seasons/requests/{season_request_id}", status_code=status.HTTP_200_OK, response_model=SeasonRequest) @router.put("/seasons/requests/{season_request_id}", status_code=status.HTTP_204_NO_CONTENT)
def update_request(db: DbSessionDependency, user: Annotated[User, Depends(current_superuser)], def update_request(db: DbSessionDependency, user: Annotated[User, Depends(current_superuser)],
season_request: UpdateSeasonRequest): season_request: UpdateSeasonRequest):
season_request: SeasonRequest = SeasonRequest.model_validate(season_request) season_request: SeasonRequest = SeasonRequest.model_validate(season_request)
season_request.requested_by = user.id season_request.requested_by = UserRead.model_validate(user)
tv.service.update_season_request(db=db, season_request=season_request) tv.service.update_season_request(db=db, season_request=season_request)
return season_request return
@router.delete("/seasons/requests/{season_request_id}", status_code=status.HTTP_204_NO_CONTENT, @router.delete("/seasons/requests/{season_request_id}", status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(current_active_user)]) dependencies=[Depends(current_active_user)])
def delete_season_request(db: DbSessionDependency, request: SeasonRequest): def delete_season_request(db: DbSessionDependency, request_id: SeasonRequestId):
tv.service.unrequest_season(db=db, season_request=request) tv.service.delete_season_request(db=db, season_request_id=request_id)
# -------------------------------- # --------------------------------

View File

@@ -5,6 +5,7 @@ from uuid import UUID
from pydantic import BaseModel, Field, ConfigDict from pydantic import BaseModel, Field, ConfigDict
from tvdb_v4_official import Request from tvdb_v4_official import Request
from auth.schemas import UserRead
from torrent.models import Quality from torrent.models import Quality
from torrent.schemas import TorrentId, TorrentStatus from torrent.schemas import TorrentId, TorrentStatus
@@ -55,12 +56,12 @@ class Show(BaseModel):
class SeasonRequestBase(BaseModel): class SeasonRequestBase(BaseModel):
season_id: SeasonId
min_quality: Quality min_quality: Quality
wanted_quality: Quality wanted_quality: Quality
class CreateSeasonRequest(SeasonRequestBase): class CreateSeasonRequest(SeasonRequestBase):
season_id: SeasonId
pass pass
@@ -72,17 +73,15 @@ class SeasonRequest(SeasonRequestBase):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
id: SeasonRequestId = Field(default_factory=uuid.uuid4) id: SeasonRequestId = Field(default_factory=uuid.uuid4)
season: Season
requested_by: UUID | None = None requested_by: UserRead | None = None
authorized: bool = False authorized: bool = False
authorized_by: UUID | None = None authorized_by: UserRead | None = None
class RichSeasonRequest(SeasonRequest): class RichSeasonRequest(SeasonRequest):
show_id: ShowId show: Show
show_name: str
show_year: int | None
season_number: SeasonNumber
class SeasonFile(BaseModel): class SeasonFile(BaseModel):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)

View File

@@ -15,7 +15,7 @@ from tv import log
from tv.exceptions import MediaAlreadyExists from tv.exceptions import MediaAlreadyExists
from tv.repository import add_season_file, get_season_files_by_season_id from tv.repository import add_season_file, get_season_files_by_season_id
from tv.schemas import Show, ShowId, SeasonRequest, SeasonFile, SeasonId, Season, RichShowTorrent, RichSeasonTorrent, \ from tv.schemas import Show, ShowId, SeasonRequest, SeasonFile, SeasonId, Season, RichShowTorrent, RichSeasonTorrent, \
PublicSeason, PublicShow, PublicSeasonFile, SeasonNumber, SeasonRequestId PublicSeason, PublicShow, PublicSeasonFile, SeasonNumber, SeasonRequestId, RichSeasonRequest
def add_show(db: Session, external_id: int, metadata_provider: str) -> Show | None: def add_show(db: Session, external_id: int, metadata_provider: str) -> Show | None:
@@ -34,8 +34,9 @@ def request_season(db: Session, season_request: SeasonRequest) -> None:
def update_season_request(db: Session, season_request: SeasonRequest) -> None: def update_season_request(db: Session, season_request: SeasonRequest) -> None:
tv.repository.update_season_request(db=db, season_request=season_request) tv.repository.update_season_request(db=db, season_request=season_request)
def unrequest_season(db: Session, season_request: SeasonRequest) -> None:
tv.repository.remove_season_from_requested_list(db=db, season_request=season_request) def delete_season_request(db: Session, season_request_id: SeasonRequestId) -> None:
tv.repository.delete_season_request(db=db, season_request_id=season_request_id)
def get_public_season_files_by_season_id(db: Session, season_id: SeasonId) -> list[PublicSeasonFile]: def get_public_season_files_by_season_id(db: Session, season_id: SeasonId) -> list[PublicSeasonFile]:
@@ -148,7 +149,7 @@ def get_season(db: Session, season_id: SeasonId) -> Season:
return tv.repository.get_season(season_id=season_id, db=db) return tv.repository.get_season(season_id=season_id, db=db)
def get_all_season_requests(db: Session) -> list[SeasonRequest]: def get_all_season_requests(db: Session) -> list[RichSeasonRequest]:
return tv.repository.get_season_requests(db=db) return tv.repository.get_season_requests(db=db)

View File

@@ -80,7 +80,7 @@
if (response.ok) { if (response.ok) {
console.log('Registration successful!'); console.log('Registration successful!');
console.log('Received User Data: ', response); console.log('Received User Data: ', response);
goto('/dashboard'); tabValue = "login"; // Switch to login tab after successful registration
errorMessage = 'Registration successful! Redirecting...'; errorMessage = 'Registration successful! Redirecting...';
} else { } else {
let errorText = await response.text(); let errorText = await response.text();

View File

@@ -17,23 +17,9 @@
import {base} from '$app/paths'; import {base} from '$app/paths';
import {env} from "$env/dynamic/public"; import {env} from "$env/dynamic/public";
import {goto} from '$app/navigation'; import {goto} from '$app/navigation';
import {handleLogout} from '$lib/utils.ts';
const user: () => User = getContext('user'); const user: () => User = getContext('user');
const sidebar = useSidebar(); const sidebar = useSidebar();
const apiUrl = env.PUBLIC_API_URL;
async function handleLogout() {
const response = await fetch(apiUrl + '/auth/cookie/logout', {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
console.log('Logout successful!');
await goto(base + '/login');
} else {
console.error('Logout failed:', response.status);
}
}
</script> </script>

View File

@@ -0,0 +1,62 @@
<script lang="ts">
import {
convertTorrentSeasonRangeToIntegerRange, getFullyQualifiedShowName,
getTorrentQualityString,
getTorrentStatusString
} from "$lib/utils.js";
import type {SeasonRequest} from "$lib/types.js";
import CheckmarkX from "$lib/components/checkmark-x.svelte";
import * as Table from "$lib/components/ui/table/index.js";
let {
requests, filter = () => {
return true
}
}: { requests: SeasonRequest[], filter: (SeasonRequest) => boolean } = $props();
</script>
<Table.Root>
<Table.Caption>A list of all requests.</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Head>Show</Table.Head>
<Table.Head>Season</Table.Head>
<Table.Head>Minimum Quality</Table.Head>
<Table.Head>Wanted Quality</Table.Head>
<Table.Head>Requested by</Table.Head>
<Table.Head>Approved</Table.Head>
<Table.Head>Approved by</Table.Head>
<Table.Head>Actions</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each requests as request (request.id)}
{#if filter(request)}
<Table.Row>
<Table.Cell class="font-medium">
{getFullyQualifiedShowName(request.show)}
</Table.Cell>
<Table.Cell>
{request.season.number}
</Table.Cell>
<Table.Cell class="font-medium">
{getTorrentQualityString(request.min_quality)}
</Table.Cell>
<Table.Cell class="font-medium">
{getTorrentQualityString(request.wanted_quality)}
</Table.Cell>
<Table.Cell>
{request.requested_by?.email}
</Table.Cell>
<Table.Cell>
<CheckmarkX state={request.authorized}/>
</Table.Cell>
<Table.Cell>
{request.authorized_by?.email}
</Table.Cell>
</Table.Row>
{/if}
{/each}
</Table.Body>
</Table.Root>

View File

@@ -94,11 +94,6 @@ export interface PublicSeason {
episodes: Episode[]; // items: { $ref: #/components/schemas/Episode }, type: array episodes: Episode[]; // items: { $ref: #/components/schemas/Episode }, type: array
id?: string; // type: string, format: uuid id?: string; // type: string, format: uuid
} }
export interface SeasonRequest {
season_id: string; // type: string, format: uuid
min_quality: Quality; // $ref: #/components/schemas/Quality
wanted_quality: Quality; // $ref: #/components/schemas/Quality
}
export interface Show { export interface Show {
name: string; name: string;
@@ -172,3 +167,25 @@ export interface RichShowTorrent {
metadata_provider: string; metadata_provider: string;
torrents: RichSeasonTorrent[]; torrents: RichSeasonTorrent[];
} }
interface SeasonRequestBase {
min_quality: Quality;
wanted_quality: Quality;
}
export interface CreateSeasonRequest extends SeasonRequestBase {
season_id: string;
}
export interface UpdateSeasonRequest extends SeasonRequestBase {
id: string;
}
export interface SeasonRequest extends SeasonRequestBase {
id: string;
season: Season;
requested_by?: User;
authorized: boolean;
authorized_by?: User;
show: Show;
}

View File

@@ -1,5 +1,10 @@
import {type ClassValue, clsx} from 'clsx'; import {type ClassValue, clsx} from 'clsx';
import {twMerge} from 'tailwind-merge'; import {twMerge} from 'tailwind-merge';
import {env} from "$env/dynamic/public";
import {goto} from '$app/navigation';
import {base} from '$app/paths';
const apiUrl = env.PUBLIC_API_URL;
export const qualityMap: { [key: number]: string } = { export const qualityMap: { [key: number]: string } = {
1: 'high', 1: 'high',
@@ -43,3 +48,16 @@ export function convertTorrentSeasonRangeToIntegerRange(torrent: any): string {
} }
} }
export async function handleLogout(): null {
const response = await fetch(apiUrl + '/auth/cookie/logout', {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
console.log('Logout successful!');
await goto(base + '/login');
} else {
console.error('Logout failed:', response.status);
}
}

View File

@@ -1,4 +1,11 @@
<script lang="ts"> <script lang="ts">
import {goto} from "$app/navigation";
import {base} from "$app/paths";
import {onMount} from 'svelte';
onMount(() => {
goto(base + '/dashboard');
});
</script> </script>
<svelte:head> <svelte:head>

View File

@@ -8,12 +8,11 @@ export const load: LayoutLoad = async ({params, fetch}) => {
return { return {
showData: null, showData: null,
torrentsData: null, torrentsData: null,
error: 'Show ID is missing'
}; };
} }
try { try {
const response = await fetch(`${env.PUBLIC_API_URL}/tv/shows/${showId}`, { const show = await fetch(`${env.PUBLIC_API_URL}/tv/shows/${showId}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -29,16 +28,15 @@ export const load: LayoutLoad = async ({params, fetch}) => {
credentials: 'include' credentials: 'include'
}); });
if (!response.ok || !torrents.ok) { if (!show.ok || !torrents.ok) {
console.error(`Failed to fetch show ${showId}: ${response.statusText}`); console.error(`Failed to fetch show ${showId}: ${show.statusText}`);
return { return {
showData: null, showData: null,
torrentsData: null, torrentsData: null,
error: `Failed to load show or/and its torrents: ${response.statusText}`
}; };
} }
const showData = await response.json(); const showData = await show.json();
const torrentsData = await torrents.json(); const torrentsData = await torrents.json();
console.log('Fetched show data:', showData); console.log('Fetched show data:', showData);
console.log('Fetched torrents data:', torrentsData); console.log('Fetched torrents data:', torrentsData);
@@ -52,7 +50,6 @@ export const load: LayoutLoad = async ({params, fetch}) => {
return { return {
showData: null, showData: null,
torrentsData: null, torrentsData: null,
error: 'An error occurred while fetching show data.'
}; };
} }
}; };

View File

@@ -0,0 +1,33 @@
import {env} from '$env/dynamic/public';
import type {LayoutLoad} from './$types';
export const load: LayoutLoad = async ({params, fetch}) => {
try {
const requests = await fetch(`${env.PUBLIC_API_URL}/tv/seasons/requests`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
});
if (!requests.ok) {
console.error(`Failed to fetch season requests ${requests.statusText}`);
return {
requestsData: null,
};
}
const requestsData = await requests.json();
console.log('Fetched season requests:', requestsData);
return {
requestsData: requestsData,
};
} catch (error) {
console.error('Error fetching season requests:', error);
return {
requestsData: null,
};
}
};

View File

@@ -0,0 +1,49 @@
<script lang="ts">
import {page} from '$app/state';
import {Separator} from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import * as Table from '$lib/components/ui/table/index.js';
import {getTorrentQualityString, getTorrentStatusString} from '$lib/utils';
import type {SeasonRequest} from '$lib/types';
import {getFullyQualifiedShowName} from '$lib/utils';
import * as Accordion from '$lib/components/ui/accordion/index.js';
import CheckmarkX from '$lib/components/checkmark-x.svelte';
import * as Card from "$lib/components/ui/card/index.js";
import RequestsTable from "$lib/components/season-requests-table.svelte";
let requests: SeasonRequest[] = $state(page.data.requestsData);
</script>
<header class="flex h-16 shrink-0 items-center gap-2">
<div class="flex items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1"/>
<Separator class="mr-2 h-4" orientation="vertical"/>
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item class="hidden md:block">
<Breadcrumb.Link href="/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="/dashboard/tv">Shows</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>TV Torrents</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
</header>
<div class="flex w-full flex-1 flex-col items-center gap-4 p-4 pt-0">
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
Season Requests
</h1>
<RequestsTable requests={requests}/>
</div>

View File

@@ -2,7 +2,7 @@
import {UserCheck} from 'lucide-svelte'; import {UserCheck} from 'lucide-svelte';
import {Button} from '$lib/components/ui/button/index.js'; import {Button} from '$lib/components/ui/button/index.js';
import Logo from '$lib/components/logo-side-by-side.svelte'; import Logo from '$lib/components/logo-side-by-side.svelte';
import {handleLogout} from '$lib/utils.ts';
</script> </script>
@@ -10,10 +10,11 @@
<div class="absolute top-4 left-4"> <div class="absolute top-4 left-4">
<Logo/> <Logo/>
</div> </div>
<div class="absolute top-4 right-4">
<Button onclick={()=>handleLogout()} variant="outline">Logout</Button>
</div>
<div class="mx-auto w-full max-w-md text-center"> <div class="mx-auto w-full max-w-md text-center">
<div class="mb-6">
<div class="mb-6">
<UserCheck class="mx-auto h-16 w-16 text-primary"/> <UserCheck class="mx-auto h-16 w-16 text-primary"/>
</div> </div>
<h1 class="mt-4 text-3xl font-bold tracking-tight text-foreground sm:text-4xl"> <h1 class="mt-4 text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
@@ -30,6 +31,7 @@
The above button will only work once your account is verified. The above button will only work once your account is verified.
</p> </p>
<p class="mt-10 text-sm text-muted-foreground end"> <p class="mt-10 text-sm text-muted-foreground end">
If you have any questions, please contact an administrator.</p> If you have any questions, please contact an administrator.
</p>
</div> </div>
</div> </div>