diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1b5f011 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: Next.js CI + +on: + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install Dependencies + run: npm ci + + - name: Run Lint + run: npm run lint + + - name: Build Application + run: npm run build diff --git a/app/about/page.tsx b/app/about/page.tsx index 7570c7c..5b8ff1e 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -111,7 +111,7 @@ export default function AboutPage() {

We envision a future where fans, analysts, and football enthusiasts can easily access reliable information about - Cameroon's local leagues. By combining real-time match updates + Cameroon's local leagues. By combining real-time match updates with a growing historical data archive, Scoreboards aims to enhance the experience of following football in Cameroon.

@@ -127,7 +127,7 @@ export default function AboutPage() {

About Our Data

At Scoreboards, we are committed to collecting accurate and - comprehensive data from Cameroon's local football leagues. Every + comprehensive data from Cameroon's local football leagues. Every match, player performance, and team statistic is carefully recorded to create a reliable historical archive.

diff --git a/app/contact/page.tsx b/app/contact/page.tsx index ad261b0..223a56d 100644 --- a/app/contact/page.tsx +++ b/app/contact/page.tsx @@ -29,7 +29,7 @@ export default function ContactPage() {

Contact Us

- Have questions or feedback? Fill out the form below and we'll get + Have questions or feedback? Fill out the form below and we'll get back to you as soon as possible.

diff --git a/app/leagues/[id]/ChampionshipClient.tsx b/app/leagues/[id]/ChampionshipClient.tsx index 15961e1..131bde8 100644 --- a/app/leagues/[id]/ChampionshipClient.tsx +++ b/app/leagues/[id]/ChampionshipClient.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useState } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter, useSearchParams, usePathname } from "next/navigation"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ArrowLeft, MapPin } from "lucide-react"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; @@ -22,9 +22,10 @@ interface Props { export default function ChampionshipClient({ id }: Props) { const router = useRouter(); const searchParams = useSearchParams(); + const pathname = usePathname(); const [edition, setEdition] = useState(null); - const [championshipTab, setChampionshipTab] = useState("table"); + const championshipTab = searchParams.get("tab") || "table"; const [matchTab, setMatchTab] = useState("all"); useEffect(() => { @@ -33,10 +34,15 @@ export default function ChampionshipClient({ id }: Props) { setEdition(data); }; loadEdition(); + // Tab logic removed from here to prevent cascading renders + }, [id]); - const tab = searchParams.get("tab"); - if (tab) setChampionshipTab(tab); - }, [id, searchParams]); + // 2. Helper to update tabs via URL + const handleTabChange = (value: string) => { + const params = new URLSearchParams(searchParams.toString()); + params.set("tab", value); + router.replace(`${pathname}?${params.toString()}`, { scroll: false }); + }; if (!edition) { return
Loading...
; @@ -46,21 +52,19 @@ export default function ChampionshipClient({ id }: Props) {
- {edition.championship?.logo && - - {edition.label} - } + {edition.championship?.logo && ( + + + {edition.label} + + )} -

- {edition.label} -

+

{edition.label}

{edition.championship.country && (
- - {edition.championship.country} - + {edition.championship.country}
)}
@@ -70,24 +74,16 @@ export default function ChampionshipClient({ id }: Props) { onClick={() => router.back()} className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 mb-4" > - - Back + Back - + Table Teams Matches - - Team Stats - - - Player Stats - + Team Stats + Player Stats @@ -109,7 +105,10 @@ export default function ChampionshipClient({ id }: Props) { - + diff --git a/components/StandingCard.tsx b/components/StandingCard.tsx index 21c5cb6..fb4062d 100644 --- a/components/StandingCard.tsx +++ b/components/StandingCard.tsx @@ -1,5 +1,6 @@ import React from "react"; import { IStanding } from "../interfaces/IStanding"; +import Image from "next/image"; interface IStandingCardProps { standing: IStanding; @@ -14,9 +15,11 @@ export const StandingCard: React.FC = ( {props.index + 1} {props.standing.participation.team.logo && ( - {props.standing.participation.team.name} )} diff --git a/components/matchs/TimelineItem.tsx b/components/matchs/TimelineItem.tsx index 7d1cb9f..e8f2dfe 100644 --- a/components/matchs/TimelineItem.tsx +++ b/components/matchs/TimelineItem.tsx @@ -25,7 +25,7 @@ export const TimelineItem: React.FC = ({ event }) => { {event.minute} - {event.stoppage_minute ? `+${event.stoppage_minute}` : ""}' + {event.stoppage_minute ? `+${event.stoppage_minute}` : ""}'
diff --git a/components/players/PlayerTeams.tsx b/components/players/PlayerTeams.tsx index 97ef23c..360c6cb 100644 --- a/components/players/PlayerTeams.tsx +++ b/components/players/PlayerTeams.tsx @@ -4,6 +4,7 @@ import React, { useEffect, useState } from "react"; import { PlayerService } from "@/services/PlayerService"; import { IPlayer, IPlayerTeamHistory } from "@/interfaces/IPlayers"; import { Loader2 } from "lucide-react"; +import Image from "next/image"; interface IPlayerTeamsProps { player: IPlayer; @@ -61,9 +62,11 @@ export const PlayerTeams: React.FC = ({ player }) => { className="flex items-center gap-4 border rounded-lg p-4 shadow-sm hover:shadow-md transition" > {h.team.logo && ( - {h.team.name} )} diff --git a/components/players/PlayerTransferts.tsx b/components/players/PlayerTransferts.tsx index 2cf4a13..32c089b 100644 --- a/components/players/PlayerTransferts.tsx +++ b/components/players/PlayerTransferts.tsx @@ -6,6 +6,7 @@ import { IPlayer } from "@/interfaces/IPlayers"; import { IPlayerTransfers } from "@/interfaces/ITransfers"; import { Loader2 } from "lucide-react"; import { ArrowRight } from "lucide-react"; +import Image from "next/image"; interface IPlayerTransfertsProps { player: IPlayer; @@ -59,9 +60,11 @@ export const PlayerTransferts: React.FC = ({ {/* From team */}
{t.team?.logo && ( - {t.team.name} )} @@ -76,9 +79,11 @@ export const PlayerTransferts: React.FC = ({ {/* To team */}
{t.team?.logo && ( - {t.team.name} )} diff --git a/components/standings/StandingDataTable.tsx b/components/standings/StandingDataTable.tsx index 15be8fb..653ae8b 100644 --- a/components/standings/StandingDataTable.tsx +++ b/components/standings/StandingDataTable.tsx @@ -1,4 +1,6 @@ -import React from "react"; +"use client"; + +import React, { useMemo } from "react"; import { ColumnDef, flexRender, @@ -21,60 +23,62 @@ interface IDataTableProps { standings: IStanding[]; } -export const StandingDataTable: React.FC = ( - props: IDataTableProps -) => { +export const StandingDataTable: React.FC = ({ + columns: propColumns, + standings, +}) => { + const columns = useMemo(() => propColumns, [propColumns]); + + const data = useMemo(() => [...standings].sort(standingSort), [standings]); + const table = useReactTable({ - data: props.standings.sort(standingSort), - columns: props.columns, + data, + columns, getCoreRowModel: getCoreRowModel(), }); return ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, - header.getContext() + header.getContext(), )} - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - ))} - )) - ) : ( - - - No results. - - - )} - -
+ ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + + +
); }; diff --git a/components/teams/ChampionshipTeamList.tsx b/components/teams/ChampionshipTeamList.tsx index 18c0fab..6cdc8d4 100644 --- a/components/teams/ChampionshipTeamList.tsx +++ b/components/teams/ChampionshipTeamList.tsx @@ -31,9 +31,6 @@ export const ChampionshipTeamList: React.FC = ({ return (
-

- {championship.name}'s Teams -

{teams.map((team) => ( = ({ - columns, - data, + columns: propColumns, + data: propData, }) => { const [sorting, setSorting] = useState([]); + const columns = useMemo(() => propColumns, [propColumns]); + + const data = useMemo(() => propData, [propData]); + const table = useReactTable({ data, columns, @@ -42,9 +46,10 @@ export const TeamStatDataTable: React.FC = ({ debugTable: false, }); - const renderSortingIcon = (column: any) => { + const renderSortingIcon = (column: Column) => { if (!column.getCanSort() || ["position", "team"].includes(column.id)) return null; + const sorted = column.getIsSorted(); if (sorted === "asc") return ; if (sorted === "desc") return ; @@ -65,7 +70,7 @@ export const TeamStatDataTable: React.FC = ({ > {flexRender( header.column.columnDef.header, - header.getContext() + header.getContext(), )} {renderSortingIcon(header.column)} @@ -75,15 +80,23 @@ export const TeamStatDataTable: React.FC = ({ - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} + {table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + - ))} + )}
diff --git a/constants/urls.ts b/constants/urls.ts index a9366c2..999fbf8 100644 --- a/constants/urls.ts +++ b/constants/urls.ts @@ -3,7 +3,7 @@ if (!process.env.NEXT_PUBLIC_API_HOST) { } export const HOST = process.env.NEXT_PUBLIC_API_HOST; -export let PROTOCOLE: string = +export const PROTOCOLE: string = process.env.NODE_ENV === "production" ? "https" : "http"; const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION ?? "v1"; @@ -42,6 +42,7 @@ export const URLs = { BY_EDITION: `${BASE_URL}/matchs/edition/$editionId/`, APPEARANCE: `${BASE_URL}/appearances/match/$matchId/`, SUBTITUTIONS: `${BASE_URL}/matchs/$matchId/substitutions/`, + LINEUPS: `${BASE_URL}/lineups/match/$matchId/`, }, PLAYERS: { ALL: `${BASE_URL}/players/`, diff --git a/contexts/DeviceContext.tsx b/contexts/DeviceContext.tsx deleted file mode 100644 index 50a4500..0000000 --- a/contexts/DeviceContext.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { createContext, useContext, useEffect, useState } from "react"; -import { getDeviceId } from "@/utils/device"; -import { NotificationService } from "@/services/NotificationService"; -import { WSClient } from "@/services/wsClient"; -import { URLs } from "@/constants/urls"; - -interface DeviceContextValue { - deviceId: string | null; - ws: WSClient | null; - connected: boolean; -} - -const DeviceContext = createContext({ - deviceId: null, - ws: null, - connected: false, -}); - -export const useDevice = () => useContext(DeviceContext); - -export const DeviceProvider: React.FC<{ children: React.ReactNode }> = ({ - children, -}) => { - const [deviceId, setDeviceId] = useState(null); - const [connected, setConnected] = useState(false); - const [wsClient, setWsClient] = useState(null); - - useEffect(() => { - const initializeDeviceId = async () => { - let currentId = getDeviceId(); - - if (!currentId) { - try { - currentId = await NotificationService.registerDeviceOnServer(); - } catch (error) { - console.error("Failed to register device:", error); - } - } - - if (currentId) { - setDeviceId(currentId); - } - }; - - initializeDeviceId(); - }, []); - - useEffect(() => { - if (!deviceId) return; - - const client = new WSClient({ - url: URLs.NOTIFICATIONS.WEBSOCKET.replace("$deviceId", deviceId), - reconnectInterval: 1000, - maxReconnectInterval: 30000, - reconnectDecay: 1.5, - heartbeatInterval: 25000, - onOpen: () => setConnected(true), - onClose: () => setConnected(false), - }); - - client.on("message", (msg: any) => { - if (typeof window !== "undefined") { - window.dispatchEvent( - new CustomEvent("notifications.ws.message", { detail: msg }) - ); - } - }); - - setWsClient(client); - - return () => { - client.close(); - setConnected(false); - setWsClient(null); - }; - }, [deviceId]); - - return ( - - {children} - - ); -}; diff --git a/contexts/NotificationsContext.tsx b/contexts/NotificationsContext.tsx deleted file mode 100644 index afcae19..0000000 --- a/contexts/NotificationsContext.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { - createContext, - useContext, - useEffect, - useRef, - useState, - ReactNode, -} from "react"; -import { URLs } from "@/constants/urls"; -import { getDeviceId } from "@/utils/device"; - -export interface NotificationPayload { - id?: string; - title: string; - message: string; - created_at?: string; - type?: string; -} - -interface NotificationContextType { - notifications: NotificationPayload[]; -} - -const NotificationContext = createContext( - undefined -); - -interface ProviderProps { - children: ReactNode; -} - -export const NotificationProvider: React.FC = ({ children }) => { - const [notifications, setNotifications] = useState([]); - const wsRef = useRef(null); - - useEffect(() => { - connectWebSocket(); - - return () => { - wsRef.current?.close(); - }; - }, []); - - const connectWebSocket = () => { - const deviceId = getDeviceId(); - if (deviceId) { - wsRef.current = new WebSocket( - URLs.NOTIFICATIONS.WEBSOCKET.replace("$deviceId", deviceId) - ); - - wsRef.current.onopen = () => { - console.log("WebSocket connected"); - }; - - wsRef.current.onmessage = (event: MessageEvent) => { - try { - const data = JSON.parse(event.data); - setNotifications((prev) => [...prev, data]); - } catch (err) { - console.error("Invalid WS notification data:", err); - } - }; - - wsRef.current.onclose = () => { - console.log("WebSocket disconnected… reconnecting"); - setTimeout(connectWebSocket, 2000); - }; - - wsRef.current.onerror = (err) => { - console.error("WebSocket error", err); - wsRef.current?.close(); - }; - } - }; - - return ( - - {children} - - ); -}; - -export const useNotifications = () => { - const context = useContext(NotificationContext); - if (!context) - throw new Error( - "useNotifications must be used inside NotificationProvider" - ); - return context; -}; diff --git a/interfaces/IMatch.d.ts b/interfaces/IMatch.d.ts index 9bdd25c..b9c7308 100644 --- a/interfaces/IMatch.d.ts +++ b/interfaces/IMatch.d.ts @@ -12,6 +12,16 @@ interface IMatchTeam { logo: string; } +interface IMatchLineUp { + id: number, + player: IBasePlayer; + team: ITeamBase, + match: number, + poistion: string; + is_starting: boolean; + minutes_played: number; +} + export interface IMatchBase { id: number; type: MatchType; @@ -43,16 +53,7 @@ export interface IMatch extends IMatchBase { goals: IGoal[]; cards: ICard[]; substitutions: ISubtitution[]; -} - - -export interface IMatchAppearance { - id: number; - player: IBasePlayer; - team: ITeamBase; - position: string; - is_starting: boolean; - minutesPlayed: number; + lineups: IMatchLineUp[]; } diff --git a/services/MatchService.ts b/services/MatchService.ts index 4ce315b..b26b40a 100644 --- a/services/MatchService.ts +++ b/services/MatchService.ts @@ -1,18 +1,23 @@ import { URLs } from "../constants/urls"; -import { IMatchBase, IMatch, IMatchAppearance, IMatchEvent } from "@/interfaces/IMatch"; +import { + IMatchBase, + IMatch, + IMatchEvent, + IMatchLineUp, +} from "@/interfaces/IMatch"; export class MatchService { public static async getMatchsByDay(date: Date): Promise { const response = await fetch( URLs.MATCHS.BY_DAY + - `?date=${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}` + `?date=${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`, ); return response.json() as Promise; } public static async getMatchById(matchId: number): Promise { const response = await fetch( - URLs.MATCHS.BY_ID.replace("$id", matchId.toString()) + URLs.MATCHS.BY_ID.replace("$id", matchId.toString()), ); return response.json() as Promise; } @@ -20,11 +25,11 @@ export class MatchService { public static async getMatchsByChampionshipEdition( championshipId: number, editionId: number, - status: string = "" + status: string = "", ): Promise { let url: string = URLs.MATCHS.BY_CHAMPIONSHIP_EDITION.replace( "$championshipId", - championshipId.toString() + championshipId.toString(), ).replace("$editionId", editionId.toString()); if (status !== "") { url += `?status=${status}`; @@ -35,11 +40,11 @@ export class MatchService { public static async getMatchsByEdition( editionId: number, - status: string = "" + status: string = "", ): Promise { let url: string = URLs.MATCHS.BY_EDITION.replace( "$editionId", - editionId.toString() + editionId.toString(), ); if (status !== "") { url += `?status=${status}`; @@ -50,7 +55,7 @@ export class MatchService { public static async getMatchsByTeam( teamId: number, - status: string = "" + status: string = "", ): Promise { let url: string = URLs.MATCHS.BY_TEAM.replace("$teamId", teamId.toString()); if (status !== "") { @@ -60,24 +65,26 @@ export class MatchService { return response.json() as Promise; } - public static async getMatchAppearances( - matchId: number - ): Promise { - const response = await fetch( - URLs.MATCHS.APPEARANCE.replace("$matchId", matchId.toString()) + public static async getMatchLineUp(matchId: number): Promise { + const url: string = URLs.MATCHS.LINEUPS.replace( + "$matchId", + matchId.toString(), ); - return response.json() as Promise; + const response = await fetch(url); + return response.json() as Promise; } - public static async getMatchTimeline(_matchId: number): Promise{ + public static async getMatchTimeline( + _matchId: number, + ): Promise { + console.log(_matchId); return []; } - public static async getUpcomingMatchs(limit: number = 10): Promise{ - const response = await fetch( - URLs.MATCHS.UPCOMING + - `?limit=${limit}` - ); + public static async getUpcomingMatchs( + limit: number = 10, + ): Promise { + const response = await fetch(URLs.MATCHS.UPCOMING + `?limit=${limit}`); return response.json() as Promise; } } diff --git a/services/NotificationService.ts b/services/NotificationService.ts deleted file mode 100644 index 20aece2..0000000 --- a/services/NotificationService.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { URLs } from "@/constants/urls"; - - -export class NotificationService { - public static async registerDeviceOnServer(): Promise { - try { - const res = await fetch(URLs.DEVICES.REGISTER, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({}), - }); - - if (!res.ok) { - console.warn("Device registration returned non-OK:", res.status); - } - const data = await res.json() as {device_id: string} - return data.device_id; - } catch (err) { - console.warn("Device registration error:", err); - return ""; - } - } -} diff --git a/services/wsClient.ts b/services/wsClient.ts deleted file mode 100644 index a953899..0000000 --- a/services/wsClient.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { BrowserEmitter } from "@/utils/BrowserEmitter"; - -export type WSMessage = any; - -export interface WSClientOptions { - url: string; - protocols?: string | string[]; - reconnectInterval?: number; // initial ms - maxReconnectInterval?: number; - reconnectDecay?: number; - heartbeatInterval?: number; - onOpen?: () => void; - onClose?: () => void; - onMessage?: (msg: WSMessage) => void; -} - -export class WSClient extends BrowserEmitter { - private url: string; - private protocols?: string | string[]; - private ws?: WebSocket | null; - private reconnectInterval: number; - private maxReconnectInterval: number; - private reconnectDecay: number; - private heartbeatInterval: number; - private heartbeatTimer?: number | null; - private reconnectTimer?: number | null; - private shouldReconnect = true; - private connected = false; - private onOpen?: () => void; - private onClose?: () => void; - private onMessage?: (msg: WSMessage) => void; - - constructor(opts: WSClientOptions) { - super(); - this.url = opts.url; - this.protocols = opts.protocols; - this.reconnectInterval = opts.reconnectInterval ?? 1000; // 1s - this.maxReconnectInterval = opts.maxReconnectInterval ?? 30000; // 30s - this.reconnectDecay = opts.reconnectDecay ?? 1.5; - this.heartbeatInterval = opts.heartbeatInterval ?? 25000; // 25s - this.onOpen = opts.onOpen; - this.onClose = opts.onClose; - this.onMessage = opts.onMessage; - this.connect(); - } - - private connect() { - this.clearTimers(); - try { - this.ws = new WebSocket(this.url, this.protocols ?? undefined); - } catch (err) { - this.scheduleReconnect(); - return; - } - - this.ws.onopen = () => { - this.connected = true; - this.emit("open"); - this.onOpen?.(); - this.startHeartbeat(); - }; - - this.ws.onmessage = (ev) => { - let data: any = ev.data; - try { - data = JSON.parse(ev.data); - } catch { - // keep raw - } - this.emit("message", data); - this.onMessage?.(data); - }; - - this.ws.onclose = (ev) => { - this.connected = false; - this.emit("close", ev); - this.onClose?.(); - if (this.shouldReconnect) this.scheduleReconnect(); - }; - - this.ws.onerror = (ev) => { - this.emit("error", ev); - // underlying socket will call onclose, schedule reconnect there - }; - } - - private startHeartbeat() { - this.stopHeartbeat(); - this.heartbeatTimer = window.setInterval(() => { - try { - if (this.ws && this.ws.readyState === WebSocket.OPEN) { - // send ping frame as JSON {type: 'ping'} - this.ws.send(JSON.stringify({ type: "ping", ts: Date.now() })); - } else { - // not open, reconnect - this.scheduleReconnect(); - } - } catch { - this.scheduleReconnect(); - } - }, this.heartbeatInterval); - } - - private stopHeartbeat() { - if (this.heartbeatTimer) { - window.clearInterval(this.heartbeatTimer); - this.heartbeatTimer = null; - } - } - - private scheduleReconnect() { - // compute next interval - const next = Math.min( - this.reconnectInterval * this.reconnectDecay, - this.maxReconnectInterval - ); - this.reconnectInterval = next; - this.clearTimers(); - // schedule reconnect - this.reconnectTimer = window.setTimeout(() => { - this.connect(); - }, next); - } - - private clearTimers() { - if (this.reconnectTimer) { - window.clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; - } - } - - public send(data: any) { - if (this.ws && this.ws.readyState === WebSocket.OPEN) { - const payload = typeof data === "string" ? data : JSON.stringify(data); - this.ws.send(payload); - } else { - throw new Error("WebSocket not open"); - } - } - - public close() { - this.shouldReconnect = false; - this.stopHeartbeat(); - if (this.ws) { - try { - this.ws.close(); - } catch {} - } - this.clearTimers(); - } - - public isConnected() { - return this.connected; - } -} diff --git a/tailwind.config.js b/tailwind.config.js index ed6c49f..98d7936 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,84 +1,85 @@ /** @type {import('tailwindcss').Config} */ +import tailwindcssAnimate from "tailwindcss-animate"; + export default { - darkMode: ["class"], - content: [ + darkMode: ["class"], + content: [ "./app/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}", "./pages/**/*.{js,ts,jsx,tsx}", ], theme: { - extend: { - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)' - }, - colors: { - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))' - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))' - }, - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))' - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))' - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))' - }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - chart: { - '1': 'hsl(var(--chart-1))', - '2': 'hsl(var(--chart-2))', - '3': 'hsl(var(--chart-3))', - '4': 'hsl(var(--chart-4))', - '5': 'hsl(var(--chart-5))' - } - }, - keyframes: { - 'accordion-down': { - from: { - height: '0' - }, - to: { - height: 'var(--radix-accordion-content-height)' - } - }, - 'accordion-up': { - from: { - height: 'var(--radix-accordion-content-height)' - }, - to: { - height: '0' - } - } - }, - animation: { - 'accordion-down': 'accordion-down 0.2s ease-out', - 'accordion-up': 'accordion-up 0.2s ease-out' - } - } + extend: { + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + colors: { + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + chart: { + 1: "hsl(var(--chart-1))", + 2: "hsl(var(--chart-2))", + 3: "hsl(var(--chart-3))", + 4: "hsl(var(--chart-4))", + 5: "hsl(var(--chart-5))", + }, + }, + keyframes: { + "accordion-down": { + from: { + height: "0", + }, + to: { + height: "var(--radix-accordion-content-height)", + }, + }, + "accordion-up": { + from: { + height: "var(--radix-accordion-content-height)", + }, + to: { + height: "0", + }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, }, - plugins: [require("tailwindcss-animate")], -} - + plugins: [tailwindcssAnimate], +}; diff --git a/utils/BrowserEmitter.ts b/utils/BrowserEmitter.ts deleted file mode 100644 index ea705e9..0000000 --- a/utils/BrowserEmitter.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type Listener = (...args: any[]) => void; - -export class BrowserEmitter { - private events: Record = {}; - - on(event: string, listener: Listener) { - if (!this.events[event]) this.events[event] = []; - this.events[event].push(listener); - } - - off(event: string, listener: Listener) { - if (!this.events[event]) return; - this.events[event] = this.events[event].filter((l) => l !== listener); - } - - emit(event: string, ...args: any[]) { - if (!this.events[event]) return; - this.events[event].forEach((l) => l(...args)); - } -} diff --git a/utils/device.ts b/utils/device.ts deleted file mode 100644 index 90f7deb..0000000 --- a/utils/device.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { DEVICE_KEY } from "@/constants/commons"; - -export function getDeviceId(): string { - let deviceId = localStorage.getItem(DEVICE_KEY); - if (!deviceId) { - return ""; - } - return deviceId as string; -} - -export function clearDeviceId() { - localStorage.removeItem(DEVICE_KEY); -}