diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..438657a9e --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# 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? diff --git a/README.md b/README.md index 0e1211217..c6e4c4da1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# [your app name here] +# Ramblr CodePath WEB103 Final Project -Designed and developed by: [your names here] +Designed and developed by: Zainab Akhtar, Jasper Caballero, Aquila Nuzhat πŸ”— Link to deployed app: @@ -10,11 +10,11 @@ Designed and developed by: [your names here] ### Description and Purpose -[text goes here] +A web platform designed to help travelers plan out every aspect of their trip in one place! ### Inspiration -[text goes here] +As we have experienced the struggle to plan a trip across different platforms for lodging, activities, etc. we shared the sentiment of wanting to able to plan it all in one place. And thus, Ramblr became our theme! ## Tech Stack @@ -24,25 +24,29 @@ Backend: ## Features -### [Name of Feature 1] +### 1: Trips -[short description goes here] +Will store a list of all the trips the user has planned for in the user profile -[gif goes here] +### 2: Trip -### [Name of Feature 2] +Will display dates of trip, an itinerary of activities, packing list, and expenses of the trip -[short description goes here] +### 3: Activity -[gif goes here] +Will store the location and type of activity to be part of the trip itinerary -### [Name of Feature 3] +### 4: Expenses -[short description goes here] +Will add up the costs of all the activities planned + lodging + transportation to give user a summary of trip expenses -[gif goes here] +### 5: Packing List -### [ADDITIONAL FEATURES GO HERE - ADD ALL FEATURES HERE IN THE FORMAT ABOVE; you will check these off and add gifs as you complete them] +Will store all the items the user plans to take (ie. clothes, travel sized lotion, scuba gear) + +### 6: Weather API + +A tab displaying the weather for all the days of the trip so that the user can plan accordingly ## Installation Instructions diff --git a/client/index.html b/client/index.html new file mode 100644 index 000000000..c117b025e --- /dev/null +++ b/client/index.html @@ -0,0 +1,12 @@ + + + + + + Ramblr + + +
+ + + \ No newline at end of file diff --git a/client/public/.gitkeep b/client/public/.gitkeep new file mode 100644 index 000000000..cc312075f --- /dev/null +++ b/client/public/.gitkeep @@ -0,0 +1,2 @@ +# Placeholder file to keep this empty folder tracked by git. +# Delete this once real files are added here. diff --git a/client/src/App.jsx b/client/src/App.jsx new file mode 100644 index 000000000..b4faf5173 --- /dev/null +++ b/client/src/App.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useRoutes } from 'react-router-dom'; +import Profile from './pages/Profile.jsx'; + +// TODO: replace with real pages/components (Navigation, Home, Trips, TripDetails, etc.) +const App = () => { + let element = useRoutes([ + { path: '/', element:

Ramblr 🧳 β€” placeholder home page

}, + { path: '/profile', element: }, + ]); + + return ( +
+ {element} +
+ ); +}; + +export default App; \ No newline at end of file diff --git a/client/src/assets/.gitkeep b/client/src/assets/.gitkeep new file mode 100644 index 000000000..cc312075f --- /dev/null +++ b/client/src/assets/.gitkeep @@ -0,0 +1,2 @@ +# Placeholder file to keep this empty folder tracked by git. +# Delete this once real files are added here. diff --git a/client/src/components/.gitkeep b/client/src/components/.gitkeep new file mode 100644 index 000000000..cc312075f --- /dev/null +++ b/client/src/components/.gitkeep @@ -0,0 +1,2 @@ +# Placeholder file to keep this empty folder tracked by git. +# Delete this once real files are added here. diff --git a/client/src/components/Sidebar.jsx b/client/src/components/Sidebar.jsx new file mode 100644 index 000000000..5a83640c0 --- /dev/null +++ b/client/src/components/Sidebar.jsx @@ -0,0 +1,99 @@ +import { Link, useLocation } from "react-router-dom"; +import "../css/Sidebar.css"; + +// Simple inline stroke icons so the sidebar doesn't need an icon library dependency. +const icons = { + dashboard: ( + + + + + + + ), + trips: ( + + + + + + ), + itinerary: ( + + + + + + + ), + budget: ( + + + + + ), + packing: ( + + + + + + ), + profile: ( + + + + + ), + logout: ( + + + + + + ), +}; + +const NAV_ITEMS = [ + { label: "Dashboard", path: "/dashboard", icon: "dashboard" }, + { label: "My Trips", path: "/trips", icon: "trips" }, + { label: "Itinerary", path: "/itinerary", icon: "itinerary" }, + { label: "Budget", path: "/budget", icon: "budget" }, + { label: "Packing List", path: "/packing-list", icon: "packing" }, + { label: "Profile", path: "/profile", icon: "profile" }, +]; + +const Sidebar = () => { + const { pathname } = useLocation(); + + return ( + + ); +}; + +export default Sidebar; diff --git a/client/src/css/.gitkeep b/client/src/css/.gitkeep new file mode 100644 index 000000000..cc312075f --- /dev/null +++ b/client/src/css/.gitkeep @@ -0,0 +1,2 @@ +# Placeholder file to keep this empty folder tracked by git. +# Delete this once real files are added here. diff --git a/client/src/css/Profile.css b/client/src/css/Profile.css new file mode 100644 index 000000000..26666a5cb --- /dev/null +++ b/client/src/css/Profile.css @@ -0,0 +1,236 @@ +.page { + display: flex; + min-height: 100vh; + background: var(--sand-bg); +} + +.profile { + flex: 1; + padding: 32px 40px 60px; + max-width: 720px; + box-sizing: border-box; +} + +.profile__header h1 { + font-size: 22px; + font-weight: 600; + color: var(--sand-text); + margin: 0 0 20px; +} + +.profile__card { + background: var(--sand-surface); + border: 1px solid var(--sand-border); + border-radius: 14px; + padding: 22px 24px; + margin-bottom: 18px; +} + +.profile__loading, +.profile__error-card { + color: var(--sand-text-soft); + font-size: 14px; +} + +.profile__error-card { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + color: var(--sand-error-text); +} + +.profile__identity { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.profile__identity-left { + display: flex; + align-items: center; + gap: 16px; +} + +.profile__avatar { + width: 56px; + height: 56px; + border-radius: 50%; + background: var(--sand-chip); + color: var(--sand-accent); + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 16px; + flex-shrink: 0; +} + +.profile__name { + margin: 0; + font-size: 17px; + font-weight: 600; + color: var(--sand-text); +} + +.profile__subtitle { + margin: 2px 0 0; + font-size: 13px; + color: var(--sand-text-soft); +} + +.profile__toast { + background: var(--sand-success-bg); + border: 1px solid var(--sand-success-border); + color: var(--sand-success-text); + font-size: 13px; + padding: 10px 14px; + border-radius: 10px; + margin-bottom: 18px; +} + +.profile__settings-title { + margin: 0 0 14px; + font-size: 14px; + font-weight: 600; + color: var(--sand-text); +} + +.settings-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 0; + border-top: 1px solid var(--sand-border-soft); +} + +.settings-row:first-of-type { + border-top: none; + padding-top: 0; +} + +.settings-row__label { + font-size: 13px; + color: var(--sand-text-soft); + margin-bottom: 2px; +} + +.settings-row__value { + font-size: 14px; + color: var(--sand-text); + font-weight: 500; +} + +.settings-row--muted { + color: var(--sand-muted); +} + +.settings-row__badge { + font-size: 11px; + color: var(--sand-muted); + background: var(--sand-chip); + border-radius: 999px; + padding: 3px 10px; +} + +.profile__form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.profile__form-error { + background: var(--sand-error-bg); + border: 1px solid var(--sand-error-border); + color: var(--sand-error-text); + font-size: 13px; + padding: 10px 14px; + border-radius: 10px; +} + +.field { + display: flex; + flex-direction: column; + gap: 6px; +} + +.field__label { + font-size: 13px; + font-weight: 500; + color: var(--sand-text); +} + +.field input, +.field select { + font-size: 14px; + padding: 10px 12px; + border: 1px solid var(--sand-border); + border-radius: 9px; + color: var(--sand-text); + background: var(--sand-surface); + font-family: inherit; +} + +.field input:focus, +.field select:focus { + outline: none; + border-color: var(--sand-accent); +} + +.field input:disabled, +.field select:disabled { + background: var(--sand-chip); + color: var(--sand-muted); +} + +.field__error { + font-size: 12px; + color: var(--sand-error-text); +} + +.field__hint { + font-size: 12px; + color: var(--sand-muted); +} + +.profile__form-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 4px; +} + +.btn { + font-size: 13px; + font-weight: 500; + padding: 9px 16px; + border-radius: 9px; + border: 1px solid transparent; + cursor: pointer; + font-family: inherit; +} + +.btn:disabled { + cursor: not-allowed; + opacity: 0.6; +} + +.btn--primary { + background: var(--sand-accent); + color: #fffcf7; +} + +.btn--primary:hover:not(:disabled) { + background: var(--sand-accent-hover); +} + +.btn--secondary { + background: var(--sand-surface); + color: var(--sand-text); + border-color: var(--sand-border); +} + +.btn--secondary:hover:not(:disabled) { + background: var(--sand-chip); +} \ No newline at end of file diff --git a/client/src/css/Sidebar.css b/client/src/css/Sidebar.css new file mode 100644 index 000000000..693174e2d --- /dev/null +++ b/client/src/css/Sidebar.css @@ -0,0 +1,81 @@ +.sidebar { + width: 220px; + flex-shrink: 0; + min-height: 100vh; + background: var(--sand-surface); + border-right: 1px solid var(--sand-border); + padding: 24px 16px; + display: flex; + flex-direction: column; + gap: 28px; + box-sizing: border-box; +} + +.sidebar__brand { + display: flex; + align-items: center; + gap: 8px; + padding: 0 8px; + font-weight: 600; + font-size: 15px; + color: var(--sand-text); +} + +.sidebar__brand-mark { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + border-radius: 8px; + background: var(--sand-chip); + font-size: 13px; +} + +.sidebar__nav { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; +} + +.sidebar__link { + display: flex; + align-items: center; + gap: 10px; + padding: 9px 10px; + border-radius: 8px; + font-size: 14px; + color: var(--sand-text-soft); + text-decoration: none; + border: none; + background: none; + cursor: pointer; + text-align: left; + font-family: inherit; +} + +.sidebar__link:hover { + background: var(--sand-chip); + color: var(--sand-text); +} + +.sidebar__link--active { + background: var(--sand-accent); + color: #fffcf7; +} + +.sidebar__link--active .sidebar__icon { + color: #fffcf7; +} + +.sidebar__icon { + display: inline-flex; + width: 18px; + height: 18px; + color: var(--sand-muted); +} + +.sidebar__logout { + margin-top: auto; +} \ No newline at end of file diff --git a/client/src/helpers/.gitkeep b/client/src/helpers/.gitkeep new file mode 100644 index 000000000..cc312075f --- /dev/null +++ b/client/src/helpers/.gitkeep @@ -0,0 +1,2 @@ +# Placeholder file to keep this empty folder tracked by git. +# Delete this once real files are added here. diff --git a/client/src/helpers/currencies.js b/client/src/helpers/currencies.js new file mode 100644 index 000000000..38316ef13 --- /dev/null +++ b/client/src/helpers/currencies.js @@ -0,0 +1,29 @@ +// A curated list of common travel currencies for the home-currency selector. +// Not exhaustive -- mirrors the coverage in server/data/countryCurrency.js. + + +export const CURRENCIES = [ + { code: "USD", label: "USD - US Dollar" }, + { code: "EUR", label: "EUR - Euro" }, + { code: "GBP", label: "GBP - British Pound" }, + { code: "CAD", label: "CAD - Canadian Dollar" }, + { code: "AUD", label: "AUD - Australian Dollar" }, + { code: "NZD", label: "NZD - New Zealand Dollar" }, + { code: "JPY", label: "JPY - Japanese Yen" }, + { code: "CNY", label: "CNY - Chinese Yuan" }, + { code: "KRW", label: "KRW - South Korean Won" }, + { code: "INR", label: "INR - Indian Rupee" }, + { code: "SGD", label: "SGD - Singapore Dollar" }, + { code: "HKD", label: "HKD - Hong Kong Dollar" }, + { code: "THB", label: "THB - Thai Baht" }, + { code: "VND", label: "VND - Vietnamese Dong" }, + { code: "MXN", label: "MXN - Mexican Peso" }, + { code: "BRL", label: "BRL - Brazilian Real" }, + { code: "CHF", label: "CHF - Swiss Franc" }, + { code: "SEK", label: "SEK - Swedish Krona" }, + { code: "NOK", label: "NOK - Norwegian Krone" }, + { code: "DKK", label: "DKK - Danish Krone" }, + { code: "ZAR", label: "ZAR - South African Rand" }, + { code: "AED", label: "AED - UAE Dirham" }, + { code: "TRY", label: "TRY - Turkish Lira" }, +]; diff --git a/client/src/index.css b/client/src/index.css new file mode 100644 index 000000000..0c1cacd58 --- /dev/null +++ b/client/src/index.css @@ -0,0 +1,26 @@ + +:root { + /* Sand / beige palette -- shared across pages so new pages match this one. */ + --sand-bg: #f7f2ea; /* page background */ + --sand-surface: #fffcf7; /* cards, inputs */ + --sand-border: #e6dcc8; /* card + input borders */ + --sand-border-soft: #efe8da; /* dividers inside cards */ + --sand-muted: #a89a83; /* placeholder/hint text, disabled */ + --sand-text-soft: #8a7c66; /* secondary text */ + --sand-text: #4a3f2f; /* primary text */ + --sand-accent: #6b5335; /* primary buttons, active nav */ + --sand-accent-hover: #5a4529; + --sand-chip: #efe6d6; /* badges, avatar background */ + --sand-error-bg: #fbeee9; + --sand-error-border: #eecdbd; + --sand-error-text: #a4462a; + --sand-success-bg: #f2f2e2; + --sand-success-border: #d9dcb4; + --sand-success-text: #6b7a3c; +} + +body { + margin: 0; + font-family: system-ui, sans-serif; + background: var(--sand-bg); +} \ No newline at end of file diff --git a/client/src/main.jsx b/client/src/main.jsx new file mode 100644 index 000000000..a2ca76165 --- /dev/null +++ b/client/src/main.jsx @@ -0,0 +1,13 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + + + +) \ No newline at end of file diff --git a/client/src/pages/.gitkeep b/client/src/pages/.gitkeep new file mode 100644 index 000000000..cc312075f --- /dev/null +++ b/client/src/pages/.gitkeep @@ -0,0 +1,2 @@ +# Placeholder file to keep this empty folder tracked by git. +# Delete this once real files are added here. diff --git a/client/src/pages/Profile.jsx b/client/src/pages/Profile.jsx new file mode 100644 index 000000000..ebda073b1 --- /dev/null +++ b/client/src/pages/Profile.jsx @@ -0,0 +1,255 @@ +import { useEffect, useState } from "react"; +import Sidebar from "../components/Sidebar.jsx"; +import { fetchCurrentUser, updateCurrentUser } from "../services/users.js"; +import { CURRENCIES } from "../helpers/currencies.js"; +import "../css/Profile.css"; + +const DEFAULT_CURRENCY = "USD"; + +const formatMemberSince = (createdAt) => { + if (!createdAt) return null; + const date = new Date(createdAt); + if (Number.isNaN(date.getTime())) return null; + return date.toLocaleDateString(undefined, { month: "long", year: "numeric" }); +}; + +const getInitials = (name) => { + if (!name) return "?"; + return name + .trim() + .split(/\s+/) + .slice(0, 2) + .map((part) => part[0]?.toUpperCase()) + .join(""); +}; + +const Profile = () => { + // Profile data as last confirmed by the server. + const [user, setUser] = useState(null); + const [loadStatus, setLoadStatus] = useState("loading"); // loading | ready | error + const [loadError, setLoadError] = useState(""); + + // Edit form state -- kept separate from `user` so a failed save never + // clobbers the last-known-good profile info on screen. + const [isEditing, setIsEditing] = useState(false); + const [form, setForm] = useState({ displayName: "", homeCurrency: DEFAULT_CURRENCY }); + const [formError, setFormError] = useState(""); + + const [saveStatus, setSaveStatus] = useState("idle"); // idle | saving | error + const [saveError, setSaveError] = useState(""); + const [justSaved, setJustSaved] = useState(false); + + const loadProfile = () => { + setLoadStatus("loading"); + setLoadError(""); + fetchCurrentUser() + .then((data) => { + setUser(data); + setLoadStatus("ready"); + }) + .catch((err) => { + setLoadError(err.message || "Something went wrong loading your profile."); + setLoadStatus("error"); + }); + }; + + useEffect(() => { + loadProfile(); + }, []); + + const startEditing = () => { + setForm({ + displayName: user?.display_name || "", + homeCurrency: user?.home_currency || DEFAULT_CURRENCY, + }); + setFormError(""); + setSaveStatus("idle"); + setSaveError(""); + setIsEditing(true); + }; + + const cancelEditing = () => { + setIsEditing(false); + setFormError(""); + setSaveStatus("idle"); + setSaveError(""); + }; + + const handleFieldChange = (field) => (e) => { + setForm((prev) => ({ ...prev, [field]: e.target.value })); + if (field === "displayName" && formError) setFormError(""); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + const trimmedName = form.displayName.trim(); + if (!trimmedName) { + setFormError("Display name is required."); + return; + } + + setFormError(""); + setSaveStatus("saving"); + setSaveError(""); + + try { + const updated = await updateCurrentUser({ + displayName: trimmedName, + homeCurrency: form.homeCurrency || DEFAULT_CURRENCY, + }); + // Update from the server's response so the screen reflects exactly what was persisted. + setUser(updated); + setIsEditing(false); + setSaveStatus("idle"); + setJustSaved(true); + window.setTimeout(() => setJustSaved(false), 3000); + } catch (err) { + // Keep the existing profile info on screen; only surface the error. + setSaveStatus("error"); + setSaveError(err.message || "Failed to update profile. Please try again."); + } + }; + + return ( +
+ + +
+
+

Profile

+
+ + {loadStatus === "loading" && ( +
Loading your profile…
+ )} + + {loadStatus === "error" && ( +
+

{loadError}

+ +
+ )} + + {loadStatus === "ready" && user && ( + <> +
+
+ +
+

{user.display_name}

+

+ {formatMemberSince(user.created_at) + ? `Explorer since ${formatMemberSince(user.created_at)}` + : "Explorer"} +

+
+
+ + {!isEditing && ( + + )} +
+ + {justSaved && !isEditing && ( +
+ Profile updated. +
+ )} + +
+

Account Settings

+ + {!isEditing && ( + <> +
+
+
Display Name
+
{user.display_name}
+
+
+
+
+
Home Currency
+
{user.home_currency || DEFAULT_CURRENCY}
+
+
+
+
Notifications
+ Coming soon +
+
+
Help & Support
+ Coming soon +
+ + )} + + {isEditing && ( +
+ {saveStatus === "error" && ( +
+ {saveError} +
+ )} + + + + + +
+ + +
+
+ )} +
+ + )} +
+
+ ); +}; + +export default Profile; diff --git a/client/src/services/.gitkeep b/client/src/services/.gitkeep new file mode 100644 index 000000000..cc312075f --- /dev/null +++ b/client/src/services/.gitkeep @@ -0,0 +1,2 @@ +# Placeholder file to keep this empty folder tracked by git. +# Delete this once real files are added here. diff --git a/client/src/services/users.js b/client/src/services/users.js new file mode 100644 index 000000000..32acca26b --- /dev/null +++ b/client/src/services/users.js @@ -0,0 +1,41 @@ +// Thin wrapper around the Users API (server/routes/users.js). +// No auth yet -- /me always resolves to the seeded current user (T1). + +const BASE_URL = "/api/users"; + +const parseJsonSafe = async (res) => { + try { + return await res.json(); + } catch { + return {}; + } +}; + +// GET /api/users/me +export const fetchCurrentUser = async () => { + const res = await fetch(`${BASE_URL}/me`); + const body = await parseJsonSafe(res); + + if (!res.ok) { + throw new Error(body.error || "Failed to load profile."); + } + + return body; +}; + +// PATCH /api/users/me +// Accepts the fields that are actually editable for a user: displayName and/or homeCurrency. +export const updateCurrentUser = async ({ displayName, homeCurrency }) => { + const res = await fetch(`${BASE_URL}/me`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ displayName, homeCurrency }), + }); + const body = await parseJsonSafe(res); + + if (!res.ok) { + throw new Error(body.error || "Failed to update profile."); + } + + return body; +}; diff --git a/client/vite.config.js b/client/vite.config.js new file mode 100644 index 000000000..84bbd4a23 --- /dev/null +++ b/client/vite.config.js @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../server/public', + emptyOutDir: true + }, + server: { + proxy: { + '/api': { + target: 'http://localhost:3000' + } + } + } +}) \ No newline at end of file diff --git a/milestones/milestone1.md b/milestones/milestone1.md index 52b9b0038..fdf05229d 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 was able to successfully brainstorm many different project ideas that everyone was excited about. After discussing a lot of concepts, we decided to choose the Travel Planner app as it not only is practical, but also contains enough features to meet the requirements of the project. We started by discussing the core features, planning the documents, and tried to gain a better understanding of the requirements for the project. Then, as a team, we analyzed how our features and database would work together to help meet the requirements of the course. In addition, each of us put in great communication efforts to continue making progress on the project. We listened to each other's opinions, shared ideas, and worked together to come up with a decision. We stayed in touch throughout the process to discuss the overall concepts of the app. Overall, we developed a strong foundation of communication for the rest of the capstone project and feel confident in our abilities to move onto the development phase. ### 2. What were some challenges your group faced in this unit? -[πŸ‘‰πŸΎπŸ‘‰πŸΎπŸ‘‰πŸΎ your answer here] +One of the first challenges we encountered as a group was deciding about which project idea to choose from. We had four strong ideas, a music app, media tracker, sleep tracker and a travel planner. Each one of these ideas had its own strengths and weaknesses. We had several different discussions which compared each idea and also determined which of these would not only be fun to build but also meet the technical requirements of this capstone project. In order to finalize an idea, we were forced to think beyond the concept itself and had to think about which of these would support the features such as many-to-many and one-to-many database relationships, filtering, CRUD functionality, and scalability. As a group, we finally decided that Travel planner would fit the requirements the best as it provided the best balance between feasibility, functionality and creativity. ### 3. What additional support will you need in upcoming units as you continue to work on your final project? -[πŸ‘‰πŸΎπŸ‘‰πŸΎπŸ‘‰πŸΎ your answer here] +As we move forward on the progress for our project, we will most likely need the most assistance with designing the database schema as well as developing the entity relationship diagram. Because our Travel Planner app consists of multiple entities such as destinations, trips, users, activities, and packaging lists, we want to ensure that our many-to-many and one-to-many relationships are designed correctly prior to starting the development process. In addition, another area where guidance would be helpful is when connecting our react frontend to our Express backend, making sure database operations, frontend components, and API routes all work together coherently. diff --git a/milestones/milestone2.md b/milestones/milestone2.md index e3178cd81..5eb8ea409 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] +Our group was able to flesh out the hierarchy of our data and produce our entity relationship diagram, while simplifying our database design by removing many-to-many relationships where they were unnecessary. This created a clearer flow of foreign keys that will be easier to implement. We also developed the main pages of our wireframes and are continuing to refine the design with user flow and the features we want to emphasize in mind. Overall, everyone was communicative and proactive with their responsibilities, which allowed us to complete the milestone on time while still leaving room to gather feedback from one another and our TF before presenting our pitch. ### 2. What were some challenges your group faced in this unit? -[πŸ‘‰πŸΎπŸ‘‰πŸΎπŸ‘‰πŸΎ your answer here] +One challenge we faced was determining the best way to structure our database while keeping it scalable and avoiding unnecessary complexity. We initially included several many-to-many relationships before realizing a simpler schema would better fit our application's needs. We also spent time prioritizing which features should be included in our minimum viable product (MVP) versus those that could be implemented later, ensuring that our project remains realistic for the course timeline. For example, we have set the login authenticator and the weather API to display the weather on days that the user has scheduled a trip as additional features rather than part of the MVP. ### 3. What additional support will you need in upcoming units as you continue to work on your final project? -[πŸ‘‰πŸΎπŸ‘‰πŸΎπŸ‘‰πŸΎ your answer here] +Guidance on organizing issues, setting milestones and tasks, as well as feedback on our database and front-end design as we move into the implementation phase would be greatly appreciated. With this support, we hope to set realistic and achievable goals within the time we have, while making more informed technical and design decisions throughout the development process. diff --git a/milestones/milestone3.md b/milestones/milestone3.md index b45ad8561..56adff67a 100644 --- a/milestones/milestone3.md +++ b/milestones/milestone3.md @@ -8,44 +8,46 @@ 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. User (table + profile APIT) + 2. Trips (table + CRUD API + budget filter) + 3. Destinations (table + CRUD API, nested under trip) + 4. Activities + (table + CRUD API, nested under trip) + 5. Expenses (table + CRUD API + spending summary) +- [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. Milestone 3: Unit 7 + 2. Milestone 4: Unit 8 + 3. Milestone 5: Unit 9 + - [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] +What went well during this unit was the amount of planning that our team completed prior to the beginning of the development. Taking time to identify dependencies, create detailed Github issues, and organize our sprint planning helped to develop a clever understanding od how each of the features fit into the overall structure of the application. In addition it made the assigning work much more efficient as each of us had a well-defined understanding of their responsibilities and the order in which the tasks needed to be completed. Although this took some time to do, it will make the development process much more organized by minimizing duplicate work, reducing confusion and allowing the team to collaborate with one another more effectively. ### 2. What were some challenges your group faced in this unit? -[πŸ‘‰πŸΎπŸ‘‰πŸΎπŸ‘‰πŸΎ your answer here] +One of the biggest challenges that we faced was organizing the structure of the project in a way which allowed everyone to contribute effectively while still minimizing the overlapping work. A lot of our features depend on one another, so it was imperative we identified which tasks needed to be completed first and how to better sequence the remaining work. These discussions helped to create a much more realistic development plan and gave our team greater confidence as we moved into implementation. ### 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] +We successfully completed all of the tasks in our sprint plan for this week. Our goal for this milestone was to establish our issues in the GitHub, and organize the project's structure so that all planned features, functionality, and dependencies were clearly defined. This organization will allow our team to work on different parts of the project simultaneously, and minimize merge conflicts and overlapping work as development progresses. + ### 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 features most "at risk" are the ones that depend on other tickets being done first; mainly Destinations and Activities, since they can't start until Users and Trips are finished. That makes Users and Trips the foundation of the whole app, so they aren't really "at risk" themselves, but they have to be done early or everything after them gets delayed. Anything tied to login is also at risk, which is why we made authentication a stretch feature and used a single default user for now so the core app doesn't rely on it. To avoid problems, we'd focus on finishing Users and Trips as soon as possible, since completing Trips alone lets three other tickets be worked on at the same time. If those tickets stayed at risk, we'd break these big tickets (Users/Trips) into smaller tasks so groupmates can share the work and make it more manageable. ### 5. What additional support will you need in upcoming units as you continue to work on your final project? -[πŸ‘‰πŸΎπŸ‘‰πŸΎπŸ‘‰πŸΎ your answer here] +As we move into the phase of development, the biggest support needed would be when we start integrating everyone's work into one main application. Since each group member is responsible for different sections of the project, it is imperative that our backend, frontend, APIs and database are all working together effortlessly. It would also be helpful to receive feedback on the overall code organization and project structure as we continue building in order to catch the potential issues early on rather than having to make major changes later. diff --git a/milestones/milestone4.md b/milestones/milestone4.md index 61dc73bb9..d3cc8396c 100644 --- a/milestones/milestone4.md +++ b/milestones/milestone4.md @@ -15,11 +15,11 @@ This unit, be sure to complete all tasks listed below. To complete a task, place ### 1. What went well during this unit? -[πŸ‘‰πŸΎπŸ‘‰πŸΎπŸ‘‰πŸΎ your answer here] +During this unit, the team collaboration and Git workflow process went well. We were efficiently using commits, branches, and pull requests to combine everyone's contributions and organize our work. Each of us were able to work on our features without messing with one anothers code, while the pull requests allowed us to review change and provide feedback. This helped prevent merge conflicts and made the development process much more structured and clear. Overall, utilizing git allowed out team to efficiently communicate and made it easier to collaborate on different parts of the application. ### 2. What were some challenges your group faced in this unit? -[πŸ‘‰πŸΎπŸ‘‰πŸΎπŸ‘‰πŸΎ your answer here] +There were some instances in which our render dashboard was not connected properly, causing issues with testing the application. We had to troubleshoot configuration settings in order to verify our back-end and front-end were working as expected. However, by communication with one another using slack and reviewing changes made carefully, we were able to resolve these issues while keeping the project moving. ### 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? diff --git a/milestones/progress_report_3.md b/milestones/progress_report_3.md new file mode 100644 index 000000000..b6b259672 --- /dev/null +++ b/milestones/progress_report_3.md @@ -0,0 +1,29 @@ +# 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: +- User (table + profile APIT) +- Trips (table + CRUD API + budget filter) +- Destinations (table + CRUD API, nested under trip) +- Activities(table + CRUD API, nested under trip) +- Expenses (table + CRUD API + spending summary) + +## GitHub Milestones Created +List the name of each GitHub Milestone you created, one per line: +- Milestone 3 - Unit 7 +- Milestone 4 - Unit 8 +- Milestone 5 - Unit 9 + +## Milestone 3 Completion +Replace the blank before the percent sign with a number from 0 to 100. +Completion percentage for Milestone 3 - Unit 7: 100% +List each issue you completed this unit and the main file or folder where that work lives, one per line: +- Initial Project Setup & Repository Scaffold: `/server` and `/client` + +## Features Completed This Unit +List each feature you completed and checked off in readme.md this unit, one per line: +- No features for this milestone \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..2757a3184 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3407 @@ +{ + "name": "ramblr", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ramblr", + "dependencies": { + "concurrently": "^8.2.2", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "nodemon": "^3.1.0", + "pg": "^8.11.5", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.23.0" + }, + "devDependencies": { + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.2.0" + } + }, + "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/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "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/@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/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "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": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.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/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/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/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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/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": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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/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/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^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/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==", + "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/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.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "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/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/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/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-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/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/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/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/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/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": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/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-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": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "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": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/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/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/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/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/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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/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.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "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/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/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.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": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/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/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/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/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/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": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "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/package.json b/package.json new file mode 100644 index 000000000..b2b95813b --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "ramblr", + "type": "module", + "scripts": { + "dev": "concurrently \"cd client && vite\" \"cd server && nodemon server\"", + "start": "cd server && node server", + "build": "cd client && vite build", + "reset": "node server/config/reset.js" + }, + "dependencies": { + "concurrently": "^8.2.2", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "nodemon": "^3.1.0", + "pg": "^8.11.5", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.23.0" + }, + "devDependencies": { + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.2.0" + } +} diff --git a/planning/entity_relationship_diagram.md b/planning/entity_relationship_diagram.md index 12c25f62c..23c22fd9d 100644 --- a/planning/entity_relationship_diagram.md +++ b/planning/entity_relationship_diagram.md @@ -3,15 +3,79 @@ Reference the Creating an Entity Relationship Diagram final project guide in the course portal for more information about how to complete this deliverable. ## Create the List of Tables - -[πŸ‘‰πŸΎπŸ‘‰πŸΎπŸ‘‰πŸΎ List each table in your diagram] +- Users +- Trips +- Destinations +- Activities +- Expenses +- Packing List ## 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.] +entity relationship diagram image + +### Users +| Column Name | Type | Description | +| ------------- | ---------- | ------------------------------------------------------------- | +| id | integer | Primary key, auto-increment | +| email | varchar | Unique email address, required | +| password_hash | varchar | Hashed password, required | +| display_name | varchar | User's display name | +| home_currency | varchar(3) | Default currency (`USD`) | +| created_at | timestamp | Timestamp when account was created (defaults to current time) | + +### Trips +| Column Name | Type | Description | +| ----------- | ------------- | ---------------------------------------------------------- | +| id | integer | Primary key, auto-increment | +| user_id | integer | Foreign key referencing `users.id` | +| title | varchar | Trip title, required | +| start_date | date | Trip start date | +| end_date | date | Trip end date | +| budget | decimal(12,2) | Planned budget | +| created_at | timestamp | Timestamp when trip was created (defaults to current time) | + +### Destinations +| Column Name | Type | Description | +| ------------- | ---------- | ---------------------------------------------- | +| id | integer | Primary key, auto-increment | +| trip_id | integer | Foreign key referencing `trips.id` | +| city | varchar | Destination city, required | +| country | varchar | Destination country, required | +| currency_code | varchar(3) | Local currency code | +| arrival_order | integer | Order of this destination in a multi-stop trip | +| start_date | date | Arrival/start date | +| end_date | date | Departure/end date | + +### Activities +| Column Name | Type | Description | +| ---------------- | ------- | ----------------------------------------- | +| id | integer | Primary key, auto-increment | +| destination_id | integer | Foreign key referencing `destinations.id` | +| name | varchar | Activity name, required | +| scheduled_date | date | Date of the activity (optional) | +| start_time | time | Planned start time | +| duration_minutes | integer | Estimated duration in minutes | +| notes | text | Additional activity notes | + +### Expenses +| Column Name | Type | Description | +| ---------------- | ------- | ----------------------------------------- | +| id | integer | Primary key, auto-increment | +| destination_id | integer | Foreign key referencing `destinations.id` | +| name | varchar | Activity name, required | +| scheduled_date | date | Date of the activity (optional) | +| start_time | time | Planned start time | +| duration_minutes | integer | Estimated duration in minutes | +| notes | text | Additional activity notes | + +### Packing List +| Column Name | Type | Description | +| ----------------- | ------- | ---------------------------------------------------------------------- | +| id | integer | Primary key, auto-increment | +| trip_id | integer | Foreign key referencing `trips.id` | +| name | varchar | Item name, required | +| is_packed | boolean | Whether the item has been packed (defaults to `false`) | +| is_auto_generated | boolean | Whether the app automatically suggested the item (defaults to `false`) | + -| Column Name | Type | Description | -|-------------|------|-------------| -| id | integer | primary key | -| name | text | name of the shoe model | -| ... | ... | ... | diff --git a/planning/img/Activities (For a Destination).png b/planning/img/Activities (For a Destination).png new file mode 100644 index 000000000..785b57869 Binary files /dev/null and b/planning/img/Activities (For a Destination).png differ diff --git a/planning/img/Edit Trip.png b/planning/img/Edit Trip.png new file mode 100644 index 000000000..f5afb7063 Binary files /dev/null and b/planning/img/Edit Trip.png differ diff --git a/planning/img/My Trips (Dashboard).png b/planning/img/My Trips (Dashboard).png new file mode 100644 index 000000000..adb4874d9 Binary files /dev/null and b/planning/img/My Trips (Dashboard).png differ diff --git a/planning/img/Ramblr ERD.png b/planning/img/Ramblr ERD.png new file mode 100644 index 000000000..b8d1ba758 Binary files /dev/null and b/planning/img/Ramblr ERD.png differ diff --git a/planning/img/Signup.png b/planning/img/Signup.png new file mode 100644 index 000000000..a8578e100 Binary files /dev/null and b/planning/img/Signup.png differ diff --git a/planning/img/Trip Dashboard (Overview).png b/planning/img/Trip Dashboard (Overview).png new file mode 100644 index 000000000..76c87c6d1 Binary files /dev/null and b/planning/img/Trip Dashboard (Overview).png differ diff --git a/planning/user_stories.md b/planning/user_stories.md index 1e55ecbcd..71761da62 100644 --- a/planning/user_stories.md +++ b/planning/user_stories.md @@ -4,10 +4,37 @@ 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.] +- [ ] **Traveler** β€” A user planning to travel who creates trips and organizes everything (itinerary, budget, packing, etc.) in one place ## Draft User Stories + +### Profile & Trip Management +- [ ] As a traveler, I want to have a profile so that I can view all the trips I have made in one place +- [ ] As a traveler, I want to create a new trip so that I can start planning +- [ ] As a traveler, I want to edit or delete a trip so that I can keep my trip list accurate +- [ ] As a traveler, I want to filter my trips by budget so that I can quickly find trips that fit my price range +### Itinerary & Activities +- [ ] As a traveler, I want to set a start and end date for my trip so that my itinerary is organized into specific days +- [ ] As a traveler, I want to add activities for each day of the trip so that I can plan an itinerary +- [ ] As a traveler, I want to assign a time to an activity so that I know when it's happening during the day +- [ ] As a traveler, I want to add an activity without assigning it to a specific day so that I can keep a flexible list of things I might want to do +- [ ] As a traveler, I want to move an unscheduled activity onto a specific day so that I can lock it into my itinerary once I decide +- [ ] As a traveler, I want to move a scheduled activity back to "unscheduled" so that I can un-commit to plans that change +- [ ] As a traveler, I want to edit or remove activities so that I can adjust my plans as they change +- [ ] As a traveler, I want to add destinations to my trip so that I can organize multi-stop travel -[πŸ‘‰πŸΎπŸ‘‰πŸΎπŸ‘‰πŸΎ Include at least at least 10 user stories in this format:] -1. As a [user role], I want to [what], so that [why]. +### Budget & Expenses +- [ ] As a traveler, I want to add my expenses in advance so that I can budget for my trip before I leave +- [ ] As a traveler, I want to log my expenses throughout the trip so that I can keep track of my spending in real time +- [ ] As a traveler, I want to see a summary of my total spending so that I can tell if I'm staying within budget +- [ ] As a traveler, I want to see the current exchange rate between my home currency and my destination's currency (e.g., $1 β‰ˆ Β₯150) so that I have a quick reference while spending. For now I think we can just default this to USD -> Destination Currency +### Packing List +- [ ] As a traveler, I want to create a packing list for my trip so that I don't forget anything +- [ ] As a traveler, I want the app to autogenerate a packing checklist based on my trip details (destination, weather, length) so that I can save time planning +### Weather +- [ ] As a traveler, I want to check the weather for each day of the trip so that I can plan my activities accordingly + - NOTE: This might have to be conditionally rendered to only show for when the trip is getting closer (MY suggestion is 2 weeks) since its impossible to tell the weather 6 months in. +### Trip Countdown +- [ ] As a traveler, I want to see a countdown to my trip so that I can build excitement and stay on top of prep deadlines + diff --git a/planning/wireframes.md b/planning/wireframes.md index fbcd15a0c..de0481e77 100644 --- a/planning/wireframes.md +++ b/planning/wireframes.md @@ -4,18 +4,29 @@ 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] +| Page | Wireframed? | +|------|-------------| +| Home / Dashboard | ⭐ | +| Create Trip | ⭐ | +| Trip Details | ⭐ | +| Destination Details | ⭐ | +| Create Activity | | +| Budget & Expenses | | +| Packing List | | +| Login | ⭐ | -## Wireframe 1: [page title] -[πŸ‘‰πŸΎπŸ‘‰πŸΎπŸ‘‰πŸΎ include wireframe 1] +## Wireframe 1: Login/Signup -## Wireframe 2: [page title] +signup wireframe image -[πŸ‘‰πŸΎπŸ‘‰πŸΎπŸ‘‰πŸΎ include wireframe 2] +## Wireframe 2: My Trips (User Dashboard) -## Wireframe 3: [page title] +user dashboard wireframe image + +## Wireframe 3: Trip Dashboard + +trip wireframe image -[πŸ‘‰πŸΎπŸ‘‰πŸΎπŸ‘‰πŸΎ include wireframe 3] [πŸ‘‰πŸΎπŸ‘‰πŸΎπŸ‘‰πŸΎ include more wireframes as desired] diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 000000000..be7284905 --- /dev/null +++ b/server/.env.example @@ -0,0 +1,3 @@ +PORT=3000 +NODE_ENV=development +DATABASE_URL=postgres://user:password@localhost:5432/ramblr diff --git a/server/config/database.js b/server/config/database.js new file mode 100644 index 000000000..6271bf598 --- /dev/null +++ b/server/config/database.js @@ -0,0 +1,14 @@ +import pg from "pg"; + +const config = { + user: process.env.PGUSER, + password: process.env.PGPASSWORD, + host: process.env.PGHOST, + port: process.env.PGPORT, + database: process.env.PGDATABASE, + ssl: { + rejectUnauthorized: false, + }, +}; + +export const pool = new pg.Pool(config); diff --git a/server/config/dotenv.js b/server/config/dotenv.js new file mode 100644 index 000000000..4bf698f32 --- /dev/null +++ b/server/config/dotenv.js @@ -0,0 +1,5 @@ +// Connects my env variables via process.env +// Without it, process.env.DATABASE_URL would just be undefined β€” Node doesn't automatically read .env files. dotenv.config() is what bridges the gap between the file and your code + +import dotenv from "dotenv"; +dotenv.config({ path: "./server/.env" }); diff --git a/server/config/reset.js b/server/config/reset.js new file mode 100644 index 000000000..9823da204 --- /dev/null +++ b/server/config/reset.js @@ -0,0 +1,130 @@ +// Where all the database reset and seeding logic is located. This file is run with the command `npm run reset` in the terminal. + +import "./dotenv.js"; +import { pool } from "./database.js"; +import { users } from "../data/users.js"; + +// Down/Up comments is convention when it comes to db migrations. +// It tells the order of operations when rolling back (down) or applying (up) migrations. +// In our simple case, the down migration is always run first, so we can safely drop tables before creating them again. +// In bigger migration cases, the order of operations matters more and should be carefully considered/documented! But this is for practice! + +// Down: DROP TABLE IF EXISTS users CASCADE; +// Up: CREATE TABLE users (...) below. + +// CASCADE also drops trips and destinations (and any future table referencing +// users), since this DROP always runs first and trips.user_id depends on +// users.id, with destinations depending on trips in turn. +const createUsersTableQuery = ` + DROP TABLE IF EXISTS users CASCADE; + + CREATE TABLE users ( + id SERIAL PRIMARY KEY, + display_name VARCHAR NOT NULL, + home_currency VARCHAR(3) NOT NULL DEFAULT 'USD', + created_at TIMESTAMP NOT NULL DEFAULT NOW() + ); +`; + +const createUsersTable = async () => { + try { + await pool.query(createUsersTableQuery); + console.log("βœ… users table created successfully"); + } catch (error) { + console.error("⚠️ Error creating users table:", error); + } +}; + +const insertUserQuery = + "INSERT INTO users (display_name, home_currency) VALUES ($1, $2)"; + +const seedUsers = async () => { + await createUsersTable(); + + for (const user of users) { + try { + await pool.query(insertUserQuery, [user.displayName, user.homeCurrency]); + console.log(`βœ… Seeded user: ${user.displayName}`); + } catch (error) { + console.error(`⚠️ Error seeding user ${user.displayName}:`, error); + } + } +}; + +// Down: DROP TABLE IF EXISTS trips CASCADE; +// Up: CREATE TABLE trips (...) below. + +// New Concept: CASCADE: In our case, it mostly relates to FK. Can think of it as automatic cleanup of child records when a parent record is deleted. +// In this case, if a trip is deleted, all its child records (destinations, packing_list, etc.) should also be deleted. This is done by adding ON DELETE CASCADE to the FK constraint in the child table. + +// CASCADE here also drops destinations (and any future table referencing +// trips), so this reset always succeeds regardless of what state the shared +// dev database is in. +// Future child tables (activities, packing_list, etc.) should put +// ON DELETE CASCADE on their own trip_id/destination_id FK instead of here, +// so a trip delete cascades all the way down. +const createTripsTableQuery = ` + DROP TABLE IF EXISTS trips CASCADE; + + CREATE TABLE trips ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title VARCHAR NOT NULL, + start_date DATE, + end_date DATE, + budget DECIMAL(12, 2), + created_at TIMESTAMP NOT NULL DEFAULT NOW() + ); +`; + +const createTripsTable = async () => { + try { + await pool.query(createTripsTableQuery); + console.log("βœ… trips table created successfully"); + } catch (error) { + console.error("⚠️ Error creating trips table:", error); + } +}; + +// Down: DROP TABLE IF EXISTS destinations; +// Up: CREATE TABLE destinations (...) below. + +// Future child tables (activities) should put ON DELETE CASCADE on their own +// destination_id FK instead of here, so a destination delete cascades too. +const createDestinationsTableQuery = ` + DROP TABLE IF EXISTS destinations; + + CREATE TABLE destinations ( + id SERIAL PRIMARY KEY, + trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE, + city VARCHAR NOT NULL, + country VARCHAR NOT NULL, + currency_code VARCHAR(3), + arrival_order INTEGER, + start_date DATE, + end_date DATE + ); +`; + +const createDestinationsTable = async () => { + try { + await pool.query(createDestinationsTableQuery); + console.log("βœ… destinations table created successfully"); + } catch (error) { + console.error("⚠️ Error creating destinations table:", error); + } +}; + +const seedDBs = async () => { + try { + await seedUsers(); + await createTripsTable(); + await createDestinationsTable(); + console.log("Database seeding complete."); + } catch (err) { + console.error("Database seeding failed:", err); + process.exitCode = 1; + } +}; + +seedDBs(); diff --git a/server/controllers/activitiesController.js b/server/controllers/activitiesController.js new file mode 100644 index 000000000..655d84304 --- /dev/null +++ b/server/controllers/activitiesController.js @@ -0,0 +1,40 @@ +import { pool } from "../config/database.js"; + +// TODO: placeholder query, adjust columns/table once the activities schema is finalized +const getAllActivities = async (req, res) => { + try { + const { rows } = await pool.query("SELECT * FROM activities"); + res.json(rows); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +// TODO: placeholder query, adjust columns/table once the activities schema is finalized +const getActivityById = async (req, res) => { + try { + const { id } = req.params; + const { rows } = await pool.query("SELECT * FROM activities WHERE id = $1", [id]); + if (!rows.length) return res.status(404).json({ error: "Activity not found" }); + res.json(rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +// TODO: not implemented yet +const createActivity = async (req, res) => { + res.status(501).json({ error: "Not implemented" }); +}; + +// TODO: not implemented yet +const updateActivity = async (req, res) => { + res.status(501).json({ error: "Not implemented" }); +}; + +// TODO: not implemented yet +const deleteActivity = async (req, res) => { + res.status(501).json({ error: "Not implemented" }); +}; + +export { getAllActivities, getActivityById, createActivity, updateActivity, deleteActivity }; diff --git a/server/controllers/destinations.js b/server/controllers/destinations.js new file mode 100644 index 000000000..df97d80ac --- /dev/null +++ b/server/controllers/destinations.js @@ -0,0 +1,203 @@ +import { pool } from "../config/database.js"; +import { getCurrentUserId } from "../helpers/currentUser.js"; +import { getOwnedTrip } from "../helpers/tripOwnership.js"; +import { getCurrencyForCountry } from "../helpers/currency.js"; +import { isValidId, isValidDate } from "../helpers/validation.js"; +import { isDateBefore, isDateAfter } from "../helpers/dates.js"; + +// Destinations have no user_id of their own; ownership is checked via the parent trip through this join. +const getOwnedDestination = async (destinationId, userId) => { + const { rows } = await pool.query( + `SELECT destinations.* + FROM destinations + JOIN trips ON destinations.trip_id = trips.id + WHERE destinations.id = $1 AND trips.user_id = $2`, + [destinationId, userId], + ); + return rows[0] ?? null; +}; + +// GET /api/trips/:tripId/destinations β€” list a trip's destinations, ordered for a multi-stop itinerary. +const getDestinationsForTrip = async (req, res) => { + const { tripId } = req.params; + if (!isValidId(tripId)) return res.status(400).json({ error: "Invalid trip id" }); + + try { + const userId = await getCurrentUserId(); + if (!userId) return res.status(404).json({ error: "User not found" }); + + const trip = await getOwnedTrip(tripId, userId); + if (!trip) return res.status(404).json({ error: "Trip not found" }); + + const { rows } = await pool.query( + `SELECT * FROM destinations + WHERE trip_id = $1 + ORDER BY arrival_order ASC NULLS LAST, start_date ASC NULLS LAST`, + [tripId], + ); + res.json(rows); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +// POST /api/trips/:tripId/destinations β€” add a destination to a trip; currency_code is derived from country. +const createDestination = async (req, res) => { + const { tripId } = req.params; + const { city, country, startDate, endDate, arrivalOrder } = req.body; + + if (!isValidId(tripId)) return res.status(400).json({ error: "Invalid trip id" }); + if (!city) return res.status(400).json({ error: "city is required" }); + if (!country) return res.status(400).json({ error: "country is required" }); + if (startDate !== undefined && !isValidDate(startDate)) { + return res.status(400).json({ error: "start_date is invalid" }); + } + if (endDate !== undefined && !isValidDate(endDate)) { + return res.status(400).json({ error: "end_date is invalid" }); + } + if (arrivalOrder !== undefined && Number.isNaN(Number(arrivalOrder))) { + return res.status(400).json({ error: "arrival_order is invalid" }); + } + if (startDate && endDate && isDateBefore(endDate, startDate)) { + return res.status(400).json({ error: "end_date cannot precede start_date" }); + } + + // country is a free-text country name, e.g. "United States" -> resolves to "USD" via data/countryCurrency.js + const currencyCode = getCurrencyForCountry(country); + if (!currencyCode) return res.status(400).json({ error: "Unknown country" }); + + try { + const userId = await getCurrentUserId(); + if (!userId) return res.status(404).json({ error: "User not found" }); + + const trip = await getOwnedTrip(tripId, userId); + if (!trip) return res.status(404).json({ error: "Trip not found" }); + + // reject destination dates that fall outside the parent trip's range + if (trip.start_date && startDate && isDateBefore(startDate, trip.start_date)) { + return res.status(400).json({ error: "start_date falls before the trip's start_date" }); + } + if (trip.end_date && endDate && isDateAfter(endDate, trip.end_date)) { + return res.status(400).json({ error: "end_date falls after the trip's end_date" }); + } + + const { rows } = await pool.query( + `INSERT INTO destinations (trip_id, city, country, currency_code, arrival_order, start_date, end_date) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING *`, + [tripId, city, country, currencyCode, arrivalOrder, startDate, endDate], + ); + res.status(201).json(rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +// GET /api/destinations/:id β€” get one destination owned (via its trip) by the current user. +const getDestinationById = async (req, res) => { + const { id } = req.params; + if (!isValidId(id)) return res.status(400).json({ error: "Invalid destination id" }); + + try { + const userId = await getCurrentUserId(); + if (!userId) return res.status(404).json({ error: "User not found" }); + + const destination = await getOwnedDestination(id, userId); + if (!destination) return res.status(404).json({ error: "Destination not found" }); + res.json(destination); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +// PATCH /api/destinations/:id β€” edit a destination owned (via its trip) by the current user. +const updateDestination = async (req, res) => { + const { id } = req.params; + const { city, country, startDate, endDate, arrivalOrder } = req.body; + + if (!isValidId(id)) return res.status(400).json({ error: "Invalid destination id" }); + if (startDate !== undefined && !isValidDate(startDate)) { + return res.status(400).json({ error: "start_date is invalid" }); + } + if (endDate !== undefined && !isValidDate(endDate)) { + return res.status(400).json({ error: "end_date is invalid" }); + } + if (arrivalOrder !== undefined && Number.isNaN(Number(arrivalOrder))) { + return res.status(400).json({ error: "arrival_order is invalid" }); + } + + // country is a free-text country name, e.g. "United States" -> resolves to "USD" via data/countryCurrency.js + // only re-derive currency_code if the caller is actually changing the country + let currencyCode; + if (country !== undefined) { + currencyCode = getCurrencyForCountry(country); + if (!currencyCode) return res.status(400).json({ error: "Unknown country" }); + } + + try { + const userId = await getCurrentUserId(); + if (!userId) return res.status(404).json({ error: "User not found" }); + + const existingDestination = await getOwnedDestination(id, userId); + if (!existingDestination) return res.status(404).json({ error: "Destination not found" }); + + const trip = await getOwnedTrip(existingDestination.trip_id, userId); + + // a PATCH can send either date, both, or neither; for whichever one isn't + // sent, fall back to the destination's existing value, then validate that pair + const mergedStartDate = startDate ?? existingDestination.start_date; + const mergedEndDate = endDate ?? existingDestination.end_date; + if (mergedStartDate && mergedEndDate && isDateBefore(mergedEndDate, mergedStartDate)) { + return res.status(400).json({ error: "end_date cannot precede start_date" }); + } + if (trip.start_date && mergedStartDate && isDateBefore(mergedStartDate, trip.start_date)) { + return res.status(400).json({ error: "start_date falls before the trip's start_date" }); + } + if (trip.end_date && mergedEndDate && isDateAfter(mergedEndDate, trip.end_date)) { + return res.status(400).json({ error: "end_date falls after the trip's end_date" }); + } + + const { rows } = await pool.query( + `UPDATE destinations + SET city = COALESCE($1, city), + country = COALESCE($2, country), + currency_code = COALESCE($3, currency_code), + arrival_order = COALESCE($4, arrival_order), + start_date = COALESCE($5, start_date), + end_date = COALESCE($6, end_date) + WHERE id = $7 + RETURNING *`, + [city, country, currencyCode, arrivalOrder, startDate, endDate, id], + ); + res.json(rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +// DELETE /api/destinations/:id β€” delete a destination owned (via its trip) by the current user. +const deleteDestination = async (req, res) => { + const { id } = req.params; + if (!isValidId(id)) return res.status(400).json({ error: "Invalid destination id" }); + + try { + const userId = await getCurrentUserId(); + if (!userId) return res.status(404).json({ error: "User not found" }); + + const destination = await getOwnedDestination(id, userId); + if (!destination) return res.status(404).json({ error: "Destination not found" }); + + await pool.query("DELETE FROM destinations WHERE id = $1", [id]); + res.status(204).send(); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +export { + getDestinationsForTrip, + createDestination, + getDestinationById, + updateDestination, + deleteDestination, +}; diff --git a/server/controllers/expenses.js b/server/controllers/expenses.js new file mode 100644 index 000000000..202468428 --- /dev/null +++ b/server/controllers/expenses.js @@ -0,0 +1,40 @@ +import { pool } from "../config/database.js"; + +// TODO: placeholder query, adjust columns/table once the expenses schema is finalized +const getAllExpenses = async (req, res) => { + try { + const { rows } = await pool.query("SELECT * FROM expenses"); + res.json(rows); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +// TODO: placeholder query, adjust columns/table once the expenses schema is finalized +const getExpenseById = async (req, res) => { + try { + const { id } = req.params; + const { rows } = await pool.query("SELECT * FROM expenses WHERE id = $1", [id]); + if (!rows.length) return res.status(404).json({ error: "Expense not found" }); + res.json(rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +// TODO: not implemented yet +const createExpense = async (req, res) => { + res.status(501).json({ error: "Not implemented" }); +}; + +// TODO: not implemented yet +const updateExpense = async (req, res) => { + res.status(501).json({ error: "Not implemented" }); +}; + +// TODO: not implemented yet +const deleteExpense = async (req, res) => { + res.status(501).json({ error: "Not implemented" }); +}; + +export { getAllExpenses, getExpenseById, createExpense, updateExpense, deleteExpense }; diff --git a/server/controllers/packingList.js b/server/controllers/packingList.js new file mode 100644 index 000000000..1f49ba0ba --- /dev/null +++ b/server/controllers/packingList.js @@ -0,0 +1,46 @@ +import { pool } from "../config/database.js"; + +// TODO: placeholder query, adjust columns/table once the packing list schema is finalized +const getAllPackingListItems = async (req, res) => { + try { + const { rows } = await pool.query("SELECT * FROM packing_list_items"); + res.json(rows); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +// TODO: placeholder query, adjust columns/table once the packing list schema is finalized +const getPackingListItemById = async (req, res) => { + try { + const { id } = req.params; + const { rows } = await pool.query("SELECT * FROM packing_list_items WHERE id = $1", [id]); + if (!rows.length) return res.status(404).json({ error: "Packing list item not found" }); + res.json(rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +// TODO: not implemented yet +const createPackingListItem = async (req, res) => { + res.status(501).json({ error: "Not implemented" }); +}; + +// TODO: not implemented yet +const updatePackingListItem = async (req, res) => { + res.status(501).json({ error: "Not implemented" }); +}; + +// TODO: not implemented yet +const deletePackingListItem = async (req, res) => { + res.status(501).json({ error: "Not implemented" }); +}; + +export { + getAllPackingListItems, + getPackingListItemById, + createPackingListItem, + updatePackingListItem, + deletePackingListItem, +}; diff --git a/server/controllers/trips.js b/server/controllers/trips.js new file mode 100644 index 000000000..bed529c5b --- /dev/null +++ b/server/controllers/trips.js @@ -0,0 +1,154 @@ +import { pool } from "../config/database.js"; +import { getCurrentUserId } from "../helpers/currentUser.js"; +import { isValidId, isValidDate, isValidBudget } from "../helpers/validation.js"; +import { isDateBefore } from "../helpers/dates.js"; + +// GET /api/trips β€” list the current user's trips. +const getAllTrips = async (req, res) => { + try { + const userId = await getCurrentUserId(); + if (!userId) return res.status(404).json({ error: "User not found" }); + + const { rows } = await pool.query( + "SELECT * FROM trips WHERE user_id = $1 ORDER BY id ASC", + [userId], + ); + res.json(rows); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +// GET /api/trips/:id β€” get one trip owned by the current user. +const getTripById = async (req, res) => { + const { id } = req.params; + if (!isValidId(id)) return res.status(400).json({ error: "Invalid trip id" }); + + try { + const userId = await getCurrentUserId(); + if (!userId) return res.status(404).json({ error: "User not found" }); + + const { rows } = await pool.query( + "SELECT * FROM trips WHERE id = $1 AND user_id = $2", + [id, userId], + ); + if (!rows.length) return res.status(404).json({ error: "Trip not found" }); + res.json(rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +// POST /api/trips β€” create a trip owned by the current user. +const createTrip = async (req, res) => { + const { title, startDate, endDate, budget } = req.body; + + if (!title) return res.status(400).json({ error: "title is required" }); + if (startDate !== undefined && !isValidDate(startDate)) { + return res.status(400).json({ error: "start_date is invalid" }); + } + if (endDate !== undefined && !isValidDate(endDate)) { + return res.status(400).json({ error: "end_date is invalid" }); + } + if (budget !== undefined && !isValidBudget(budget)) { + return res.status(400).json({ error: "budget is invalid" }); + } + if (startDate && endDate && new Date(endDate) < new Date(startDate)) { + return res + .status(400) + .json({ error: "end_date cannot precede start_date" }); + } + + try { + const userId = await getCurrentUserId(); + if (!userId) return res.status(404).json({ error: "User not found" }); + + const { rows } = await pool.query( + `INSERT INTO trips (user_id, title, start_date, end_date, budget) + VALUES ($1, $2, $3, $4, $5) + RETURNING *`, + [userId, title, startDate, endDate, budget], + ); + res.status(201).json(rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +// PATCH /api/trips/:id β€” edit a trip owned by the current user (only fields present in the body are changed). +const updateTrip = async (req, res) => { + const { id } = req.params; + const { title, startDate, endDate, budget } = req.body; + if (!isValidId(id)) return res.status(400).json({ error: "Invalid trip id" }); + if (startDate !== undefined && !isValidDate(startDate)) { + return res.status(400).json({ error: "start_date is invalid" }); + } + if (endDate !== undefined && !isValidDate(endDate)) { + return res.status(400).json({ error: "end_date is invalid" }); + } + if (budget !== undefined && !isValidBudget(budget)) { + return res.status(400).json({ error: "budget is invalid" }); + } + + try { + const userId = await getCurrentUserId(); + if (!userId) return res.status(404).json({ error: "User not found" }); + + const existingTrip = await pool.query( + "SELECT * FROM trips WHERE id = $1 AND user_id = $2", + [id, userId], + ); + if (!existingTrip.rows.length) + return res.status(404).json({ error: "Trip not found" }); + + // a PATCH can send either date, both, or neither; for whichever one isn't + // sent, fall back to the trip's existing value, then validate that pair + const mergedStartDate = startDate ?? existingTrip.rows[0].start_date; + const mergedEndDate = endDate ?? existingTrip.rows[0].end_date; + if ( + mergedStartDate && + mergedEndDate && + isDateBefore(mergedEndDate, mergedStartDate) + ) { + return res + .status(400) + .json({ error: "end_date cannot precede start_date" }); + } + + const { rows } = await pool.query( + `UPDATE trips + SET title = COALESCE($1, title), + start_date = COALESCE($2, start_date), + end_date = COALESCE($3, end_date), + budget = COALESCE($4, budget) + WHERE id = $5 AND user_id = $6 + RETURNING *`, + [title, startDate, endDate, budget, id, userId], + ); + res.json(rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +// DELETE /api/trips/:id β€” delete a trip owned by the current user. +const deleteTrip = async (req, res) => { + const { id } = req.params; + if (!isValidId(id)) return res.status(400).json({ error: "Invalid trip id" }); + + try { + const userId = await getCurrentUserId(); + if (!userId) return res.status(404).json({ error: "User not found" }); + + const { rows } = await pool.query( + "DELETE FROM trips WHERE id = $1 AND user_id = $2 RETURNING *", + [id, userId], + ); + if (!rows.length) return res.status(404).json({ error: "Trip not found" }); + res.status(204).send(); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +export { getAllTrips, getTripById, createTrip, updateTrip, deleteTrip }; diff --git a/server/controllers/users.js b/server/controllers/users.js new file mode 100644 index 000000000..f7985bbad --- /dev/null +++ b/server/controllers/users.js @@ -0,0 +1,45 @@ +import { pool } from "../config/database.js"; +import { getCurrentUserId } from "../helpers/currentUser.js"; + +// GET /api/users/me β€” get the current user's profile. +const getCurrentUser = async (req, res) => { + const getQuery = "SELECT * FROM users WHERE id = $1"; + try { + const userId = await getCurrentUserId(); + if (!userId) return res.status(404).json({ error: "User not found" }); + + const { rows } = await pool.query(getQuery, [userId]); + if (!rows.length) return res.status(404).json({ error: "User not found" }); + res.status(200).json(rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +// PATCH /api/users/me β€” update the current user's displayName and/or homeCurrency. +// This only updates the displayName and homeCurrency fields, since those are the only fields that are editable for a user in this app. If more fields are added to the user model in the future, this function should be updated to handle those fields (example password). +const updateCurrentUser = async (req, res) => { + const { displayName, homeCurrency } = req.body; + + // To follow PATCH protocol, we use COALESCE to only update fields that are provided in the request body. If a field is not provided, it will retain its current value. + const updateQuery = ` + UPDATE users + SET display_name = COALESCE($1, display_name), + home_currency = COALESCE($2, home_currency) + WHERE id = $3 + RETURNING * + `; + + try { + const userId = await getCurrentUserId(); + if (!userId) return res.status(404).json({ error: "User not found" }); + + const { rows } = await pool.query(updateQuery, [displayName, homeCurrency, userId]); + if (!rows.length) return res.status(404).json({ error: "User not found" }); + res.json(rows[0]); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +export { getCurrentUser, updateCurrentUser }; diff --git a/server/data/countryCurrency.js b/server/data/countryCurrency.js new file mode 100644 index 000000000..d3350b8b6 --- /dev/null +++ b/server/data/countryCurrency.js @@ -0,0 +1,53 @@ +// Country name (lowercase) -> ISO 4217 currency code. +// Not exhaustive -- covers common travel destinations for now. +// Lookup should normalize (trim + lowercase) the incoming country before checking this map. +export const countryCurrency = { + "united states": "USD", + "usa": "USD", + "canada": "CAD", + "mexico": "MXN", + "united kingdom": "GBP", + "uk": "GBP", + "ireland": "EUR", + "france": "EUR", + "germany": "EUR", + "spain": "EUR", + "italy": "EUR", + "portugal": "EUR", + "netherlands": "EUR", + "belgium": "EUR", + "austria": "EUR", + "greece": "EUR", + "switzerland": "CHF", + "norway": "NOK", + "sweden": "SEK", + "denmark": "DKK", + "iceland": "ISK", + "poland": "PLN", + "czech republic": "CZK", + "hungary": "HUF", + "japan": "JPY", + "china": "CNY", + "south korea": "KRW", + "india": "INR", + "thailand": "THB", + "vietnam": "VND", + "singapore": "SGD", + "malaysia": "MYR", + "indonesia": "IDR", + "philippines": "PHP", + "australia": "AUD", + "new zealand": "NZD", + "brazil": "BRL", + "argentina": "ARS", + "chile": "CLP", + "peru": "PEN", + "colombia": "COP", + "south africa": "ZAR", + "egypt": "EGP", + "morocco": "MAD", + "turkey": "TRY", + "united arab emirates": "AED", + "uae": "AED", + "israel": "ILS", +}; diff --git a/server/data/users.js b/server/data/users.js new file mode 100644 index 000000000..3801d04cb --- /dev/null +++ b/server/data/users.js @@ -0,0 +1,9 @@ +// My default user for testing purposes + +// DEV NOTE: id and created_at omitted from seed since it is auto generated in SQL statements +export const users = [ + { + displayName: "Jane Doe", + homeCurrency: "USD", + }, +]; diff --git a/server/helpers/currency.js b/server/helpers/currency.js new file mode 100644 index 000000000..8cc9ff0e1 --- /dev/null +++ b/server/helpers/currency.js @@ -0,0 +1,9 @@ +import { countryCurrency } from "../data/countryCurrency.js"; + +// Returns the ISO 4217 currency code for a country name, or null if unrecognized. +const getCurrencyForCountry = (country) => { + if (typeof country !== "string") return null; + return countryCurrency[country.trim().toLowerCase()] ?? null; +}; + +export { getCurrencyForCountry }; diff --git a/server/helpers/currentUser.js b/server/helpers/currentUser.js new file mode 100644 index 000000000..34ea28744 --- /dev/null +++ b/server/helpers/currentUser.js @@ -0,0 +1,15 @@ +import { pool } from "../config/database.js"; + +// Single source of truth for "who is the current user." +// No auth yet, so this resolves to the one seeded user. +// It is used extensively in the controllers to ensure that all operations are scoped to the current user + +// TODO: once auth exists, resolve the user id from the session/token instead. +const getCurrentUserId = async () => { + const { rows } = await pool.query( + "SELECT id FROM users ORDER BY id ASC LIMIT 1", + ); + return rows[0]?.id; +}; + +export { getCurrentUserId }; diff --git a/server/helpers/dates.js b/server/helpers/dates.js new file mode 100644 index 000000000..9a5c57606 --- /dev/null +++ b/server/helpers/dates.js @@ -0,0 +1,20 @@ +// pg parses DATE columns into JS Date objects at local midnight, while +// request bodies give plain "YYYY-MM-DD" strings (parsed elsewhere as UTC +// midnight if ever turned into a Date). Comparing those two forms directly +// with `new Date(a) < new Date(b)` can misjudge the same calendar date as +// earlier/later. Normalize both to a "YYYY-MM-DD" string first and compare +// those instead, since it sorts identically to chronological order. +const toDateOnly = (value) => { + if (value instanceof Date) { + const year = value.getFullYear(); + const month = String(value.getMonth() + 1).padStart(2, "0"); + const day = String(value.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + } + return value; +}; + +const isDateBefore = (a, b) => toDateOnly(a) < toDateOnly(b); +const isDateAfter = (a, b) => toDateOnly(a) > toDateOnly(b); + +export { isDateBefore, isDateAfter }; diff --git a/server/helpers/tripOwnership.js b/server/helpers/tripOwnership.js new file mode 100644 index 000000000..60c40ff0d --- /dev/null +++ b/server/helpers/tripOwnership.js @@ -0,0 +1,13 @@ +import { pool } from "../config/database.js"; + +// Destinations (and other trip-scoped resources) have no user_id of their own, +// so ownership must be checked by confirming the parent trip belongs to the user. +const getOwnedTrip = async (tripId, userId) => { + const { rows } = await pool.query( + "SELECT * FROM trips WHERE id = $1 AND user_id = $2", + [tripId, userId], + ); + return rows[0] ?? null; +}; + +export { getOwnedTrip }; diff --git a/server/helpers/validation.js b/server/helpers/validation.js new file mode 100644 index 000000000..fe5e56697 --- /dev/null +++ b/server/helpers/validation.js @@ -0,0 +1,5 @@ +const isValidId = (id) => /^\d+$/.test(id); +const isValidDate = (value) => !Number.isNaN(Date.parse(value)); +const isValidBudget = (value) => !Number.isNaN(Number(value)); + +export { isValidId, isValidDate, isValidBudget }; diff --git a/server/routes/activities.js b/server/routes/activities.js new file mode 100644 index 000000000..be943ecd9 --- /dev/null +++ b/server/routes/activities.js @@ -0,0 +1,18 @@ +import { Router } from "express"; +import { + getAllActivities, + getActivityById, + createActivity, + updateActivity, + deleteActivity, +} from "../controllers/activitiesController.js"; + +const router = Router(); + +router.get("/", getAllActivities); +router.get("/:id", getActivityById); +router.post("/", createActivity); +router.put("/:id", updateActivity); +router.delete("/:id", deleteActivity); + +export default router; diff --git a/server/routes/destinations.js b/server/routes/destinations.js new file mode 100644 index 000000000..646b1e194 --- /dev/null +++ b/server/routes/destinations.js @@ -0,0 +1,15 @@ +// Mounted at /api/destinations β€” get a single destination, or edit/delete it by its own id. +import { Router } from "express"; +import { + getDestinationById, + updateDestination, + deleteDestination, +} from "../controllers/destinations.js"; + +const router = Router(); + +router.get("/:id", getDestinationById); +router.patch("/:id", updateDestination); +router.delete("/:id", deleteDestination); + +export default router; diff --git a/server/routes/destinationsForTrip.js b/server/routes/destinationsForTrip.js new file mode 100644 index 000000000..fb6b1ca6a --- /dev/null +++ b/server/routes/destinationsForTrip.js @@ -0,0 +1,10 @@ +// Mounted at /api/trips/:tripId/destinations β€” list all destinations for a trip, or create a new one. +import { Router } from "express"; +import { getDestinationsForTrip, createDestination } from "../controllers/destinations.js"; + +const router = Router({ mergeParams: true }); + +router.get("/", getDestinationsForTrip); +router.post("/", createDestination); + +export default router; diff --git a/server/routes/expenses.js b/server/routes/expenses.js new file mode 100644 index 000000000..ac44e04e5 --- /dev/null +++ b/server/routes/expenses.js @@ -0,0 +1,18 @@ +import { Router } from "express"; +import { + getAllExpenses, + getExpenseById, + createExpense, + updateExpense, + deleteExpense, +} from "../controllers/expenses.js"; + +const router = Router(); + +router.get("/", getAllExpenses); +router.get("/:id", getExpenseById); +router.post("/", createExpense); +router.put("/:id", updateExpense); +router.delete("/:id", deleteExpense); + +export default router; diff --git a/server/routes/packingList.js b/server/routes/packingList.js new file mode 100644 index 000000000..71f923b56 --- /dev/null +++ b/server/routes/packingList.js @@ -0,0 +1,18 @@ +import { Router } from "express"; +import { + getAllPackingListItems, + getPackingListItemById, + createPackingListItem, + updatePackingListItem, + deletePackingListItem, +} from "../controllers/packingList.js"; + +const router = Router(); + +router.get("/", getAllPackingListItems); +router.get("/:id", getPackingListItemById); +router.post("/", createPackingListItem); +router.put("/:id", updatePackingListItem); +router.delete("/:id", deletePackingListItem); + +export default router; diff --git a/server/routes/trips.js b/server/routes/trips.js new file mode 100644 index 000000000..a9b8ebf3a --- /dev/null +++ b/server/routes/trips.js @@ -0,0 +1,18 @@ +import { Router } from "express"; +import { + getAllTrips, + getTripById, + createTrip, + updateTrip, + deleteTrip, +} from "../controllers/trips.js"; + +const router = Router(); + +router.get("/", getAllTrips); +router.get("/:id", getTripById); +router.post("/", createTrip); +router.patch("/:id", updateTrip); +router.delete("/:id", deleteTrip); + +export default router; diff --git a/server/routes/users.js b/server/routes/users.js new file mode 100644 index 000000000..201c2cbf9 --- /dev/null +++ b/server/routes/users.js @@ -0,0 +1,11 @@ +import { Router } from "express"; +import { getCurrentUser, updateCurrentUser } from "../controllers/users.js"; + +// DEV NOTE: /me is the observed pattern for getting the current user in many APIs. It is used here to avoid confusion with the /users/:id route, which is not implemented yet since we don't have auth yet. +const router = Router(); + +router.get("/me", getCurrentUser); +// Using PATCH here instead of PUT since we are only updating homeCurrency and displayName. This is more in line with RESTful principles. +router.patch("/me", updateCurrentUser); + +export default router; diff --git a/server/server.js b/server/server.js new file mode 100644 index 000000000..c02a265c3 --- /dev/null +++ b/server/server.js @@ -0,0 +1,38 @@ +import express from "express"; +import path from "path"; +import dotenv from "dotenv"; +import tripsRouter from "./routes/trips.js"; +import activitiesRouter from "./routes/activities.js"; +import expensesRouter from "./routes/expenses.js"; +import packingListRouter from "./routes/packingList.js"; +import usersRouter from "./routes/users.js"; +import destinationsForTripRouter from "./routes/destinationsForTrip.js"; +import destinationsRouter from "./routes/destinations.js"; + +dotenv.config(); + +const PORT = process.env.PORT || 3000; + +const app = express(); + +app.use(express.json()); + +if (process.env.NODE_ENV === "production") { + app.use(express.static("public")); +} + +app.use("/api/trips", tripsRouter); +app.use("/api/activities", activitiesRouter); +app.use("/api/expenses", expensesRouter); +app.use("/api/packing-list", packingListRouter); +app.use("/api/users", usersRouter); +app.use("/api/trips/:tripId/destinations", destinationsForTripRouter); +app.use("/api/destinations", destinationsRouter); + +if (process.env.NODE_ENV === "production") { + app.get("/*", (_, res) => res.sendFile(path.resolve("public", "index.html"))); +} + +app.listen(PORT, () => { + console.log(`server listening on http://localhost:${PORT}`); +});