diff --git a/README.md b/README.md index 31466b54c..2fe79e95f 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,62 @@ -# Final Project +# Balans — Energy Management App -Replace this readme with your own information about your project. +**Balans** is a mobile-first web app for people who need to manage their daily energy carefully. Inspired by Spoon Theory — a framework often used by people with chronic illness (depression, burnout etc) to describe limited energy resources — Balans helps you plan your day around how much energy you actually have, not how much you wish you had. -Start by briefly describing the assignment in a sentence or two. Keep it short and to the point. +--- -## The problem +## The Problem -Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next? +People living with chronic fatigue or energy-limiting conditions often struggle to plan their days in a way that respects their capacity. Traditional to-do apps treat all tasks as equal and don't account for energy cost. Balans fills that gap by letting you log your current energy, plan activities based on what drains or restores you, and reflect on patterns over time. -## View it live +--- -Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about. \ No newline at end of file +## Features + +- **Energy check-in** — Log your energy level (1–10) at the start of each day using an interactive battery visualisation +- **Activity planner** — Add activities to your day, each tagged as energy-draining or energy-restoring, with a cost/gain value +- **Day summary** — See a breakdown of your planned day and get a personalised tip from the mascot based on your energy balance +- **History** — Review past days and track your energy patterns over time +- **Custom activities** — Add your own activities (private to your account) alongside the built-in defaults +- **Animated mascot** — EnergyBlob reacts to your energy level with different expressions and animations +- **User accounts** — Register, log in, and keep your data private with JWT-based authentication + +--- + +## Tech Stack + +### Frontend +- React + Vite +- styled-components (with CSS custom properties for theming) +- Framer Motion (animations) +- Zustand (global state) +- React Router +- Phosphor Icons + +### Backend +- Node.js + Express +- MongoDB + Mongoose +- bcrypt (password hashing) +- Babel + Nodemon + +--- + +## Deployment + +- **Frontend:** [Netlify](https://final-project-balans.netlify.app) +- **Backend:** [Render](https://project-final-balans.onrender.com) + +--- + +## What's Next + +- Mood tracking alongside energy levels +- AI-powered suggestions based on your personal energy history +- Weekly and monthly energy overview charts +- Adding a calender to plan a week ahead +- Checkboxes in history, to be able to check the activities you completed in the end om the day + +--- + +## About the Name + +*Balans* is the Swedish word for balance — because the goal isn't to do more, it's to find the balance that works for you. diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index d1438c910..000000000 --- a/backend/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Backend part of Final Project - -This project includes the packages and babel setup for an express server, and is just meant to make things a little simpler to get up and running with. - -## Getting Started - -1. Install the required dependencies using `npm install`. -2. Start the development server using `npm run dev`. \ No newline at end of file diff --git a/backend/data/seedActivities.js b/backend/data/seedActivities.js new file mode 100644 index 000000000..4e5aa8049 --- /dev/null +++ b/backend/data/seedActivities.js @@ -0,0 +1,90 @@ +import { Activity } from "../models/Activity.js"; + +export const defaults = [ + // Activities that gives energy + { + name: "Promenad", + energyImpact: 2, + category: "rörelse" + }, + { + name: "Yoga", + energyImpact: 3, + category: "rörelse" + }, + { + name: "Träning lätt", + energyImpact: 1, + category: "rörelse" + }, + { + name: "Meditation", + energyImpact: 3, + category: "vila" + }, + { + name: "Powernap", + energyImpact: 3, + category: "vila", + }, + { + name: "Umgås", + energyImpact: 2, + category: "socialt" + }, + { + name: "Natur", + energyImpact: 2, + category: "vardag", + }, + + // Activities that takes energy. + { + name: "Jobb", + energyImpact: -2, + category: "jobb" + }, + { + name: "Möte", + energyImpact: -2, + category: "jobb" + }, + { + name: "Städning", + energyImpact: -2, + category: "vardag" + }, + { + name: "Skärmtid", + energyImpact: -1, + category: "vardag" + }, + { + name: "Matlagning", + energyImpact: -1, + category: "vardag" + }, + { + name: "Pendling", + energyImpact: -2, + category: "vardag" + }, + { + name: "Handla", + energyImpact: -2, + category: "vardag" + }, + { + name: "Träning tung", + energyImpact: -3, + category: "rörelse", + } +]; + +// Fills the database with standard activities once when the app starts. +export const seedActivities = async () => { + const count = await Activity.countDocuments(); + if (count === 0) { + await Activity.insertMany(defaults); + } +}; diff --git a/backend/middleware/authMiddleware.js b/backend/middleware/authMiddleware.js new file mode 100644 index 000000000..996f74045 --- /dev/null +++ b/backend/middleware/authMiddleware.js @@ -0,0 +1,16 @@ +import { User } from "../models/User.js"; + +//This is the "safeguard" for the protected endpoints, the one that uses auth. +export const authenticateUser = async (req, res, next) => { + const user = await User.findOne({ + accessToken: req.header("Authorization") + }); + if (user) { + req.user = user + next(); + } else { + res.status(401).json({ + loggedOut: true + }); + } +}; \ No newline at end of file diff --git a/backend/models/Activity.js b/backend/models/Activity.js new file mode 100644 index 000000000..17ae3c629 --- /dev/null +++ b/backend/models/Activity.js @@ -0,0 +1,22 @@ +import { Schema, model } from "mongoose"; + +const activitySchema = new Schema({ + name: { + type: String, + required: true, + }, + user: { + type: Schema.Types.ObjectId, + ref: "User", + }, + energyImpact: { + type: Number, + required: true, + }, + category: { + type: String, + required: true, + }, +}); + +export const Activity = model("activity", activitySchema); \ No newline at end of file diff --git a/backend/models/DailyPlan.js b/backend/models/DailyPlan.js new file mode 100644 index 000000000..6ec7f6156 --- /dev/null +++ b/backend/models/DailyPlan.js @@ -0,0 +1,32 @@ +import { Schema, model } from "mongoose"; + +const dailyPlanSchema = new Schema({ + // One unique id-person that points to the User model. The owner of the dailyplan. + user: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + date: { + type: Date, + default: () => new Date(), + }, + startingEnergy: { + type: Number, + }, + + activities: [{ + type: Schema.Types.ObjectId, + ref: "activity", + }], + currentEnergy: { + type: Number, + }, + // The users own note about the day (in History.jsx) + notes: { + type: String, + default: "", + } +}); + +export const dailyPlan = model("dailyPlan", dailyPlanSchema); \ No newline at end of file diff --git a/backend/models/User.js b/backend/models/User.js new file mode 100644 index 000000000..d232424f9 --- /dev/null +++ b/backend/models/User.js @@ -0,0 +1,25 @@ +import { Schema, model } from "mongoose"; +import crypto from "crypto"; + +// These are required (name, email, password). accessToken is generated with crypto. +const userSchema = new Schema({ + name: { + type: String, + required: true, + }, + email: { + type: String, + required: true, + unique: true, + }, + password: { + type: String, + required: true, + }, + accessToken: { + type: String, + default: () => crypto.randomBytes(128).toString("hex") + } +}); + +export const User = model("User", userSchema); \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 08f29f244..17afddf82 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,9 +12,12 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcrypt": "^6.0.0", "cors": "^2.8.5", + "dotenv": "^17.2.4", "express": "^4.17.3", + "express-list-endpoints": "^7.1.1", "mongoose": "^8.4.0", "nodemon": "^3.0.1" } -} \ No newline at end of file +} diff --git a/backend/routes/activitiesRoutes.js b/backend/routes/activitiesRoutes.js new file mode 100644 index 000000000..53a09d609 --- /dev/null +++ b/backend/routes/activitiesRoutes.js @@ -0,0 +1,64 @@ +import express from "express"; +import { Activity } from "../models/Activity.js"; +import { authenticateUser } from "../middleware/authMiddleware.js"; + +export const router = express.Router(); + +// GET /activities. Gets default activities + the current user's own activities. +router.get("/activities", authenticateUser, async (req, res) => { + try { + const activities = await Activity.find({ + $or: [{ user: { $exists: false } }, { user: null }, { user: req.user._id }] + }); + res.json(activities); + } catch (error) { + res.status(500).json({ error: "Could not fetch activities" }); + } +}); + +// POST /activities. Adding a new activity to the database. needs a auth. +router.post("/activities", authenticateUser, async (req, res) => { + try { + const { name, energyImpact, category } = req.body; + if (!name || !energyImpact || !category) { + return res.status(400).json({ error: "Name, energyImpact and category are required" }); + } + + const activity = new Activity({ name, energyImpact, category, user: req.user._id }); + await activity.save(); + res.status(201).json(activity); + } catch (error) { + res.status(500).json({ error: "Could not create activity" }); + } +}); + +// PATCH /activities. Updates an activity. +router.patch("/activities/:id", authenticateUser, async (req, res) => { + try { + const { id } = req.params; + const updates = req.body; + const activity = await Activity.findByIdAndUpdate(id, updates, { new: true }); + + if (!activity) { + return res.status(404).json({ error: "Activity not found" }); + } + res.json(activity); + } catch (error) { + res.status(400).json({ error: "Could not update activity" }); + } +}); + +// DELETE /activities. Deletes an activity +router.delete("/activities/:id", authenticateUser, async (req, res) => { + try { + const { id } = req.params; + const deletedActivity = await Activity.findByIdAndDelete(id); + + if (!deletedActivity) { + return res.status(404).json({ error: `Activity with id ${id} does not exist` }); + } + res.json(deletedActivity); + } catch (error) { + res.status(500).json({ error: "Could not delete activity" }); + } +}); \ No newline at end of file diff --git a/backend/routes/dailyplanRoutes.js b/backend/routes/dailyplanRoutes.js new file mode 100644 index 000000000..fb2acbd12 --- /dev/null +++ b/backend/routes/dailyplanRoutes.js @@ -0,0 +1,74 @@ +import express from "express"; +import { dailyPlan } from "../models/DailyPlan.js"; +import { authenticateUser } from "../middleware/authMiddleware.js"; + +export const router = express.Router(); + +// POST /dailyplan. Creates a new plan. You can only create 1 plan a day, otherwise a errormessage will show (409). +router.post("/dailyplan", authenticateUser, async (req, res) => { + try { + const { date, startingEnergy, activities, currentEnergy } = req.body; + const start = new Date(date); + start.setHours(0, 0, 0, 0); + const end = new Date(date); + end.setHours(23, 59, 59, 999); + + const existing = await dailyPlan.findOne({ + user: req.user._id, + date: { $gte: start, $lte: end } + }); + + if (existing) { + return res.status(409).json({ error: "Du har redan sparat en plan idag, du kan gå tillbaka och ändra din nuvarande plan" }); + } + + const newPlan = new dailyPlan({ user: req.user._id, date, startingEnergy, activities, currentEnergy }); + await newPlan.save(); + res.status(201).json(newPlan); + } catch (error) { + res.status(500).json({ error: "Could not create dailyplan" }); + } +}); + +// GET /dailyplan. Gets all the plans for the logged in user. Uses populate witch gets the hole activity object, not just the id. +router.get("/dailyplan", authenticateUser, async (req, res) => { + try { + const plan = await dailyPlan.find({ user: req.user._id }).populate("activities"); + + res.json(plan); + } catch (error) { + res.status(400).json({ error: "Could not fetch dailyplan" }); + } +}); + + +// PATCH /dailyplan. Updates a plan, is used for notes in History.jsx. +router.patch("/dailyplan/:id", authenticateUser, async (req, res) => { + try { + const { id } = req.params; + const updates = req.body; + const updatedPlan = await dailyPlan.findByIdAndUpdate(id, updates, { new: true }); + + if (!updatedPlan) { + return res.status(404).json({ error: "Could not find dailyplan" }); + } + res.json(updatedPlan); + } catch (error) { + res.status(400).json({ error: "Could not update your plan" }); + } +}); + +// DELETE /dailyplan. Deletes a dailyplan. +router.delete("/dailyplan/:id", authenticateUser, async (req, res) => { + try { + const { id } = req.params; + const deletedDailyPlan = await dailyPlan.findByIdAndDelete(id); + + if (!deletedDailyPlan) { + return res.status(404).json({ error: `Dailyplan with id ${id} does not exist` }); + } + res.json(deletedDailyPlan); + } catch (error) { + res.status(500).json({ error: "Could not delete dailyplan" }); + } +}); \ No newline at end of file diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js new file mode 100644 index 000000000..31279b2f9 --- /dev/null +++ b/backend/routes/userRoutes.js @@ -0,0 +1,93 @@ +import express from "express"; +import bcrypt from "bcrypt"; +import { authenticateUser } from "../middleware/authMiddleware.js"; +import { User } from "../models/User.js"; + +export const router = express.Router(); + +// POST /signup. Validates, checks if the mail exists, hashes the password with bcrypt+salt, then saves the user. +router.post("/signup", async (req, res) => { + try { + // Validation - pick out email and password from req, and see if they both are there. + const { name, email, password } = req.body; + + if (!name || !email || !password) { + return res.status(400).json({ + success: false, + message: "Email and password are required" + }); + } + //Checks if user exists. + const existingUser = await User.findOne({ email: email.toLowerCase() }); + + if (existingUser) { + return res.status(400).json({ + success: false, + message: "Invalid email or password", + }); + } + // Encrypts the password with salt (random string) Hash = mix salt with password = unreadable hash + const salt = bcrypt.genSaltSync(10); + const hashedPassword = bcrypt.hashSync(password, salt); + const user = new User({ name, email, password: hashedPassword }); + + // The user will be saved in MongoDB + await user.save(); + res.status(201).json({ + success: true, + message: "User created successfully", + response: { + email: user.email, + name: user.name, + id: user.id, + accessToken: user.accessToken, + } + }); + //If something goes wrong, catch and send error message. + } catch (error) { + res.status(400).json({ + success: false, + message: "Could not create user", + response: error, + }); + } +}); + +// POST /login. Finds the user, compare hashed password and returns accessToken. +router.post("/login", async (req, res) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ + success: false, + message: "Email and password are required", + }); + } + + const user = await User.findOne({ email: email.toLowerCase() }); + + // Compares the typed password with the hashed one. Not possible to unhash the bcrypted password. + if (!user || !bcrypt.compareSync(password, user.password)) { + return res.status(401).json({ + success: false, + message: "Invalid email or password", + }); + } + res.json({ + success: true, + message: "Login successful", + response: { + email: user.email, + name: user.name, + id: user.id, + accessToken: user.accessToken + } + }); + } catch (error) { + res.status(500).json({ + success: false, + message: "Something went wrong", + }); + } +}); \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 070c87518..9d15ea1ff 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,10 +1,18 @@ +import "dotenv/config"; import express from "express"; import cors from "cors"; import mongoose from "mongoose"; +import listEndpoints from "express-list-endpoints"; +import { router as userRouter } from "./routes/userRoutes.js"; +import { router as activitiesRouter } from "./routes/activitiesRoutes.js"; +import { router as dailyPlanRouter } from "./routes/dailyplanRoutes.js"; +import { seedActivities } from "./data/seedActivities.js"; +// Connecting to the Mongo database const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/final-project"; -mongoose.connect(mongoUrl); -mongoose.Promise = Promise; +mongoose.connect(mongoUrl).then(() => { + seedActivities(); +}); const port = process.env.PORT || 8080; const app = express(); @@ -12,10 +20,21 @@ const app = express(); app.use(cors()); app.use(express.json()); +// Root-endpoint with Welcome message and a list of all endpoints app.get("/", (req, res) => { - res.send("Hello Technigo!"); + const endpoints = listEndpoints(app); + + res.json([{ + message: "Welcome to the Balans API", + endpoints: endpoints, + }]); }); +// Connecting the different routes with endpoints. +app.use(userRouter); +app.use(activitiesRouter); +app.use(dailyPlanRouter); + // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 5cdb1d9cf..000000000 --- a/frontend/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Frontend part of Final Project - -This boilerplate is designed to give you a head start in your React projects, with a focus on understanding the structure and components. As a student of Technigo, you'll find this guide helpful in navigating and utilizing the repository. - -## Getting Started - -1. Install the required dependencies using `npm install`. -2. Start the development server using `npm run dev`. \ No newline at end of file diff --git a/frontend/dist/assets/About-BpqE09hM.js b/frontend/dist/assets/About-BpqE09hM.js new file mode 100644 index 000000000..7f52bd288 --- /dev/null +++ b/frontend/dist/assets/About-BpqE09hM.js @@ -0,0 +1,43 @@ +import{u as i,j as r,N as t,s as o,l as e}from"./index-BFuSrCVB.js";const x=()=>{const a=i();return r.jsxs(r.Fragment,{children:[r.jsx(t,{}),r.jsx(s,{children:r.jsxs(l,{onClick:()=>a(-1),children:[r.jsx(o,{size:20})," Tillbaka"]})}),r.jsxs(d,{children:[r.jsx("h2",{children:"Om balans"}),r.jsx(n,{children:r.jsx("p",{children:"Balans är en app för dig som vill planera din dag utifrån den energi du har. Genom att välja aktiviteter och se hur de påverkar din energinivå kan du hitta en balans som fungerar för dig."})}),r.jsxs(n,{children:[r.jsx("h3",{children:"Hur funkar det?"}),r.jsx("p",{children:"1. Välj hur mycket energi du har idag (1-10)"}),r.jsx("p",{children:"2. Lägg till aktiviteter som tar eller ger energi"}),r.jsx("p",{children:"3. Se en sammanfattning av din dag"})]})]})]})},d=e.div` + padding: 16px; + display: flex; + flex-direction: column; + gap: 20px; + + h2 { + text-align: center; + } +`,n=e.div` + background: rgba(255, 255, 255, 0.4); + backdrop-filter: blur(6px); + border-radius: 12px; + padding: 24px; + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.08); + + + h3 { + margin: 0 0 12px 0; + } + p { + margin: 0 0 8px 0; + line-height: 1.6; + color: var(--color-text); + } +`,s=e.div` + padding: 8px 16px; +`,l=e.button` + display: flex; + align-items: center; + gap: 6px; + background: none; + border: none; + cursor: pointer; + color: var(--color-text); + font-size: 14px; + padding: 4px 0; + margin-bottom: 8px; + + &:hover { + color: var(--color-primary); + } +`;export{x as About}; diff --git a/frontend/dist/assets/History-DsNoGyUr.js b/frontend/dist/assets/History-DsNoGyUr.js new file mode 100644 index 000000000..2901f1ab5 --- /dev/null +++ b/frontend/dist/assets/History-DsNoGyUr.js @@ -0,0 +1,173 @@ +import{r as t,p as u,j as e,l as n,u as V,N as y,s as E,f as b}from"./index-BFuSrCVB.js";const H=new Map([["bold",t.createElement(t.Fragment,null,t.createElement("path",{d:"M208,28H188V24a12,12,0,0,0-24,0v4H92V24a12,12,0,0,0-24,0v4H48A20,20,0,0,0,28,48V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V48A20,20,0,0,0,208,28ZM68,52a12,12,0,0,0,24,0h72a12,12,0,0,0,24,0h16V76H52V52ZM52,204V100H204V204Z"}))],["duotone",t.createElement(t.Fragment,null,t.createElement("path",{d:"M216,48V88H40V48a8,8,0,0,1,8-8H208A8,8,0,0,1,216,48Z",opacity:"0.2"}),t.createElement("path",{d:"M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM72,48v8a8,8,0,0,0,16,0V48h80v8a8,8,0,0,0,16,0V48h24V80H48V48ZM208,208H48V96H208V208Z"}))],["fill",t.createElement(t.Fragment,null,t.createElement("path",{d:"M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32Zm0,48H48V48H72v8a8,8,0,0,0,16,0V48h80v8a8,8,0,0,0,16,0V48h24Z"}))],["light",t.createElement(t.Fragment,null,t.createElement("path",{d:"M208,34H182V24a6,6,0,0,0-12,0V34H86V24a6,6,0,0,0-12,0V34H48A14,14,0,0,0,34,48V208a14,14,0,0,0,14,14H208a14,14,0,0,0,14-14V48A14,14,0,0,0,208,34ZM48,46H74V56a6,6,0,0,0,12,0V46h84V56a6,6,0,0,0,12,0V46h26a2,2,0,0,1,2,2V82H46V48A2,2,0,0,1,48,46ZM208,210H48a2,2,0,0,1-2-2V94H210V208A2,2,0,0,1,208,210Z"}))],["regular",t.createElement(t.Fragment,null,t.createElement("path",{d:"M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM72,48v8a8,8,0,0,0,16,0V48h80v8a8,8,0,0,0,16,0V48h24V80H48V48ZM208,208H48V96H208V208Z"}))],["thin",t.createElement(t.Fragment,null,t.createElement("path",{d:"M208,36H180V24a4,4,0,0,0-8,0V36H84V24a4,4,0,0,0-8,0V36H48A12,12,0,0,0,36,48V208a12,12,0,0,0,12,12H208a12,12,0,0,0,12-12V48A12,12,0,0,0,208,36ZM48,44H76V56a4,4,0,0,0,8,0V44h88V56a4,4,0,0,0,8,0V44h28a4,4,0,0,1,4,4V84H44V48A4,4,0,0,1,48,44ZM208,212H48a4,4,0,0,1-4-4V92H212V208A4,4,0,0,1,208,212Z"}))]]),j=new Map([["bold",t.createElement(t.Fragment,null,t.createElement("path",{d:"M216.49,104.49l-80,80a12,12,0,0,1-17,0l-80-80a12,12,0,0,1,17-17L128,159l71.51-71.52a12,12,0,0,1,17,17Z"}))],["duotone",t.createElement(t.Fragment,null,t.createElement("path",{d:"M208,96l-80,80L48,96Z",opacity:"0.2"}),t.createElement("path",{d:"M215.39,92.94A8,8,0,0,0,208,88H48a8,8,0,0,0-5.66,13.66l80,80a8,8,0,0,0,11.32,0l80-80A8,8,0,0,0,215.39,92.94ZM128,164.69,67.31,104H188.69Z"}))],["fill",t.createElement(t.Fragment,null,t.createElement("path",{d:"M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,48,88H208a8,8,0,0,1,5.66,13.66Z"}))],["light",t.createElement(t.Fragment,null,t.createElement("path",{d:"M212.24,100.24l-80,80a6,6,0,0,1-8.48,0l-80-80a6,6,0,0,1,8.48-8.48L128,167.51l75.76-75.75a6,6,0,0,1,8.48,8.48Z"}))],["regular",t.createElement(t.Fragment,null,t.createElement("path",{d:"M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z"}))],["thin",t.createElement(t.Fragment,null,t.createElement("path",{d:"M210.83,98.83l-80,80a4,4,0,0,1-5.66,0l-80-80a4,4,0,0,1,5.66-5.66L128,170.34l77.17-77.17a4,4,0,1,1,5.66,5.66Z"}))]]),f=t.forwardRef((a,o)=>t.createElement(u,{ref:o,...a,weights:H}));f.displayName="CalendarBlankIcon";const w=f,v=t.forwardRef((a,o)=>t.createElement(u,{ref:o,...a,weights:j}));v.displayName="CaretDownIcon";const $=v,M=({plans:a})=>{const[o,l]=t.useState(7),c=new Date,s=a.filter(r=>(c-new Date(r.date))/(1e3*60*60*24)<=o).sort((r,i)=>new Date(r.date)-new Date(i.date)),d=r=>r>=7?"#a8d5ba":r>=4?"#f0c060":"#c47a7a";return e.jsxs(Z,{children:[e.jsxs(k,{children:[e.jsx(A,{children:"Energiöversikt"}),e.jsxs(z,{children:[e.jsx(p,{$active:o===7,onClick:()=>l(7),children:"Vecka"}),e.jsx(p,{$active:o===30,onClick:()=>l(30),children:"Månad"})]})]}),s.length===0?e.jsx(_,{children:"Ingen data för perioden"}):e.jsx(F,{children:s.map(r=>e.jsxs(B,{children:[e.jsx(C,{children:e.jsx(D,{$height:r.currentEnergy/10*100,$color:d(r.currentEnergy)})}),e.jsx(L,{children:new Date(r.date).toLocaleDateString("sv-SE",{weekday:"short"})}),e.jsx(S,{children:r.currentEnergy})]},r._id))})]})},Z=n.div` + background: rgba(255, 255, 255, 0.4); + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.08); + backdrop-filter: blur(6px); + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; + overflow: hidden; +`,k=n.div` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +`,A=n.h3` + margin: 0; + font-size: 15px; + color: var(--color-text); +`,z=n.div` + display: flex; + gap: 6px; +`,p=n.button` + padding: 4px 12px; + border-radius: 20px; + border: 1px solid var(--color-border); + background: ${a=>a.$active?"var(--color-primary)":"transparent"}; + color: ${a=>a.$active?"white":"var(--color-text)"}; + font-size: 13px; + cursor: pointer; +`,F=n.div` + display: flex; + align-items: flex-end; + gap: 6px; + justify-content: space-around; + overflow-x: auto; +`,B=n.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + flex: 1; +`,C=n.div` + width: 100%; + height: 100px; + display: flex; + align-items: flex-end; +`,D=n.div` + width: 100%; + height: ${a=>a.$height}%; + background: ${a=>a.$color}; + border-radius: 6px 6px 2px 2px; + transition: height 0.4s ease; + min-height: 4px; +`,L=n.span` + font-size: 11px; + color: var(--color-text-muted); + text-transform: capitalize; +`,S=n.span` + font-size: 12px; + font-weight: 600; + color: var(--color-text); +`,_=n.p` + text-align: center; + color: var(--color-text-muted); + font-size: 14px; + padding: 24px 0; +`,X=()=>{const[a,o]=t.useState([]),[l,c]=t.useState(null),s=V(),d=(r,i)=>{const x=i-r;return x>=0?"Energin höll i sig bra idag!":x>=2?"Lite tyngre dag - men du tog dig igenom den.":"Tuff dag idag. Lägg in en vilosam aktivitet extra i morgon"};return t.useEffect(()=>{(async()=>{const i=await b();o(i)})()},[]),e.jsxs(e.Fragment,{children:[e.jsx(y,{}),e.jsx(K,{children:e.jsxs(Q,{onClick:()=>s(-1),children:[e.jsx(E,{size:20})," Tillbaka"]})}),e.jsx(N,{children:a.length===0?e.jsxs(R,{children:[e.jsx("span",{children:"🌿"}),e.jsx("p",{children:"Du har inte loggat någon dag ännu!"}),e.jsx("p",{children:"Gå tillbaka och planera din första dag."})]}):e.jsxs(e.Fragment,{children:[e.jsx(M,{plans:a}),a.map(r=>e.jsxs(G,{$energy:r.currentEnergy,$open:l===r._id,onClick:()=>c(l===r._id?null:r._id),children:[e.jsxs(I,{$open:l===r._id,children:[e.jsx(w,{size:16,weight:"fill"}),e.jsx("h3",{children:new Date(r.date).toLocaleDateString("sv-SE",{weekday:"long",day:"numeric",month:"long"})}),e.jsx($,{size:14,style:{marginLeft:"auto",transition:"transform 0.2s",transform:l===r._id?"rotate(180deg)":"rotate(0deg)"}})]}),l===r._id&&e.jsxs(e.Fragment,{children:[e.jsx(P,{children:e.jsxs(T,{children:[e.jsxs(m,{children:[e.jsx(g,{children:r.startingEnergy}),e.jsx(h,{children:"start"})]}),e.jsx(O,{$positive:r.currentEnergy>=r.startingEnergy,children:r.currentEnergy>=r.startingEnergy?"↑":"↓"}),e.jsxs(m,{children:[e.jsx(g,{$end:!0,$positive:r.currentEnergy>=r.startingEnergy,children:r.currentEnergy}),e.jsx(h,{children:"slut"})]})]})}),e.jsx(W,{children:d(r.startingEnergy,r.currentEnergy)}),e.jsx(q,{children:r.activities.map(i=>e.jsx(J,{$positive:i.energyImpact>0,children:i.name},i._id))})]})]},r._id))]})})]})},N=n.div` + padding: 8px 16px 16px; +`,R=n.div` + text-align: center; + padding: 48px 16px; + color: var(--color-text); + + span { + font-size: 48px; + } +`,G=n.div` + background: rgba(255, 255, 255, 0.4); + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.08); + backdrop-filter: blur(6px); + border-left: 5px solid ${a=>a.$energy>=7?"#a8d5ba":a.$energy>=4?"#f0c060":"#c47a7a"}; + + border-radius: 12px; + padding: ${a=>a.$open?"10px":"8px 10px"}; + margin-bottom: 10px; + cursor: pointer; + + h3 { + margin: 0 0 8px 0; + text-transform: capitalize; + } + + p { + margin: 0 0 12px 0; + color: var(--color-text-muted); + } +`,I=n.div` + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 12px; + color: var(--color-text-muted); + + h3 { + margin: 0; + text-transform: capitalize; + font-size: 15px; + } +`,P=n.div` + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +`,T=n.div` + display: flex; + align-items: baseline; + gap: 6px; + flex-shrink: 0; +`,W=n.p` + flex: 1; + font-size: 13px; + color: var(--color-text-muted); + font-style: italic; + margin: 0 0 12px 0; +`,g=n.span` + font-size: 36px; + font-weight: 700; + line-height: 1; + color: ${({$end:a,$positive:o})=>a?o?"var(--color-forest)":"var(--color-error)":"var(--color-text-muted)"}; +`,m=n.div` + display: flex; + flex-direction: column; + align-items: center; +`,h=n.span` + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--color-text-muted); +`,O=n.span` + font-size: 24px; + align-self: center; + color: ${({$positive:a})=>a?"var(--color-forest)":"var(--color-error)"}; +`,q=n.span` + display: flex; + flex-wrap: wrap; + gap: 8px; +`,J=n.span` + display: inline-block; + padding: 10px 14px; + border-radius: 16px; + font-size: 14px; + font-weight: 500; + color: var(--color-text); + background: ${a=>a.$positive?"rgba(74, 124, 89, 0.15)":"var(--color-error-light)"}; + border: 1px solid ${a=>a.$positive?"var(--color-forest)":"var(--color-error)"}; +`,K=n.div` + padding: 4px 16px; +`,Q=n.button` + display: flex; + align-items: center; + gap: 6px; + background: none; + border: none; + cursor: pointer; + color: var(--color-text); + font-size: 14px; + padding: 4px 0; + margin-bottom: 8px; + + &:hover { + color: var(--color-primary); +} +`;export{X as History}; diff --git a/frontend/dist/assets/index-BFuSrCVB.js b/frontend/dist/assets/index-BFuSrCVB.js new file mode 100644 index 000000000..482b9ec82 --- /dev/null +++ b/frontend/dist/assets/index-BFuSrCVB.js @@ -0,0 +1,952 @@ +(function(){const r=document.createElement("link").relList;if(r&&r.supports&&r.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))a(l);new MutationObserver(l=>{for(const f of l)if(f.type==="childList")for(const d of f.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&a(d)}).observe(document,{childList:!0,subtree:!0});function i(l){const f={};return l.integrity&&(f.integrity=l.integrity),l.referrerPolicy&&(f.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?f.credentials="include":l.crossOrigin==="anonymous"?f.credentials="omit":f.credentials="same-origin",f}function a(l){if(l.ep)return;l.ep=!0;const f=i(l);fetch(l.href,f)}})();function mh(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var m0={exports:{}},qi={},g0={exports:{}},de={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var sd;function c6(){if(sd)return de;sd=1;var t=Symbol.for("react.element"),r=Symbol.for("react.portal"),i=Symbol.for("react.fragment"),a=Symbol.for("react.strict_mode"),l=Symbol.for("react.profiler"),f=Symbol.for("react.provider"),d=Symbol.for("react.context"),p=Symbol.for("react.forward_ref"),m=Symbol.for("react.suspense"),g=Symbol.for("react.memo"),y=Symbol.for("react.lazy"),x=Symbol.iterator;function E(P){return P===null||typeof P!="object"?null:(P=x&&P[x]||P["@@iterator"],typeof P=="function"?P:null)}var C={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},V=Object.assign,A={};function T(P,b,ue){this.props=P,this.context=b,this.refs=A,this.updater=ue||C}T.prototype.isReactComponent={},T.prototype.setState=function(P,b){if(typeof P!="object"&&typeof P!="function"&&P!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,P,b,"setState")},T.prototype.forceUpdate=function(P){this.updater.enqueueForceUpdate(this,P,"forceUpdate")};function H(){}H.prototype=T.prototype;function Z(P,b,ue){this.props=P,this.context=b,this.refs=A,this.updater=ue||C}var F=Z.prototype=new H;F.constructor=Z,V(F,T.prototype),F.isPureReactComponent=!0;var D=Array.isArray,_=Object.prototype.hasOwnProperty,z={current:null},O={key:!0,ref:!0,__self:!0,__source:!0};function I(P,b,ue){var ce,pe={},he=null,we=null;if(b!=null)for(ce in b.ref!==void 0&&(we=b.ref),b.key!==void 0&&(he=""+b.key),b)_.call(b,ce)&&!O.hasOwnProperty(ce)&&(pe[ce]=b[ce]);var me=arguments.length-2;if(me===1)pe.children=ue;else if(1>>1,b=K[P];if(0>>1;Pl(pe,G))hel(we,pe)?(K[P]=we,K[he]=G,P=he):(K[P]=pe,K[ce]=G,P=ce);else if(hel(we,G))K[P]=we,K[he]=G,P=he;else break e}}return q}function l(K,q){var G=K.sortIndex-q.sortIndex;return G!==0?G:K.id-q.id}if(typeof performance=="object"&&typeof performance.now=="function"){var f=performance;t.unstable_now=function(){return f.now()}}else{var d=Date,p=d.now();t.unstable_now=function(){return d.now()-p}}var m=[],g=[],y=1,x=null,E=3,C=!1,V=!1,A=!1,T=typeof setTimeout=="function"?setTimeout:null,H=typeof clearTimeout=="function"?clearTimeout:null,Z=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function F(K){for(var q=i(g);q!==null;){if(q.callback===null)a(g);else if(q.startTime<=K)a(g),q.sortIndex=q.expirationTime,r(m,q);else break;q=i(g)}}function D(K){if(A=!1,F(K),!V)if(i(m)!==null)V=!0,Xe(_);else{var q=i(g);q!==null&&le(D,q.startTime-K)}}function _(K,q){V=!1,A&&(A=!1,H(I),I=-1),C=!0;var G=E;try{for(F(q),x=i(m);x!==null&&(!(x.expirationTime>q)||K&&!ae());){var P=x.callback;if(typeof P=="function"){x.callback=null,E=x.priorityLevel;var b=P(x.expirationTime<=q);q=t.unstable_now(),typeof b=="function"?x.callback=b:x===i(m)&&a(m),F(q)}else a(m);x=i(m)}if(x!==null)var ue=!0;else{var ce=i(g);ce!==null&&le(D,ce.startTime-q),ue=!1}return ue}finally{x=null,E=G,C=!1}}var z=!1,O=null,I=-1,re=5,se=-1;function ae(){return!(t.unstable_now()-seK||125P?(K.sortIndex=G,r(g,K),i(m)===null&&K===i(g)&&(A?(H(I),I=-1):A=!0,le(D,G-P))):(K.sortIndex=b,r(m,K),V||C||(V=!0,Xe(_))),K},t.unstable_shouldYield=ae,t.unstable_wrapCallback=function(K){var q=E;return function(){var G=E;E=q;try{return K.apply(this,arguments)}finally{E=G}}}})(x0)),x0}var dd;function p6(){return dd||(dd=1,v0.exports=h6()),v0.exports}/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var hd;function m6(){if(hd)return gt;hd=1;var t=vu(),r=p6();function i(e){for(var n="https://reactjs.org/docs/error-decoder.html?invariant="+e,o=1;o"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),m=Object.prototype.hasOwnProperty,g=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,y={},x={};function E(e){return m.call(x,e)?!0:m.call(y,e)?!1:g.test(e)?x[e]=!0:(y[e]=!0,!1)}function C(e,n,o,s){if(o!==null&&o.type===0)return!1;switch(typeof n){case"function":case"symbol":return!0;case"boolean":return s?!1:o!==null?!o.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function V(e,n,o,s){if(n===null||typeof n>"u"||C(e,n,o,s))return!0;if(s)return!1;if(o!==null)switch(o.type){case 3:return!n;case 4:return n===!1;case 5:return isNaN(n);case 6:return isNaN(n)||1>n}return!1}function A(e,n,o,s,c,h,v){this.acceptsBooleans=n===2||n===3||n===4,this.attributeName=s,this.attributeNamespace=c,this.mustUseProperty=o,this.propertyName=e,this.type=n,this.sanitizeURL=h,this.removeEmptyString=v}var T={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){T[e]=new A(e,0,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var n=e[0];T[n]=new A(n,1,!1,e[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){T[e]=new A(e,2,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){T[e]=new A(e,2,!1,e,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){T[e]=new A(e,3,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){T[e]=new A(e,3,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){T[e]=new A(e,4,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){T[e]=new A(e,6,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){T[e]=new A(e,5,!1,e.toLowerCase(),null,!1,!1)});var H=/[\-:]([a-z])/g;function Z(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var n=e.replace(H,Z);T[n]=new A(n,1,!1,e,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var n=e.replace(H,Z);T[n]=new A(n,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var n=e.replace(H,Z);T[n]=new A(n,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){T[e]=new A(e,1,!1,e.toLowerCase(),null,!1,!1)}),T.xlinkHref=new A("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){T[e]=new A(e,1,!1,e.toLowerCase(),null,!0,!0)});function F(e,n,o,s){var c=T.hasOwnProperty(n)?T[n]:null;(c!==null?c.type!==0:s||!(2S||c[v]!==h[S]){var k=` +`+c[v].replace(" at new "," at ");return e.displayName&&k.includes("")&&(k=k.replace("",e.displayName)),k}while(1<=v&&0<=S);break}}}finally{ue=!1,Error.prepareStackTrace=o}return(e=e?e.displayName||e.name:"")?b(e):""}function pe(e){switch(e.tag){case 5:return b(e.type);case 16:return b("Lazy");case 13:return b("Suspense");case 19:return b("SuspenseList");case 0:case 2:case 15:return e=ce(e.type,!1),e;case 11:return e=ce(e.type.render,!1),e;case 1:return e=ce(e.type,!0),e;default:return""}}function he(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case O:return"Fragment";case z:return"Portal";case re:return"Profiler";case I:return"StrictMode";case Se:return"Suspense";case $e:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case ae:return(e.displayName||"Context")+".Consumer";case se:return(e._context.displayName||"Context")+".Provider";case ke:var n=e.render;return e=e.displayName,e||(e=n.displayName||n.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case tt:return n=e.displayName||null,n!==null?n:he(e.type)||"Memo";case Xe:n=e._payload,e=e._init;try{return he(e(n))}catch{}}return null}function we(e){var n=e.type;switch(e.tag){case 24:return"Cache";case 9:return(n.displayName||"Context")+".Consumer";case 10:return(n._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=n.render,e=e.displayName||e.name||"",n.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return n;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return he(n);case 8:return n===I?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof n=="function")return n.displayName||n.name||null;if(typeof n=="string")return n}return null}function me(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function ve(e){var n=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(n==="checkbox"||n==="radio")}function Ke(e){var n=ve(e)?"checked":"value",o=Object.getOwnPropertyDescriptor(e.constructor.prototype,n),s=""+e[n];if(!e.hasOwnProperty(n)&&typeof o<"u"&&typeof o.get=="function"&&typeof o.set=="function"){var c=o.get,h=o.set;return Object.defineProperty(e,n,{configurable:!0,get:function(){return c.call(this)},set:function(v){s=""+v,h.call(this,v)}}),Object.defineProperty(e,n,{enumerable:o.enumerable}),{getValue:function(){return s},setValue:function(v){s=""+v},stopTracking:function(){e._valueTracker=null,delete e[n]}}}}function Wt(e){e._valueTracker||(e._valueTracker=Ke(e))}function kt(e){if(!e)return!1;var n=e._valueTracker;if(!n)return!0;var o=n.getValue(),s="";return e&&(s=ve(e)?e.checked?"true":"false":e.value),e=s,e!==o?(n.setValue(e),!0):!1}function xn(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function wn(e,n){var o=n.checked;return G({},n,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:o??e._wrapperState.initialChecked})}function nn(e,n){var o=n.defaultValue==null?"":n.defaultValue,s=n.checked!=null?n.checked:n.defaultChecked;o=me(n.value!=null?n.value:o),e._wrapperState={initialChecked:s,initialValue:o,controlled:n.type==="checkbox"||n.type==="radio"?n.checked!=null:n.value!=null}}function p1(e,n){n=n.checked,n!=null&&F(e,"checked",n,!1)}function As(e,n){p1(e,n);var o=me(n.value),s=n.type;if(o!=null)s==="number"?(o===0&&e.value===""||e.value!=o)&&(e.value=""+o):e.value!==""+o&&(e.value=""+o);else if(s==="submit"||s==="reset"){e.removeAttribute("value");return}n.hasOwnProperty("value")?Cs(e,n.type,o):n.hasOwnProperty("defaultValue")&&Cs(e,n.type,me(n.defaultValue)),n.checked==null&&n.defaultChecked!=null&&(e.defaultChecked=!!n.defaultChecked)}function m1(e,n,o){if(n.hasOwnProperty("value")||n.hasOwnProperty("defaultValue")){var s=n.type;if(!(s!=="submit"&&s!=="reset"||n.value!==void 0&&n.value!==null))return;n=""+e._wrapperState.initialValue,o||n===e.value||(e.value=n),e.defaultValue=n}o=e.name,o!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,o!==""&&(e.name=o)}function Cs(e,n,o){(n!=="number"||xn(e.ownerDocument)!==e)&&(o==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+o&&(e.defaultValue=""+o))}var hi=Array.isArray;function Sr(e,n,o,s){if(e=e.options,n){n={};for(var c=0;c"+n.valueOf().toString()+"",n=Vo.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;n.firstChild;)e.appendChild(n.firstChild)}});function pi(e,n){if(n){var o=e.firstChild;if(o&&o===e.lastChild&&o.nodeType===3){o.nodeValue=n;return}}e.textContent=n}var mi={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},pm=["Webkit","ms","Moz","O"];Object.keys(mi).forEach(function(e){pm.forEach(function(n){n=n+e.charAt(0).toUpperCase()+e.substring(1),mi[n]=mi[e]})});function E1(e,n,o){return n==null||typeof n=="boolean"||n===""?"":o||typeof n!="number"||n===0||mi.hasOwnProperty(e)&&mi[e]?(""+n).trim():n+"px"}function S1(e,n){e=e.style;for(var o in n)if(n.hasOwnProperty(o)){var s=o.indexOf("--")===0,c=E1(o,n[o],s);o==="float"&&(o="cssFloat"),s?e.setProperty(o,c):e[o]=c}}var mm=G({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Ps(e,n){if(n){if(mm[e]&&(n.children!=null||n.dangerouslySetInnerHTML!=null))throw Error(i(137,e));if(n.dangerouslySetInnerHTML!=null){if(n.children!=null)throw Error(i(60));if(typeof n.dangerouslySetInnerHTML!="object"||!("__html"in n.dangerouslySetInnerHTML))throw Error(i(61))}if(n.style!=null&&typeof n.style!="object")throw Error(i(62))}}function Vs(e,n){if(e.indexOf("-")===-1)return typeof n.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Ts=null;function Rs(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Ls=null,Ar=null,Cr=null;function A1(e){if(e=_i(e)){if(typeof Ls!="function")throw Error(i(280));var n=e.stateNode;n&&(n=Xo(n),Ls(e.stateNode,e.type,n))}}function C1(e){Ar?Cr?Cr.push(e):Cr=[e]:Ar=e}function k1(){if(Ar){var e=Ar,n=Cr;if(Cr=Ar=null,A1(e),n)for(e=0;e>>=0,e===0?32:31-(Mm(e)/Pm|0)|0}var Ho=64,Fo=4194304;function xi(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Do(e,n){var o=e.pendingLanes;if(o===0)return 0;var s=0,c=e.suspendedLanes,h=e.pingedLanes,v=o&268435455;if(v!==0){var S=v&~c;S!==0?s=xi(S):(h&=v,h!==0&&(s=xi(h)))}else v=o&~c,v!==0?s=xi(v):h!==0&&(s=xi(h));if(s===0)return 0;if(n!==0&&n!==s&&(n&c)===0&&(c=s&-s,h=n&-n,c>=h||c===16&&(h&4194240)!==0))return n;if((s&4)!==0&&(s|=o&16),n=e.entangledLanes,n!==0)for(e=e.entanglements,n&=s;0o;o++)n.push(e);return n}function wi(e,n,o){e.pendingLanes|=n,n!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,n=31-Dt(n),e[n]=o}function Lm(e,n){var o=e.pendingLanes&~n;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=n,e.mutableReadLanes&=n,e.entangledLanes&=n,n=e.entanglements;var s=e.eventTimes;for(e=e.expirationTimes;0=Vi),J1=" ",ec=!1;function tc(e,n){switch(e){case"keyup":return o4.indexOf(n.keyCode)!==-1;case"keydown":return n.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function nc(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Pr=!1;function s4(e,n){switch(e){case"compositionend":return nc(n);case"keypress":return n.which!==32?null:(ec=!0,J1);case"textInput":return e=n.data,e===J1&&ec?null:e;default:return null}}function l4(e,n){if(Pr)return e==="compositionend"||!Ys&&tc(e,n)?(e=K1(),No=zs=kn=null,Pr=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(n.ctrlKey||n.altKey||n.metaKey)||n.ctrlKey&&n.altKey){if(n.char&&1=n)return{node:o,offset:n-e};e=s}e:{for(;o;){if(o.nextSibling){o=o.nextSibling;break e}o=o.parentNode}o=void 0}o=uc(o)}}function fc(e,n){return e&&n?e===n?!0:e&&e.nodeType===3?!1:n&&n.nodeType===3?fc(e,n.parentNode):"contains"in e?e.contains(n):e.compareDocumentPosition?!!(e.compareDocumentPosition(n)&16):!1:!1}function dc(){for(var e=window,n=xn();n instanceof e.HTMLIFrameElement;){try{var o=typeof n.contentWindow.location.href=="string"}catch{o=!1}if(o)e=n.contentWindow;else break;n=xn(e.document)}return n}function qs(e){var n=e&&e.nodeName&&e.nodeName.toLowerCase();return n&&(n==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||n==="textarea"||e.contentEditable==="true")}function y4(e){var n=dc(),o=e.focusedElem,s=e.selectionRange;if(n!==o&&o&&o.ownerDocument&&fc(o.ownerDocument.documentElement,o)){if(s!==null&&qs(o)){if(n=s.start,e=s.end,e===void 0&&(e=n),"selectionStart"in o)o.selectionStart=n,o.selectionEnd=Math.min(e,o.value.length);else if(e=(n=o.ownerDocument||document)&&n.defaultView||window,e.getSelection){e=e.getSelection();var c=o.textContent.length,h=Math.min(s.start,c);s=s.end===void 0?h:Math.min(s.end,c),!e.extend&&h>s&&(c=s,s=h,h=c),c=cc(o,h);var v=cc(o,s);c&&v&&(e.rangeCount!==1||e.anchorNode!==c.node||e.anchorOffset!==c.offset||e.focusNode!==v.node||e.focusOffset!==v.offset)&&(n=n.createRange(),n.setStart(c.node,c.offset),e.removeAllRanges(),h>s?(e.addRange(n),e.extend(v.node,v.offset)):(n.setEnd(v.node,v.offset),e.addRange(n)))}}for(n=[],e=o;e=e.parentNode;)e.nodeType===1&&n.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof o.focus=="function"&&o.focus(),o=0;o=document.documentMode,Vr=null,Js=null,ji=null,el=!1;function hc(e,n,o){var s=o.window===o?o.document:o.nodeType===9?o:o.ownerDocument;el||Vr==null||Vr!==xn(s)||(s=Vr,"selectionStart"in s&&qs(s)?s={start:s.selectionStart,end:s.selectionEnd}:(s=(s.ownerDocument&&s.ownerDocument.defaultView||window).getSelection(),s={anchorNode:s.anchorNode,anchorOffset:s.anchorOffset,focusNode:s.focusNode,focusOffset:s.focusOffset}),ji&&Li(ji,s)||(ji=s,s=Go(Js,"onSelect"),0Hr||(e.current=dl[Hr],dl[Hr]=null,Hr--)}function Ce(e,n){Hr++,dl[Hr]=e.current,e.current=n}var Tn={},nt=Vn(Tn),ft=Vn(!1),qn=Tn;function Fr(e,n){var o=e.type.contextTypes;if(!o)return Tn;var s=e.stateNode;if(s&&s.__reactInternalMemoizedUnmaskedChildContext===n)return s.__reactInternalMemoizedMaskedChildContext;var c={},h;for(h in o)c[h]=n[h];return s&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=n,e.__reactInternalMemoizedMaskedChildContext=c),c}function dt(e){return e=e.childContextTypes,e!=null}function qo(){Pe(ft),Pe(nt)}function Vc(e,n,o){if(nt.current!==Tn)throw Error(i(168));Ce(nt,n),Ce(ft,o)}function Tc(e,n,o){var s=e.stateNode;if(n=n.childContextTypes,typeof s.getChildContext!="function")return o;s=s.getChildContext();for(var c in s)if(!(c in n))throw Error(i(108,we(e)||"Unknown",c));return G({},o,s)}function Jo(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Tn,qn=nt.current,Ce(nt,e),Ce(ft,ft.current),!0}function Rc(e,n,o){var s=e.stateNode;if(!s)throw Error(i(169));o?(e=Tc(e,n,qn),s.__reactInternalMemoizedMergedChildContext=e,Pe(ft),Pe(nt),Ce(nt,e)):Pe(ft),Ce(ft,o)}var on=null,ea=!1,hl=!1;function Lc(e){on===null?on=[e]:on.push(e)}function T4(e){ea=!0,Lc(e)}function Rn(){if(!hl&&on!==null){hl=!0;var e=0,n=Ee;try{var o=on;for(Ee=1;e>=v,c-=v,an=1<<32-Dt(n)+c|o<oe?(Qe=ne,ne=null):Qe=ne.sibling;var ye=N(R,ne,L[oe],W);if(ye===null){ne===null&&(ne=Qe);break}e&&ne&&ye.alternate===null&&n(R,ne),M=h(ye,M,oe),te===null?ee=ye:te.sibling=ye,te=ye,ne=Qe}if(oe===L.length)return o(R,ne),Re&&er(R,oe),ee;if(ne===null){for(;oeoe?(Qe=ne,ne=null):Qe=ne.sibling;var In=N(R,ne,ye.value,W);if(In===null){ne===null&&(ne=Qe);break}e&&ne&&In.alternate===null&&n(R,ne),M=h(In,M,oe),te===null?ee=In:te.sibling=In,te=In,ne=Qe}if(ye.done)return o(R,ne),Re&&er(R,oe),ee;if(ne===null){for(;!ye.done;oe++,ye=L.next())ye=B(R,ye.value,W),ye!==null&&(M=h(ye,M,oe),te===null?ee=ye:te.sibling=ye,te=ye);return Re&&er(R,oe),ee}for(ne=s(R,ne);!ye.done;oe++,ye=L.next())ye=Y(ne,R,oe,ye.value,W),ye!==null&&(e&&ye.alternate!==null&&ne.delete(ye.key===null?oe:ye.key),M=h(ye,M,oe),te===null?ee=ye:te.sibling=ye,te=ye);return e&&ne.forEach(function(u6){return n(R,u6)}),Re&&er(R,oe),ee}function be(R,M,L,W){if(typeof L=="object"&&L!==null&&L.type===O&&L.key===null&&(L=L.props.children),typeof L=="object"&&L!==null){switch(L.$$typeof){case _:e:{for(var ee=L.key,te=M;te!==null;){if(te.key===ee){if(ee=L.type,ee===O){if(te.tag===7){o(R,te.sibling),M=c(te,L.props.children),M.return=R,R=M;break e}}else if(te.elementType===ee||typeof ee=="object"&&ee!==null&&ee.$$typeof===Xe&&_c(ee)===te.type){o(R,te.sibling),M=c(te,L.props),M.ref=bi(R,te,L),M.return=R,R=M;break e}o(R,te);break}else n(R,te);te=te.sibling}L.type===O?(M=lr(L.props.children,R.mode,W,L.key),M.return=R,R=M):(W=Va(L.type,L.key,L.props,null,R.mode,W),W.ref=bi(R,M,L),W.return=R,R=W)}return v(R);case z:e:{for(te=L.key;M!==null;){if(M.key===te)if(M.tag===4&&M.stateNode.containerInfo===L.containerInfo&&M.stateNode.implementation===L.implementation){o(R,M.sibling),M=c(M,L.children||[]),M.return=R,R=M;break e}else{o(R,M);break}else n(R,M);M=M.sibling}M=c0(L,R.mode,W),M.return=R,R=M}return v(R);case Xe:return te=L._init,be(R,M,te(L._payload),W)}if(hi(L))return X(R,M,L,W);if(q(L))return J(R,M,L,W);ia(R,L)}return typeof L=="string"&&L!==""||typeof L=="number"?(L=""+L,M!==null&&M.tag===6?(o(R,M.sibling),M=c(M,L),M.return=R,R=M):(o(R,M),M=u0(L,R.mode,W),M.return=R,R=M),v(R)):o(R,M)}return be}var br=bc(!0),Ic=bc(!1),oa=Vn(null),aa=null,Ir=null,xl=null;function wl(){xl=Ir=aa=null}function El(e){var n=oa.current;Pe(oa),e._currentValue=n}function Sl(e,n,o){for(;e!==null;){var s=e.alternate;if((e.childLanes&n)!==n?(e.childLanes|=n,s!==null&&(s.childLanes|=n)):s!==null&&(s.childLanes&n)!==n&&(s.childLanes|=n),e===o)break;e=e.return}}function Nr(e,n){aa=e,xl=Ir=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&n)!==0&&(ht=!0),e.firstContext=null)}function Vt(e){var n=e._currentValue;if(xl!==e)if(e={context:e,memoizedValue:n,next:null},Ir===null){if(aa===null)throw Error(i(308));Ir=e,aa.dependencies={lanes:0,firstContext:e}}else Ir=Ir.next=e;return n}var tr=null;function Al(e){tr===null?tr=[e]:tr.push(e)}function Nc(e,n,o,s){var c=n.interleaved;return c===null?(o.next=o,Al(n)):(o.next=c.next,c.next=o),n.interleaved=o,ln(e,s)}function ln(e,n){e.lanes|=n;var o=e.alternate;for(o!==null&&(o.lanes|=n),o=e,e=e.return;e!==null;)e.childLanes|=n,o=e.alternate,o!==null&&(o.childLanes|=n),o=e,e=e.return;return o.tag===3?o.stateNode:null}var Ln=!1;function Cl(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Oc(e,n){e=e.updateQueue,n.updateQueue===e&&(n.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function un(e,n){return{eventTime:e,lane:n,tag:0,payload:null,callback:null,next:null}}function jn(e,n,o){var s=e.updateQueue;if(s===null)return null;if(s=s.shared,(ge&2)!==0){var c=s.pending;return c===null?n.next=n:(n.next=c.next,c.next=n),s.pending=n,ln(e,o)}return c=s.interleaved,c===null?(n.next=n,Al(s)):(n.next=c.next,c.next=n),s.interleaved=n,ln(e,o)}function sa(e,n,o){if(n=n.updateQueue,n!==null&&(n=n.shared,(o&4194240)!==0)){var s=n.lanes;s&=e.pendingLanes,o|=s,n.lanes=o,bs(e,o)}}function $c(e,n){var o=e.updateQueue,s=e.alternate;if(s!==null&&(s=s.updateQueue,o===s)){var c=null,h=null;if(o=o.firstBaseUpdate,o!==null){do{var v={eventTime:o.eventTime,lane:o.lane,tag:o.tag,payload:o.payload,callback:o.callback,next:null};h===null?c=h=v:h=h.next=v,o=o.next}while(o!==null);h===null?c=h=n:h=h.next=n}else c=h=n;o={baseState:s.baseState,firstBaseUpdate:c,lastBaseUpdate:h,shared:s.shared,effects:s.effects},e.updateQueue=o;return}e=o.lastBaseUpdate,e===null?o.firstBaseUpdate=n:e.next=n,o.lastBaseUpdate=n}function la(e,n,o,s){var c=e.updateQueue;Ln=!1;var h=c.firstBaseUpdate,v=c.lastBaseUpdate,S=c.shared.pending;if(S!==null){c.shared.pending=null;var k=S,j=k.next;k.next=null,v===null?h=j:v.next=j,v=k;var $=e.alternate;$!==null&&($=$.updateQueue,S=$.lastBaseUpdate,S!==v&&(S===null?$.firstBaseUpdate=j:S.next=j,$.lastBaseUpdate=k))}if(h!==null){var B=c.baseState;v=0,$=j=k=null,S=h;do{var N=S.lane,Y=S.eventTime;if((s&N)===N){$!==null&&($=$.next={eventTime:Y,lane:0,tag:S.tag,payload:S.payload,callback:S.callback,next:null});e:{var X=e,J=S;switch(N=n,Y=o,J.tag){case 1:if(X=J.payload,typeof X=="function"){B=X.call(Y,B,N);break e}B=X;break e;case 3:X.flags=X.flags&-65537|128;case 0:if(X=J.payload,N=typeof X=="function"?X.call(Y,B,N):X,N==null)break e;B=G({},B,N);break e;case 2:Ln=!0}}S.callback!==null&&S.lane!==0&&(e.flags|=64,N=c.effects,N===null?c.effects=[S]:N.push(S))}else Y={eventTime:Y,lane:N,tag:S.tag,payload:S.payload,callback:S.callback,next:null},$===null?(j=$=Y,k=B):$=$.next=Y,v|=N;if(S=S.next,S===null){if(S=c.shared.pending,S===null)break;N=S,S=N.next,N.next=null,c.lastBaseUpdate=N,c.shared.pending=null}}while(!0);if($===null&&(k=B),c.baseState=k,c.firstBaseUpdate=j,c.lastBaseUpdate=$,n=c.shared.interleaved,n!==null){c=n;do v|=c.lane,c=c.next;while(c!==n)}else h===null&&(c.shared.lanes=0);ir|=v,e.lanes=v,e.memoizedState=B}}function zc(e,n,o){if(e=n.effects,n.effects=null,e!==null)for(n=0;no?o:4,e(!0);var s=Tl.transition;Tl.transition={};try{e(!1),n()}finally{Ee=o,Tl.transition=s}}function uf(){return Tt().memoizedState}function H4(e,n,o){var s=Zn(e);if(o={lane:s,action:o,hasEagerState:!1,eagerState:null,next:null},cf(e))ff(n,o);else if(o=Nc(e,n,o,s),o!==null){var c=ct();Ot(o,e,s,c),df(o,n,s)}}function F4(e,n,o){var s=Zn(e),c={lane:s,action:o,hasEagerState:!1,eagerState:null,next:null};if(cf(e))ff(n,c);else{var h=e.alternate;if(e.lanes===0&&(h===null||h.lanes===0)&&(h=n.lastRenderedReducer,h!==null))try{var v=n.lastRenderedState,S=h(v,o);if(c.hasEagerState=!0,c.eagerState=S,Zt(S,v)){var k=n.interleaved;k===null?(c.next=c,Al(n)):(c.next=k.next,k.next=c),n.interleaved=c;return}}catch{}finally{}o=Nc(e,n,c,s),o!==null&&(c=ct(),Ot(o,e,s,c),df(o,n,s))}}function cf(e){var n=e.alternate;return e===He||n!==null&&n===He}function ff(e,n){$i=fa=!0;var o=e.pending;o===null?n.next=n:(n.next=o.next,o.next=n),e.pending=n}function df(e,n,o){if((o&4194240)!==0){var s=n.lanes;s&=e.pendingLanes,o|=s,n.lanes=o,bs(e,o)}}var pa={readContext:Vt,useCallback:rt,useContext:rt,useEffect:rt,useImperativeHandle:rt,useInsertionEffect:rt,useLayoutEffect:rt,useMemo:rt,useReducer:rt,useRef:rt,useState:rt,useDebugValue:rt,useDeferredValue:rt,useTransition:rt,useMutableSource:rt,useSyncExternalStore:rt,useId:rt,unstable_isNewReconciler:!1},D4={readContext:Vt,useCallback:function(e,n){return Qt().memoizedState=[e,n===void 0?null:n],e},useContext:Vt,useEffect:ef,useImperativeHandle:function(e,n,o){return o=o!=null?o.concat([e]):null,da(4194308,4,rf.bind(null,n,e),o)},useLayoutEffect:function(e,n){return da(4194308,4,e,n)},useInsertionEffect:function(e,n){return da(4,2,e,n)},useMemo:function(e,n){var o=Qt();return n=n===void 0?null:n,e=e(),o.memoizedState=[e,n],e},useReducer:function(e,n,o){var s=Qt();return n=o!==void 0?o(n):n,s.memoizedState=s.baseState=n,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:n},s.queue=e,e=e.dispatch=H4.bind(null,He,e),[s.memoizedState,e]},useRef:function(e){var n=Qt();return e={current:e},n.memoizedState=e},useState:qc,useDebugValue:Zl,useDeferredValue:function(e){return Qt().memoizedState=e},useTransition:function(){var e=qc(!1),n=e[0];return e=j4.bind(null,e[1]),Qt().memoizedState=e,[n,e]},useMutableSource:function(){},useSyncExternalStore:function(e,n,o){var s=He,c=Qt();if(Re){if(o===void 0)throw Error(i(407));o=o()}else{if(o=n(),Ye===null)throw Error(i(349));(rr&30)!==0||Kc(s,n,o)}c.memoizedState=o;var h={value:o,getSnapshot:n};return c.queue=h,ef(Yc.bind(null,s,h,e),[e]),s.flags|=2048,Ui(9,Gc.bind(null,s,h,o,n),void 0,null),o},useId:function(){var e=Qt(),n=Ye.identifierPrefix;if(Re){var o=sn,s=an;o=(s&~(1<<32-Dt(s)-1)).toString(32)+o,n=":"+n+"R"+o,o=zi++,0<\/script>",e=e.removeChild(e.firstChild)):typeof s.is=="string"?e=v.createElement(o,{is:s.is}):(e=v.createElement(o),o==="select"&&(v=e,s.multiple?v.multiple=!0:s.size&&(v.size=s.size))):e=v.createElementNS(e,o),e[Gt]=n,e[Zi]=s,jf(e,n,!1,!1),n.stateNode=e;e:{switch(v=Vs(o,s),o){case"dialog":Me("cancel",e),Me("close",e),c=s;break;case"iframe":case"object":case"embed":Me("load",e),c=s;break;case"video":case"audio":for(c=0;cUr&&(n.flags|=128,s=!0,Wi(h,!1),n.lanes=4194304)}else{if(!s)if(e=ua(v),e!==null){if(n.flags|=128,s=!0,o=e.updateQueue,o!==null&&(n.updateQueue=o,n.flags|=4),Wi(h,!0),h.tail===null&&h.tailMode==="hidden"&&!v.alternate&&!Re)return it(n),null}else 2*_e()-h.renderingStartTime>Ur&&o!==1073741824&&(n.flags|=128,s=!0,Wi(h,!1),n.lanes=4194304);h.isBackwards?(v.sibling=n.child,n.child=v):(o=h.last,o!==null?o.sibling=v:n.child=v,h.last=v)}return h.tail!==null?(n=h.tail,h.rendering=n,h.tail=n.sibling,h.renderingStartTime=_e(),n.sibling=null,o=je.current,Ce(je,s?o&1|2:o&1),n):(it(n),null);case 22:case 23:return a0(),s=n.memoizedState!==null,e!==null&&e.memoizedState!==null!==s&&(n.flags|=8192),s&&(n.mode&1)!==0?(wt&1073741824)!==0&&(it(n),n.subtreeFlags&6&&(n.flags|=8192)):it(n),null;case 24:return null;case 25:return null}throw Error(i(156,n.tag))}function z4(e,n){switch(ml(n),n.tag){case 1:return dt(n.type)&&qo(),e=n.flags,e&65536?(n.flags=e&-65537|128,n):null;case 3:return Or(),Pe(ft),Pe(nt),Vl(),e=n.flags,(e&65536)!==0&&(e&128)===0?(n.flags=e&-65537|128,n):null;case 5:return Ml(n),null;case 13:if(Pe(je),e=n.memoizedState,e!==null&&e.dehydrated!==null){if(n.alternate===null)throw Error(i(340));_r()}return e=n.flags,e&65536?(n.flags=e&-65537|128,n):null;case 19:return Pe(je),null;case 4:return Or(),null;case 10:return El(n.type._context),null;case 22:case 23:return a0(),null;case 24:return null;default:return null}}var va=!1,ot=!1,B4=typeof WeakSet=="function"?WeakSet:Set,Q=null;function zr(e,n){var o=e.ref;if(o!==null)if(typeof o=="function")try{o(null)}catch(s){De(e,n,s)}else o.current=null}function Gl(e,n,o){try{o()}catch(s){De(e,n,s)}}var Df=!1;function U4(e,n){if(al=bo,e=dc(),qs(e)){if("selectionStart"in e)var o={start:e.selectionStart,end:e.selectionEnd};else e:{o=(o=e.ownerDocument)&&o.defaultView||window;var s=o.getSelection&&o.getSelection();if(s&&s.rangeCount!==0){o=s.anchorNode;var c=s.anchorOffset,h=s.focusNode;s=s.focusOffset;try{o.nodeType,h.nodeType}catch{o=null;break e}var v=0,S=-1,k=-1,j=0,$=0,B=e,N=null;t:for(;;){for(var Y;B!==o||c!==0&&B.nodeType!==3||(S=v+c),B!==h||s!==0&&B.nodeType!==3||(k=v+s),B.nodeType===3&&(v+=B.nodeValue.length),(Y=B.firstChild)!==null;)N=B,B=Y;for(;;){if(B===e)break t;if(N===o&&++j===c&&(S=v),N===h&&++$===s&&(k=v),(Y=B.nextSibling)!==null)break;B=N,N=B.parentNode}B=Y}o=S===-1||k===-1?null:{start:S,end:k}}else o=null}o=o||{start:0,end:0}}else o=null;for(sl={focusedElem:e,selectionRange:o},bo=!1,Q=n;Q!==null;)if(n=Q,e=n.child,(n.subtreeFlags&1028)!==0&&e!==null)e.return=n,Q=e;else for(;Q!==null;){n=Q;try{var X=n.alternate;if((n.flags&1024)!==0)switch(n.tag){case 0:case 11:case 15:break;case 1:if(X!==null){var J=X.memoizedProps,be=X.memoizedState,R=n.stateNode,M=R.getSnapshotBeforeUpdate(n.elementType===n.type?J:bt(n.type,J),be);R.__reactInternalSnapshotBeforeUpdate=M}break;case 3:var L=n.stateNode.containerInfo;L.nodeType===1?L.textContent="":L.nodeType===9&&L.documentElement&&L.removeChild(L.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(i(163))}}catch(W){De(n,n.return,W)}if(e=n.sibling,e!==null){e.return=n.return,Q=e;break}Q=n.return}return X=Df,Df=!1,X}function Ki(e,n,o){var s=n.updateQueue;if(s=s!==null?s.lastEffect:null,s!==null){var c=s=s.next;do{if((c.tag&e)===e){var h=c.destroy;c.destroy=void 0,h!==void 0&&Gl(n,o,h)}c=c.next}while(c!==s)}}function xa(e,n){if(n=n.updateQueue,n=n!==null?n.lastEffect:null,n!==null){var o=n=n.next;do{if((o.tag&e)===e){var s=o.create;o.destroy=s()}o=o.next}while(o!==n)}}function Yl(e){var n=e.ref;if(n!==null){var o=e.stateNode;switch(e.tag){case 5:e=o;break;default:e=o}typeof n=="function"?n(e):n.current=e}}function Zf(e){var n=e.alternate;n!==null&&(e.alternate=null,Zf(n)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(n=e.stateNode,n!==null&&(delete n[Gt],delete n[Zi],delete n[fl],delete n[P4],delete n[V4])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function _f(e){return e.tag===5||e.tag===3||e.tag===4}function bf(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||_f(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Ql(e,n,o){var s=e.tag;if(s===5||s===6)e=e.stateNode,n?o.nodeType===8?o.parentNode.insertBefore(e,n):o.insertBefore(e,n):(o.nodeType===8?(n=o.parentNode,n.insertBefore(e,o)):(n=o,n.appendChild(e)),o=o._reactRootContainer,o!=null||n.onclick!==null||(n.onclick=Qo));else if(s!==4&&(e=e.child,e!==null))for(Ql(e,n,o),e=e.sibling;e!==null;)Ql(e,n,o),e=e.sibling}function Xl(e,n,o){var s=e.tag;if(s===5||s===6)e=e.stateNode,n?o.insertBefore(e,n):o.appendChild(e);else if(s!==4&&(e=e.child,e!==null))for(Xl(e,n,o),e=e.sibling;e!==null;)Xl(e,n,o),e=e.sibling}var qe=null,It=!1;function Hn(e,n,o){for(o=o.child;o!==null;)If(e,n,o),o=o.sibling}function If(e,n,o){if(Kt&&typeof Kt.onCommitFiberUnmount=="function")try{Kt.onCommitFiberUnmount(jo,o)}catch{}switch(o.tag){case 5:ot||zr(o,n);case 6:var s=qe,c=It;qe=null,Hn(e,n,o),qe=s,It=c,qe!==null&&(It?(e=qe,o=o.stateNode,e.nodeType===8?e.parentNode.removeChild(o):e.removeChild(o)):qe.removeChild(o.stateNode));break;case 18:qe!==null&&(It?(e=qe,o=o.stateNode,e.nodeType===8?cl(e.parentNode,o):e.nodeType===1&&cl(e,o),ki(e)):cl(qe,o.stateNode));break;case 4:s=qe,c=It,qe=o.stateNode.containerInfo,It=!0,Hn(e,n,o),qe=s,It=c;break;case 0:case 11:case 14:case 15:if(!ot&&(s=o.updateQueue,s!==null&&(s=s.lastEffect,s!==null))){c=s=s.next;do{var h=c,v=h.destroy;h=h.tag,v!==void 0&&((h&2)!==0||(h&4)!==0)&&Gl(o,n,v),c=c.next}while(c!==s)}Hn(e,n,o);break;case 1:if(!ot&&(zr(o,n),s=o.stateNode,typeof s.componentWillUnmount=="function"))try{s.props=o.memoizedProps,s.state=o.memoizedState,s.componentWillUnmount()}catch(S){De(o,n,S)}Hn(e,n,o);break;case 21:Hn(e,n,o);break;case 22:o.mode&1?(ot=(s=ot)||o.memoizedState!==null,Hn(e,n,o),ot=s):Hn(e,n,o);break;default:Hn(e,n,o)}}function Nf(e){var n=e.updateQueue;if(n!==null){e.updateQueue=null;var o=e.stateNode;o===null&&(o=e.stateNode=new B4),n.forEach(function(s){var c=e6.bind(null,e,s);o.has(s)||(o.add(s),s.then(c,c))})}}function Nt(e,n){var o=n.deletions;if(o!==null)for(var s=0;sc&&(c=v),s&=~h}if(s=c,s=_e()-s,s=(120>s?120:480>s?480:1080>s?1080:1920>s?1920:3e3>s?3e3:4320>s?4320:1960*K4(s/1960))-s,10e?16:e,Dn===null)var s=!1;else{if(e=Dn,Dn=null,Ca=0,(ge&6)!==0)throw Error(i(331));var c=ge;for(ge|=4,Q=e.current;Q!==null;){var h=Q,v=h.child;if((Q.flags&16)!==0){var S=h.deletions;if(S!==null){for(var k=0;k_e()-e0?ar(e,0):Jl|=o),mt(e,n)}function Jf(e,n){n===0&&((e.mode&1)===0?n=1:(n=Fo,Fo<<=1,(Fo&130023424)===0&&(Fo=4194304)));var o=ct();e=ln(e,n),e!==null&&(wi(e,n,o),mt(e,o))}function J4(e){var n=e.memoizedState,o=0;n!==null&&(o=n.retryLane),Jf(e,o)}function e6(e,n){var o=0;switch(e.tag){case 13:var s=e.stateNode,c=e.memoizedState;c!==null&&(o=c.retryLane);break;case 19:s=e.stateNode;break;default:throw Error(i(314))}s!==null&&s.delete(n),Jf(e,o)}var ed;ed=function(e,n,o){if(e!==null)if(e.memoizedProps!==n.pendingProps||ft.current)ht=!0;else{if((e.lanes&o)===0&&(n.flags&128)===0)return ht=!1,O4(e,n,o);ht=(e.flags&131072)!==0}else ht=!1,Re&&(n.flags&1048576)!==0&&jc(n,na,n.index);switch(n.lanes=0,n.tag){case 2:var s=n.type;ya(e,n),e=n.pendingProps;var c=Fr(n,nt.current);Nr(n,o),c=Ll(null,n,s,e,c,o);var h=jl();return n.flags|=1,typeof c=="object"&&c!==null&&typeof c.render=="function"&&c.$$typeof===void 0?(n.tag=1,n.memoizedState=null,n.updateQueue=null,dt(s)?(h=!0,Jo(n)):h=!1,n.memoizedState=c.state!==null&&c.state!==void 0?c.state:null,Cl(n),c.updater=ma,n.stateNode=c,c._reactInternals=n,bl(n,s,e,o),n=$l(null,n,s,!0,h,o)):(n.tag=0,Re&&h&&pl(n),ut(null,n,c,o),n=n.child),n;case 16:s=n.elementType;e:{switch(ya(e,n),e=n.pendingProps,c=s._init,s=c(s._payload),n.type=s,c=n.tag=n6(s),e=bt(s,e),c){case 0:n=Ol(null,n,s,e,o);break e;case 1:n=Mf(null,n,s,e,o);break e;case 11:n=Ef(null,n,s,e,o);break e;case 14:n=Sf(null,n,s,bt(s.type,e),o);break e}throw Error(i(306,s,""))}return n;case 0:return s=n.type,c=n.pendingProps,c=n.elementType===s?c:bt(s,c),Ol(e,n,s,c,o);case 1:return s=n.type,c=n.pendingProps,c=n.elementType===s?c:bt(s,c),Mf(e,n,s,c,o);case 3:e:{if(Pf(n),e===null)throw Error(i(387));s=n.pendingProps,h=n.memoizedState,c=h.element,Oc(e,n),la(n,s,null,o);var v=n.memoizedState;if(s=v.element,h.isDehydrated)if(h={element:s,isDehydrated:!1,cache:v.cache,pendingSuspenseBoundaries:v.pendingSuspenseBoundaries,transitions:v.transitions},n.updateQueue.baseState=h,n.memoizedState=h,n.flags&256){c=$r(Error(i(423)),n),n=Vf(e,n,s,o,c);break e}else if(s!==c){c=$r(Error(i(424)),n),n=Vf(e,n,s,o,c);break e}else for(xt=Pn(n.stateNode.containerInfo.firstChild),vt=n,Re=!0,_t=null,o=Ic(n,null,s,o),n.child=o;o;)o.flags=o.flags&-3|4096,o=o.sibling;else{if(_r(),s===c){n=cn(e,n,o);break e}ut(e,n,s,o)}n=n.child}return n;case 5:return Bc(n),e===null&&yl(n),s=n.type,c=n.pendingProps,h=e!==null?e.memoizedProps:null,v=c.children,ll(s,c)?v=null:h!==null&&ll(s,h)&&(n.flags|=32),kf(e,n),ut(e,n,v,o),n.child;case 6:return e===null&&yl(n),null;case 13:return Tf(e,n,o);case 4:return kl(n,n.stateNode.containerInfo),s=n.pendingProps,e===null?n.child=br(n,null,s,o):ut(e,n,s,o),n.child;case 11:return s=n.type,c=n.pendingProps,c=n.elementType===s?c:bt(s,c),Ef(e,n,s,c,o);case 7:return ut(e,n,n.pendingProps,o),n.child;case 8:return ut(e,n,n.pendingProps.children,o),n.child;case 12:return ut(e,n,n.pendingProps.children,o),n.child;case 10:e:{if(s=n.type._context,c=n.pendingProps,h=n.memoizedProps,v=c.value,Ce(oa,s._currentValue),s._currentValue=v,h!==null)if(Zt(h.value,v)){if(h.children===c.children&&!ft.current){n=cn(e,n,o);break e}}else for(h=n.child,h!==null&&(h.return=n);h!==null;){var S=h.dependencies;if(S!==null){v=h.child;for(var k=S.firstContext;k!==null;){if(k.context===s){if(h.tag===1){k=un(-1,o&-o),k.tag=2;var j=h.updateQueue;if(j!==null){j=j.shared;var $=j.pending;$===null?k.next=k:(k.next=$.next,$.next=k),j.pending=k}}h.lanes|=o,k=h.alternate,k!==null&&(k.lanes|=o),Sl(h.return,o,n),S.lanes|=o;break}k=k.next}}else if(h.tag===10)v=h.type===n.type?null:h.child;else if(h.tag===18){if(v=h.return,v===null)throw Error(i(341));v.lanes|=o,S=v.alternate,S!==null&&(S.lanes|=o),Sl(v,o,n),v=h.sibling}else v=h.child;if(v!==null)v.return=h;else for(v=h;v!==null;){if(v===n){v=null;break}if(h=v.sibling,h!==null){h.return=v.return,v=h;break}v=v.return}h=v}ut(e,n,c.children,o),n=n.child}return n;case 9:return c=n.type,s=n.pendingProps.children,Nr(n,o),c=Vt(c),s=s(c),n.flags|=1,ut(e,n,s,o),n.child;case 14:return s=n.type,c=bt(s,n.pendingProps),c=bt(s.type,c),Sf(e,n,s,c,o);case 15:return Af(e,n,n.type,n.pendingProps,o);case 17:return s=n.type,c=n.pendingProps,c=n.elementType===s?c:bt(s,c),ya(e,n),n.tag=1,dt(s)?(e=!0,Jo(n)):e=!1,Nr(n,o),pf(n,s,c),bl(n,s,c,o),$l(null,n,s,!0,e,o);case 19:return Lf(e,n,o);case 22:return Cf(e,n,o)}throw Error(i(156,n.tag))};function td(e,n){return H1(e,n)}function t6(e,n,o,s){this.tag=e,this.key=o,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=n,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=s,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Lt(e,n,o,s){return new t6(e,n,o,s)}function l0(e){return e=e.prototype,!(!e||!e.isReactComponent)}function n6(e){if(typeof e=="function")return l0(e)?1:0;if(e!=null){if(e=e.$$typeof,e===ke)return 11;if(e===tt)return 14}return 2}function bn(e,n){var o=e.alternate;return o===null?(o=Lt(e.tag,n,e.key,e.mode),o.elementType=e.elementType,o.type=e.type,o.stateNode=e.stateNode,o.alternate=e,e.alternate=o):(o.pendingProps=n,o.type=e.type,o.flags=0,o.subtreeFlags=0,o.deletions=null),o.flags=e.flags&14680064,o.childLanes=e.childLanes,o.lanes=e.lanes,o.child=e.child,o.memoizedProps=e.memoizedProps,o.memoizedState=e.memoizedState,o.updateQueue=e.updateQueue,n=e.dependencies,o.dependencies=n===null?null:{lanes:n.lanes,firstContext:n.firstContext},o.sibling=e.sibling,o.index=e.index,o.ref=e.ref,o}function Va(e,n,o,s,c,h){var v=2;if(s=e,typeof e=="function")l0(e)&&(v=1);else if(typeof e=="string")v=5;else e:switch(e){case O:return lr(o.children,c,h,n);case I:v=8,c|=8;break;case re:return e=Lt(12,o,n,c|2),e.elementType=re,e.lanes=h,e;case Se:return e=Lt(13,o,n,c),e.elementType=Se,e.lanes=h,e;case $e:return e=Lt(19,o,n,c),e.elementType=$e,e.lanes=h,e;case le:return Ta(o,c,h,n);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case se:v=10;break e;case ae:v=9;break e;case ke:v=11;break e;case tt:v=14;break e;case Xe:v=16,s=null;break e}throw Error(i(130,e==null?e:typeof e,""))}return n=Lt(v,o,n,c),n.elementType=e,n.type=s,n.lanes=h,n}function lr(e,n,o,s){return e=Lt(7,e,s,n),e.lanes=o,e}function Ta(e,n,o,s){return e=Lt(22,e,s,n),e.elementType=le,e.lanes=o,e.stateNode={isHidden:!1},e}function u0(e,n,o){return e=Lt(6,e,null,n),e.lanes=o,e}function c0(e,n,o){return n=Lt(4,e.children!==null?e.children:[],e.key,n),n.lanes=o,n.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},n}function r6(e,n,o,s,c){this.tag=n,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=_s(0),this.expirationTimes=_s(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=_s(0),this.identifierPrefix=s,this.onRecoverableError=c,this.mutableSourceEagerHydrationData=null}function f0(e,n,o,s,c,h,v,S,k){return e=new r6(e,n,o,S,k),n===1?(n=1,h===!0&&(n|=8)):n=0,h=Lt(3,null,null,n),e.current=h,h.stateNode=e,h.memoizedState={element:s,isDehydrated:o,cache:null,transitions:null,pendingSuspenseBoundaries:null},Cl(h),e}function i6(e,n,o){var s=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(r){console.error(r)}}return t(),y0.exports=m6(),y0.exports}var md;function y6(){if(md)return Za;md=1;var t=g6();return Za.createRoot=t.createRoot,Za.hydrateRoot=t.hydrateRoot,Za}var v6=y6();const x6=mh(v6),w6="modulepreload",E6=function(t){return"/"+t},gd={},gh=function(r,i,a){let l=Promise.resolve();if(i&&i.length>0){let d=function(g){return Promise.all(g.map(y=>Promise.resolve(y).then(x=>({status:"fulfilled",value:x}),x=>({status:"rejected",reason:x}))))};document.getElementsByTagName("link");const p=document.querySelector("meta[property=csp-nonce]"),m=(p==null?void 0:p.nonce)||(p==null?void 0:p.getAttribute("nonce"));l=d(i.map(g=>{if(g=E6(g),g in gd)return;gd[g]=!0;const y=g.endsWith(".css"),x=y?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${g}"]${x}`))return;const E=document.createElement("link");if(E.rel=y?"stylesheet":w6,y||(E.as="script"),E.crossOrigin="",E.href=g,m&&E.setAttribute("nonce",m),document.head.appendChild(E),y)return new Promise((C,V)=>{E.addEventListener("load",C),E.addEventListener("error",()=>V(new Error(`Unable to preload CSS for ${g}`)))})}))}function f(d){const p=new Event("vite:preloadError",{cancelable:!0});if(p.payload=d,window.dispatchEvent(p),!p.defaultPrevented)throw d}return l.then(d=>{for(const p of d||[])p.status==="rejected"&&f(p.reason);return r().catch(f)})};/** + * react-router v7.13.0 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */var yd="popstate";function S6(t={}){function r(a,l){let{pathname:f,search:d,hash:p}=a.location;return z0("",{pathname:f,search:d,hash:p},l.state&&l.state.usr||null,l.state&&l.state.key||"default")}function i(a,l){return typeof l=="string"?l:fo(l)}return C6(r,i,null,t)}function Le(t,r){if(t===!1||t===null||typeof t>"u")throw new Error(r)}function Ut(t,r){if(!t){typeof console<"u"&&console.warn(r);try{throw new Error(r)}catch{}}}function A6(){return Math.random().toString(36).substring(2,10)}function vd(t,r){return{usr:t.state,key:t.key,idx:r}}function z0(t,r,i=null,a){return{pathname:typeof t=="string"?t:t.pathname,search:"",hash:"",...typeof r=="string"?si(r):r,state:i,key:r&&r.key||a||A6()}}function fo({pathname:t="/",search:r="",hash:i=""}){return r&&r!=="?"&&(t+=r.charAt(0)==="?"?r:"?"+r),i&&i!=="#"&&(t+=i.charAt(0)==="#"?i:"#"+i),t}function si(t){let r={};if(t){let i=t.indexOf("#");i>=0&&(r.hash=t.substring(i),t=t.substring(0,i));let a=t.indexOf("?");a>=0&&(r.search=t.substring(a),t=t.substring(0,a)),t&&(r.pathname=t)}return r}function C6(t,r,i,a={}){let{window:l=document.defaultView,v5Compat:f=!1}=a,d=l.history,p="POP",m=null,g=y();g==null&&(g=0,d.replaceState({...d.state,idx:g},""));function y(){return(d.state||{idx:null}).idx}function x(){p="POP";let T=y(),H=T==null?null:T-g;g=T,m&&m({action:p,location:A.location,delta:H})}function E(T,H){p="PUSH";let Z=z0(A.location,T,H);g=y()+1;let F=vd(Z,g),D=A.createHref(Z);try{d.pushState(F,"",D)}catch(_){if(_ instanceof DOMException&&_.name==="DataCloneError")throw _;l.location.assign(D)}f&&m&&m({action:p,location:A.location,delta:1})}function C(T,H){p="REPLACE";let Z=z0(A.location,T,H);g=y();let F=vd(Z,g),D=A.createHref(Z);d.replaceState(F,"",D),f&&m&&m({action:p,location:A.location,delta:0})}function V(T){return k6(T)}let A={get action(){return p},get location(){return t(l,d)},listen(T){if(m)throw new Error("A history only accepts one active listener");return l.addEventListener(yd,x),m=T,()=>{l.removeEventListener(yd,x),m=null}},createHref(T){return r(l,T)},createURL:V,encodeLocation(T){let H=V(T);return{pathname:H.pathname,search:H.search,hash:H.hash}},push:E,replace:C,go(T){return d.go(T)}};return A}function k6(t,r=!1){let i="http://localhost";typeof window<"u"&&(i=window.location.origin!=="null"?window.location.origin:window.location.href),Le(i,"No window.location.(origin|href) available to create URL");let a=typeof t=="string"?t:fo(t);return a=a.replace(/ $/,"%20"),!r&&a.startsWith("//")&&(a=i+a),new URL(a,i)}function yh(t,r,i="/"){return M6(t,r,i,!1)}function M6(t,r,i,a){let l=typeof r=="string"?si(r):r,f=yn(l.pathname||"/",i);if(f==null)return null;let d=vh(t);P6(d);let p=null;for(let m=0;p==null&&m{let y={relativePath:g===void 0?d.path||"":g,caseSensitive:d.caseSensitive===!0,childrenIndex:p,route:d};if(y.relativePath.startsWith("/")){if(!y.relativePath.startsWith(a)&&m)return;Le(y.relativePath.startsWith(a),`Absolute route path "${y.relativePath}" nested under path "${a}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),y.relativePath=y.relativePath.slice(a.length)}let x=pn([a,y.relativePath]),E=i.concat(y);d.children&&d.children.length>0&&(Le(d.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${x}".`),vh(d.children,r,E,x,m)),!(d.path==null&&!d.index)&&r.push({path:x,score:F6(x,d.index),routesMeta:E})};return t.forEach((d,p)=>{var m;if(d.path===""||!((m=d.path)!=null&&m.includes("?")))f(d,p);else for(let g of xh(d.path))f(d,p,!0,g)}),r}function xh(t){let r=t.split("/");if(r.length===0)return[];let[i,...a]=r,l=i.endsWith("?"),f=i.replace(/\?$/,"");if(a.length===0)return l?[f,""]:[f];let d=xh(a.join("/")),p=[];return p.push(...d.map(m=>m===""?f:[f,m].join("/"))),l&&p.push(...d),p.map(m=>t.startsWith("/")&&m===""?"/":m)}function P6(t){t.sort((r,i)=>r.score!==i.score?i.score-r.score:D6(r.routesMeta.map(a=>a.childrenIndex),i.routesMeta.map(a=>a.childrenIndex)))}var V6=/^:[\w-]+$/,T6=3,R6=2,L6=1,j6=10,H6=-2,xd=t=>t==="*";function F6(t,r){let i=t.split("/"),a=i.length;return i.some(xd)&&(a+=H6),r&&(a+=R6),i.filter(l=>!xd(l)).reduce((l,f)=>l+(V6.test(f)?T6:f===""?L6:j6),a)}function D6(t,r){return t.length===r.length&&t.slice(0,-1).every((a,l)=>a===r[l])?t[t.length-1]-r[r.length-1]:0}function Z6(t,r,i=!1){let{routesMeta:a}=t,l={},f="/",d=[];for(let p=0;p{if(y==="*"){let V=p[E]||"";d=f.slice(0,f.length-V.length).replace(/(.)\/+$/,"$1")}const C=p[E];return x&&!C?g[y]=void 0:g[y]=(C||"").replace(/%2F/g,"/"),g},{}),pathname:f,pathnameBase:d,pattern:t}}function _6(t,r=!1,i=!0){Ut(t==="*"||!t.endsWith("*")||t.endsWith("/*"),`Route path "${t}" will be treated as if it were "${t.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${t.replace(/\*$/,"/*")}".`);let a=[],l="^"+t.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(d,p,m)=>(a.push({paramName:p,isOptional:m!=null}),m?"/?([^\\/]+)?":"/([^\\/]+)")).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return t.endsWith("*")?(a.push({paramName:"*"}),l+=t==="*"||t==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):i?l+="\\/*$":t!==""&&t!=="/"&&(l+="(?:(?=\\/|$))"),[new RegExp(l,r?void 0:"i"),a]}function b6(t){try{return t.split("/").map(r=>decodeURIComponent(r).replace(/\//g,"%2F")).join("/")}catch(r){return Ut(!1,`The URL path "${t}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${r}).`),t}}function yn(t,r){if(r==="/")return t;if(!t.toLowerCase().startsWith(r.toLowerCase()))return null;let i=r.endsWith("/")?r.length-1:r.length,a=t.charAt(i);return a&&a!=="/"?null:t.slice(i)||"/"}var I6=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function N6(t,r="/"){let{pathname:i,search:a="",hash:l=""}=typeof t=="string"?si(t):t,f;return i?(i=i.replace(/\/\/+/g,"/"),i.startsWith("/")?f=wd(i.substring(1),"/"):f=wd(i,r)):f=r,{pathname:f,search:z6(a),hash:B6(l)}}function wd(t,r){let i=r.replace(/\/+$/,"").split("/");return t.split("/").forEach(l=>{l===".."?i.length>1&&i.pop():l!=="."&&i.push(l)}),i.length>1?i.join("/"):"/"}function w0(t,r,i,a){return`Cannot include a '${t}' character in a manually specified \`to.${r}\` field [${JSON.stringify(a)}]. Please separate it out to the \`to.${i}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function O6(t){return t.filter((r,i)=>i===0||r.route.path&&r.route.path.length>0)}function xu(t){let r=O6(t);return r.map((i,a)=>a===r.length-1?i.pathname:i.pathnameBase)}function wu(t,r,i,a=!1){let l;typeof t=="string"?l=si(t):(l={...t},Le(!l.pathname||!l.pathname.includes("?"),w0("?","pathname","search",l)),Le(!l.pathname||!l.pathname.includes("#"),w0("#","pathname","hash",l)),Le(!l.search||!l.search.includes("#"),w0("#","search","hash",l)));let f=t===""||l.pathname==="",d=f?"/":l.pathname,p;if(d==null)p=i;else{let x=r.length-1;if(!a&&d.startsWith("..")){let E=d.split("/");for(;E[0]==="..";)E.shift(),x-=1;l.pathname=E.join("/")}p=x>=0?r[x]:"/"}let m=N6(l,p),g=d&&d!=="/"&&d.endsWith("/"),y=(f||d===".")&&i.endsWith("/");return!m.pathname.endsWith("/")&&(g||y)&&(m.pathname+="/"),m}var pn=t=>t.join("/").replace(/\/\/+/g,"/"),$6=t=>t.replace(/\/+$/,"").replace(/^\/*/,"/"),z6=t=>!t||t==="?"?"":t.startsWith("?")?t:"?"+t,B6=t=>!t||t==="#"?"":t.startsWith("#")?t:"#"+t,U6=class{constructor(t,r,i,a=!1){this.status=t,this.statusText=r||"",this.internal=a,i instanceof Error?(this.data=i.toString(),this.error=i):this.data=i}};function W6(t){return t!=null&&typeof t.status=="number"&&typeof t.statusText=="string"&&typeof t.internal=="boolean"&&"data"in t}function K6(t){return t.map(r=>r.route.path).filter(Boolean).join("/").replace(/\/\/*/g,"/")||"/"}var wh=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Eh(t,r){let i=t;if(typeof i!="string"||!I6.test(i))return{absoluteURL:void 0,isExternal:!1,to:i};let a=i,l=!1;if(wh)try{let f=new URL(window.location.href),d=i.startsWith("//")?new URL(f.protocol+i):new URL(i),p=yn(d.pathname,r);d.origin===f.origin&&p!=null?i=p+d.search+d.hash:l=!0}catch{Ut(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:a,isExternal:l,to:i}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var Sh=["POST","PUT","PATCH","DELETE"];new Set(Sh);var G6=["GET",...Sh];new Set(G6);var li=u.createContext(null);li.displayName="DataRouter";var fs=u.createContext(null);fs.displayName="DataRouterState";var Y6=u.createContext(!1),Ah=u.createContext({isTransitioning:!1});Ah.displayName="ViewTransition";var Q6=u.createContext(new Map);Q6.displayName="Fetchers";var X6=u.createContext(null);X6.displayName="Await";var Ct=u.createContext(null);Ct.displayName="Navigation";var Eo=u.createContext(null);Eo.displayName="Location";var tn=u.createContext({outlet:null,matches:[],isDataRoute:!1});tn.displayName="Route";var Eu=u.createContext(null);Eu.displayName="RouteError";var Ch="REACT_ROUTER_ERROR",q6="REDIRECT",J6="ROUTE_ERROR_RESPONSE";function eg(t){if(t.startsWith(`${Ch}:${q6}:{`))try{let r=JSON.parse(t.slice(28));if(typeof r=="object"&&r&&typeof r.status=="number"&&typeof r.statusText=="string"&&typeof r.location=="string"&&typeof r.reloadDocument=="boolean"&&typeof r.replace=="boolean")return r}catch{}}function tg(t){if(t.startsWith(`${Ch}:${J6}:{`))try{let r=JSON.parse(t.slice(40));if(typeof r=="object"&&r&&typeof r.status=="number"&&typeof r.statusText=="string")return new U6(r.status,r.statusText,r.data)}catch{}}function ng(t,{relative:r}={}){Le(ui(),"useHref() may be used only in the context of a component.");let{basename:i,navigator:a}=u.useContext(Ct),{hash:l,pathname:f,search:d}=So(t,{relative:r}),p=f;return i!=="/"&&(p=f==="/"?i:pn([i,f])),a.createHref({pathname:p,search:d,hash:l})}function ui(){return u.useContext(Eo)!=null}function Gn(){return Le(ui(),"useLocation() may be used only in the context of a component."),u.useContext(Eo).location}var kh="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function Mh(t){u.useContext(Ct).static||u.useLayoutEffect(t)}function xr(){let{isDataRoute:t}=u.useContext(tn);return t?mg():rg()}function rg(){Le(ui(),"useNavigate() may be used only in the context of a component.");let t=u.useContext(li),{basename:r,navigator:i}=u.useContext(Ct),{matches:a}=u.useContext(tn),{pathname:l}=Gn(),f=JSON.stringify(xu(a)),d=u.useRef(!1);return Mh(()=>{d.current=!0}),u.useCallback((m,g={})=>{if(Ut(d.current,kh),!d.current)return;if(typeof m=="number"){i.go(m);return}let y=wu(m,JSON.parse(f),l,g.relative==="path");t==null&&r!=="/"&&(y.pathname=y.pathname==="/"?r:pn([r,y.pathname])),(g.replace?i.replace:i.push)(y,g.state,g)},[r,i,f,l,t])}u.createContext(null);function So(t,{relative:r}={}){let{matches:i}=u.useContext(tn),{pathname:a}=Gn(),l=JSON.stringify(xu(i));return u.useMemo(()=>wu(t,JSON.parse(l),a,r==="path"),[t,l,a,r])}function ig(t,r){return Ph(t,r)}function Ph(t,r,i,a,l){var Z;Le(ui(),"useRoutes() may be used only in the context of a component.");let{navigator:f}=u.useContext(Ct),{matches:d}=u.useContext(tn),p=d[d.length-1],m=p?p.params:{},g=p?p.pathname:"/",y=p?p.pathnameBase:"/",x=p&&p.route;{let F=x&&x.path||"";Th(g,!x||F.endsWith("*")||F.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${g}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let E=Gn(),C;if(r){let F=typeof r=="string"?si(r):r;Le(y==="/"||((Z=F.pathname)==null?void 0:Z.startsWith(y)),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${y}" but pathname "${F.pathname}" was given in the \`location\` prop.`),C=F}else C=E;let V=C.pathname||"/",A=V;if(y!=="/"){let F=y.replace(/^\//,"").split("/");A="/"+V.replace(/^\//,"").split("/").slice(F.length).join("/")}let T=yh(t,{pathname:A});Ut(x||T!=null,`No routes matched location "${C.pathname}${C.search}${C.hash}" `),Ut(T==null||T[T.length-1].route.element!==void 0||T[T.length-1].route.Component!==void 0||T[T.length-1].route.lazy!==void 0,`Matched leaf route at location "${C.pathname}${C.search}${C.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let H=ug(T&&T.map(F=>Object.assign({},F,{params:Object.assign({},m,F.params),pathname:pn([y,f.encodeLocation?f.encodeLocation(F.pathname.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:F.pathname]),pathnameBase:F.pathnameBase==="/"?y:pn([y,f.encodeLocation?f.encodeLocation(F.pathnameBase.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:F.pathnameBase])})),d,i,a,l);return r&&H?u.createElement(Eo.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",...C},navigationType:"POP"}},H):H}function og(){let t=pg(),r=W6(t)?`${t.status} ${t.statusText}`:t instanceof Error?t.message:JSON.stringify(t),i=t instanceof Error?t.stack:null,a="rgba(200,200,200, 0.5)",l={padding:"0.5rem",backgroundColor:a},f={padding:"2px 4px",backgroundColor:a},d=null;return console.error("Error handled by React Router default ErrorBoundary:",t),d=u.createElement(u.Fragment,null,u.createElement("p",null,"💿 Hey developer 👋"),u.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",u.createElement("code",{style:f},"ErrorBoundary")," or"," ",u.createElement("code",{style:f},"errorElement")," prop on your route.")),u.createElement(u.Fragment,null,u.createElement("h2",null,"Unexpected Application Error!"),u.createElement("h3",{style:{fontStyle:"italic"}},r),i?u.createElement("pre",{style:l},i):null,d)}var ag=u.createElement(og,null),Vh=class extends u.Component{constructor(t){super(t),this.state={location:t.location,revalidation:t.revalidation,error:t.error}}static getDerivedStateFromError(t){return{error:t}}static getDerivedStateFromProps(t,r){return r.location!==t.location||r.revalidation!=="idle"&&t.revalidation==="idle"?{error:t.error,location:t.location,revalidation:t.revalidation}:{error:t.error!==void 0?t.error:r.error,location:r.location,revalidation:t.revalidation||r.revalidation}}componentDidCatch(t,r){this.props.onError?this.props.onError(t,r):console.error("React Router caught the following error during render",t)}render(){let t=this.state.error;if(this.context&&typeof t=="object"&&t&&"digest"in t&&typeof t.digest=="string"){const i=tg(t.digest);i&&(t=i)}let r=t!==void 0?u.createElement(tn.Provider,{value:this.props.routeContext},u.createElement(Eu.Provider,{value:t,children:this.props.component})):this.props.children;return this.context?u.createElement(sg,{error:t},r):r}};Vh.contextType=Y6;var E0=new WeakMap;function sg({children:t,error:r}){let{basename:i}=u.useContext(Ct);if(typeof r=="object"&&r&&"digest"in r&&typeof r.digest=="string"){let a=eg(r.digest);if(a){let l=E0.get(r);if(l)throw l;let f=Eh(a.location,i);if(wh&&!E0.get(r))if(f.isExternal||a.reloadDocument)window.location.href=f.absoluteURL||f.to;else{const d=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(f.to,{replace:a.replace}));throw E0.set(r,d),d}return u.createElement("meta",{httpEquiv:"refresh",content:`0;url=${f.absoluteURL||f.to}`})}}return t}function lg({routeContext:t,match:r,children:i}){let a=u.useContext(li);return a&&a.static&&a.staticContext&&(r.route.errorElement||r.route.ErrorBoundary)&&(a.staticContext._deepestRenderedBoundaryId=r.route.id),u.createElement(tn.Provider,{value:t},i)}function ug(t,r=[],i=null,a=null,l=null){if(t==null){if(!i)return null;if(i.errors)t=i.matches;else if(r.length===0&&!i.initialized&&i.matches.length>0)t=i.matches;else return null}let f=t,d=i==null?void 0:i.errors;if(d!=null){let y=f.findIndex(x=>x.route.id&&(d==null?void 0:d[x.route.id])!==void 0);Le(y>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(d).join(",")}`),f=f.slice(0,Math.min(f.length,y+1))}let p=!1,m=-1;if(i)for(let y=0;y=0?f=f.slice(0,m+1):f=[f[0]];break}}}let g=i&&a?(y,x)=>{var E,C;a(y,{location:i.location,params:((C=(E=i.matches)==null?void 0:E[0])==null?void 0:C.params)??{},unstable_pattern:K6(i.matches),errorInfo:x})}:void 0;return f.reduceRight((y,x,E)=>{let C,V=!1,A=null,T=null;i&&(C=d&&x.route.id?d[x.route.id]:void 0,A=x.route.errorElement||ag,p&&(m<0&&E===0?(Th("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),V=!0,T=null):m===E&&(V=!0,T=x.route.hydrateFallbackElement||null)));let H=r.concat(f.slice(0,E+1)),Z=()=>{let F;return C?F=A:V?F=T:x.route.Component?F=u.createElement(x.route.Component,null):x.route.element?F=x.route.element:F=y,u.createElement(lg,{match:x,routeContext:{outlet:y,matches:H,isDataRoute:i!=null},children:F})};return i&&(x.route.ErrorBoundary||x.route.errorElement||E===0)?u.createElement(Vh,{location:i.location,revalidation:i.revalidation,component:A,error:C,children:Z(),routeContext:{outlet:null,matches:H,isDataRoute:!0},onError:g}):Z()},null)}function Su(t){return`${t} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function cg(t){let r=u.useContext(li);return Le(r,Su(t)),r}function fg(t){let r=u.useContext(fs);return Le(r,Su(t)),r}function dg(t){let r=u.useContext(tn);return Le(r,Su(t)),r}function Au(t){let r=dg(t),i=r.matches[r.matches.length-1];return Le(i.route.id,`${t} can only be used on routes that contain a unique "id"`),i.route.id}function hg(){return Au("useRouteId")}function pg(){var a;let t=u.useContext(Eu),r=fg("useRouteError"),i=Au("useRouteError");return t!==void 0?t:(a=r.errors)==null?void 0:a[i]}function mg(){let{router:t}=cg("useNavigate"),r=Au("useNavigate"),i=u.useRef(!1);return Mh(()=>{i.current=!0}),u.useCallback(async(l,f={})=>{Ut(i.current,kh),i.current&&(typeof l=="number"?await t.navigate(l):await t.navigate(l,{fromRouteId:r,...f}))},[t,r])}var Ed={};function Th(t,r,i){!r&&!Ed[t]&&(Ed[t]=!0,Ut(!1,i))}u.memo(gg);function gg({routes:t,future:r,state:i,onError:a}){return Ph(t,void 0,i,a,r)}function yg({to:t,replace:r,state:i,relative:a}){Le(ui()," may be used only in the context of a component.");let{static:l}=u.useContext(Ct);Ut(!l," must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.");let{matches:f}=u.useContext(tn),{pathname:d}=Gn(),p=xr(),m=wu(t,xu(f),d,a==="path"),g=JSON.stringify(m);return u.useEffect(()=>{p(JSON.parse(g),{replace:r,state:i,relative:a})},[p,g,a,r,i]),null}function cr(t){Le(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function vg({basename:t="/",children:r=null,location:i,navigationType:a="POP",navigator:l,static:f=!1,unstable_useTransitions:d}){Le(!ui(),"You cannot render a inside another . You should never have more than one in your app.");let p=t.replace(/^\/*/,"/"),m=u.useMemo(()=>({basename:p,navigator:l,static:f,unstable_useTransitions:d,future:{}}),[p,l,f,d]);typeof i=="string"&&(i=si(i));let{pathname:g="/",search:y="",hash:x="",state:E=null,key:C="default"}=i,V=u.useMemo(()=>{let A=yn(g,p);return A==null?null:{location:{pathname:A,search:y,hash:x,state:E,key:C},navigationType:a}},[p,g,y,x,E,C,a]);return Ut(V!=null,` is not able to match the URL "${g}${y}${x}" because it does not start with the basename, so the won't render anything.`),V==null?null:u.createElement(Ct.Provider,{value:m},u.createElement(Eo.Provider,{children:r,value:V}))}function xg({children:t,location:r}){return ig(B0(t),r)}function B0(t,r=[]){let i=[];return u.Children.forEach(t,(a,l)=>{if(!u.isValidElement(a))return;let f=[...r,l];if(a.type===u.Fragment){i.push.apply(i,B0(a.props.children,f));return}Le(a.type===cr,`[${typeof a.type=="string"?a.type:a.type.name}] is not a component. All component children of must be a or `),Le(!a.props.index||!a.props.children,"An index route cannot have child routes.");let d={id:a.props.id||f.join("-"),caseSensitive:a.props.caseSensitive,element:a.props.element,Component:a.props.Component,index:a.props.index,path:a.props.path,middleware:a.props.middleware,loader:a.props.loader,action:a.props.action,hydrateFallbackElement:a.props.hydrateFallbackElement,HydrateFallback:a.props.HydrateFallback,errorElement:a.props.errorElement,ErrorBoundary:a.props.ErrorBoundary,hasErrorBoundary:a.props.hasErrorBoundary===!0||a.props.ErrorBoundary!=null||a.props.errorElement!=null,shouldRevalidate:a.props.shouldRevalidate,handle:a.props.handle,lazy:a.props.lazy};a.props.children&&(d.children=B0(a.props.children,f)),i.push(d)}),i}var za="get",Ba="application/x-www-form-urlencoded";function ds(t){return typeof HTMLElement<"u"&&t instanceof HTMLElement}function wg(t){return ds(t)&&t.tagName.toLowerCase()==="button"}function Eg(t){return ds(t)&&t.tagName.toLowerCase()==="form"}function Sg(t){return ds(t)&&t.tagName.toLowerCase()==="input"}function Ag(t){return!!(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey)}function Cg(t,r){return t.button===0&&(!r||r==="_self")&&!Ag(t)}var _a=null;function kg(){if(_a===null)try{new FormData(document.createElement("form"),0),_a=!1}catch{_a=!0}return _a}var Mg=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function S0(t){return t!=null&&!Mg.has(t)?(Ut(!1,`"${t}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Ba}"`),null):t}function Pg(t,r){let i,a,l,f,d;if(Eg(t)){let p=t.getAttribute("action");a=p?yn(p,r):null,i=t.getAttribute("method")||za,l=S0(t.getAttribute("enctype"))||Ba,f=new FormData(t)}else if(wg(t)||Sg(t)&&(t.type==="submit"||t.type==="image")){let p=t.form;if(p==null)throw new Error('Cannot submit a + + ); +}; + +// ======= STYLED COMPONENTS ======= // + +const Form = styled.form` + display: flex; + flex-direction: column; + gap: 12px; + padding: 20px; + width: 100%; + background: var(--color-glass); + border-radius: 12px; +`; + +const Title = styled.h1` + text-align: center; + color: white; + margin: 0; + font-weight: 400; +`; + +const InputWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +const Label = styled.label` + display: flex; + flex-direction: column; + gap: 4px; + font-size: 13px; + color: white; +`; + +const Input = styled.input` + padding: 8px 10px; + font-size: 16px; + border: 1px solid var(--color-border-light); + border-radius: 8px; + background: var(--color-input-bg); +`; + +const Button = styled.button` + padding: 10px; + font-size: 16px; + cursor: pointer; + border: none; + border-radius: 20px; + background-color: var(--color-primary); + color: white; + margin-top: 10px; + width: 100%; + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +`; + +const ErrorText = styled.p` + color: var(--color-error); + font-size: 13px; +`; \ No newline at end of file diff --git a/frontend/src/components/Mascot.jsx b/frontend/src/components/Mascot.jsx new file mode 100644 index 000000000..f27d4e951 --- /dev/null +++ b/frontend/src/components/Mascot.jsx @@ -0,0 +1,150 @@ +import { motion } from "framer-motion"; + +// This is the home made mascot :) Each condition has 3 shades of its color. +export const EnergyBlob = ({ energy = 7 }) => { + const isHigh = energy >= 7; + const isLow = energy < 3; + const isOkay = !isHigh && !isLow; + + // Three color sets — light (highlight), mid (body), dark (feet + shadows) + const colors = isHigh + ? { light: "#e0f5eb", mid: "#a8d5ba", dark: "#6aaf8c" } // green + : isLow + ? { light: "#f5d0d0", mid: "#c47a7a", dark: "#a05050" } // red + : { light: "#fef0c0", mid: "#f0c060", dark: "#c89020" }; // yellow + + const { light, mid, dark } = colors; + + return ( + // motion.div is from framer-motion and handles the animations. + // isHigh = blob pulses (scale 1 → 1.05 → 1), isLow = blob bobs up and down (y: 0 → 4 → 0) + // Infinity means the animation loops forever. isOkay = no animation. + + {/* aria-hidden="true" means screen readers skip this — it's purely decorative */} + + + ); +}; diff --git a/frontend/src/components/MascotTip.jsx b/frontend/src/components/MascotTip.jsx new file mode 100644 index 000000000..1673a07df --- /dev/null +++ b/frontend/src/components/MascotTip.jsx @@ -0,0 +1,87 @@ +import styled, { keyframes } from "styled-components"; +import { EnergyBlob } from "./Mascot"; + +// A list of messages for each mood. Several options per mood to avoid getting the same message every time. +const tips = { + tired: [ + "Idag ser ut att bli en tuff dag! Försök lägga in en vila tidigt.", + "Kom ihåg att ta det lugnt idag, lyssna på vad kroppen signalerar och behöver.", + "En dag i taget. En liten insats räknas också!", + ], + okay: [ + "Lagom balanserat - lyssna på kroppen under dagen.", + "Bra planering! Glöm inte att ta pauser.", + ], + happy: [ + "Fin energibalans! Du har gott om utrymme kvar idag.", + "Toppen-dag planerad! Kom ihåg att njuta av det.", + "Du tar hand om dig själv - mycket bra jobbat!", + ] +}; +// The tips is randomly chosen from the correct moodlist via Math.random. +const getTip = (energyLeft) => { + const mood = energyLeft >= 7 ? "happy" : energyLeft >= 3 ? "okay" : "tired"; + const arr = tips[mood]; + return arr[Math.floor(Math.random() * arr.length)]; +}; + +// This combines Mascot (Energyblob) with the tipbubble side by side. +export const MascotTip = ({ energyLeft }) => { + const mood = energyLeft >= 7 ? "happy" : energyLeft >= 3 ? "okay" : "tired"; + const tip = getTip(energyLeft); + + return ( + + + {tip} + + ); +}; + +// ======= KEYFRAMES ======= // + +const fadeIn = keyframes` + from { opacity: 0; transform: scale(0.9); } + to { opacity: 1; transform: scale(1); } +`; + +// ======= STYLED COMPONENTS ======= // + +const MascotRow = styled.div` + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 4px; + width: 100%; +`; + +// The bubble will have different background and border based on the mood. And the animation makes the tipbubble fade in with a 0.9 s delay. +const TipBubble = styled.div` + flex: 1; + position: relative; + z-index: 1; + background: ${props => { + if (props.$mood === "happy") return "rgba(168, 213, 186, 0.15)"; + if (props.$mood === "tired") return "rgba(196, 122, 122, 0.1)"; + return "rgba(240, 192, 96, 0.1)"; + }}; + -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(8px); + border: 1.5px solid ${props => { + if (props.$mood === "happy") return "rgba(106, 175, 140, 0.8)"; + if (props.$mood === "tired") return "rgba(196, 122, 122, 0.8)"; + return "rgba(240, 192, 96, 0.9)"; + }}; + border-radius: 12px 12px 12px 0; + box-shadow: ${props => { + if (props.$mood === "happy") return "0 6px 20px rgba(106, 175, 140, 0.35)"; + if (props.$mood === "tired") return "0 6px 20px rgba(196, 122, 122, 0.35)"; + return "0 6px 20px rgba(240, 192, 96, 0.4)"; + }}; + padding: 12px 14px; + font-size: 14px; + font-weight: 500; + color: var(--color-text); + line-height: 1.5; + animation: ${fadeIn} 0.4s ease 0.9s both; +`; diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx new file mode 100644 index 000000000..b410e4c8d --- /dev/null +++ b/frontend/src/components/Navbar.jsx @@ -0,0 +1,175 @@ +import styled from "styled-components"; +import { useNavigate, useLocation } from "react-router"; +import { useState } from "react"; +import { List, X, Leaf, Info, Lightbulb } from "@phosphor-icons/react"; +import { useUserStore } from "../stores/userStore"; + +export const Navbar = () => { + const navigate = useNavigate(); + const location = useLocation(); + const logout = useUserStore((state) => state.logout); + const [menuOpen, setMenuOpen] = useState(false); + + const handleLogout = () => { + logout(); + navigate("/"); + }; + + return ( + <> + + + {/* Slide-in menu, hidden*/} + {menuOpen && setMenuOpen(false)} />} + + + + setMenuOpen(false)} aria-label="Stäng meny"> + + + + { navigate("/about", { state: { from: location.pathname } }); setMenuOpen(false); }}> + + { navigate("/tips", { state: { from: location.pathname } }); setMenuOpen(false); }}> + + + + Logga ut + + + + ); +}; + +// ======= STYLED COMPONENTS ======= // + +const Nav = styled.nav` + display: flex; + justify-content: space-between; + align-items: center; + background: white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + padding: 16px 24px; + position: relative; +`; + +const Logo = styled.button` + font-size: 36px; + font-weight: 700; + color: var(--color-primary); + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + background: none; + border: none; +`; + +const HamburgerButton = styled.button` + background: none; + border: none; + cursor: pointer; + color: var(--color-text); + display: flex; +`; + +const DrawerOverlay = styled.div` + position: fixed; + inset: 0; + background: var(--color-overlay); + z-index: 90; +`; + +const Drawer = styled.div` + position: fixed; + top: 0; + right: 0; + height: 100%; + width: 260px; + background: white; + box-shadow: -4px 0 24px rgba(0, 0, 0, 0.12); + z-index: 100; + display: flex; + flex-direction: column; + padding: 24px; + transform: translateX(${props => props.$open ? "0" : "100%"}); + transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1); +`; + +const DrawerTop = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 32px; +`; + +const DrawerLogo = styled.div` + font-size: 20px; + font-weight: 700; + color: var(--color-primary); + display: flex; + align-items: center; + gap: 6px; +`; + +const CloseDrawerButton = styled.button` + background: none; + border: none; + cursor: pointer; + color: var(--color-text); + display: flex; +`; + +const NavLink = styled.button` + background: none; + border: none; + border-bottom: 1px solid var(--color-border); + color: var(--color-text); + cursor: pointer; + font-size: 16px; + padding: 16px 0; + text-align: left; + width: 100%; + display: flex; + align-items: center; + gap: 10px; + + &:hover { + color: var(--color-primary); + } +`; + +const DrawerBottom = styled.div` + margin-top: auto; +`; + +const LogOutButton = styled.button` + padding: 8px 16px; + background: var(--color-primary); + color: white; + border: none; + border-radius: 20px; + font-weight: 600; + cursor: pointer; + align-self: flex-end; + transition: background 0.2s; + + &:hover { + background: var(--color-primary-dark); + } +`; diff --git a/frontend/src/components/ProtectedRoute.jsx b/frontend/src/components/ProtectedRoute.jsx new file mode 100644 index 000000000..78fff7dad --- /dev/null +++ b/frontend/src/components/ProtectedRoute.jsx @@ -0,0 +1,9 @@ +import { Navigate } from "react-router"; + +// This file checks if there is an accessToken in localstorage. If not, the user is sent back to login page. If yes, the protected page will be rendered. Its used in App.jsx around the pages daily, history, about and tips. +export const ProtectedRoute = ({ children }) => { + if (!localStorage.getItem("accessToken")) { + return ; + } + return children; +}; \ No newline at end of file diff --git a/frontend/src/components/SignUpForm.jsx b/frontend/src/components/SignUpForm.jsx new file mode 100644 index 000000000..dc3cdf40c --- /dev/null +++ b/frontend/src/components/SignUpForm.jsx @@ -0,0 +1,163 @@ +import { useState } from "react"; +import { BASE_URL } from "../api/api.js"; +import styled from "styled-components"; + +export const SignUpForm = ({ handleLogin }) => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [formData, setFormData] = useState({ + name: "", + email: "", + password: "", + }); + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!formData.name || !formData.email || !formData.password) { + setError("Fyll i alla fälten"); + return; + } + setError(""); + setLoading(true); + + try { + const response = await fetch(`${BASE_URL}/signup`, { + method: "POST", + body: JSON.stringify({ + name: formData.name, + email: formData.email, + password: formData.password, + }), + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Kunde inte skapa konto, vänligen försök igen!"); + } + + const resJson = await response.json(); + + if (!resJson.success) { + throw new Error(resJson.message || "Kunde inte skapa konto, vänligen försök igen!"); + } + + handleLogin(resJson.response); + e.target.reset(); + } catch (error) { + setError(error.message); + } finally { + setLoading(false); + } + }; + + const handleChange = (e) => { + const { name, value } = e.target; + + setFormData((prevFormData) => ({ ...prevFormData, [name]: value })); + }; + + return ( +
+ Registrera dig + + + + + + {error && {error}} + +
+ ); +}; + +// ======= STYLED COMPONENTS ======= // + +const Form = styled.form` + display: flex; + flex-direction: column; + gap: 12px; + padding: 20px; + width: 100%; + background: var(--color-glass); + border-radius: 12px; +`; + +const Title = styled.h1` + text-align: center; + color: white; + margin: 0; + font-weight: 400; +`; + +const InputWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +const Label = styled.label` + display: flex; + flex-direction: column; + gap: 4px; + font-size: 13px; + color: white; +`; + +const Input = styled.input` + padding: 8px 10px; + font-size: 16px; + border: 1px solid var(--color-border-light); + border-radius: 8px; + background: var(--color-input-bg); +`; + +const Button = styled.button` + padding: 10px; + font-size: 16px; + cursor: pointer; + border: none; + border-radius: 20px; + background-color: var(--color-primary); + color: white; + margin-top: 10px; + width: 100%; + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +`; + +const ErrorText = styled.p` + color: var(--color-error); + font-size: 13px; +`; \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index e69de29bb..a96554869 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -0,0 +1,4 @@ +/* Prevent iOS Safari from auto-zooming when input fields are focused */ +input, select, textarea { + font-size: 16px !important; +} diff --git a/frontend/src/pages/About.jsx b/frontend/src/pages/About.jsx new file mode 100644 index 000000000..b086bf531 --- /dev/null +++ b/frontend/src/pages/About.jsx @@ -0,0 +1,85 @@ +import styled from "styled-components"; +import { Navbar } from "../components/Navbar"; +import { useNavigate, useLocation } from "react-router"; +import { ArrowLeft } from "@phosphor-icons/react"; + +export const About = () => { + const navigate = useNavigate(); + const location = useLocation(); + + return ( + <> + + + navigate(location.state?.from || "/daily")}> + + + + Om balans + +

Balans är en app för dig som vill planera din dag utifrån den energi du har. Genom att välja aktiviteter och se hur de påverkar din energinivå kan du hitta en balans som fungerar för dig.

+
+ +

Hur funkar det?

+

1. Välj hur mycket energi du har idag (1-10)

+

2. Lägg till aktiviteter som tar eller ger energi

+

3. Se en sammanfattning av din dag

+

4. Följ din energi över tid i historiken och lägg till egna anteckningar om din dag

+
+
+ + ); +}; + +// ======= STYLED COMPONENTS ======= // + +const PageWrapper = styled.div` + padding: 16px; + display: flex; + flex-direction: column; + gap: 20px; +`; + +const PageTitle = styled.h2` + text-align: center; +`; + +const Card = styled.div` + background: var(--color-glass-card); + -webkit-backdrop-filter: blur(6px); + backdrop-filter: blur(6px); + border-radius: 12px; + padding: 24px; + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.08); + + h3 { + margin: 0 0 12px 0; + } + p { + margin: 0 0 8px 0; + line-height: 1.6; + color: var(--color-text); + } +`; + +const BackRow = styled.div` + padding: 8px 16px; +`; + +const BackButton = styled.button` + display: flex; + align-items: center; + gap: 6px; + background: none; + border: none; + cursor: pointer; + color: var(--color-text); + font-size: 14px; + padding: 4px 0; + margin-bottom: 8px; + + &:hover { + color: var(--color-primary); + } +`; \ No newline at end of file diff --git a/frontend/src/pages/DailyPlan.jsx b/frontend/src/pages/DailyPlan.jsx new file mode 100644 index 000000000..7e7024f8a --- /dev/null +++ b/frontend/src/pages/DailyPlan.jsx @@ -0,0 +1,172 @@ +import { Navbar } from "../components/Navbar"; +import { useState, useEffect, useCallback } from "react"; +import { fetchActivities, createActivity, deleteActivities, createDailyPlan } from "../api/api"; +import styled from "styled-components"; +import { EnergyPicker } from "../components/EnergyPicker"; +import { ActivityPlanner } from "../components/ActivityPlanner"; +import { DaySummary } from "../components/DaySummary"; +import { ArrowLeft } from "@phosphor-icons/react"; + +// This page is the heart of the app. It holds most states and logic and is built to delegate rendering to 3 under components, step 1 = EnergyPicker, step 2 = ActivityPlanner and step 3 = DaySummary. +export const DailyPlan = () => { + const [activities, setActivities] = useState([]); + const [selectedActivities, setSelectedActivities] = useState([]); + const [energyLevel, setEnergyLevel] = useState(null); + const [showForm, setShowForm] = useState(false); + const [step, setStep] = useState(1); + const [batteryPulse, setBatteryPulse] = useState(false); + const [isSaved, setIsSaved] = useState(false); + const [saveError, setSaveError] = useState(null); + + // SHOWING ALL ACTIVITIES + useEffect(() => { + const loadActivities = async () => { + try { + const data = await fetchActivities(); + setActivities(data); + } catch (error) { + console.error("Kunde inte ladda aktiviteter:", error); + } + }; + + loadActivities(); + }, []); + + // TOGGLING ACTIVITIES + making the battery pulsate. Adds or removes activities from list and triggers the battery animation. + const toggleActivity = useCallback((activityId) => { + setSelectedActivities((prev) => + prev.includes(activityId) + ? prev.filter((id) => id !== activityId) + : [...prev, activityId] + ); + setBatteryPulse(true); + setTimeout(() => setBatteryPulse(false), 400); + }, []); + + // CREATE NEW ACTIVITY. This is where you can choose and add your own activity to the list. You add name, energyImpact and category. + const handleAddActivity = async (e) => { + e.preventDefault(); + const formData = new FormData(e.target); + const name = formData.get("name"); + const energyImpact = Number(formData.get("energyImpact")); + const category = formData.get("category"); + try { + const saved = await createActivity({ name, energyImpact, category }); + setActivities((prev) => [...prev, saved]); + e.target.reset(); + setShowForm(false); + } catch (error) { + console.error("Kunde inte spara aktivitet:", error); + } + }; + + // SAVES DAILYPLAN IN DATABASE + const handleSave = async () => { + try { + setSaveError(null); + await createDailyPlan({ + date: new Date(), + startingEnergy: energyLevel, + activities: selectedActivities, + currentEnergy: energyLeft, + }); + setIsSaved(true); + } catch (error) { + setSaveError(error.message); + } + }; + + // DELETES AN ACTIVITY. Is only used in activities that the user created, not activities from the ordinary list. + const handleDelete = async (activityId) => { + try { + await deleteActivities(activityId); + setActivities((prev) => prev.filter((a) => a._id !== activityId)); + } catch (error) { + console.error("Kunde inte ta bort aktivitet:", error) + } + }; + + // ENERGY LEFT PER DAY. This will be calculated live based on chosen activities. + const energyLeft = energyLevel + ? energyLevel + activities + .filter((a) => selectedActivities.includes(a._id)) + .reduce((sum, a) => sum + a.energyImpact, 0) + : 0; + + return ( + <> + + + {step > 1 && ( + setStep(step - 1)}> + + )} + + + {step === 1 && ( + setStep(2)} + /> + )} + + {step === 2 && ( + setStep(3)} + /> + )} + + {step === 3 && ( + { setStep(2); setIsSaved(false); }} + onSave={handleSave} + isSaved={isSaved} + saveError={saveError} + /> + )} + + + ); +}; + +// ======= STYLED COMPONENTS ======= // + +const PageWrapper = styled.div` + padding: 8px 16px 16px; +`; + +const BackRow = styled.div` + padding: 4px 16px; +`; + +const BackButton = styled.button` + display: flex; + align-items: center; + gap: 6px; + background: none; + border: none; + cursor: pointer; + color: var(--color-text); + font-size: 14px; + padding: 4px 0; + margin-bottom: 8px; + + &:hover { + color: var(--color-primary); + } +`; \ No newline at end of file diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx new file mode 100644 index 000000000..dac912025 --- /dev/null +++ b/frontend/src/pages/History.jsx @@ -0,0 +1,291 @@ +import { useState, useEffect } from "react"; +import { fetchDailyPlan, patchDailyPlan } from "../api/api"; +import { Navbar } from "../components/Navbar"; +import styled from "styled-components"; +import { ArrowLeft, CalendarBlank, CaretDown } from "@phosphor-icons/react"; +import { useNavigate } from "react-router"; +import { EnergyGraf } from "../components/EnergyHistory.jsx"; + + +export const History = () => { + const [plans, setPlans] = useState([]); + const [openId, setOpenId] = useState(null); // Show witch card that is expanded. + const [notes, setNotes] = useState({}); + const [isLoading, setIsLoading] = useState(true); + + const navigate = useNavigate(); + + // Gets all the plans at mount and builds noteMap from the data. + useEffect(() => { + const loadPlans = async () => { + try { + const data = await fetchDailyPlan(); + setPlans(data); + const notesMap = {}; + data.forEach(p => { notesMap[p._id] = p.notes || ""; }); + setNotes(notesMap); + } catch (error) { + console.error("Kunde inte hämta plan:", error); + } finally { + setIsLoading(false); + } + }; + loadPlans(); + }, []); + + return ( + <> + + + navigate(-1)}> + + + {/* If plan is empty, show EmptyState, otherwise show EnergyGraf + Plancards */} + {isLoading ? ( + Laddar historik... + ) : plans.length === 0 ? ( + + 🌿 +

Du har inte loggat någon dag ännu!

+

Gå tillbaka och planera din första dag.

+
+ ) : ( + <> + + + {[...plans].sort((a, b) => new Date(b.date) - new Date(a.date)).map((plan) => ( + setOpenId(openId === plan._id ? null : plan._id)} // Opens a new card, and closes the previous. + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") + setOpenId(openId === plan._id ? null : plan._id); + }} + aria-expanded={openId === plan._id} + > + + + {openId === plan._id && ( + <> + + + + {plan.startingEnergy} + start + + = plan.startingEnergy ? "energi ökade" : "energi minskade"} + $energy={plan.currentEnergy} + > + {plan.currentEnergy >= plan.startingEnergy ? "↑" : "↓"} + + + + {plan.currentEnergy} + + slut + + + + + {[...plan.activities] + .sort((a, b) => b.energyImpact - a.energyImpact) + .map(a => 0}>{a.name})} + + setNotes(prev => ({ ...prev, [plan._id]: e.target.value }))} + onBlur={() => patchDailyPlan(plan._id, { notes: notes[plan._id] })} // Saves the note when you click outside of the field. + onClick={e => e.stopPropagation()} + onKeyDown={e => e.stopPropagation()} // stops the note from closing when clicking in the notarea. + placeholder="Egna tankar om dagen..." + rows={2} + aria-label="Egna tankar om dagen" + /> + + )} + + ))} + + )} +
+ + ); +}; + +// ======= STYLED COMPONENTS ======= // + +const BackRow = styled.div` + padding: 4px 16px; +`; + +const BackButton = styled.button` + display: flex; + align-items: center; + gap: 6px; + background: none; + border: none; + cursor: pointer; + color: var(--color-text); + font-size: 14px; + padding: 4px 0; + margin-bottom: 8px; + + &:hover { + color: var(--color-primary); + } +`; + +const PageWrapper = styled.div` + padding: 8px 16px 16px; +`; + +const LoadingText = styled.p` + text-align: center; + padding: 48px 16px; + color: var(--color-text-muted); +`; + +const EmptyState = styled.div` + text-align: center; + padding: 48px 16px; + color: var(--color-text); + + span { + font-size: 48px; + } +`; + +const EnergyRow = styled.div` + display: flex; + align-items: center; + justify-content: flex-start; + gap: 12px; + margin-bottom: 12px; +`; + +const EnergyNumbers = styled.div` + display: flex; + align-items: baseline; + gap: 6px; + flex-shrink: 0; +`; + +const EnergyBlock = styled.div` + display: flex; + flex-direction: column; + align-items: center; +`; + +const EnergyNum = styled.span` + font-size: 36px; + font-weight: 700; + line-height: 1; + color: ${({ $end, $energy }) => + !$end ? "var(--color-text-muted)" : + $energy >= 7 ? "var(--color-forest-dark)" : + $energy >= 4 ? "var(--color-energy-mid-dark)" : + "var(--color-error-dark)" + }; +`; + +const EnergyLabel = styled.span` + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--color-text-muted); +`; + +const Arrow = styled.span` + font-size: 24px; + align-self: center; + color: ${({ $energy }) => + $energy >= 7 ? "var(--color-forest-dark)" : + $energy >= 4 ? "var(--color-energy-mid-dark)" : + "var(--color-error-dark)" + }; +`; + +const ActivityChips = styled.div` + display: flex; + flex-wrap: wrap; + gap: 8px; +`; + +const Chip = styled.span` + display: inline-block; + padding: 10px 14px; + border-radius: 16px; + font-size: 14px; + font-weight: 500; + color: var(--color-text); + background: ${props => props.$positive + ? "var(--color-forest-subtle)" + : "var(--color-error-light)" + }; + border: 1px solid ${props => props.$positive + ? "var(--color-forest)" + : "var(--color-error-dark)" + }; +`; + +const NoteArea = styled.textarea` + width: 100%; + margin-top: 12px; + padding: 10px 12px; + border-radius: 8px; + border: 1px solid var(--color-border); + background: var(--color-input-bg); + font-size: 16px; + color: var(--color-text); + resize: none; + font-family: inherit; + line-height: 1.5; + + &:focus { + outline: none; + border-color: var(--color-primary); + } + + &::placeholder { + color: var(--color-text-muted); + } +`; + +const PlanCard = styled.div` + background: var(--color-glass-card); + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.08); + -webkit-backdrop-filter: blur(6px); + backdrop-filter: blur(6px); + border-left: 5px solid ${props => { + if (props.$energy >= 7) return "var(--color-energy-high)"; + if (props.$energy >= 4) return "var(--color-energy-mid)"; + return "var(--color-energy-low)"; + }}; + + border-radius: 12px; + padding: ${props => props.$open ? "10px 16px" : "8px 10px"}; + margin-bottom: 10px; + cursor: pointer; +`; + +const CardHeader = styled.div` + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 12px; + color: var(--color-text-muted); + + h3 { + margin: 0; + text-transform: capitalize; + font-size: 15px; + } +`; \ No newline at end of file diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx new file mode 100644 index 000000000..d9b91bfdc --- /dev/null +++ b/frontend/src/pages/Home.jsx @@ -0,0 +1,109 @@ +import { LogInForm } from "../components/LoginForm"; +import { SignUpForm } from "../components/SignUpForm"; +import { useNavigate } from "react-router"; +import styled from "styled-components"; +import { useState } from "react"; +import { useUserStore } from "../stores/userStore"; +import { Leaf } from "@phosphor-icons/react"; + +// This is the home of the app. A description of what the app does, and also sign up and log in. +export const Home = () => { + const navigate = useNavigate(); + const login = useUserStore((state) => state.login); + + const [showLogin, setShowLogin] = useState(false); + const [showSignup, setShowSignup] = useState(false); + + // Gets userdata from the form, saves token in Zustand-store and navigates to /daily. + const handleLogin = (userData) => { + if (userData.accessToken) { + login(userData.accessToken, userData.name); + navigate("/daily"); + } + }; + + return ( + + + + + Planera din dag med den energi du har. + En app för dig som behöver göra en sak i taget och få energin att räcka hela dagen + + + { setShowLogin(!showLogin); setShowSignup(false); }}> + Logga in + + { setShowSignup(!showSignup); setShowLogin(false); }}> + Skapa konto + + + + {showLogin && } + {showSignup && } + + ); +}; + +// ======= STYLED COMPONENTS ======= // + +const HomeWrapper = styled.main` + padding: 10vh 16px 40px; + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + background: rgba(0, 0, 0, 0.1); +`; + +const AppTitle = styled.h1` + font-size: clamp(36px, 12vw, 64px); + color: white; + margin: 0; + font-weight: 300; + letter-spacing: 4px; + display: flex; + align-items: center; + gap: 12px; + text-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); +`; + +const SubTitle = styled.p` + color: white; + font-size: 20px; + margin: 0 0 8px 0; + text-align: center; + text-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); +`; + +const SubText = styled.p` + color: white; + font-size: 14px; + text-align: center; + text-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); +`; + +const ButtonRow = styled.div` + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 16px; + margin-bottom: 50px; +`; + +const OutlinedButton = styled.button` + padding: 12px 24px; + min-width: 140px; + border: 2px solid white; + border-radius: 24px; + background: transparent; + color: white; + font-size: 16px; + cursor: pointer; + margin-top: 30px; + + &:hover { + background: rgba(255, 255, 255, 0.15); + } +`; \ No newline at end of file diff --git a/frontend/src/pages/Tips.jsx b/frontend/src/pages/Tips.jsx new file mode 100644 index 000000000..be56da66c --- /dev/null +++ b/frontend/src/pages/Tips.jsx @@ -0,0 +1,141 @@ +import styled from "styled-components"; +import { Navbar } from "../components/Navbar"; +import { ArrowLeft, ArrowSquareOut } from "@phosphor-icons/react"; +import { useNavigate, useLocation } from "react-router"; + +export const Tips = () => { + const navigate = useNavigate(); + const location = useLocation(); + + return ( + <> + + + navigate(location.state?.from || "/daily")}> + + + + Tips och länkar + +

Förstå din energi

+ + Spoon Theory - Planera din energi (YouTube) + +
+ + +

Stresshantering

+ + 1177 - Stresshantering och sömn + + + Din Psykiska Hälsa – Stress + + + Hjärnfonden – 8 tips för att minska stressen + +
+ + +

Sömn

+ + 1177 – Sömnsskola + + + Sunt Arbetsliv – 5 tips för bättre sömn + +
+ + +

Rörelse

+ + Stressfri Kropp – Träna utan bakslag med utmattningssyndrom + + + Hjärnfonden – Fysisk träning och depression + +
+
+ + ); +}; + +// ======= STYLED COMPONENTS ======= // + +const BackRow = styled.div` + padding: 8px 16px; +`; + +const BackButton = styled.button` + display: flex; + align-items: center; + gap: 6px; + background: none; + border: none; + cursor: pointer; + color: var(--color-text); + font-size: 14px; + padding: 4px 0; + margin-bottom: 8px; + + &:hover { + color: var(--color-primary); + } +`; + +const PageWrapper = styled.div` + padding: 16px; + display: flex; + flex-direction: column; + gap: 20px; +`; + +const PageTitle = styled.h2` + text-align: center; +`; + +const Card = styled.div` + background: var(--color-glass-card); + -webkit-backdrop-filter: blur(6px); + backdrop-filter: blur(6px); + border-radius: 12px; + padding: 24px; + + h3 { + margin: 0 0 12px 0; + } +`; + +const LinkItem = styled.a` + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + margin-bottom: 8px; + border-radius: 8px; + background: white; + color: var(--color-primary); + text-decoration: none; + font-weight: 500; + border: 1px solid var(--color-border); + + svg { + flex-shrink: 0; + margin-top: 2px; + } + + &:last-child { margin-bottom: 0; } + + &:hover { + background: rgba(107, 94, 117, 0.08); + } +`; \ No newline at end of file diff --git a/frontend/src/stores/userStore.js b/frontend/src/stores/userStore.js new file mode 100644 index 000000000..490eea5fb --- /dev/null +++ b/frontend/src/stores/userStore.js @@ -0,0 +1,19 @@ +import { create } from "zustand"; + +// This is the global state of the app, Zustand. It contains accessToken, username, login and logout. AccessToken will be saved both here and in localstorage. +export const useUserStore = create((set) => ({ + accessToken: localStorage.getItem("accessToken"), + username: localStorage.getItem("username"), + + login: (token, username) => { + localStorage.setItem("accessToken", token); + localStorage.setItem("username", username); + set({ accessToken: token, username }); + }, + + logout: () => { + localStorage.removeItem("accessToken"); + localStorage.removeItem("username"); + set({ accessToken: null, username: null }); + }, +})); diff --git a/frontend/src/styles/GlobalStyle.js b/frontend/src/styles/GlobalStyle.js new file mode 100644 index 000000000..d4f56d6f0 --- /dev/null +++ b/frontend/src/styles/GlobalStyle.js @@ -0,0 +1,69 @@ +import { createGlobalStyle } from "styled-components"; + +export const GlobalStyles = createGlobalStyle` + +:root { + --color-primary: #6b5e75; + --color-primary-dark: #4d4f59; + --color-forest: #4a7c59; + --color-forest-dark: #3a6347; + --color-forest-light: rgba(74, 124, 89, 0.25); + --color-card: rgba(227, 224, 217, 0.85); + --color-text: #302e2f; + --color-border: #bcb3a8; + --color-border-light: rgba(255, 255, 255, 0.3); + --color-energy-high: #a8d5ba; + --color-energy-mid: #f0c060; + --color-energy-mid-dark: #c89020; + --color-energy-low: #c26e6eff; + --color-error: #c47a7a; + --color-error-light: rgba(186, 78, 78, 0.12); + --color-error-dark: #7a3030; + --color-text-muted: #646774; + --color-input-bg: rgba(227, 224, 217, 0.9); + --color-glass: rgba(227, 224, 217, 0.3); + --color-glass-card: rgba(255, 255, 255, 0.4); + --color-overlay: rgba(0, 0, 0, 0.4); + --color-forest-subtle: rgba(74, 124, 89, 0.15); + +} + +* { + box-sizing: border-box; +} + +input, select, textarea { + font-size: 16px; +} + +h1, h2, h3, h4 { + margin: 0; + line-height: 1.2; + color: var(--color-text); +} + +h1 { font-size: clamp(24px, 6vw, 36px); } +h2 { font-size: clamp(20px, 5vw, 28px); } +h3 { font-size: clamp(16px, 4vw, 20px); } + +body { + margin: 0; + padding: 0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-image: url("/background2.jpg"); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-attachment: fixed; + min-height: 100vh; + overflow-x: hidden; +} + +main { + max-width: 430px; + margin: 0 auto; + min-height: 100vh; + box-shadow: 0 0 60px rgba(0, 0, 0, 0.18); + overflow-x: hidden; +} +`; \ No newline at end of file diff --git a/package.json b/package.json index 680d19077..e16e075ad 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,12 @@ { "name": "project-final-parent", "version": "1.0.0", + "type": "module", "scripts": { - "postinstall": "npm install --prefix backend" + "postinstall": "npm install --prefix backend", + "start": "node backend/server.js" + }, + "dependencies": { + "framer-motion": "^12.34.3" } -} \ No newline at end of file +}