diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..3fb491164 Binary files /dev/null and b/.DS_Store differ diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..b1c3c3bf5 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,16 @@ +{ + "permissions": { + "allow": [ + "PowerShell(Add-Type -AssemblyName System.Drawing; $img = [System.Drawing.Image]::FromFile\\('c:\\\\Users\\\\shalo\\\\MatchLens\\\\Code\\\\client\\\\src\\\\assets\\\\ball.png'\\); \"$\\($img.Width\\)x$\\($img.Height\\)\")", + "Bash(chromium-cli --help)", + "Bash(npx --yes playwright --version)", + "Bash(curl -s http://localhost:5173/ -o /dev/null -w \"%{http_code}\\\\n\")", + "Bash(npx playwright *)", + "mcp__claude_ai_Figma__get_design_context", + "Bash(python3 -c ' *)", + "Bash(taskkill //PID 27472 //F)", + "Bash(git status *)", + "Bash(powershell.exe -NoProfile -Command \"\\(Get-Content 'Code/client/src/pages/Leaderboard.jsx' -Raw\\) -replace 'dash-mlx','primary' | Set-Content -NoNewline -Encoding utf8 'Code/client/src/pages/Leaderboard.jsx'\")" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..d63dcac3d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +Code/CLAUDE.md +planning/frontend_audit.md +Code/CLAUDE.md +planning/frontend_audit.md diff --git a/Code/.gitignore b/Code/.gitignore new file mode 100644 index 000000000..7a70a85ec --- /dev/null +++ b/Code/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.env + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# vite build output (client vite.config.js builds to ../server/public) +server/public diff --git a/Code/client/.env.example b/Code/client/.env.example new file mode 100644 index 000000000..939b3716c --- /dev/null +++ b/Code/client/.env.example @@ -0,0 +1,13 @@ +# API origin. Leave unset for both local dev (the Vite proxy forwards /api) and +# production (the client build is served by Express itself, so same-origin). +# Only needed if the client and API are deployed to different origins. +# +# NOTE: VITE_* values are embedded into the public client bundle. Never secrets. +VITE_API_URL= + +# Cloudinary unsigned upload preset — used for real avatar uploads on the +# Profile page. No backend involvement: the browser uploads directly to +# Cloudinary using these two public values. Fill in your own account's +# cloud name and unsigned upload preset name. +VITE_CLOUDINARY_CLOUD_NAME= +VITE_CLOUDINARY_UPLOAD_PRESET= diff --git a/Code/client/index.html b/Code/client/index.html new file mode 100644 index 000000000..bc06331d0 --- /dev/null +++ b/Code/client/index.html @@ -0,0 +1,16 @@ + + +
+ + ++ MatchLens +
+{subtitle}
+OR CONTINUE WITH
+ ++ {footerPrompt} {footerLinkLabel} +
+Terms of Service · Privacy Policy · Help Center
+v2.4.0-Stable · Systems Operational
++ MatchLens +
+Your ultimate arena companion
+💬 Match Comments
+{comment.author}
+{comment.text}
+Add a comment...
+{formatDate(date)}
++ {isLive && minute != null ? `LIVE ${minute}'` : status} +
+{home}
++ {home_score ?? '–'} - {away_score ?? '–'} +
+{away}
+{team}
+{name}
++ {position} • {assists} assists +
+Goals:
+{goals}
+RANK
+USERNAME
+LOCATION / TITLE
+POINTS
+TOP TEAM
+TREND
+{row.rank}
+{row.username}
+{row.location}
+{row.title}
++ {row.points.toLocaleString()} +
+{row.topTeam}
+RANK
+TEAM
+W
+D
+L
+PTS
++ {rank} +
+ +{team.name}
+ +{team.wins}
+{team.draws}
+{team.losses}
++ {team.points} +
+{homeValue}
+{label}
+{awayValue}
+{team.name}
+{team.region}
+{title}
++ Find and follow national teams and players competing in the FIFA World Cup 2026. +
+Trending:
+ {TRENDING_TAGS.map((tag) => ( + + {tag} + + ))} +Filter by League
+Top Featured Teams
+SEE ALL
+{followsLoadError}
} + {followError &&{followError}
} +{column.title}
++ {link} +
+ ))} ++ MatchLens +
+© 2026 MatchLens Platform. All rights reserved.
+{String(value).padStart(2, '0')}
+{label}
+{home}
+{away}
+{error}
++ {new Date(upcomingMatch.date).toLocaleDateString('en-GB', { + weekday: 'long', + day: '2-digit', + month: 'long', + year: 'numeric', + })}{' '} + • {upcomingMatch.venue} +
+ > + ) : ( +Check back soon for the next fixture.
+ )} +Latest Match
+Coming Match
+Pre-season
+Live Games
+SEE ALL
+No matches scheduled right now.
+ ) : ( +Grand Final
++ {knockoutBracket.final.home} vs {knockoutBracket.final.away} +
+{knockoutBracket.final.venue}
+MATCHDAY 24
+No standings available right now.
+ ) : ( +No player stats available right now.
+ ) : ( ++ 2026 FIFA World Cup Edition — Compete with fans worldwide +
+How To Earn Points & Climb
+{step.title}
+{step.body}
+#47
+Your Predictions Progress
+8,230 pts
++ Top Earning Team: Arsenal +
+ rather than failing silently.
+const OAUTH_ERRORS = {
+ oauth_denied: 'Sign-in was cancelled.',
+ oauth_failed: 'Something went wrong signing in with GitHub. Please try again.',
+ account_conflict: 'That email is already registered to a different account. Try signing in with your password.'
+}
+
+const Login = ({ title }) => {
+ const navigate = useNavigate()
+ const location = useLocation()
+ const [searchParams] = useSearchParams()
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+ const [error, setError] = useState(null)
+ const [loading, setLoading] = useState(false)
+
+ // Where RequireAuth turned this visitor away from, so signing in resumes
+ // the page they actually asked for instead of always landing on the dashboard.
+ const destination = location.state?.from?.pathname ?? '/'
+
+ useEffect(() => {
+ document.title = title
+ }, [title])
+
+ useEffect(() => {
+ const code = searchParams.get('error')
+ if (code) setError(OAUTH_ERRORS[code] ?? OAUTH_ERRORS.oauth_failed)
+ }, [searchParams])
+
+ const handleSignIn = async (event) => {
+ event.preventDefault()
+
+ // Quick client-side check for instant feedback...
+ const validationError = validateLogin(email, password)
+ if (validationError) {
+ setError(validationError)
+ return
+ }
+
+ // ...but the server has the final say on whether login succeeds.
+ try {
+ setLoading(true)
+ const user = await UsersAPI.login(email, password)
+ localStorage.setItem('matchlens_user', JSON.stringify(user))
+ navigate(destination, { replace: true })
+ } catch (err) {
+ setError(err.message)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ )
+}
+
+export default Login
diff --git a/Code/client/src/pages/MatchDetail.jsx b/Code/client/src/pages/MatchDetail.jsx
new file mode 100644
index 000000000..f9cb07d61
--- /dev/null
+++ b/Code/client/src/pages/MatchDetail.jsx
@@ -0,0 +1,167 @@
+import { useEffect, useState } from 'react'
+import { Link, useParams } from 'react-router-dom'
+import VideoPlayer from '../components/VideoPlayer'
+import CommentThread from '../components/CommentThread'
+import Crest from '../components/Crest'
+
+const API_URL = import.meta.env.VITE_API_URL ?? ''
+
+const TABS = ['Overview', 'Lineup', 'Stats', 'Comments', 'Highlights']
+
+function formatDate(dateString) {
+ return new Date(dateString).toLocaleDateString('en-GB', {
+ weekday: 'long',
+ day: '2-digit',
+ month: 'long',
+ year: 'numeric',
+ })
+}
+
+function InProgress({ label }) {
+ return (
+
+ {label} isn't available from the API yet.
+ This section is in progress.
+
+ )
+}
+
+export default function MatchDetail() {
+ const { matchId } = useParams()
+ const [activeTab, setActiveTab] = useState('Overview')
+ const [match, setMatch] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [notFound, setNotFound] = useState(false)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ setLoading(true)
+ setNotFound(false)
+ setError(null)
+ fetch(`${API_URL}/api/matches/${matchId}`)
+ .then((res) => {
+ if (res.status === 404) {
+ setNotFound(true)
+ return null
+ }
+ if (!res.ok) throw new Error('Failed to load match')
+ return res.json()
+ })
+ .then((data) => data && setMatch(data))
+ .catch((err) => {
+ console.error('Failed to load match', err)
+ setError('Could not load this match. Please try again.')
+ })
+ .finally(() => setLoading(false))
+ }, [matchId])
+
+ if (loading) {
+ return (
+
+ Loading match…
+
+ )
+ }
+
+ if (notFound) {
+ return (
+
+ Match not found
+ This match doesn't exist or may have been removed.
+
+ ← Back to Matches
+
+
+ )
+ }
+
+ if (error || !match) {
+ return (
+
+ {error ?? 'Something went wrong.'}
+
+ )
+ }
+
+ const isLiveOrHt = match.status === 'LIVE' || match.status === 'HT'
+
+ return (
+
+
+ {/* Hero */}
+
+
+
+
+ {formatDate(match.date)} • {match.venue}
+ {isLiveOrHt && (
+
+ {match.status === 'HT' ? 'HALF-TIME' : `LIVE ${match.minute}'`}
+
+ )}
+
+
+
+ {match.home}
+
+
+
+
+ {match.home_score ?? '–'} - {match.away_score ?? '–'}
+
+
+
+
+ {match.away}
+
+
+
+
+
+ {/* Tabs */}
+
+ {TABS.map((tab) => {
+ const isActive = tab === activeTab
+ return (
+
+ )
+ })}
+
+
+ {/* Tab content */}
+ {activeTab === 'Overview' && }
+ {activeTab === 'Lineup' && }
+ {activeTab === 'Stats' && }
+ {activeTab === 'Comments' && }
+ {activeTab === 'Highlights' && }
+
+
+ {/* Right sidebar */}
+
+
+ )
+}
diff --git a/Code/client/src/pages/Matches.jsx b/Code/client/src/pages/Matches.jsx
new file mode 100644
index 000000000..d9279ff3f
--- /dev/null
+++ b/Code/client/src/pages/Matches.jsx
@@ -0,0 +1,182 @@
+import { useEffect, useMemo, useState } from 'react'
+import MatchCard from '../components/MatchCard'
+
+const API_URL = import.meta.env.VITE_API_URL ?? ''
+
+// The live API only ever emits LIVE | HT | UPCOMING today. FT is a real
+// status the schema supports but no fixture currently carries, so the
+// Results tab is an honest empty state rather than a broken filter.
+const TABS = [
+ { label: 'Live', statuses: ['LIVE', 'HT'] },
+ { label: 'Upcoming', statuses: ['UPCOMING'] },
+ { label: 'Results', statuses: ['FT'] },
+]
+
+function SearchIcon({ className }) {
+ return (
+
+ )
+}
+
+export default function Matches() {
+ const [matches, setMatches] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [selectedDate, setSelectedDate] = useState('')
+ const [searchTerm, setSearchTerm] = useState('')
+ const [activeTab, setActiveTab] = useState('Live')
+
+ useEffect(() => {
+ setLoading(true)
+ setError(null)
+ const query = selectedDate ? `?date=${selectedDate}` : ''
+ fetch(`${API_URL}/api/matches${query}`)
+ .then((res) => {
+ if (!res.ok) throw new Error('Failed to load matches')
+ return res.json()
+ })
+ .then(setMatches)
+ .catch((err) => {
+ console.error('Failed to load matches', err)
+ setError('Could not load matches. Please try again.')
+ })
+ .finally(() => setLoading(false))
+ }, [selectedDate])
+
+ const activeStatuses = TABS.find((tab) => tab.label === activeTab).statuses
+ const filteredMatches = useMemo(() => {
+ const term = searchTerm.trim().toLowerCase()
+ return matches
+ .filter((match) => activeStatuses.includes(match.status))
+ .filter((match) =>
+ term === ''
+ ? true
+ : [match.home, match.away, match.venue].some((field) => field?.toLowerCase().includes(term)),
+ )
+ }, [matches, activeStatuses, searchTerm])
+
+ const featuredLiveMatch = matches.find((match) => match.status === 'LIVE' || match.status === 'HT')
+
+ return (
+
+
+
+ Matches
+
+ Live analytics and deep match insights across the FIFA World Cup 2026.
+
+
+
+
+
+ {TABS.map((tab) => {
+ const isActive = tab.label === activeTab
+ return (
+
+ )
+ })}
+
+
+
+ setSelectedDate(event.target.value)}
+ className="rounded-lg border border-dash bg-dash-card px-3 py-2 text-[13px] text-white focus:outline-none"
+ />
+ {selectedDate && (
+
+ )}
+
+
+ setSearchTerm(event.target.value)}
+ placeholder="Filter by team or venue..."
+ className="w-full bg-transparent text-[13px] text-white placeholder:text-secondary focus:outline-none"
+ />
+
+
+
+
+ {loading && Loading matches…
}
+ {error && {error}
}
+
+ {!loading && !error && filteredMatches.length === 0 && (
+
+ No {activeTab.toLowerCase()} matches{selectedDate ? ` on ${selectedDate}` : ''}.
+
+ )}
+
+
+ {filteredMatches.map((match) => (
+
+ ))}
+
+
+
+
+
+ )
+}
diff --git a/Code/client/src/pages/NotFound.jsx b/Code/client/src/pages/NotFound.jsx
new file mode 100644
index 000000000..56d444e39
--- /dev/null
+++ b/Code/client/src/pages/NotFound.jsx
@@ -0,0 +1,16 @@
+import { Link } from 'react-router-dom'
+
+export default function NotFound() {
+ return (
+
+ 404
+ Page not found
+
+ The page you're looking for doesn't exist or may have moved.
+
+
+ Back to Dashboard
+
+
+ )
+}
diff --git a/Code/client/src/pages/PlayerDetail.jsx b/Code/client/src/pages/PlayerDetail.jsx
new file mode 100644
index 000000000..01855abb8
--- /dev/null
+++ b/Code/client/src/pages/PlayerDetail.jsx
@@ -0,0 +1,208 @@
+import { useParams } from 'react-router-dom'
+import Avatar from '../components/Avatar'
+import { getMockPlayerDetail } from '../mocks/playerDetail'
+
+function StarRating({ rating }) {
+ const filled = Math.round(rating / 2)
+ return (
+
+ {Array.from({ length: 5 }).map((_, index) => (
+
+ ★
+
+ ))}
+
+ )
+}
+
+export default function PlayerDetail() {
+ const { playerId } = useParams()
+ const player = getMockPlayerDetail(playerId)
+
+ const statBlocks = [
+ { value: player.seasonStats.goals, label: 'Goals' },
+ { value: player.seasonStats.assists, label: 'Assists' },
+ { value: player.seasonStats.appearances, label: 'Appearances' },
+ { value: player.seasonStats.passAccuracy, label: 'Pass Accuracy' },
+ { value: player.seasonStats.keyPasses, label: 'Key Passes / 90' },
+ { value: player.seasonStats.chancesCreated, label: 'Chances Created' },
+ ]
+
+ return (
+
+
+ {/* Hero */}
+
+
+
+
+
+
+ {player.name}
+
+
+ {player.position}
+
+
+ {player.club} #{player.clubNumber} · {player.nationalTeam}
+
+
+
+ Age: {player.age} · Born: {player.born} · Height: {player.height} · Preferred Foot:{' '}
+ {player.preferredFoot}
+
+
+
+
+
+
+
+ {/* Performance overview */}
+
+ Performance Overview ({player.seasonLabel})
+
+ {statBlocks.map((stat, index) => (
+
+
+ {stat.value}
+ {stat.label}
+
+ {index < statBlocks.length - 1 && }
+
+ ))}
+
+
+
+ {/* Dual metrics */}
+
+
+
+ {player.matchLensScore}
+
+
+ MatchLens Score
+ {player.scoreBlurb}
+
+ {player.scoreBadge}
+
+
+
+
+ Market Value & Rating
+
+
+ ESTIMATED VALUE
+
+ {player.marketValue}
+ ↑ from {player.marketValuePrevious}
+
+
+
+ MATCH RATING
+
+ {player.matchRating}
+
+
+
+
+
+
+
+ {/* Career history */}
+
+ Career History
+
+
+ SEASON
+ CLUB
+ APPS
+ GOALS
+ ASSISTS
+ RATING
+
+ {player.careerHistory.map((row) => (
+
+ {row.season}
+ {row.club}
+ {row.apps}
+ {row.goals}
+ {row.assists}
+ {row.rating}
+
+ ))}
+
+
+
+ {/* Attributes + AI note */}
+
+
+ Key Attributes
+
+ {player.attributes.map((attr) => (
+
+
+ {attr.label}
+ {attr.value}
+
+
+
+
+
+ ))}
+
+
+
+
+ ✨ AI Tactical Analysis
+
+ {player.aiNote}
+
+
+
+
+ {/* Right sidebar */}
+
+
+ )
+}
diff --git a/Code/client/src/pages/Profile.jsx b/Code/client/src/pages/Profile.jsx
new file mode 100644
index 000000000..11dab7867
--- /dev/null
+++ b/Code/client/src/pages/Profile.jsx
@@ -0,0 +1,463 @@
+import { useEffect, useState } from 'react'
+import { useSessionUser } from '../hooks/useSessionUser'
+import { teams as followableTeams } from '../mocks/teams'
+import Avatar from '../components/Avatar'
+import Crest from '../components/Crest'
+import Skeleton from '../components/Skeleton'
+
+const API_URL = import.meta.env.VITE_API_URL ?? ''
+const CLOUDINARY_CLOUD_NAME = import.meta.env.VITE_CLOUDINARY_CLOUD_NAME
+const CLOUDINARY_UPLOAD_PRESET = import.meta.env.VITE_CLOUDINARY_UPLOAD_PRESET
+
+const DEFAULT_BIO = 'Tactical obsession. Data-driven insights. World Cup 2026 prediction specialist.'
+
+// Decorative only — no transactions table or trend data exists anywhere
+// (points-beyond-display is GildardoOrea's territory, #10).
+const balanceTrend = [40, 55, 35, 60, 30, 70, 50]
+const recentTransactions = [
+ { label: 'Argentina vs Mexico ✅', amount: '+320 pts', positive: true },
+ { label: 'France vs Germany ❌', amount: '-200 pts', positive: false },
+ { label: 'Brazil vs Serbia ✅', amount: '+450 pts', positive: true },
+]
+
+const connectedApps = [
+ { name: 'Twitter/X', connected: true },
+ { name: 'Discord', connected: false },
+ { name: 'Spotify', connected: false },
+]
+
+function ToggleSwitch({ checked, onChange }) {
+ return (
+
+ )
+}
+
+export default function Profile() {
+ const sessionUser = useSessionUser()
+ const userId = sessionUser?.id
+
+ const [user, setUser] = useState(null)
+ const [loadingUser, setLoadingUser] = useState(true)
+ const [userError, setUserError] = useState(null)
+ const [followedTeams, setFollowedTeams] = useState([])
+ const [loadingFollows, setLoadingFollows] = useState(true)
+ const [followsLoadError, setFollowsLoadError] = useState(null)
+ const [followError, setFollowError] = useState(null)
+ const [showTeamPicker, setShowTeamPicker] = useState(false)
+
+ const [isEditingProfile, setIsEditingProfile] = useState(false)
+ const [avatarUrl, setAvatarUrl] = useState(null)
+ const [uploading, setUploading] = useState(false)
+ const [uploadError, setUploadError] = useState(null)
+ const [bio, setBio] = useState(() => localStorage.getItem(`matchlens:bio:${userId}`) ?? DEFAULT_BIO)
+ const [bioDraft, setBioDraft] = useState(bio)
+
+ const [notifPrefs, setNotifPrefs] = useState({
+ matchAlerts: true,
+ transferNews: true,
+ worldCupUpdates: true,
+ leaderboardMilestones: false,
+ predictionResults: true,
+ })
+ const [privacyPrefs, setPrivacyPrefs] = useState({
+ profileVisibility: true,
+ dataSharing: false,
+ showPredictionHistory: true,
+ })
+
+ useEffect(() => {
+ if (!userId) return
+
+ setLoadingUser(true)
+ setUserError(null)
+ fetch(`${API_URL}/api/users/${userId}`)
+ .then((res) => {
+ if (!res.ok) throw new Error('Failed to load user')
+ return res.json()
+ })
+ .then((data) => {
+ setUser(data)
+ setAvatarUrl(data.profile_image_url)
+ })
+ .catch((err) => {
+ console.error('Failed to load user', err)
+ setUserError('Could not load your profile.')
+ })
+ .finally(() => setLoadingUser(false))
+
+ setLoadingFollows(true)
+ setFollowsLoadError(null)
+ fetch(`${API_URL}/api/follows/user/${userId}`)
+ .then((res) => {
+ if (!res.ok) throw new Error('Failed to load followed teams')
+ return res.json()
+ })
+ .then(setFollowedTeams)
+ .catch((err) => {
+ console.error('Failed to load followed teams', err)
+ setFollowsLoadError('Could not load followed teams.')
+ })
+ .finally(() => setLoadingFollows(false))
+ }, [userId])
+
+ async function handleAvatarChange(event) {
+ const file = event.target.files?.[0]
+ if (!file) return
+
+ if (!CLOUDINARY_CLOUD_NAME || !CLOUDINARY_UPLOAD_PRESET) {
+ setUploadError(
+ 'Cloudinary is not configured — set VITE_CLOUDINARY_CLOUD_NAME and VITE_CLOUDINARY_UPLOAD_PRESET in client/.env',
+ )
+ return
+ }
+
+ setUploading(true)
+ setUploadError(null)
+ try {
+ const formData = new FormData()
+ formData.append('file', file)
+ formData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET)
+
+ const uploadRes = await fetch(
+ `https://api.cloudinary.com/v1_1/${CLOUDINARY_CLOUD_NAME}/image/upload`,
+ { method: 'POST', body: formData },
+ )
+ if (!uploadRes.ok) throw new Error('Cloudinary upload failed')
+ const uploadData = await uploadRes.json()
+ setAvatarUrl(uploadData.secure_url)
+
+ const patchRes = await fetch(`${API_URL}/api/users/${userId}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ profile_image_url: uploadData.secure_url }),
+ })
+ if (!patchRes.ok) throw new Error('Failed to save avatar')
+ const updatedUser = await patchRes.json()
+ setUser(updatedUser)
+
+ // Keep the Sidebar's avatar (read from this hint) in sync without
+ // waiting for the next full session check.
+ localStorage.setItem('matchlens_user', JSON.stringify({ ...sessionUser, ...updatedUser }))
+ } catch (err) {
+ setUploadError('Upload failed. Please try again.')
+ } finally {
+ setUploading(false)
+ }
+ }
+
+ function handleSaveBio() {
+ setBio(bioDraft)
+ localStorage.setItem(`matchlens:bio:${userId}`, bioDraft)
+ setIsEditingProfile(false)
+ }
+
+ async function handleFollowTeam(team) {
+ setFollowError(null)
+ try {
+ const res = await fetch(`${API_URL}/api/follows`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ user_id: userId, api_team_id: team.api_team_id, team_name: team.name }),
+ })
+ if (!res.ok) throw new Error('Follow failed')
+ const created = await res.json()
+ setFollowedTeams((prev) => [...prev, created])
+ setShowTeamPicker(false)
+ } catch (err) {
+ setFollowError('Could not follow team. Please try again.')
+ }
+ }
+
+ async function handleUnfollowTeam(followedTeamId) {
+ setFollowError(null)
+ try {
+ const res = await fetch(`${API_URL}/api/follows/${followedTeamId}`, { method: 'DELETE' })
+ if (!res.ok) throw new Error('Unfollow failed')
+ setFollowedTeams((prev) => prev.filter((team) => team.followed_team_id !== followedTeamId))
+ } catch (err) {
+ setFollowError('Could not unfollow team. Please try again.')
+ }
+ }
+
+ const pickableTeams = followableTeams.filter(
+ (team) => !followedTeams.some((followed) => String(followed.api_team_id) === String(team.api_team_id)),
+ )
+
+ return (
+
+
+ Profile & Account Settings
+
+ Manage your predictive avatar, tracked national teams, and account safety.
+
+
+
+
+
+ {/* Profile header */}
+
+
+ {loadingUser ? (
+
+
+
+
+
+
+
+ ) : userError ? (
+ {userError}
+ ) : (
+
+
+
+ {user?.username}
+ @{user?.username}
+
+
+ )}
+
+
+
+ {isEditingProfile && (
+
+
+ Avatar
+
+ {uploading && Uploading…
}
+ {uploadError && {uploadError}
}
+
+
+ Bio
+
+
+
+ )}
+
+ {bio}
+
+ 📍 London, UK
+ Joined March 2024
+
+
+
+ {/* Points balance */}
+
+
+
+ Points Balance
+ {loadingUser ? (
+
+ ) : (
+
+ {(user?.points ?? 0).toLocaleString()}
+
+ )}
+
+
+ Top Up Balance
+
+
+
+
+
+ Balance Trend (Last 30 days)
+
+
+ {balanceTrend.map((height, index) => (
+
+ ))}
+
+
+
+
+ Recent Transactions
+ {recentTransactions.map((tx) => (
+
+ {tx.label}
+ {tx.amount}
+
+ ))}
+
+
+
+ {/* Followed teams */}
+
+ Followed Teams
+ {followError && {followError}
}
+
+ {loadingFollows &&
+ [0, 1].map((i) => (
+
+
+
+
+ ))}
+ {!loadingFollows && followsLoadError && (
+ {followsLoadError}
+ )}
+ {!loadingFollows &&
+ !followsLoadError &&
+ followedTeams.map((team) => (
+
+
+
+ {team.team_name}
+
+
+
+ ))}
+ {!loadingFollows && !followsLoadError && followedTeams.length === 0 && (
+ Not following any teams yet.
+ )}
+
+
+ {showTeamPicker ? (
+
+ {pickableTeams.map((team) => (
+
+ ))}
+ {pickableTeams.length === 0 && (
+ All available teams are already followed.
+ )}
+
+ ) : (
+
+ )}
+
+
+
+ {/* Right column */}
+
+
+ Account Settings
+
+
+ Notifications
+ {[
+ ['matchAlerts', 'Match Alerts'],
+ ['transferNews', 'Transfer News'],
+ ['worldCupUpdates', 'World Cup 2026 Updates'],
+ ['leaderboardMilestones', 'Leaderboard Milestones'],
+ ['predictionResults', 'Prediction Results'],
+ ].map(([key, label]) => (
+
+ {label}
+ setNotifPrefs((prev) => ({ ...prev, [key]: !prev[key] }))}
+ />
+
+ ))}
+
+
+
+
+
+ Privacy
+ {[
+ ['profileVisibility', 'Profile Visibility'],
+ ['dataSharing', 'Data Sharing'],
+ ['showPredictionHistory', 'Show Prediction History'],
+ ].map(([key, label]) => (
+
+ {label}
+ setPrivacyPrefs((prev) => ({ ...prev, [key]: !prev[key] }))}
+ />
+
+ ))}
+
+
+
+
+
+ Connected Apps
+ {connectedApps.map((app) => (
+
+
+ {app.name} · {app.connected ? 'Connected ✓' : 'Not Connected'}
+
+
+
+ ))}
+
+
+
+
+ Danger Zone
+
+ This action cannot be undone. All your predictions, points balance, and leaderboard history will be
+ permanently deleted.
+
+
+
+
+
+
+
+ )
+}
diff --git a/Code/client/src/pages/Signup.jsx b/Code/client/src/pages/Signup.jsx
new file mode 100644
index 000000000..23a93f22d
--- /dev/null
+++ b/Code/client/src/pages/Signup.jsx
@@ -0,0 +1,111 @@
+import { useState, useEffect } from 'react'
+import { useNavigate } from 'react-router-dom'
+import UsersAPI from '../services/UsersAPI'
+import AuthLayout from '../components/AuthLayout'
+import { TextField, PasswordField } from '../components/AuthFields'
+import { validateSignup } from '../utilities/validateSignup'
+
+const Signup = ({ title }) => {
+ const navigate = useNavigate()
+ const [email, setEmail] = useState('')
+ const [username, setUsername] = useState('')
+ const [password, setPassword] = useState('')
+ const [confirmPassword, setConfirmPassword] = useState('')
+ const [error, setError] = useState(null)
+ const [loading, setLoading] = useState(false)
+
+ useEffect(() => {
+ document.title = title
+ }, [title])
+
+ const handleSignUp = async (event) => {
+ event.preventDefault()
+
+ // Quick client-side check for instant feedback...
+ const validationError = validateSignup(email, username, password, confirmPassword)
+ if (validationError) {
+ setError(validationError)
+ return
+ }
+
+ // ...but the server owns uniqueness, so it has the final say.
+ try {
+ setLoading(true)
+ const user = await UsersAPI.createUser({ username, email, password })
+ localStorage.setItem('matchlens_user', JSON.stringify(user))
+ navigate('/', { replace: true })
+ } catch (err) {
+ setError(err.message)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ )
+}
+
+export default Signup
diff --git a/Code/client/src/pages/TeamDetail.jsx b/Code/client/src/pages/TeamDetail.jsx
new file mode 100644
index 000000000..a6774c5f9
--- /dev/null
+++ b/Code/client/src/pages/TeamDetail.jsx
@@ -0,0 +1,153 @@
+import { useState } from 'react'
+import { useParams } from 'react-router-dom'
+import PlayerCard from '../components/PlayerCard'
+import Crest from '../components/Crest'
+import { getMockTeamDetail } from '../mocks/teamDetail'
+
+export default function TeamDetail() {
+ const { teamId } = useParams()
+ const team = getMockTeamDetail(teamId)
+ const [isFollowing, setIsFollowing] = useState(team.isFollowing)
+
+ return (
+
+
+ {/* Hero */}
+
+
+
+
+
+
+ {team.name}
+ {team.tagline}
+ {team.meta}
+
+
+
+
+
+
+ {/* Recent form + metrics */}
+
+
+ Recent Form
+
+ {team.recentForm.map((entry, index) => (
+
+
+ {entry.result}
+
+ {entry.score}
+
+ ))}
+
+
+
+ Tournament Metrics
+
+
+ Goal Difference
+ {team.metrics.goalDifference}
+
+
+ Goals Scored
+ {team.metrics.goalsScored}
+
+
+ Goals Conceded
+ {team.metrics.goalsConceded}
+
+
+
+
+
+ {/* Upcoming fixtures */}
+
+ Upcoming Fixtures
+
+ {team.upcomingFixtures.map((fixture, index) => (
+
+
+ {fixture.date}
+ {fixture.time}
+
+
+
+ {fixture.homeTeam}
+
+
+ vs
+
+
+ {fixture.awayTeam}
+
+
+
+ {fixture.venue}
+
+
+ ))}
+
+ VIEW FULL SCHEDULE →
+
+
+ {/* Squad highlights */}
+
+ Squad Highlights
+
+ {team.roster.map((player) => (
+
+ ))}
+
+
+
+
+ {/* Right sidebar */}
+
+
+ )
+}
diff --git a/Code/client/src/services/AuthAPI.jsx b/Code/client/src/services/AuthAPI.jsx
new file mode 100644
index 000000000..e690fb594
--- /dev/null
+++ b/Code/client/src/services/AuthAPI.jsx
@@ -0,0 +1,27 @@
+import { AUTH_ORIGIN } from '../config/api'
+
+// OAuth lives on the API origin (port 3000 in dev), not behind the /api vite
+// proxy, so these calls are cross-origin and must send the session cookie
+// explicitly.
+const AuthAPI = {
+ // Returns the logged-in user, or null if this browser has no live session
+ getSession: async () => {
+ try {
+ const res = await fetch(`${AUTH_ORIGIN}/auth/login/success`, {
+ credentials: 'include'
+ })
+ if (!res.ok) return null
+ const data = await res.json()
+ return data.user ?? null
+ } catch {
+ return null
+ }
+ },
+
+ logout: async () => {
+ await fetch(`${AUTH_ORIGIN}/auth/logout`, { credentials: 'include' })
+ localStorage.removeItem('matchlens_user')
+ }
+}
+
+export default AuthAPI
diff --git a/Code/client/src/services/UsersAPI.jsx b/Code/client/src/services/UsersAPI.jsx
new file mode 100644
index 000000000..f69774c07
--- /dev/null
+++ b/Code/client/src/services/UsersAPI.jsx
@@ -0,0 +1,36 @@
+const BASE_URL = '/api'
+
+const UsersAPI = {
+ // Log in — server decides whether access is granted
+ login: async (email, password) => {
+ const res = await fetch(`${BASE_URL}/users/login`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email, password })
+ })
+ const data = await res.json().catch(() => ({}))
+ if (!res.ok) throw new Error(data.error || 'Login failed')
+ return data
+ },
+
+ // Fan leaderboard — all users ranked by points
+ getAllUsers: async () => {
+ const res = await fetch(`${BASE_URL}/users`)
+ if (!res.ok) throw new Error('Failed to fetch users')
+ return res.json()
+ },
+
+ // Sign up
+ createUser: async (user) => {
+ const res = await fetch(`${BASE_URL}/users`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(user)
+ })
+ const data = await res.json().catch(() => ({}))
+ if (!res.ok) throw new Error(data.error || 'Failed to create user')
+ return data
+ }
+}
+
+export default UsersAPI
diff --git a/Code/client/src/utilities/monogram.js b/Code/client/src/utilities/monogram.js
new file mode 100644
index 000000000..eed4b89a3
--- /dev/null
+++ b/Code/client/src/utilities/monogram.js
@@ -0,0 +1,49 @@
+// Shared helper for Crest and Avatar. Team crests, match badges, and player
+// photos have no image source anywhere in the API. Every team/match/player
+// endpoint is hand-written dummy data with zero image/crest/badge/photo fields.
+// Rather than a blank grey block (or worse, an invented logo), these render a
+// deterministic initials badge so the same name always gets the same look.
+
+const PALETTES = [
+ { bg: 'bg-primary/15', text: 'text-primary' },
+ { bg: 'bg-dash-away/15', text: 'text-dash-away' },
+ { bg: 'bg-dash-gold/15', text: 'text-dash-gold' },
+ { bg: 'bg-accent-pink/15', text: 'text-accent-pink' },
+ { bg: 'bg-accent-blue/15', text: 'text-accent-blue' },
+ { bg: 'bg-white/10', text: 'text-secondary' },
+]
+
+export function hashString(value) {
+ let hash = 0
+ for (let i = 0; i < value.length; i++) {
+ hash = (hash * 31 + value.charCodeAt(i)) >>> 0
+ }
+ return hash
+}
+
+export function paletteFor(seed) {
+ const value = seed || '?'
+ return PALETTES[hashString(value) % PALETTES.length]
+}
+
+// Already-short codes (API match.home/away, e.g. "MCI") pass through as-is;
+// multi-word names take first+last initials; single long words are clipped.
+export function shortLabel(value) {
+ const trimmed = (value || '?').trim()
+ if (!trimmed) return '?'
+ const words = trimmed.split(/\s+/).filter(Boolean)
+ if (words.length >= 2) {
+ return (words[0][0] + words[words.length - 1][0]).toUpperCase()
+ }
+ return trimmed.length <= 4 ? trimmed.toUpperCase() : trimmed.slice(0, 3).toUpperCase()
+}
+
+export function getInitials(value) {
+ const trimmed = (value || '?').trim()
+ if (!trimmed) return '?'
+ const words = trimmed.split(/\s+/).filter(Boolean)
+ if (words.length >= 2) {
+ return (words[0][0] + words[words.length - 1][0]).toUpperCase()
+ }
+ return trimmed.slice(0, 2).toUpperCase()
+}
diff --git a/Code/client/src/utilities/validateLogin.js b/Code/client/src/utilities/validateLogin.js
new file mode 100644
index 000000000..c583c8a06
--- /dev/null
+++ b/Code/client/src/utilities/validateLogin.js
@@ -0,0 +1,15 @@
+// Shape check only, for instant feedback before we hit the network.
+// The server verifies the credentials against the database — see
+// usersController.login.
+export const isValidEmail = (email) =>
+ /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())
+
+export const validateLogin = (email, password) => {
+ if (!isValidEmail(email)) {
+ return 'Enter a valid email address (e.g. name@example.com).'
+ }
+ if (!password || password.trim().length === 0) {
+ return 'Enter your password.'
+ }
+ return null
+}
diff --git a/Code/client/src/utilities/validateSignup.js b/Code/client/src/utilities/validateSignup.js
new file mode 100644
index 000000000..207bc3a08
--- /dev/null
+++ b/Code/client/src/utilities/validateSignup.js
@@ -0,0 +1,40 @@
+import { isValidEmail } from './validateLogin'
+
+// Shape check only, for instant feedback before we hit the network. The server
+// re-runs every one of these in usersController.createUser and owns the
+// uniqueness checks, which the client can't do.
+//
+// Keep these bounds in step with the constants in that controller — if they
+// drift, the user gets a different message depending on which side rejects.
+const USERNAME_MIN = 3
+const USERNAME_MAX = 50
+const PASSWORD_MIN = 8
+const PASSWORD_MAX = 72
+
+const hasLegalCharacters = (username) => /^[A-Za-z0-9_-]+$/.test(username)
+
+export const validateSignup = (email, username, password, confirmPassword) => {
+ if (!isValidEmail(email)) {
+ return 'Enter a valid email address (e.g. name@example.com).'
+ }
+
+ const trimmedUsername = username.trim()
+ if (trimmedUsername.length < USERNAME_MIN || trimmedUsername.length > USERNAME_MAX) {
+ return `Username must be between ${USERNAME_MIN} and ${USERNAME_MAX} characters.`
+ }
+ if (!hasLegalCharacters(trimmedUsername)) {
+ return 'Username can only contain letters, numbers, hyphens and underscores.'
+ }
+
+ if (password.length < PASSWORD_MIN) {
+ return `Password must be at least ${PASSWORD_MIN} characters.`
+ }
+ if (password.length > PASSWORD_MAX) {
+ return `Password must be ${PASSWORD_MAX} characters or fewer.`
+ }
+ if (password !== confirmPassword) {
+ return 'Passwords do not match.'
+ }
+
+ return null
+}
diff --git a/Code/client/tailwind.config.js b/Code/client/tailwind.config.js
new file mode 100644
index 000000000..3565b23d8
--- /dev/null
+++ b/Code/client/tailwind.config.js
@@ -0,0 +1,65 @@
+import defaultTheme from 'tailwindcss/defaultTheme'
+
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./index.html', './src/**/*.{js,jsx}'],
+ theme: {
+ extend: {
+ fontFamily: {
+ sans: ['Montserrat', ...defaultTheme.fontFamily.sans],
+ },
+ colors: {
+ // Design system swatches
+ primary: '#46FF6F',
+ 'primary-light': '#F1FFE2',
+ black: '#000000',
+ dark: '#1C1C1C',
+ white: '#FFFFFF',
+ surface: '#EFF4F5',
+ card: '#EDEDED',
+ secondary: '#A6A6A6',
+ tertiary: '#898989',
+ muted: '#939393',
+ disabled: '#7D7D7D',
+ ui: {
+ separator: '#D9D9D9',
+ input: '#484848',
+ 'light-gray': '#F3F3F3',
+ },
+ accent: {
+ green: '#E3FFE2',
+ blue: '#E2FAFF',
+ pink: '#FFE2EC',
+ yellow: '#FFFEE2',
+ red: '#FFE2E2',
+ 'blue-solid': '#399FFD',
+ },
+ // Dashboard dark-theme tokens (not in the official design system swatches)
+ dashboard: '#1C1C1C',
+ 'dash-card': '#222327',
+ dash: '#313131',
+ 'dash-sidebar': '#141414',
+ 'dash-input': '#2E3034',
+ 'dash-live': '#EF4444',
+ 'dash-neutral': '#3A3C42',
+ 'dash-away': '#218AF3',
+ 'dash-gold': '#FFD700',
+ },
+ fontSize: {
+ // Design system type scale (Montserrat) — name matches the Figma layer name
+ 'display-lg': ['40px', { fontWeight: '400' }],
+ 'display-md': ['33px', { fontWeight: '500' }],
+ 'display-sm': ['30px', { fontWeight: '400' }],
+ h1: ['30px', { fontWeight: '700' }],
+ h2: ['20px', { fontWeight: '600' }],
+ h3: ['20px', { fontWeight: '500' }],
+ 'body-lg': ['15px', { fontWeight: '400' }],
+ 'body-md': ['14px', { fontWeight: '400' }],
+ caption: ['12px', { fontWeight: '400' }],
+ overline: ['10px', { fontWeight: '500' }],
+ tiny: ['8px', { fontWeight: '400' }],
+ },
+ },
+ },
+ plugins: [],
+}
diff --git a/Code/client/vite.config.js b/Code/client/vite.config.js
new file mode 100644
index 000000000..a63e50bbc
--- /dev/null
+++ b/Code/client/vite.config.js
@@ -0,0 +1,30 @@
+import { defineConfig, loadEnv } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vitejs.dev/config/
+export default defineConfig(({ mode }) => {
+ const env = loadEnv(mode, process.cwd())
+ const apiUrl = env.VITE_API_URL || 'http://localhost:3000'
+
+ return {
+ plugins: [react()],
+ build: {
+ outDir: '../server/public',
+ emptyOutDir: true
+ },
+ server: {
+ proxy: {
+ // dev only — never ships. Follows VITE_API_URL so changing the API port
+ // doesn't mean editing this file.
+ //
+ // Only /api is proxied. /auth is deliberately left off: the GitHub
+ // OAuth round trip has to land back on the API's own origin for the
+ // session cookie to be set, so the client calls it absolutely instead
+ // (see src/config/api.js).
+ '/api': {
+ target: apiUrl
+ }
+ }
+ }
+ }
+})
diff --git a/Code/package-lock.json b/Code/package-lock.json
new file mode 100644
index 000000000..22ccab23f
--- /dev/null
+++ b/Code/package-lock.json
@@ -0,0 +1,3884 @@
+{
+ "name": "matchlens",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "matchlens",
+ "dependencies": {
+ "bcrypt": "^6.0.0",
+ "concurrently": "^7.6.0",
+ "cors": "^2.8.6",
+ "dotenv": "^16.0.3",
+ "express": "^4.18.2",
+ "express-session": "^1.19.0",
+ "nodemon": "^2.0.21",
+ "passport": "^0.7.0",
+ "passport-github2": "^0.1.12",
+ "pg": "^8.10.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.9.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.0.28",
+ "@types/react-dom": "^18.0.11",
+ "@vitejs/plugin-react": "^3.1.0",
+ "autoprefixer": "^10.5.4",
+ "postcss": "^8.5.23",
+ "tailwindcss": "^3.4.19",
+ "vite": "^4.2.0"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
+ "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
+ "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
+ "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
+ "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
+ "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
+ "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
+ "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
+ "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
+ "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
+ "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
+ "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
+ "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
+ "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
+ "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
+ "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
+ "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
+ "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
+ "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
+ "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
+ "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.3",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz",
+ "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.31",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
+ "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-3.1.0.tgz",
+ "integrity": "sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.20.12",
+ "@babel/plugin-transform-react-jsx-self": "^7.18.6",
+ "@babel/plugin-transform-react-jsx-source": "^7.19.6",
+ "magic-string": "^0.27.0",
+ "react-refresh": "^0.14.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.1.0-beta.0"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.5.4",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz",
+ "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.6",
+ "caniuse-lite": "^1.0.30001806",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/base64url": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
+ "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.43",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz",
+ "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/bcrypt": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
+ "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-addon-api": "^8.3.0",
+ "node-gyp-build": "^4.8.4"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.6",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz",
+ "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.15.1",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.6",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz",
+ "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.42",
+ "caniuse-lite": "^1.0.30001803",
+ "electron-to-chromium": "^1.5.389",
+ "node-releases": "^2.0.51",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001806",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
+ "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "license": "MIT"
+ },
+ "node_modules/concurrently": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.6.0.tgz",
+ "integrity": "sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "date-fns": "^2.29.1",
+ "lodash": "^4.17.21",
+ "rxjs": "^7.0.0",
+ "shell-quote": "^1.7.3",
+ "spawn-command": "^0.0.2-1",
+ "supports-color": "^8.1.0",
+ "tree-kill": "^1.2.2",
+ "yargs": "^17.3.1"
+ },
+ "bin": {
+ "conc": "dist/bin/concurrently.js",
+ "concurrently": "dist/bin/concurrently.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/date-fns": {
+ "version": "2.30.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
+ "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.21.0"
+ },
+ "engines": {
+ "node": ">=0.11"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/date-fns"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.393",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.393.tgz",
+ "integrity": "sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
+ "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.18.20",
+ "@esbuild/android-arm64": "0.18.20",
+ "@esbuild/android-x64": "0.18.20",
+ "@esbuild/darwin-arm64": "0.18.20",
+ "@esbuild/darwin-x64": "0.18.20",
+ "@esbuild/freebsd-arm64": "0.18.20",
+ "@esbuild/freebsd-x64": "0.18.20",
+ "@esbuild/linux-arm": "0.18.20",
+ "@esbuild/linux-arm64": "0.18.20",
+ "@esbuild/linux-ia32": "0.18.20",
+ "@esbuild/linux-loong64": "0.18.20",
+ "@esbuild/linux-mips64el": "0.18.20",
+ "@esbuild/linux-ppc64": "0.18.20",
+ "@esbuild/linux-riscv64": "0.18.20",
+ "@esbuild/linux-s390x": "0.18.20",
+ "@esbuild/linux-x64": "0.18.20",
+ "@esbuild/netbsd-x64": "0.18.20",
+ "@esbuild/openbsd-x64": "0.18.20",
+ "@esbuild/sunos-x64": "0.18.20",
+ "@esbuild/win32-arm64": "0.18.20",
+ "@esbuild/win32-ia32": "0.18.20",
+ "@esbuild/win32-x64": "0.18.20"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
+ "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.5",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.15.1",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-session": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz",
+ "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "~0.7.2",
+ "cookie-signature": "~1.0.7",
+ "debug": "~2.6.9",
+ "depd": "~2.0.0",
+ "on-headers": "~1.1.0",
+ "parseurl": "~1.3.3",
+ "safe-buffer": "~5.2.1",
+ "uid-safe": "~2.1.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-session/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express-session/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
+ "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.13"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.16",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
+ "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.0.tgz",
+ "integrity": "sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==",
+ "license": "MIT",
+ "engines": {
+ "node": "^18 || ^20 || >= 21"
+ }
+ },
+ "node_modules/node-gyp-build": {
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
+ "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
+ "license": "MIT",
+ "bin": {
+ "node-gyp-build": "bin.js",
+ "node-gyp-build-optional": "optional.js",
+ "node-gyp-build-test": "build-test.js"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.51",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
+ "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/nodemon": {
+ "version": "2.0.22",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz",
+ "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^3.2.7",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.1.2",
+ "pstree.remy": "^1.1.8",
+ "semver": "^5.7.1",
+ "simple-update-notifier": "^1.0.7",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nodemon/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/nodemon/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/nodemon/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/nodemon/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/oauth": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz",
+ "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==",
+ "license": "MIT"
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/passport": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
+ "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "passport-strategy": "1.x.x",
+ "pause": "0.0.1",
+ "utils-merge": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jaredhanson"
+ }
+ },
+ "node_modules/passport-github2": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz",
+ "integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==",
+ "dependencies": {
+ "passport-oauth2": "1.x.x"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/passport-oauth2": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
+ "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
+ "license": "MIT",
+ "dependencies": {
+ "base64url": "3.x.x",
+ "oauth": "0.10.x",
+ "passport-strategy": "1.x.x",
+ "uid2": "0.0.x",
+ "utils-merge": "1.x.x"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jaredhanson"
+ }
+ },
+ "node_modules/passport-strategy": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
+ "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "license": "MIT"
+ },
+ "node_modules/pause": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
+ "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
+ },
+ "node_modules/pg": {
+ "version": "8.22.0",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
+ "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.14.0",
+ "pg-pool": "^3.14.0",
+ "pg-protocol": "^1.15.0",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.4.0"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
+ "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.14.0",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
+ "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
+ "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz",
+ "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.23",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
+ "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.16",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz",
+ "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.15.3",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
+ "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "es-define-property": "^1.0.1",
+ "side-channel": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/random-bytes": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+ "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
+ "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.4",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz",
+ "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.4",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz",
+ "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.3",
+ "react-router": "6.30.4"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "3.30.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.30.0.tgz",
+ "integrity": "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=14.18.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shell-quote": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz",
+ "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
+ "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "~7.0.0"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/simple-update-notifier/node_modules/semver": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
+ "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/spawn-command": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
+ "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/uid-safe": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+ "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+ "license": "MIT",
+ "dependencies": {
+ "random-bytes": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/uid2": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
+ "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==",
+ "license": "MIT"
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vite": {
+ "version": "4.5.14",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz",
+ "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.18.10",
+ "postcss": "^8.4.27",
+ "rollup": "^3.27.1"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ },
+ "peerDependencies": {
+ "@types/node": ">= 14",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yargs": {
+ "version": "17.7.3",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz",
+ "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ }
+ }
+}
diff --git a/Code/package.json b/Code/package.json
new file mode 100644
index 000000000..dc7786016
--- /dev/null
+++ b/Code/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "matchlens",
+ "type": "module",
+ "scripts": {
+ "dev": "concurrently \"cd client && vite\" \"cd server && nodemon server\"",
+ "start": "cd server && node server",
+ "build": "cd client && vite build",
+ "reset": "cd server && node db/resetDatabase.js"
+ },
+ "dependencies": {
+ "bcrypt": "^6.0.0",
+ "concurrently": "^7.6.0",
+ "cors": "^2.8.6",
+ "dotenv": "^16.0.3",
+ "express": "^4.18.2",
+ "express-session": "^1.19.0",
+ "nodemon": "^2.0.21",
+ "passport": "^0.7.0",
+ "passport-github2": "^0.1.12",
+ "pg": "^8.10.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.9.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.0.28",
+ "@types/react-dom": "^18.0.11",
+ "@vitejs/plugin-react": "^3.1.0",
+ "autoprefixer": "^10.5.4",
+ "postcss": "^8.5.23",
+ "tailwindcss": "^3.4.19",
+ "vite": "^4.2.0"
+ }
+}
diff --git a/Code/server/.env.example b/Code/server/.env.example
new file mode 100644
index 000000000..08d494ea2
--- /dev/null
+++ b/Code/server/.env.example
@@ -0,0 +1,26 @@
+PGDATABASE=
+PGHOST=
+PGPASSWORD=
+PGPORT=
+PGUSER=
+FOOTBALL_API_KEY=
+
+# Set to false for a local Postgres that doesn't speak SSL. Anything else
+# (including unset) keeps SSL on, which hosted Postgres requires.
+PGSSL=
+
+# GitHub OAuth app — https://github.com/settings/developers
+# The app's callback URL must be exactly ${SERVER_URL}/auth/github/callback
+GITHUB_CLIENT_ID=
+GITHUB_CLIENT_SECRET=
+
+# Signs the session cookie. REQUIRED — the server refuses to start without it.
+# Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
+SESSION_SECRET=
+
+# This API's own public origin. Builds the GitHub callback URL, which GitHub
+# matches byte-for-byte — no trailing slash.
+SERVER_URL=http://localhost:3000
+
+# Where OAuth redirects back to (the Vite dev server, not this API)
+CLIENT_URL=http://localhost:5173
diff --git a/Code/server/config/auth.js b/Code/server/config/auth.js
new file mode 100644
index 000000000..4ebd5ae9e
--- /dev/null
+++ b/Code/server/config/auth.js
@@ -0,0 +1,164 @@
+import './dotenv.js'
+
+import GitHubStrategy from 'passport-github2'
+import { pool } from './database.js'
+import { GITHUB_CALLBACK_URL } from './urls.js'
+
+const options = {
+ clientID: process.env.GITHUB_CLIENT_ID,
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
+ callbackURL: GITHUB_CALLBACK_URL,
+ // Must be set here rather than on passport.authenticate(): passport-oauth2
+ // picks its state store in the constructor, and without this it falls back
+ // to a NullStore that verifies nothing — i.e. no CSRF protection at all.
+ state: true
+}
+
+// GitHub lets users hide their address, so fall back to the noreply alias
+// GitHub itself issues for that account — still a real, deliverable address.
+const resolveEmail = (profile) => {
+ const { id, login, email } = profile._json
+
+ return profile.emails?.[0]?.value
+ || email
+ || `${id}+${login}@users.noreply.github.com`
+}
+
+// matches VARCHAR(50) on users.username in schema.sql
+const USERNAME_MAX = 50
+
+// Case-insensitive to match the signup check in usersController.createUser —
+// otherwise GitHub "Carla" slips past a local "carla" and we end up with two
+// accounts that look identical to a reader.
+const isUsernameFree = async (username) => {
+ const { rowCount } = await pool.query(
+ 'SELECT 1 FROM users WHERE LOWER(username) = LOWER($1)',
+ [username]
+ )
+ return rowCount === 0
+}
+
+// A GitHub login is unique on GitHub, but username is NOT NULL UNIQUE here and
+// a local signup may already have claimed it. Fall back to a github_id-suffixed
+// variant, which can't collide with another GitHub account.
+const resolveUsername = async (login, githubId) => {
+ // Trim separators left dangling by the slice, so a truncated login yields
+ // "averylongname-123" rather than "averylongname--123".
+ const withSuffix = (suffix) =>
+ login.slice(0, USERNAME_MAX - suffix.length).replace(/[-_]+$/, '') + suffix
+
+ const candidates = [login.slice(0, USERNAME_MAX), withSuffix(`-${githubId}`)]
+
+ for (const candidate of candidates) {
+ if (await isUsernameFree(candidate)) return candidate
+ }
+
+ // Both taken — only possible if a local user picked the suffixed form by
+ // hand. Walk a counter rather than letting the insert throw.
+ for (let n = 2; n <= 50; n++) {
+ const candidate = withSuffix(`-${githubId}-${n}`)
+ if (await isUsernameFree(candidate)) return candidate
+ }
+
+ throw new Error(`could not derive a free username for GitHub login "${login}"`)
+}
+
+const verify = async (accessToken, refreshToken, profile, callback) => {
+ const {
+ _json: { id, login, avatar_url }
+ } = profile
+
+ const userData = {
+ githubId: String(id),
+ username: login,
+ avatarUrl: avatar_url,
+ // Lowercased to match how usersController stores addresses. Comparing
+ // raw meant a GitHub "Carla@Example.com" never matched the local
+ // "carla@example.com", fell through to the INSERT below, and died on
+ // the email UNIQUE constraint.
+ email: resolveEmail(profile).toLowerCase()
+ }
+
+ try {
+ // github_id, not username: GitHub logins are renameable, the id is not
+ const found = await pool.query(
+ 'SELECT * FROM users WHERE github_id = $1',
+ [userData.githubId]
+ )
+ const user = found.rows[0]
+
+ if (user) {
+ // Track a GitHub rename, but only if that name is still free here —
+ // otherwise keep the current one so a collision can't lock them out.
+ const username =
+ user.username === userData.username || !(await isUsernameFree(userData.username))
+ ? user.username
+ : userData.username
+
+ const existing = await pool.query(
+ `UPDATE users
+ SET username = $2, profile_image_url = $3
+ WHERE github_id = $1
+ RETURNING *`,
+ [userData.githubId, username, userData.avatarUrl]
+ )
+
+ return callback(null, existing.rows[0])
+ }
+
+ // No GitHub row yet. Look the address up explicitly instead of relying
+ // on a conditional UPDATE to miss: "no such email" and "email exists but
+ // is already linked to another GitHub account" both return zero rows,
+ // and only one of them should fall through to the INSERT.
+ const existingByEmail = await pool.query(
+ 'SELECT user_id, github_id FROM users WHERE LOWER(email) = $1',
+ [userData.email]
+ )
+ const match = existingByEmail.rows[0]
+
+ if (match) {
+ if (match.github_id) {
+ // Some other GitHub account already owns this address — the id
+ // lookup above would have found it if it were this one.
+ return callback(null, false, { code: 'account_conflict' })
+ }
+
+ // They already signed up locally with this address, so attach
+ // GitHub to that account. Their existing username is left alone —
+ // it's already UNIQUE and theirs to change.
+ const linked = await pool.query(
+ `UPDATE users
+ SET github_id = $2, profile_image_url = $3
+ WHERE user_id = $1
+ RETURNING *`,
+ [match.user_id, userData.githubId, userData.avatarUrl]
+ )
+
+ return callback(null, linked.rows[0])
+ }
+
+ // Brand new user. No password_hash here — github_id satisfies the
+ // users_has_credential check on its own.
+ const username = await resolveUsername(userData.username, userData.githubId)
+ const created = await pool.query(
+ `INSERT INTO users (email, username, github_id, profile_image_url)
+ VALUES($1, $2, $3, $4)
+ RETURNING *`,
+ [userData.email, username, userData.githubId, userData.avatarUrl]
+ )
+
+ return callback(null, created.rows[0])
+ } catch (error) {
+ // The lookup above is check-then-insert, so a concurrent sign-in can
+ // still beat us to the row. The unique constraint is the real guarantee
+ // — surface it as a clean failure rather than a stack.
+ if (error.code === '23505') {
+ return callback(null, false, { code: 'account_conflict' })
+ }
+ return callback(error)
+ }
+}
+
+const GitHub = new GitHubStrategy(options, verify)
+
+export default { GitHub }
diff --git a/Code/server/config/database.js b/Code/server/config/database.js
new file mode 100644
index 000000000..036d8abc6
--- /dev/null
+++ b/Code/server/config/database.js
@@ -0,0 +1,22 @@
+import pg from "pg";
+import "./dotenv.js";
+
+const pool = new pg.Pool({
+ user: process.env.PGUSER,
+ password: process.env.PGPASSWORD,
+ host: process.env.PGHOST,
+ port: process.env.PGPORT,
+ database: process.env.PGDATABASE,
+ // Render's Postgres requires SSL for external connections. Set PGSSL=false
+ // for a local Postgres, which usually doesn't speak SSL at all.
+ ssl: process.env.PGSSL === "false" ? false : { rejectUnauthorized: false },
+});
+
+// Without this, a lost connection can crash the whole server with an
+// unhandled error instead of just logging it.
+pool.on("error", (err) => {
+ console.error("Unexpected error on idle Postgres client:", err.message);
+});
+
+export { pool };
+export default pool;
diff --git a/Code/server/config/dotenv.js b/Code/server/config/dotenv.js
new file mode 100644
index 000000000..15e52984b
--- /dev/null
+++ b/Code/server/config/dotenv.js
@@ -0,0 +1,10 @@
+import dotenv from "dotenv";
+import path from "path";
+import { fileURLToPath } from "url";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const envPath = path.resolve(__dirname, "../.env");
+
+dotenv.config({ path: envPath });
diff --git a/Code/server/config/footballApi.js b/Code/server/config/footballApi.js
new file mode 100644
index 000000000..0b0183ecf
--- /dev/null
+++ b/Code/server/config/footballApi.js
@@ -0,0 +1,12 @@
+import "./dotenv.js";
+
+const BASE_URL = "https://v3.football.api-sports.io";
+
+export async function footballApiGet(path) {
+ const res = await fetch(`${BASE_URL}${path}`, {
+ headers: { "x-apisports-key": process.env.FOOTBALL_API_KEY },
+ });
+ if (!res.ok) throw new Error(`API-Football request failed: ${res.status}`);
+ const body = await res.json();
+ return body.response;
+}
diff --git a/Code/server/config/session.js b/Code/server/config/session.js
new file mode 100644
index 000000000..dd5bacbbe
--- /dev/null
+++ b/Code/server/config/session.js
@@ -0,0 +1,39 @@
+import './dotenv.js'
+
+// No dev fallback on purpose. The old `|| "codepath"` default was committed to
+// git, so any deploy missing this var silently accepted forged session cookies.
+const secret = process.env.SESSION_SECRET
+if (!secret) {
+ throw new Error(
+ 'SESSION_SECRET is not set. Add a long random string to Code/server/.env — see .env.example.\n' +
+ 'Generate one with: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"'
+ )
+}
+
+export const sessionOptions = {
+ secret,
+ resave: false,
+ // Don't mint a session for every anonymous visitor; only persist once
+ // something is actually stored (a login, or the OAuth state parameter).
+ saveUninitialized: false,
+ cookie: {
+ httpOnly: true,
+ // 'lax' still allows the top-level GET redirect back from GitHub, and
+ // :5173 -> :3000 is same-site (ports aren't part of same-site), so the
+ // credentialed getSession fetch keeps working too.
+ sameSite: 'lax',
+ secure: process.env.NODE_ENV === 'production',
+ maxAge: 1000 * 60 * 60 * 24 * 7
+ }
+}
+
+// Passport's req.logIn does NOT rotate the session id, so without regenerating
+// first, an attacker who fixed a victim's pre-login session id still holds a
+// valid one after they authenticate.
+export const establishSession = (req, user) =>
+ new Promise((resolve, reject) => {
+ req.session.regenerate((regenError) => {
+ if (regenError) return reject(regenError)
+ req.logIn(user, (loginError) => (loginError ? reject(loginError) : resolve()))
+ })
+ })
diff --git a/Code/server/config/urls.js b/Code/server/config/urls.js
new file mode 100644
index 000000000..a563655ef
--- /dev/null
+++ b/Code/server/config/urls.js
@@ -0,0 +1,16 @@
+import './dotenv.js'
+
+// Every origin the app needs, in one place. Both default to the dev ports, so a
+// fresh clone runs with no .env entries at all — set them for any non-localhost
+// deploy.
+//
+// Trailing slashes are stripped in both directions: a CLIENT_URL ending in '/'
+// would otherwise build '.../\/login', and GitHub matches the callback URL
+// byte-for-byte, so a stray slash is a redirect_uri_mismatch.
+const trim = (url) => url.replace(/\/+$/, '')
+
+export const SERVER_URL = trim(process.env.SERVER_URL || 'http://localhost:3000')
+export const CLIENT_URL = trim(process.env.CLIENT_URL || 'http://localhost:5173')
+
+// Must match the callback URL registered on the GitHub OAuth app exactly.
+export const GITHUB_CALLBACK_URL = `${SERVER_URL}/auth/github/callback`
diff --git a/Code/server/controllers/commentsController.js b/Code/server/controllers/commentsController.js
new file mode 100644
index 000000000..a59820e43
--- /dev/null
+++ b/Code/server/controllers/commentsController.js
@@ -0,0 +1,43 @@
+import { pool } from "../config/database.js";
+
+// GET /api/comments/match/:matchId — all comments on a match
+export async function getCommentsByMatch(req, res) {
+ try {
+ const matchId = req.params.matchId;
+ const result = await pool.query(
+ `SELECT c.*, u.email FROM comments c
+ JOIN users u ON c.user_id = u.user_id
+ WHERE c.api_match_id = $1 ORDER BY c.created_at DESC`,
+ [matchId],
+ );
+ res.status(200).json(result.rows);
+ } catch (error) {
+ res.status(409).json({ error: error.message });
+ }
+}
+
+// POST /api/comments — leave a comment on a match
+export async function createComment(req, res) {
+ try {
+ const { user_id, api_match_id, content } = req.body;
+ const result = await pool.query(
+ `INSERT INTO comments (user_id, api_match_id, content)
+ VALUES ($1, $2, $3) RETURNING *`,
+ [user_id, api_match_id, content],
+ );
+ res.status(201).json(result.rows[0]);
+ } catch (error) {
+ res.status(409).json({ error: error.message });
+ }
+}
+
+// DELETE /api/comments/:id — delete your comment
+export async function deleteComment(req, res) {
+ try {
+ const id = parseInt(req.params.id);
+ await pool.query("DELETE FROM comments WHERE comment_id = $1", [id]);
+ res.status(200).json({ message: "Comment deleted successfully" });
+ } catch (error) {
+ res.status(409).json({ error: error.message });
+ }
+}
diff --git a/Code/server/controllers/followsController.js b/Code/server/controllers/followsController.js
new file mode 100644
index 000000000..e51374d09
--- /dev/null
+++ b/Code/server/controllers/followsController.js
@@ -0,0 +1,41 @@
+import { pool } from '../config/database.js'
+
+// GET /api/follows/user/:userId — teams a user follows
+export async function getFollowsByUser(req, res) {
+ try {
+ const userId = parseInt(req.params.userId)
+ const result = await pool.query(
+ 'SELECT * FROM followed_teams WHERE user_id = $1 ORDER BY followed_at DESC',
+ [userId]
+ )
+ res.status(200).json(result.rows)
+ } catch (error) {
+ res.status(409).json({ error: error.message })
+ }
+}
+
+// POST /api/follows — follow a team
+export async function createFollow(req, res) {
+ try {
+ const { user_id, api_team_id, team_name } = req.body
+ const result = await pool.query(
+ `INSERT INTO followed_teams (user_id, api_team_id, team_name)
+ VALUES ($1, $2, $3) RETURNING *`,
+ [user_id, api_team_id, team_name]
+ )
+ res.status(201).json(result.rows[0])
+ } catch (error) {
+ res.status(409).json({ error: error.message })
+ }
+}
+
+// DELETE /api/follows/:id — unfollow a team
+export async function deleteFollow(req, res) {
+ try {
+ const id = parseInt(req.params.id)
+ await pool.query('DELETE FROM followed_teams WHERE followed_team_id = $1', [id])
+ res.status(200).json({ message: 'Team unfollowed successfully' })
+ } catch (error) {
+ res.status(409).json({ error: error.message })
+ }
+}
diff --git a/Code/server/controllers/matchesController.js b/Code/server/controllers/matchesController.js
new file mode 100644
index 000000000..7c27f7058
--- /dev/null
+++ b/Code/server/controllers/matchesController.js
@@ -0,0 +1,35 @@
+import { footballApiGet } from "../config/footballApi.js";
+
+function mapFixture(f) {
+ return {
+ id: f.fixture.id,
+ home: f.teams.home.name,
+ away: f.teams.away.name,
+ home_score: f.goals.home,
+ away_score: f.goals.away,
+ status: f.fixture.status.short,
+ minute: f.fixture.status.elapsed,
+ date: f.fixture.date.slice(0, 10),
+ venue: f.fixture.venue.name,
+ };
+}
+
+export async function getAllMatches(req, res) {
+ try {
+ const date = req.query.date || new Date().toISOString().slice(0, 10);
+ const data = await footballApiGet(`/fixtures?date=${date}`);
+ res.status(200).json(data.map(mapFixture));
+ } catch (error) {
+ res.status(502).json({ error: error.message });
+ }
+}
+
+export async function getMatchById(req, res) {
+ try {
+ const data = await footballApiGet(`/fixtures?id=${req.params.id}`);
+ if (!data.length) return res.status(404).json({ error: "Match not found" });
+ res.status(200).json(mapFixture(data[0]));
+ } catch (error) {
+ res.status(502).json({ error: error.message });
+ }
+}
diff --git a/Code/server/controllers/notificationsController.js b/Code/server/controllers/notificationsController.js
new file mode 100644
index 000000000..a04f431bd
--- /dev/null
+++ b/Code/server/controllers/notificationsController.js
@@ -0,0 +1,29 @@
+import { pool } from '../config/database.js'
+
+// GET /api/notifications/user/:userId — a user's notifications
+export async function getNotificationsByUser(req, res) {
+ try {
+ const userId = parseInt(req.params.userId)
+ const result = await pool.query(
+ 'SELECT * FROM notifications WHERE user_id = $1 ORDER BY created_at DESC',
+ [userId]
+ )
+ res.status(200).json(result.rows)
+ } catch (error) {
+ res.status(409).json({ error: error.message })
+ }
+}
+
+// PATCH /api/notifications/:id/read — mark a notification as read
+export async function markNotificationRead(req, res) {
+ try {
+ const id = parseInt(req.params.id)
+ const result = await pool.query(
+ 'UPDATE notifications SET is_read = TRUE WHERE notification_id = $1 RETURNING *',
+ [id]
+ )
+ res.status(200).json(result.rows[0])
+ } catch (error) {
+ res.status(409).json({ error: error.message })
+ }
+}
diff --git a/Code/server/controllers/playersController.js b/Code/server/controllers/playersController.js
new file mode 100644
index 000000000..2bf32be63
--- /dev/null
+++ b/Code/server/controllers/playersController.js
@@ -0,0 +1,49 @@
+import { footballApiGet } from "../config/footballApi.js";
+
+const LEAGUE_ID = 39;
+// Free-tier API-Football plans only cover seasons 2022-2024, not the current one.
+const SEASON = 2024;
+
+function mapPlayer(entry) {
+ const stats = entry.statistics[0];
+ return {
+ id: entry.player.id,
+ name: entry.player.name,
+ team: stats.team.name,
+ position: stats.games.position,
+ goals: stats.goals.total || 0,
+ assists: stats.goals.assists || 0,
+ };
+}
+
+export async function getAllPlayers(req, res) {
+ try {
+ const { search, sort } = req.query;
+ const data = await footballApiGet(
+ `/players/topscorers?league=${LEAGUE_ID}&season=${SEASON}`,
+ );
+ let players = data.map(mapPlayer);
+ if (search)
+ players = players.filter((p) =>
+ p.name.toLowerCase().includes(search.toLowerCase()),
+ );
+ if (sort === "goals") players.sort((a, b) => b.goals - a.goals);
+ res.status(200).json(players);
+ } catch (error) {
+ res.status(502).json({ error: error.message });
+ }
+}
+
+export async function getPlayerById(req, res) {
+ try {
+ const id = parseInt(req.params.id);
+ const data = await footballApiGet(
+ `/players?id=${id}&season=${SEASON}&league=${LEAGUE_ID}`,
+ );
+ if (!data.length)
+ return res.status(404).json({ error: "Player not found" });
+ res.status(200).json(mapPlayer(data[0]));
+ } catch (error) {
+ res.status(502).json({ error: error.message });
+ }
+}
diff --git a/Code/server/controllers/predictionsController.js b/Code/server/controllers/predictionsController.js
new file mode 100644
index 000000000..0d1c696ef
--- /dev/null
+++ b/Code/server/controllers/predictionsController.js
@@ -0,0 +1,30 @@
+import { pool } from '../config/database.js'
+
+// GET /api/predictions/user/:userId — all predictions a user has made
+export async function getPredictionsByUser(req, res) {
+ try {
+ const userId = parseInt(req.params.userId)
+ const result = await pool.query(
+ 'SELECT * FROM predictions WHERE user_id = $1 ORDER BY submitted_at DESC',
+ [userId]
+ )
+ res.status(200).json(result.rows)
+ } catch (error) {
+ res.status(409).json({ error: error.message })
+ }
+}
+
+// POST /api/predictions — place a prediction (points wager) on a match
+export async function createPrediction(req, res) {
+ try {
+ const { user_id, api_match_id, predicted_home_score, predicted_away_score } = req.body
+ const result = await pool.query(
+ `INSERT INTO predictions (user_id, api_match_id, predicted_home_score, predicted_away_score)
+ VALUES ($1, $2, $3, $4) RETURNING *`,
+ [user_id, api_match_id, predicted_home_score, predicted_away_score]
+ )
+ res.status(201).json(result.rows[0])
+ } catch (error) {
+ res.status(409).json({ error: error.message })
+ }
+}
diff --git a/Code/server/controllers/teamsController.js b/Code/server/controllers/teamsController.js
new file mode 100644
index 000000000..04374c60e
--- /dev/null
+++ b/Code/server/controllers/teamsController.js
@@ -0,0 +1,54 @@
+// Teams come from the external football data API.
+
+import { footballApiGet } from "../config/footballApi.js";
+
+const LEAGUE_ID = 39;
+// Free-tier API-Football plans only cover seasons 2022-2024, not the current one.
+const SEASON = 2024;
+
+function mapStandingRow(row) {
+ return {
+ id: row.team.id,
+ name: row.team.name,
+ league: "Premier League",
+ played: row.all.played,
+ wins: row.all.win,
+ draws: row.all.draw,
+ losses: row.all.lose,
+ goals_scored: row.all.goals.for,
+ points: row.points,
+ };
+}
+
+// GET /api/teams — supports ?search=name
+export async function getAllTeams(req, res) {
+ try {
+ const { search } = req.query;
+ const data = await footballApiGet(
+ `/standings?league=${LEAGUE_ID}&season=${SEASON}`,
+ );
+ let teams = data[0].league.standings[0].map(mapStandingRow);
+ if (search)
+ teams = teams.filter((t) =>
+ t.name.toLowerCase().includes(search.toLowerCase()),
+ );
+ res.status(200).json(teams);
+ } catch (error) {
+ res.status(502).json({ error: error.message });
+ }
+}
+
+// GET /api/teams/:id — team information page data
+export async function getTeamById(req, res) {
+ try {
+ const id = parseInt(req.params.id);
+ const data = await footballApiGet(
+ `/standings?league=${LEAGUE_ID}&season=${SEASON}`,
+ );
+ const row = data[0].league.standings[0].find((r) => r.team.id === id);
+ if (!row) return res.status(404).json({ error: "Team not found" });
+ res.status(200).json(mapStandingRow(row));
+ } catch (error) {
+ res.status(502).json({ error: error.message });
+ }
+}
diff --git a/Code/server/controllers/usersController.js b/Code/server/controllers/usersController.js
new file mode 100644
index 000000000..db29261d4
--- /dev/null
+++ b/Code/server/controllers/usersController.js
@@ -0,0 +1,206 @@
+import bcrypt from "bcrypt";
+import { pool } from "../config/database.js";
+import { establishSession } from "../config/session.js";
+
+const SALT_ROUNDS = 10;
+
+// Columns safe to hand back to the client — never password_hash
+const PUBLIC_COLUMNS = `user_id AS id, username, email, profile_image_url, total_points AS points`;
+
+// Same shape check the client runs in utilities/validateLogin.js
+const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+const USERNAME_PATTERN = /^[A-Za-z0-9_-]+$/;
+
+// Column widths come from users in db/schema.sql
+const EMAIL_MAX = 255;
+const USERNAME_MIN = 3;
+const USERNAME_MAX = 50;
+
+// bcrypt only reads the first 72 bytes of a password. Anything longer would be
+// silently truncated, so a user could "log in" with a prefix of what they typed.
+const PASSWORD_MIN = 8;
+const PASSWORD_MAX = 72;
+
+const EMAIL_TAKEN = "That email is already registered.";
+const USERNAME_TAKEN = "That username is taken.";
+
+// Returns an error string, or null when the signup body is usable.
+function validateSignup(username, email, password) {
+ if (!username || !email || !password) {
+ return "Username, email and password are required";
+ }
+ if (!EMAIL_PATTERN.test(email) || email.length > EMAIL_MAX) {
+ return "Enter a valid email address (e.g. name@example.com).";
+ }
+ if (username.length < USERNAME_MIN || username.length > USERNAME_MAX) {
+ return `Username must be between ${USERNAME_MIN} and ${USERNAME_MAX} characters.`;
+ }
+ if (!USERNAME_PATTERN.test(username)) {
+ return "Username can only contain letters, numbers, hyphens and underscores.";
+ }
+ if (password.length < PASSWORD_MIN) {
+ return `Password must be at least ${PASSWORD_MIN} characters.`;
+ }
+ if (password.length > PASSWORD_MAX) {
+ return `Password must be ${PASSWORD_MAX} characters or fewer.`;
+ }
+ return null;
+}
+
+// GET /api/users — all users ranked by points (fan leaderboard)
+export async function getAllUsers(req, res) {
+ try {
+ const result = await pool.query(
+ `SELECT ${PUBLIC_COLUMNS} FROM users ORDER BY total_points DESC`,
+ );
+ res.status(200).json(result.rows);
+ } catch (error) {
+ res.status(409).json({ error: error.message });
+ }
+}
+
+// GET /api/users/:id — a single user's profile
+export async function getUserById(req, res) {
+ try {
+ const id = parseInt(req.params.id);
+ const result = await pool.query(
+ `SELECT ${PUBLIC_COLUMNS} FROM users WHERE user_id = $1`,
+ [id],
+ );
+ if (result.rows.length === 0) {
+ return res.status(404).json({ error: "User not found" });
+ }
+ res.status(200).json(result.rows[0]);
+ } catch (error) {
+ res.status(409).json({ error: error.message });
+ }
+}
+
+// POST /api/users/login — verify credentials, return the user
+export async function login(req, res) {
+ try {
+ const { email, password } = req.body;
+
+ if (!email || !password) {
+ return res.status(401).json({ error: "Invalid email or password" });
+ }
+
+ // Signup lowercases the address before storing it, so match the same way
+ // — otherwise "Carla@Example.com" can't sign in to carla@example.com.
+ const result = await pool.query(
+ `SELECT ${PUBLIC_COLUMNS}, password_hash FROM users WHERE LOWER(email) = $1 LIMIT 1`,
+ [email.trim().toLowerCase()],
+ );
+ const user = result.rows[0];
+
+ // A GitHub-only account has a NULL password_hash — it has to sign in
+ // through OAuth. Same generic error either way, so this endpoint never
+ // reveals which emails are registered.
+ if (
+ !user ||
+ !user.password_hash ||
+ !(await bcrypt.compare(password, user.password_hash))
+ ) {
+ return res.status(401).json({ error: "Invalid email or password" });
+ }
+
+ const { password_hash: _hash, ...safeUser } = user;
+
+ // Same session an OAuth sign-in gets, so req.user is populated for every
+ // logged-in user rather than only the GitHub ones.
+ await establishSession(req, safeUser);
+
+ res.status(200).json(safeUser);
+ } catch (error) {
+ res.status(409).json({ error: error.message });
+ }
+}
+
+// POST /api/users — create an account (sign up)
+export async function createUser(req, res) {
+ try {
+ const username = (req.body.username ?? "").trim();
+ // Stored lowercased so the UNIQUE constraint actually stops someone
+ // re-registering the same address with different capitalisation.
+ const email = (req.body.email ?? "").trim().toLowerCase();
+ const password = req.body.password ?? "";
+
+ const validationError = validateSignup(username, email, password);
+ if (validationError) {
+ return res.status(400).json({ error: validationError });
+ }
+
+ // Check before hashing — bcrypt at cost 10 costs ~100ms, no sense
+ // spending it on an insert we already know will fail.
+ const existing = await pool.query(
+ `SELECT LOWER(email) = $1 AS email_taken,
+ LOWER(username) = LOWER($2) AS username_taken
+ FROM users
+ WHERE LOWER(email) = $1 OR LOWER(username) = LOWER($2)`,
+ [email, username],
+ );
+ if (existing.rows.some((row) => row.email_taken)) {
+ return res.status(409).json({ error: EMAIL_TAKEN });
+ }
+ if (existing.rows.some((row) => row.username_taken)) {
+ return res.status(409).json({ error: USERNAME_TAKEN });
+ }
+
+ const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
+ const result = await pool.query(
+ `INSERT INTO users (username, email, password_hash, total_points)
+ VALUES ($1, $2, $3, 0)
+ RETURNING ${PUBLIC_COLUMNS}`,
+ [username, email, passwordHash],
+ );
+
+ // Signup navigates straight to /home, so it needs a live session there
+ // just as much as login does.
+ await establishSession(req, result.rows[0]);
+
+ res.status(201).json(result.rows[0]);
+ } catch (error) {
+ // The check above is check-then-insert, so a concurrent signup can still
+ // beat us to the name. The unique constraint is the real guarantee.
+ if (error.code === "23505") {
+ return res.status(409).json({
+ error:
+ error.constraint === "users_username_key"
+ ? USERNAME_TAKEN
+ : EMAIL_TAKEN,
+ });
+ }
+ console.error("createUser failed:", error);
+ res.status(500).json({
+ error: "Could not create the account. Please try again.",
+ });
+ }
+}
+
+// PATCH /api/users/:id — update profile (username, image, points)
+export async function updateUser(req, res) {
+ try {
+ const id = parseInt(req.params.id);
+ const { username, profile_image_url, total_points } = req.body;
+ const result = await pool.query(
+ `UPDATE users
+ SET username = COALESCE($1, username),
+ profile_image_url = COALESCE($2, profile_image_url),
+ total_points = COALESCE($3, total_points)
+ WHERE user_id = $4
+ RETURNING ${PUBLIC_COLUMNS}`,
+ [
+ username ?? null,
+ profile_image_url ?? null,
+ total_points ?? null,
+ id,
+ ],
+ );
+ if (result.rows.length === 0) {
+ return res.status(404).json({ error: "User not found" });
+ }
+ res.status(200).json(result.rows[0]);
+ } catch (error) {
+ res.status(409).json({ error: error.message });
+ }
+}
diff --git a/Code/server/controllers/videosController.js b/Code/server/controllers/videosController.js
new file mode 100644
index 000000000..fa4012931
--- /dev/null
+++ b/Code/server/controllers/videosController.js
@@ -0,0 +1,30 @@
+import { pool } from "../config/database.js";
+
+// GET /api/videos/match/:matchId — highlight/footage links for a match
+export async function getVideosByMatch(req, res) {
+ try {
+ const matchId = req.params.matchId;
+ const result = await pool.query(
+ "SELECT * FROM video_links WHERE api_match_id = $1 ORDER BY created_at DESC",
+ [matchId],
+ );
+ res.status(200).json(result.rows);
+ } catch (error) {
+ res.status(409).json({ error: error.message });
+ }
+}
+
+// POST /api/videos — attach a video link to a match
+export async function createVideo(req, res) {
+ try {
+ const { user_id, api_match_id, title, video_url, provider } = req.body;
+ const result = await pool.query(
+ `INSERT INTO video_links (user_id, api_match_id, title, video_url, provider)
+ VALUES ($1, $2, $3, $4, $5) RETURNING *`,
+ [user_id, api_match_id, title, video_url, provider],
+ );
+ res.status(201).json(result.rows[0]);
+ } catch (error) {
+ res.status(409).json({ error: error.message });
+ }
+}
diff --git a/Code/server/db/resetDatabase.js b/Code/server/db/resetDatabase.js
new file mode 100644
index 000000000..12cab496b
--- /dev/null
+++ b/Code/server/db/resetDatabase.js
@@ -0,0 +1,147 @@
+// db/resetDatabase.js
+//
+// Run with: npm run reset
+//
+// Rebuilds the entire database from schema.sql, then inserts demo data
+// from seedData.js. Safe to run as many times as you want — schema.sql
+// drops and recreates every table, so you always end up in the same
+// known state.
+//
+
+import fs from "fs";
+import path from "path";
+import { fileURLToPath } from "url";
+import bcrypt from "bcrypt";
+import pool from "../config/database.js";
+import {
+ demoUsers,
+ demoFollowedTeams,
+ demoComments,
+ demoPredictions,
+ demoNotifications,
+ demoVideoLinks,
+} from "./seedData.js";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+const SALT_ROUNDS = 10;
+
+async function rebuildSchema() {
+ const schemaPath = path.resolve(__dirname, "schema.sql");
+ const schemaSql = fs.readFileSync(schemaPath, "utf-8");
+ await pool.query(schemaSql);
+ console.log("✅ schema rebuilt (all 6 tables created)");
+}
+
+async function seedUsers() {
+ // sequential, not Promise.all: each hash is deliberately slow and we want
+ // predictable user_id ordering for the seed rows the other tables reference
+ for (const user of demoUsers) {
+ const passwordHash = await bcrypt.hash(user.password, SALT_ROUNDS);
+ await pool.query(
+ `INSERT INTO users (username, email, password_hash, profile_image_url, total_points)
+ VALUES ($1, $2, $3, $4, $5)`,
+ [
+ user.username,
+ user.email,
+ passwordHash,
+ user.profile_image_url,
+ user.total_points,
+ ],
+ );
+ }
+ console.log(`✅ ${demoUsers.length} demo users inserted (passwords hashed)`);
+}
+
+async function seedFollowedTeams() {
+ for (const follow of demoFollowedTeams) {
+ await pool.query(
+ `INSERT INTO followed_teams (user_id, api_team_id, team_name)
+ VALUES ($1, $2, $3)`,
+ [follow.user_id, follow.api_team_id, follow.team_name],
+ );
+ }
+ console.log(`✅ ${demoFollowedTeams.length} demo followed_teams inserted`);
+}
+
+async function seedComments() {
+ for (const comment of demoComments) {
+ await pool.query(
+ `INSERT INTO comments (user_id, api_match_id, content)
+ VALUES ($1, $2, $3)`,
+ [comment.user_id, comment.api_match_id, comment.content],
+ );
+ }
+ console.log(`✅ ${demoComments.length} demo comments inserted`);
+}
+
+async function seedPredictions() {
+ for (const prediction of demoPredictions) {
+ await pool.query(
+ `INSERT INTO predictions (user_id, api_match_id, predicted_home_score, predicted_away_score, points_awarded)
+ VALUES ($1, $2, $3, $4, $5)`,
+ [
+ prediction.user_id,
+ prediction.api_match_id,
+ prediction.predicted_home_score,
+ prediction.predicted_away_score,
+ prediction.points_awarded,
+ ],
+ );
+ }
+ console.log(`✅ ${demoPredictions.length} demo predictions inserted`);
+}
+
+async function seedNotifications() {
+ for (const notification of demoNotifications) {
+ await pool.query(
+ `INSERT INTO notifications (user_id, api_team_id, api_match_id, notification_type, message)
+ VALUES ($1, $2, $3, $4, $5)`,
+ [
+ notification.user_id,
+ notification.api_team_id,
+ notification.api_match_id,
+ notification.notification_type,
+ notification.message,
+ ],
+ );
+ }
+ console.log(`✅ ${demoNotifications.length} demo notifications inserted`);
+}
+
+async function seedVideoLinks() {
+ for (const video of demoVideoLinks) {
+ await pool.query(
+ `INSERT INTO video_links (user_id, api_match_id, title, video_url, provider)
+ VALUES ($1, $2, $3, $4, $5)`,
+ [
+ video.user_id,
+ video.api_match_id,
+ video.title,
+ video.video_url,
+ video.provider,
+ ],
+ );
+ }
+ console.log(`✅ ${demoVideoLinks.length} demo video_links inserted`);
+}
+
+async function main() {
+ try {
+ await rebuildSchema();
+ await seedUsers();
+ await seedFollowedTeams();
+ await seedComments();
+ await seedPredictions();
+ await seedNotifications();
+ await seedVideoLinks();
+ console.log("🎉 database reset complete");
+ } catch (err) {
+ console.error("⚠️ reset failed:", err.message);
+ process.exitCode = 1;
+ } finally {
+ await pool.end();
+ }
+}
+
+main();
diff --git a/Code/server/db/schema.sql b/Code/server/db/schema.sql
new file mode 100644
index 000000000..e3c6c0f8c
--- /dev/null
+++ b/Code/server/db/schema.sql
@@ -0,0 +1,139 @@
+-- =========================================================
+-- MatchLens Database Schema
+--
+-- This is the ONE place that defines what the database looks like.
+-- If you need to add a column or table, change it here first, then
+-- run `npm run reset` to rebuild the database from this file.
+--
+-- Team/match/player data itself comes from an external football API —
+-- we only store api_team_id / api_match_id as reference strings, not
+-- as foreign keys to local team/match tables.
+-- =========================================================
+
+-- Drop in reverse dependency order so foreign keys don't block us
+DROP TABLE IF EXISTS video_links;
+DROP TABLE IF EXISTS notifications;
+DROP TABLE IF EXISTS predictions;
+DROP TABLE IF EXISTS comments;
+DROP TABLE IF EXISTS followed_teams;
+DROP TABLE IF EXISTS users;
+
+-- =========================================================
+-- USERS — one row per account
+--
+-- An account is reachable either by password (local signup) or by
+-- github_id (OAuth), so neither credential column can be NOT NULL on
+-- its own — the users_has_credential CHECK enforces "at least one".
+-- =========================================================
+CREATE TABLE users (
+ user_id SERIAL PRIMARY KEY,
+ username VARCHAR(50) NOT NULL UNIQUE,
+ email VARCHAR(255) NOT NULL UNIQUE,
+ password_hash VARCHAR(255), -- NULL for a GitHub-only account
+ github_id VARCHAR(255) UNIQUE, -- GitHub's numeric id, as text
+ profile_image_url VARCHAR(500), -- GitHub avatar_url for OAuth users
+ total_points INTEGER NOT NULL DEFAULT 0,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
+
+ CONSTRAINT users_has_credential
+ CHECK (password_hash IS NOT NULL OR github_id IS NOT NULL)
+);
+
+-- =========================================================
+-- FOLLOWED_TEAMS — which teams a user follows
+-- =========================================================
+CREATE TABLE followed_teams (
+ followed_team_id SERIAL PRIMARY KEY,
+ user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
+ api_team_id VARCHAR(50) NOT NULL, -- id from the external football API
+ team_name VARCHAR(150) NOT NULL, -- cached so we don't re-call the API just to show a name
+ followed_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ UNIQUE (user_id, api_team_id) -- can't follow the same team twice
+);
+
+CREATE INDEX idx_followed_teams_user_id ON followed_teams(user_id);
+CREATE INDEX idx_followed_teams_api_team_id ON followed_teams(api_team_id);
+
+-- =========================================================
+-- COMMENTS — fan chat on a match
+-- =========================================================
+CREATE TABLE comments (
+ comment_id SERIAL PRIMARY KEY,
+ user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
+ api_match_id VARCHAR(50) NOT NULL,
+ content TEXT NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW()
+);
+
+CREATE INDEX idx_comments_user_id ON comments(user_id);
+CREATE INDEX idx_comments_api_match_id ON comments(api_match_id);
+
+-- =========================================================
+-- PREDICTIONS — the wager/points mechanic
+-- =========================================================
+CREATE TABLE predictions (
+ prediction_id SERIAL PRIMARY KEY,
+ user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
+ api_match_id VARCHAR(50) NOT NULL,
+ predicted_home_score INTEGER NOT NULL,
+ predicted_away_score INTEGER NOT NULL,
+ points_awarded INTEGER NOT NULL DEFAULT 0, -- filled in after the match ends
+ submitted_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ UNIQUE (user_id, api_match_id) -- one prediction per user per match
+);
+
+CREATE INDEX idx_predictions_user_id ON predictions(user_id);
+CREATE INDEX idx_predictions_api_match_id ON predictions(api_match_id);
+
+-- =========================================================
+-- NOTIFICATIONS — in-app alerts
+-- =========================================================
+CREATE TABLE notifications (
+ notification_id SERIAL PRIMARY KEY,
+ user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
+ api_team_id VARCHAR(50), -- set if this alert is team-related
+ api_match_id VARCHAR(50), -- set if this alert is match-related
+ notification_type VARCHAR(50) NOT NULL, -- e.g. 'match_alert', 'transfer_news'
+ message TEXT NOT NULL,
+ is_read BOOLEAN NOT NULL DEFAULT FALSE,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW()
+);
+
+CREATE INDEX idx_notifications_user_id ON notifications(user_id);
+CREATE INDEX idx_notifications_unread ON notifications(user_id, is_read);
+
+-- =========================================================
+-- VIDEO_LINKS — highlight clips attached to a match
+-- =========================================================
+CREATE TABLE video_links (
+ video_link_id SERIAL PRIMARY KEY,
+ user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
+ api_match_id VARCHAR(50) NOT NULL,
+ title VARCHAR(255) NOT NULL,
+ video_url VARCHAR(500) NOT NULL,
+ provider VARCHAR(50), -- e.g. 'youtube'
+ created_at TIMESTAMP NOT NULL DEFAULT NOW()
+);
+
+CREATE INDEX idx_video_links_api_match_id ON video_links(api_match_id);
+
+-- =========================================================
+-- Auto-update `updated_at` whenever a row changes
+-- =========================================================
+CREATE OR REPLACE FUNCTION set_updated_at()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = NOW();
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER trg_users_updated_at
+ BEFORE UPDATE ON users
+ FOR EACH ROW EXECUTE FUNCTION set_updated_at();
+
+CREATE TRIGGER trg_comments_updated_at
+ BEFORE UPDATE ON comments
+ FOR EACH ROW EXECUTE FUNCTION set_updated_at();
diff --git a/Code/server/db/seedData.js b/Code/server/db/seedData.js
new file mode 100644
index 000000000..21a1e15f3
--- /dev/null
+++ b/Code/server/db/seedData.js
@@ -0,0 +1,108 @@
+// `password` is the plaintext demo credential you log in with — resetDatabase.js
+// bcrypt-hashes it before insert, so nothing plaintext ever reaches the table.
+export const demoUsers = [
+ {
+ username: "carla_t",
+ email: "demo@matchlens.com",
+ password: "password123",
+ profile_image_url: null,
+ total_points: 24580,
+ },
+ {
+ username: "jordan_m",
+ email: "fan@matchlens.com",
+ password: "letmein",
+ profile_image_url: null,
+ total_points: 18740,
+ },
+ {
+ username: "sam_fc",
+ email: "scout@matchlens.com",
+ password: "scout2024",
+ profile_image_url: null,
+ total_points: 15220,
+ },
+];
+
+export const demoFollowedTeams = [
+ { user_id: 1, api_team_id: "1", team_name: "Liverpool" },
+ { user_id: 1, api_team_id: "3", team_name: "Man City" },
+ { user_id: 2, api_team_id: "2", team_name: "Arsenal" },
+ { user_id: 3, api_team_id: "1", team_name: "Liverpool" },
+];
+
+export const demoComments = [
+ {
+ user_id: 1,
+ api_match_id: "101",
+ content: "What a finish, did not see that coming!",
+ },
+ {
+ user_id: 2,
+ api_match_id: "101",
+ content: "Ref missed a clear penalty there.",
+ },
+ {
+ user_id: 3,
+ api_match_id: "102",
+ content: "This defense has been shaky all season.",
+ },
+];
+
+export const demoPredictions = [
+ {
+ user_id: 1,
+ api_match_id: "101",
+ predicted_home_score: 2,
+ predicted_away_score: 1,
+ points_awarded: 50,
+ },
+ {
+ user_id: 2,
+ api_match_id: "101",
+ predicted_home_score: 1,
+ predicted_away_score: 1,
+ points_awarded: 0,
+ },
+ {
+ user_id: 3,
+ api_match_id: "102",
+ predicted_home_score: 3,
+ predicted_away_score: 0,
+ points_awarded: 0,
+ },
+];
+
+export const demoNotifications = [
+ {
+ user_id: 1,
+ api_team_id: "1",
+ api_match_id: null,
+ notification_type: "match_alert",
+ message: "Liverpool kicks off in 1 hour!",
+ },
+ {
+ user_id: 2,
+ api_team_id: "2",
+ api_match_id: null,
+ notification_type: "transfer_news",
+ message: "Arsenal announced a new signing.",
+ },
+];
+
+export const demoVideoLinks = [
+ {
+ user_id: 1,
+ api_match_id: "101",
+ title: "Match Highlights - Full Time",
+ video_url: "https://youtube.com/watch?v=example1",
+ provider: "youtube",
+ },
+ {
+ user_id: 3,
+ api_match_id: "102",
+ title: "Best Saves of the Match",
+ video_url: "https://youtube.com/watch?v=example2",
+ provider: "youtube",
+ },
+];
diff --git a/Code/server/routes/auth.js b/Code/server/routes/auth.js
new file mode 100644
index 000000000..c915ecb10
--- /dev/null
+++ b/Code/server/routes/auth.js
@@ -0,0 +1,70 @@
+import express from 'express'
+import passport from 'passport'
+
+import { CLIENT_URL } from '../config/urls.js'
+import { establishSession } from '../config/session.js'
+
+const router = express.Router()
+
+router.get('/login/success', (req, res) => {
+ if (!req.user) {
+ return res.status(401).json({ success: false, message: 'not authenticated' })
+ }
+ res.status(200).json({ success: true, user: req.user })
+})
+
+router.get('/logout', (req, res, next) => {
+ req.logout((err) => {
+ if (err) {
+ return next(err)
+ }
+
+ req.session.destroy((destroyErr) => {
+ if (destroyErr) {
+ return next(destroyErr)
+ }
+ res.clearCookie('connect.sid')
+ res.json({ status: "logout", user: {} })
+ })
+ })
+})
+
+router.get(
+ '/github',
+ passport.authenticate('github', {
+ // user:email also returns addresses the user keeps private
+ scope: [ 'read:user', 'user:email' ]
+ })
+)
+
+// A custom callback rather than successRedirect/failureRedirect: failureRedirect
+// does NOT fire when the verify callback errors — passport hands those to
+// next(err) — so the declarative form can't get the user back to the client at
+// all. This shape covers error, refusal and consent-denied the same way.
+router.get('/github/callback', (req, res, next) => {
+ passport.authenticate('github', async (err, user, info) => {
+ // Failures go back to /login, which is the only page that reads
+ // ?error= and turns the code into a message.
+ if (err) {
+ console.error('github oauth failed:', err)
+ return res.redirect(`${CLIENT_URL}/login?error=oauth_failed`)
+ }
+
+ // Also covers the user cancelling at GitHub and a bad state parameter:
+ // passport-oauth2 turns both into a fail().
+ if (!user) {
+ return res.redirect(`${CLIENT_URL}/login?error=${info?.code ?? 'oauth_denied'}`)
+ }
+
+ try {
+ await establishSession(req, user)
+ // '/' is the dashboard; the client's route guard revalidates the
+ // session it was just handed.
+ res.redirect(`${CLIENT_URL}/`)
+ } catch (loginError) {
+ next(loginError)
+ }
+ })(req, res, next)
+})
+
+export default router
diff --git a/Code/server/routes/commentsRoutes.js b/Code/server/routes/commentsRoutes.js
new file mode 100644
index 000000000..63d93e3ec
--- /dev/null
+++ b/Code/server/routes/commentsRoutes.js
@@ -0,0 +1,15 @@
+import express from 'express'
+import { getCommentsByMatch, createComment, deleteComment } from '../controllers/commentsController.js'
+
+const router = express.Router()
+
+// GET /api/comments/match/:matchId
+router.get('/match/:matchId', getCommentsByMatch)
+
+// POST /api/comments
+router.post('/', createComment)
+
+// DELETE /api/comments/:id
+router.delete('/:id', deleteComment)
+
+export default router
diff --git a/Code/server/routes/followsRoutes.js b/Code/server/routes/followsRoutes.js
new file mode 100644
index 000000000..70f79a7f8
--- /dev/null
+++ b/Code/server/routes/followsRoutes.js
@@ -0,0 +1,15 @@
+import express from 'express'
+import { getFollowsByUser, createFollow, deleteFollow } from '../controllers/followsController.js'
+
+const router = express.Router()
+
+// GET /api/follows/user/:userId
+router.get('/user/:userId', getFollowsByUser)
+
+// POST /api/follows
+router.post('/', createFollow)
+
+// DELETE /api/follows/:id
+router.delete('/:id', deleteFollow)
+
+export default router
diff --git a/Code/server/routes/matchesRoutes.js b/Code/server/routes/matchesRoutes.js
new file mode 100644
index 000000000..368b61e27
--- /dev/null
+++ b/Code/server/routes/matchesRoutes.js
@@ -0,0 +1,12 @@
+import express from 'express'
+import { getAllMatches, getMatchById } from '../controllers/matchesController.js'
+
+const router = express.Router()
+
+// GET /api/matches (optionally ?date=YYYY-MM-DD)
+router.get('/', getAllMatches)
+
+// GET /api/matches/:id
+router.get('/:id', getMatchById)
+
+export default router
diff --git a/Code/server/routes/notificationsRoutes.js b/Code/server/routes/notificationsRoutes.js
new file mode 100644
index 000000000..dfdc744a7
--- /dev/null
+++ b/Code/server/routes/notificationsRoutes.js
@@ -0,0 +1,12 @@
+import express from 'express'
+import { getNotificationsByUser, markNotificationRead } from '../controllers/notificationsController.js'
+
+const router = express.Router()
+
+// GET /api/notifications/user/:userId
+router.get('/user/:userId', getNotificationsByUser)
+
+// PATCH /api/notifications/:id/read
+router.patch('/:id/read', markNotificationRead)
+
+export default router
diff --git a/Code/server/routes/playersRoutes.js b/Code/server/routes/playersRoutes.js
new file mode 100644
index 000000000..bacf2f771
--- /dev/null
+++ b/Code/server/routes/playersRoutes.js
@@ -0,0 +1,12 @@
+import express from 'express'
+import { getAllPlayers, getPlayerById } from '../controllers/playersController.js'
+
+const router = express.Router()
+
+// GET /api/players (optionally ?search=salah or ?sort=goals)
+router.get('/', getAllPlayers)
+
+// GET /api/players/:id
+router.get('/:id', getPlayerById)
+
+export default router
diff --git a/Code/server/routes/predictionsRoutes.js b/Code/server/routes/predictionsRoutes.js
new file mode 100644
index 000000000..1921b29df
--- /dev/null
+++ b/Code/server/routes/predictionsRoutes.js
@@ -0,0 +1,12 @@
+import express from 'express'
+import { getPredictionsByUser, createPrediction } from '../controllers/predictionsController.js'
+
+const router = express.Router()
+
+// GET /api/predictions/user/:userId
+router.get('/user/:userId', getPredictionsByUser)
+
+// POST /api/predictions
+router.post('/', createPrediction)
+
+export default router
diff --git a/Code/server/routes/teamsRoutes.js b/Code/server/routes/teamsRoutes.js
new file mode 100644
index 000000000..eb057639c
--- /dev/null
+++ b/Code/server/routes/teamsRoutes.js
@@ -0,0 +1,12 @@
+import express from 'express'
+import { getAllTeams, getTeamById } from '../controllers/teamsController.js'
+
+const router = express.Router()
+
+// GET /api/teams (optionally ?search=liverpool)
+router.get('/', getAllTeams)
+
+// GET /api/teams/:id
+router.get('/:id', getTeamById)
+
+export default router
diff --git a/Code/server/routes/usersRoutes.js b/Code/server/routes/usersRoutes.js
new file mode 100644
index 000000000..f065b858e
--- /dev/null
+++ b/Code/server/routes/usersRoutes.js
@@ -0,0 +1,28 @@
+import express from 'express'
+import {
+ getAllUsers,
+ getUserById,
+ login,
+ createUser,
+ updateUser
+} from '../controllers/usersController.js'
+
+const router = express.Router()
+
+// GET /api/users — fan leaderboard (all users by points)
+router.get('/', getAllUsers)
+
+// POST /api/users/login
+// ⚠️ Must be defined BEFORE /:id so Express doesn't treat "login" as an id
+router.post('/login', login)
+
+// GET /api/users/:id
+router.get('/:id', getUserById)
+
+// POST /api/users — sign up
+router.post('/', createUser)
+
+// PATCH /api/users/:id — update profile
+router.patch('/:id', updateUser)
+
+export default router
diff --git a/Code/server/routes/videosRoutes.js b/Code/server/routes/videosRoutes.js
new file mode 100644
index 000000000..7b1bed7ca
--- /dev/null
+++ b/Code/server/routes/videosRoutes.js
@@ -0,0 +1,12 @@
+import express from 'express'
+import { getVideosByMatch, createVideo } from '../controllers/videosController.js'
+
+const router = express.Router()
+
+// GET /api/videos/match/:matchId
+router.get('/match/:matchId', getVideosByMatch)
+
+// POST /api/videos
+router.post('/', createVideo)
+
+export default router
diff --git a/Code/server/server.js b/Code/server/server.js
new file mode 100644
index 000000000..f85ad622b
--- /dev/null
+++ b/Code/server/server.js
@@ -0,0 +1,104 @@
+// must come first: loads .env before any module below reads process.env
+import "./config/dotenv.js";
+
+import express from "express";
+import path from "path";
+import cors from "cors";
+
+import passport from "passport";
+import session from "express-session";
+import strategies from "./config/auth.js";
+import { sessionOptions } from "./config/session.js";
+import { CLIENT_URL, SERVER_URL } from "./config/urls.js";
+import { pool } from "./config/database.js";
+
+// import the router from each routes file
+import usersRouter from "./routes/usersRoutes.js";
+import matchesRouter from "./routes/matchesRoutes.js";
+import playersRouter from "./routes/playersRoutes.js";
+import teamsRouter from "./routes/teamsRoutes.js";
+import predictionsRouter from "./routes/predictionsRoutes.js";
+import commentsRouter from "./routes/commentsRoutes.js";
+import followsRouter from "./routes/followsRoutes.js";
+import notificationsRouter from "./routes/notificationsRoutes.js";
+import videosRouter from "./routes/videosRoutes.js";
+import authRouter from "./routes/auth.js";
+
+const PORT = process.env.PORT || 3000;
+
+const app = express();
+
+// Behind Render's proxy Express sees the internal http hop, so without this the
+// `secure` session cookie is never set at all.
+if (process.env.NODE_ENV === "production") {
+ app.set("trust proxy", 1);
+}
+
+app.use(express.json());
+app.use(session(sessionOptions));
+app.use(
+ cors({
+ origin: CLIENT_URL,
+ methods: "GET,POST,PUT,DELETE,PATCH",
+ credentials: true,
+ }),
+);
+
+// passport setup + strategies
+app.use(passport.initialize());
+app.use(passport.session());
+passport.use(strategies.GitHub);
+
+// session support: store only the id in the session, then re-read the row on
+// each request so password_hash never reaches the session store or the client
+// the strategy hands back a raw row (user_id); a deserialized row is aliased to id
+passport.serializeUser((user, done) => {
+ done(null, user.user_id ?? user.id);
+});
+passport.deserializeUser(async (id, done) => {
+ try {
+ const result = await pool.query(
+ `SELECT user_id AS id, username, email, profile_image_url, total_points AS points
+ FROM users WHERE user_id = $1`,
+ [id],
+ );
+ done(null, result.rows[0] || false);
+ } catch (error) {
+ done(error);
+ }
+});
+
+if (process.env.NODE_ENV === "production") {
+ app.use(express.static("public"));
+}
+
+// specify the api path for the server to use
+app.use("/auth", authRouter);
+app.use("/api/users", usersRouter);
+app.use("/api/matches", matchesRouter);
+app.use("/api/players", playersRouter);
+app.use("/api/teams", teamsRouter);
+app.use("/api/predictions", predictionsRouter);
+app.use("/api/comments", commentsRouter);
+app.use("/api/follows", followsRouter);
+app.use("/api/notifications", notificationsRouter);
+app.use("/api/videos", videosRouter);
+
+if (process.env.NODE_ENV === "production") {
+ app.get("/*", (_, res) =>
+ res.sendFile(path.resolve("public", "index.html")),
+ );
+}
+
+// Must be last, and must take four arguments — that arity is the only way
+// Express recognises an error handler. Without it a throw anywhere (notably in
+// the OAuth verify callback) renders a raw stack page to the browser.
+app.use((err, req, res, next) => {
+ console.error("unhandled error:", err);
+ if (res.headersSent) return next(err);
+ res.status(500).json({ error: "Something went wrong." });
+});
+
+app.listen(PORT, () => {
+ console.log(`server listening on port ${PORT} (public origin: ${SERVER_URL})`);
+});
diff --git a/Gif/Login.gif b/Gif/Login.gif
new file mode 100644
index 000000000..e83fe1bbd
Binary files /dev/null and b/Gif/Login.gif differ
diff --git a/Gif/match.gif b/Gif/match.gif
new file mode 100644
index 000000000..520aadb16
Binary files /dev/null and b/Gif/match.gif differ
diff --git a/Gif/profile.gif b/Gif/profile.gif
new file mode 100644
index 000000000..ba740f9b7
Binary files /dev/null and b/Gif/profile.gif differ
diff --git a/README.md b/README.md
index 0e1211217..e84f2d9ac 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
-# [your app name here]
+# [MatchLens]
CodePath WEB103 Final Project
-Designed and developed by: [your names here]
+Designed and developed by: Cristobal Talamantes, Eric Lee, Gildardo Orea, Javohir Ergashev, Rijul Poudel, Shalom Donga
🔗 Link to deployed app:
@@ -10,38 +10,86 @@ Designed and developed by: [your names here]
### Description and Purpose
-[text goes here]
+A football dashboard that brings match results, in-depth stats, player data, and standings together in one place. Instead of jumping between multiple sites to keep up with the game, users get everything they need in a single, clean interface — match details, team and player statistics, and up-to-date standings all at their fingertips.
+
+The goal is to make following football simple and enjoyable, whether you're checking a scoreline, digging into the numbers behind a performance, or tracking how teams stack up across a competition.
### Inspiration
-[text goes here]
+This project was inspired by the FIFA World Cup 2026. Watching the tournament unfold sparked the idea of building a single hub where all the key football data — results, stats, and standings — could live together, styled after the clean, familiar look of major football sites. What started as a way to follow the World Cup grew into a broader dashboard for football fans in general.
## Tech Stack
Frontend:
+- React
+- React Router
+- Vite
+- Tailwind CSS
Backend:
+- Node.js
+- Express
+- PostgreSQL (node-postgres / pg)
+- External football data API (proxied server-side)
+- Render
## Features
-### [Name of Feature 1]
+### ✅Authentication
+
+Users can create an account and log in securely to access their profile and personalized features.
+
+
+
+### Search
+
+Users can search for players, teams, and matches to quickly find what they're looking for.
+
+[gif goes here]
+
+### ✅Data Display
+
+Users can browse players (history, team, and stats), match details (score, lineup, venue, weather), and a calendar of fixtures.
+
+
+
+### Leaderboards
-[short description goes here]
+Users can view ranked tables of top players by their stats and top users by their virtual points.
[gif goes here]
-### [Name of Feature 2]
+### Score Predictions
-[short description goes here]
+Users can predict the score of upcoming matches to earn points and move
+up the user leaderboard.
[gif goes here]
-### [Name of Feature 3]
+### Team Following
-[short description goes here]
+Users can follow a team to get its latest news and match notifications.
[gif goes here]
+### Comments
+
+Users can comment on matches to share their thoughts with other fans.
+
+[gif goes here]
+
+### Video Links
+
+Users can access video highlights or full-game footage of a match.
+
+[gif goes here]
+
+### ✅User Profile
+
+Users can customize their profile with a profile image and personal details.
+
+
+
### [ADDITIONAL FEATURES GO HERE - ADD ALL FEATURES HERE IN THE FORMAT ABOVE; you will check these off and add gifs as you complete them]
## Installation Instructions
diff --git a/milestones/milestone1.md b/milestones/milestone1.md
index 52b9b0038..11fa6bcb9 100644
--- a/milestones/milestone1.md
+++ b/milestones/milestone1.md
@@ -6,27 +6,27 @@ This document should be completed and submitted during **Unit 5** of this course
This unit, be sure to complete all tasks listed below. To complete a task, place an `x` between the brackets.
-- [ ] Read and understand all required features
- - [ ] Understand you **must** implement **all** baseline features and **two** custom features
-- [ ] In `readme.md`: update app name to your app's name
-- [ ] In `readme.md`: add all group members' names
-- [ ] In `readme.md`: complete the **Description and Purpose** section
-- [ ] In `readme.md`: complete the **Inspiration** section
-- [ ] In `readme.md`: list a name and description for all features (minimum 6 for full points) you intend to include in your app (in future units, you will check off features as you complete them and add GIFs demonstrating the features)
-- [ ] In `planning/user_stories.md`: add all user stories (minimum 10 for full points)
-- [ ] In `planning/user_stories.md`: use 1-3 unique user roles in your user stories
-- [ ] In this document, complete all thre questions in the **Reflection** section below
+- [x] Read and understand all required features
+- [x] Understand you **must** implement **all** baseline features and **two** custom features
+- [x] In `readme.md`: update app name to your app's name
+- [x] In `readme.md`: add all group members' names
+- [x] In `readme.md`: complete the **Description and Purpose** section
+- [x] In `readme.md`: complete the **Inspiration** section
+- [x] In `readme.md`: list a name and description for all features (minimum 6 for full points) you intend to include in your app (in future units, you will check off features as you complete them and add GIFs demonstrating the features)
+- [x] In `planning/user_stories.md`: add all user stories (minimum 10 for full points)
+- [x] In `planning/user_stories.md`: use 1-3 unique user roles in your user stories
+- [x] In this document, complete all thre questions in the **Reflection** section below
## Reflection
### 1. What went well during this unit?
-[👉🏾👉🏾👉🏾 your answer here]
+Our group collaborated effectively during our team meetings this unit. Meeting together allowed us to align on ideas, divide tasks clearly, and make decisions as a group, which kept our project on track.
### 2. What were some challenges your group faced in this unit?
-[👉🏾👉🏾👉🏾 your answer here]
+Our main challenge was communication over text. Relying on messaging sometimes led to misunderstandings and delayed responses, making it harder to coordinate and stay aligned on responsibilities.
### 3. What additional support will you need in upcoming units as you continue to work on your final project?
-[👉🏾👉🏾👉🏾 your answer here]
+We will need support with implementing user authentication and deploying it on Render. Guidance on configuring secure login within a hosted environment, along with common pitfalls to avoid, would help us move forward with our project.
\ No newline at end of file
diff --git a/milestones/milestone2.md b/milestones/milestone2.md
index e3178cd81..605ca78e5 100644
--- a/milestones/milestone2.md
+++ b/milestones/milestone2.md
@@ -6,24 +6,24 @@ This document should be completed and submitted during **Unit 6** of this course
This unit, be sure to complete all tasks listed below. To complete a task, place an `x` between the brackets.
-- [ ] In `planning/wireframes.md`: add wireframes for at least three pages in your web app.
- - [ ] Include a list of pages in your app
-- [ ] In `planning/entity_relationship_diagram.md`: add the entity relationship diagram you developed for your database.
- - [ ] Your entity relationship diagram should include the tables in your database.
-- [ ] Prepare your three-minute pitch presentation, to be presented during Unit 7 (the next unit).
- - [ ] You do **not** need to submit any materials in advance of your pitch.
-- [ ] In this document, complete all three questions in the **Reflection** section below
+- [x] In `planning/wireframes.md`: add wireframes for at least three pages in your web app.
+ - [x] Include a list of pages in your app
+- [x] In `planning/entity_relationship_diagram.md`: add the entity relationship diagram you developed for your database.
+ - [x] Your entity relationship diagram should include the tables in your database.
+- [x] Prepare your three-minute pitch presentation, to be presented during Unit 7 (the next unit).
+ - [x] You do **not** need to submit any materials in advance of your pitch.
+- [x] In this document, complete all three questions in the **Reflection** section below
## Reflection
### 1. What went well during this unit?
-[👉🏾👉🏾👉🏾 your answer here]
+We made strong progress on our wireframes, completing designs for nearly all of the core pages in the app — Home, Sign Up/Log In, Matches, Match Detail, Players & Teams, Team Information, Player Information, Fan Leaderboard, and User Profile. Team collaboration was also a highlight: we worked well together to align on the app's structure and navigation, which made it easier to move quickly from wireframes into a cohesive design system.
### 2. What were some challenges your group faced in this unit?
-[👉🏾👉🏾👉🏾 your answer here]
+The biggest challenge was scoping our features without overbuilding. Because MatchLens brings together several distinct systems — match data, player/team stats, authentication, a fan-following system, comments, and our Fan Leaderboard wagering mechanic — it was easy to keep adding "nice to have" functionality to each page. Figuring out how much of that to actually commit to for this milestone, versus treating it as a future enhancement, took more discussion than we expected.
### 3. What additional support will you need in upcoming units as you continue to work on your final project?
-[👉🏾👉🏾👉🏾 your answer here]
+Going into the next unit, we'd benefit most from help scoping which features count as MVP versus stretch goals. Given the number of features we've designed (authentication, search, match/player/team data display, leaderboards, virtual points/wagering, team following, comments, video links, and user profiles), we want to make sure we're prioritizing the right subset to build first so we deliver a solid, functional core before layering in the more complex or "nice to have" pieces like the fan dominance analysis or video highlights.
diff --git a/milestones/milestone3.md b/milestones/milestone3.md
index b45ad8561..cd055b4e4 100644
--- a/milestones/milestone3.md
+++ b/milestones/milestone3.md
@@ -8,44 +8,44 @@ This unit, be sure to complete all tasks listed below. To complete a task, place
You will need to reference the GitHub Project Management guide in the course portal for more information about how to complete each of these steps.
-- [ ] In your repo, create a project board.
+- [x] In your repo, create a project board.
- *Please be sure to share your project board with the grading team's GitHub **codepathreview**. This is separate from your repository's sharing settings.*
-- [ ] In your repo, create at least 5 issues from the features on your feature list.
+- [x] In your repo, create at least 5 issues from the features on your feature list.
- List the title of each issue you created:
- 1.
- 2.
- 3.
- 4.
- 5.
-- [ ] In your repo, update the status of issues in your project board.
-- [ ] In your repo, create a GitHub Milestone for each final project unit, corresponding to each of the 5 milestones in your `milestones/` directory.
+ 1. Player Profile & Stats
+ 2. Match Details View
+ 3. Fixture Calendar
+ 4. Secure Login
+ 5. Player Stats Leaderboard
+- [x] In your repo, update the status of issues in your project board.
+- [x] In your repo, create a GitHub Milestone for each final project unit, corresponding to each of the 5 milestones in your `milestones/` directory.
- List the name of each milestone you created:
- 1.
- 2.
- 3.
- - [ ] Set the completion percentage of each milestone. The GitHub Milestone for this unit (Milestone 3 - Unit 7) should be 100% completed when you submit for full points.
-- [ ] In `readme.md`, check off the features you have completed in this unit by adding a ✅ emoji in front of the feature's name.
- - [ ] Under each feature you have completed, include a GIF showing feature functionality.
-- [ ] In this documents, complete all five questions in the **Reflection** section below.
+ 1. Setting up
+ 2. Feature implementation
+ 3. Finishing up and deploy
+ - [x] Set the completion percentage of each milestone. The GitHub Milestone for this unit (Milestone 3 - Unit 7) should be 100% completed when you submit for full points.
+- [x] In `readme.md`, check off the features you have completed in this unit by adding a ✅ emoji in front of the feature's name.
+ - [x] Under each feature you have completed, include a GIF showing feature functionality.
+- [x] In this documents, complete all five questions in the **Reflection** section below.
## Reflection
### 1. What went well during this unit?
-[👉🏾👉🏾👉🏾 your answer here]
+Planning went well this unit. We were able to map out our full feature list, break it down into issues, and set up our project board and milestones to guide the rest of the project.
### 2. What were some challenges your group faced in this unit?
-[👉🏾👉🏾👉🏾 your answer here]
+Meeting and communication were our main challenges. Coordinating schedules across the team made it harder to sync up regularly and stay aligned on progress and next steps.
### Did you finish all of your tasks in your sprint plan for this week? If you did not finish all of the planned tasks, how would you prioritize the remaining tasks on your list?
-[👉🏾👉🏾👉🏾 your answer here]
+Yes, we finished the tasks planned for this sprint.
-### Which features and user stories would you consider “at risk”? How will you change your plan if those items remain “at risk”?
+### Which features and user stories would you consider "at risk"? How will you change your plan if those items remain "at risk"?
-[👉🏾👉🏾👉🏾 your answer here]
+Everything related to virtual points (supporting a team with points and the fan leaderboard tied to it) is at risk. If this remains at risk, we'll scope it down to a simpler version or push it later in our timeline so it doesn't block core features from being finished.
### 5. What additional support will you need in upcoming units as you continue to work on your final project?
-[👉🏾👉🏾👉🏾 your answer here]
+Time. Balancing this project alongside other coursework and commitments has been the biggest constraint, so more time to focus on implementation would help.
\ No newline at end of file
diff --git a/milestones/milestone4.md b/milestones/milestone4.md
index 61dc73bb9..3361ec2c3 100644
--- a/milestones/milestone4.md
+++ b/milestones/milestone4.md
@@ -6,29 +6,29 @@ This document should be completed and submitted during **Unit 8** of this course
This unit, be sure to complete all tasks listed below. To complete a task, place an `x` between the brackets.
-- [ ] Update the completion percentage of each GitHub Milestone. The milestone for this unit (Milestone 4 - Unit 8) should be 100% completed when you submit for full points.
-- [ ] In `readme.md`, check off the features you have completed in this unit by adding a ✅ emoji in front of the feature's name.
- - [ ] Under each feature you have completed, include a GIF showing feature functionality.
-- [ ] In this document, complete all five questions in the **Reflection** section below.
+- [x] Update the completion percentage of each GitHub Milestone. The milestone for this unit (Milestone 4 - Unit 8) should be 100% completed when you submit for full points.
+- [x] In `readme.md`, check off the features you have completed in this unit by adding a ✅ emoji in front of the feature's name.
+ - [x] Under each feature you have completed, include a GIF showing feature functionality.
+- [x] In this document, complete all five questions in the **Reflection** section below.
## Reflection
### 1. What went well during this unit?
-[👉🏾👉🏾👉🏾 your answer here]
+The division of labor went particularly well, with evryone being able to work independently but collaboratively on their assigned tasks.
### 2. What were some challenges your group faced in this unit?
-[👉🏾👉🏾👉🏾 your answer here]
+The most recent has been code updates that affect certain scopes that are shared among teammates, and having to rewire certain parts of your already completed codebase to work well in the overall system.
### Did you finish all of your tasks in your sprint plan for this week? If you did not finish all of the planned tasks, how would you prioritize the remaining tasks on your list?
-[👉🏾👉🏾👉🏾 your answer here]
+The team gas not finished all the tasks, but we plan to tackle this through increased accountability that is not limited to class time and our once a week check-in time.
### Which features and user stories would you consider “at risk”? How will you change your plan if those items remain “at risk”?
-[👉🏾👉🏾👉🏾 your answer here]
+The virtual points feature might be at risk as it is waning in its significance to the entire project and seems like a stretch.
### 5. What additional support will you need in upcoming units as you continue to work on your final project?
-[👉🏾👉🏾👉🏾 your answer here]
+Some resources to industry best practices of PRs etc, and docs that have to be done before a project starts, e.g. PRDs -> that lane of learning would be very beneficial.
diff --git a/milestones/progress_report_3.md b/milestones/progress_report_3.md
new file mode 100644
index 000000000..532a874cf
--- /dev/null
+++ b/milestones/progress_report_3.md
@@ -0,0 +1,39 @@
+# Milestone 3 Progress Report
+
+## GitHub Project Setup
+Project board created: yes
+Project board shared with codepathreview: yes
+
+## Issues Created
+List the title of each issue you created, one per line:
+- Player Profile & Stats
+- Match Details View
+- Fixture Calendar
+- Secure Login
+- Player Stats Leaderboard
+- Search
+- Match Comments
+- Match Video Highlights
+- Follow a Team + Notifications
+- Virtual Points to Support Your Team
+- Fan Leaderboard
+- Profile Customization
+- Database
+- FrontEnd
+- Backend
+
+## GitHub Milestones Created
+List the name of each GitHub Milestone you created, one per line:
+- Setting up
+- Feature implementation
+- Finishing up and deploy
+
+## Milestone 3 Completion
+Replace the blank before the percent sign with a number from 0 to 100.
+Completion percentage for Milestone 3 - Unit 7: 0%
+List each issue you completed this unit and the main file or folder where that work lives, one per line:
+-
+
+## Features Completed This Unit
+List each feature you completed and checked off in readme.md this unit, one per line:
+-
\ No newline at end of file
diff --git a/milestones/progress_report_4.md b/milestones/progress_report_4.md
new file mode 100644
index 000000000..552b1fd1e
--- /dev/null
+++ b/milestones/progress_report_4.md
@@ -0,0 +1,23 @@
+# Milestone 4 Progress Report
+
+## Milestone 4 Completion
+Replace the blank before the percent sign with a number from 0 to 100.
+Completion percentage for Milestone 4 - Unit 8: 42%
+List each issue you completed this unit and the main file or folder where that work lives, one per line:
+- Example: User login - server/routes/auth.js
+- Profile Customization - client/src/pages/Profile.jsx
+- FrontEnd - client/
+- Fixture Calendar - client/src/pages/Matches.jsx
+
+
+## Features Completed This Unit
+List each feature you completed and checked off in readme.md this unit, one per line:
+- Example: Search events by category
+- Authentication (including OAuth)
+- Profile Customization
+- Fixture Calendar
+- Dashboard/Home (view/layout)
+- Discover page (view/layout)
+- Team Profile (view/layout)
+- Match Details (view/layout)
+- Fan Leaderboard (view/layout)
diff --git a/planning/entity_relationship_diagram.md b/planning/entity_relationship_diagram.md
index 12c25f62c..94ae19565 100644
--- a/planning/entity_relationship_diagram.md
+++ b/planning/entity_relationship_diagram.md
@@ -4,14 +4,22 @@ Reference the Creating an Entity Relationship Diagram final project guide in the
## Create the List of Tables
-[👉🏾👉🏾👉🏾 List each table in your diagram]
+- **Users** — core account table: `user_id` (PK), `username` (unique), `email` (unique), `password_hash`, `profile_image_url`, `total_points`, `created_at`, `updated_at`
+- **Followed_Teams** — join table tracking which teams a user follows: `followed_team_id` (PK), `user_id` (FK → Users), `api_team_id`, `team_name`, `followed_at`
+- **Predictions** — a user's wager/prediction on a match's outcome: `prediction_id` (PK), `user_id` (FK → Users), `api_match_id`, `predicted_home_score`, `predicted_away_score`, `points_awarded`, `submitted_at`
+- **Comments** — user comments left on a match: `comment_id` (PK), `user_id` (FK → Users), `api_match_id`, `content`, `created_at`, `updated_at`
+- **Notifications** — alerts sent to a user (match alerts, transfer news, etc.): `notification_id` (PK), `user_id` (FK → Users), `api_team_id`, `api_match_id`, `notification_type`, `message`, `is_read`, `created_at`
+- **Video_Links** — video highlights/footage associated with a match: `video_link_id` (PK), `user_id` (FK → Users), `api_match_id`, `title`, `video_url`, `provider`, `created_at`
+ **Relationships:** One user can follow many teams, submit many predictions, write many comments, receive many notifications, and be associated with many video links — all one-to-many from `Users` out to the other five tables via `user_id`.
+
+Note: `api_match_id` and `api_team_id` are stored as external reference identifiers (from the football data API) rather than foreign keys into local tables, since match and team data are sourced externally rather than stored as their own entities in this schema.
## Add the Entity Relationship Diagram
-[👉🏾👉🏾👉🏾 Include an image or images of the diagram below. You may also wish to use the following markdown syntax to outline each table, as per your preference.]
+**Initial draft (whiteboard):**
+
+
+
+**Finalized diagram:**
-| Column Name | Type | Description |
-|-------------|------|-------------|
-| id | integer | primary key |
-| name | text | name of the shoe model |
-| ... | ... | ... |
+
diff --git a/planning/images/ER Diagram MatchLens.png b/planning/images/ER Diagram MatchLens.png
new file mode 100644
index 000000000..7c78f4291
Binary files /dev/null and b/planning/images/ER Diagram MatchLens.png differ
diff --git a/planning/images/ER Diagram v1.png b/planning/images/ER Diagram v1.png
new file mode 100644
index 000000000..345da3e35
Binary files /dev/null and b/planning/images/ER Diagram v1.png differ
diff --git a/planning/images/Wireframe Fan Leaderboard page.png b/planning/images/Wireframe Fan Leaderboard page.png
new file mode 100644
index 000000000..075378e20
Binary files /dev/null and b/planning/images/Wireframe Fan Leaderboard page.png differ
diff --git a/planning/images/Wireframe Home page.png b/planning/images/Wireframe Home page.png
new file mode 100644
index 000000000..4758fb2b6
Binary files /dev/null and b/planning/images/Wireframe Home page.png differ
diff --git a/planning/images/Wireframe Match info page.png b/planning/images/Wireframe Match info page.png
new file mode 100644
index 000000000..ba4667514
Binary files /dev/null and b/planning/images/Wireframe Match info page.png differ
diff --git a/planning/images/Wireframe Matches home page.png b/planning/images/Wireframe Matches home page.png
new file mode 100644
index 000000000..778dd49ea
Binary files /dev/null and b/planning/images/Wireframe Matches home page.png differ
diff --git a/planning/images/Wireframe Payers&Teams home page.png b/planning/images/Wireframe Payers&Teams home page.png
new file mode 100644
index 000000000..69d26051f
Binary files /dev/null and b/planning/images/Wireframe Payers&Teams home page.png differ
diff --git a/planning/images/Wireframe Player info page.png b/planning/images/Wireframe Player info page.png
new file mode 100644
index 000000000..88760f207
Binary files /dev/null and b/planning/images/Wireframe Player info page.png differ
diff --git a/planning/images/Wireframe Sign up Log In page.png b/planning/images/Wireframe Sign up Log In page.png
new file mode 100644
index 000000000..c3644ff77
Binary files /dev/null and b/planning/images/Wireframe Sign up Log In page.png differ
diff --git a/planning/images/Wireframe Team information page.png b/planning/images/Wireframe Team information page.png
new file mode 100644
index 000000000..d650be5f2
Binary files /dev/null and b/planning/images/Wireframe Team information page.png differ
diff --git a/planning/images/Wireframe User profile page.png b/planning/images/Wireframe User profile page.png
new file mode 100644
index 000000000..ed37ea7df
Binary files /dev/null and b/planning/images/Wireframe User profile page.png differ
diff --git a/planning/user_stories.md b/planning/user_stories.md
index 1e55ecbcd..8a0e655c0 100644
--- a/planning/user_stories.md
+++ b/planning/user_stories.md
@@ -4,10 +4,31 @@ Reference the Writing User Stories final project guide in the course portal for
## Outline User Roles
-[👉🏾👉🏾👉🏾 Include at least at least 1, but no more than 3, user roles.]
+- **User** - A registered account holder who can browse football data, interact with matches, follow teams, and manage their profile.
+
## Draft User Stories
-[👉🏾👉🏾👉🏾 Include at least at least 10 user stories in this format:]
+1. As a user, I want to view a player's history, team, and stats such as goals and other key information, so that I can learn more about the players I'm interested in.
+
+2. As a user, I want to see match details like the score, lineups, venue, and weather, so that I have full context on any game I'm following.
+
+3. As a user, I want to view a calendar of match dates and fixtures, so that I know when upcoming games are being played.
+
+4. As a user, I want to log in with secure authentication, so that I can access my account and personalized features.
+
+5. As a user, I want to view a leaderboard ranking players by their stats, such as goals, so that I can see who the top performers are.
+
+6. As a user, I want to search for players, teams, and matches, so that I can quickly find the information I'm looking for.
+
+7. As a user, I want to comment on a match, so that I can share my thoughts and engage with other fans.
+
+8. As a user, I want to access a video link of a match, so that I can watch the highlights or full game.
+
+9. As a user, I want to follow a team and receive news and match notifications, so that I can stay up to date with the teams I care about.
+
+10. As a user, I want to use virtual points to support my team, so that I can show my backing and improve my standing on the leaderboard.
+
+11. As a user, I want to see a leaderboard of users with the most points, so that I can compare my ranking against other fans.
-1. As a [user role], I want to [what], so that [why].
+12. As a user, I want to customize my profile with a profile image, so that I can personalize my account.
\ No newline at end of file
diff --git a/planning/wireframes.md b/planning/wireframes.md
index fbcd15a0c..95029c5c8 100644
--- a/planning/wireframes.md
+++ b/planning/wireframes.md
@@ -4,18 +4,66 @@ Reference the Creating an Entity Relationship Diagram final project guide in the
## List of Pages
-[👉🏾👉🏾👉🏾 List the pages you expect to have in your app, with a ⭐ next to pages you have wireframed]
+- Home ⭐
+- Sign Up / Log In ⭐
+- Matches (list/browse) ⭐
+- Match Detail (score, lineup, stats, comments, highlights) ⭐
+- Players & Teams (search/discover) ⭐
+- Team Information ⭐
+- Player Information ⭐
+- Fan Leaderboard ⭐
+- User Profile ⭐
-## Wireframe 1: [page title]
+## Wireframe 1: Home
-[👉🏾👉🏾👉🏾 include wireframe 1]
+
-## Wireframe 2: [page title]
+The landing dashboard after login. Surfaces live match scores in a ticker, a tournament bracket path, a Fan Leaderboard preview with the user's weekly point allowance and current balance, a followed-teams standings table, and a top scorers/player-of-the-week panel.
-[👉🏾👉🏾👉🏾 include wireframe 2]
+## Wireframe 2: Sign Up / Log In
-## Wireframe 3: [page title]
+
-[👉🏾👉🏾👉🏾 include wireframe 3]
+Authentication entry point. Email/password fields, forgot-password link, social sign-in via Google and Apple, and a toggle link to the sign-up flow.
-[👉🏾👉🏾👉🏾 include more wireframes as desired]
+## Wireframe 3: Matches
+
+
+
+Browse view for all fixtures, filterable by Live / Upcoming / Results. Each match card shows league, score or kickoff time, live status, and a match-probability bar with win/draw/loss percentages.
+
+## Wireframe 4: Match Detail
+
+
+
+Deep-dive page for a single match. Includes win-probability bar, live score header, a wager entry point ("Place Wager"), tabbed content (Overview, Lineup, Stats, Comments, Highlights), match events timeline, lineup visualization, key stats comparison, possession split, and a live fan chat/comments feed.
+
+## Wireframe 5: Players & Teams
+
+
+
+Discovery hub for searching teams and players, with trending suggestions and a grid of featured teams that can be followed directly from this view
+
+## Wireframe 6: Team Information
+
+
+
+Individual team profile. Shows team header (stadium, location, follower count) with a Follow Team action, recent form (last 5 results), upcoming fixtures, and the first-team roster with player ratings.
+
+## Wireframe 7: Player Information
+
+
+
+Individual player profile. Displays a MatchLens performance score with trend, current club, season stats (goals, assists, pass accuracy, key passes), career history by season, market value estimate, and attribute breakdown (vision, technique, passing, tactical IQ).
+
+## Wireframe 8: Fan Leaderboard
+
+
+
+Rankings page for MatchLens' signature feature. Lists top users by total points earned through wagering, their top-earning team, and point trend, alongside an explainer of the earning mechanics (follow a team → place a wager → win to double your stake) and a recent-top-earners feed.
+
+## Wireframe 9: User Profile
+
+
+
+The signed-in user's own account page. Shows profile photo/bio, virtual points balance with a top-up action, followed teams with live/next-match status, notification preferences (match alerts, transfer news), privacy settings (profile visibility, data sharing, connected apps), and account deactivation.