diff --git a/README.md b/README.md index 31466b54c..82a0f134c 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,6 @@ Describe how you approached to problem, and what tools and techniques you used t ## 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 +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. + +https://stoppa-proppen.netlify.app/ \ No newline at end of file diff --git a/backend/models/User.js b/backend/models/User.js new file mode 100644 index 000000000..2ee12dbc2 --- /dev/null +++ b/backend/models/User.js @@ -0,0 +1,25 @@ +import mongoose from "mongoose"; +import crypto from "crypto"; + +const userSchema = new mongoose.Schema({ + + email: { + type: String, + required: true, + unique: true, + lowercase: true, + }, + password: { + type: String, + required: true + }, + accessToken: { + type: String, + default: () => crypto.randomUUID() + } +}, { timestamps: true }); +//creates a user modell based on schema +const User = mongoose.model("User", userSchema) + + +export default User diff --git a/backend/package.json b/backend/package.json index 08f29f244..266fe19c2 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.3.1", "express": "^4.17.3", + "express-session": "^1.19.0", "mongoose": "^8.4.0", "nodemon": "^3.0.1" } -} \ No newline at end of file +} diff --git a/backend/routes/auth.js b/backend/routes/auth.js new file mode 100644 index 000000000..c0ad2927f --- /dev/null +++ b/backend/routes/auth.js @@ -0,0 +1,90 @@ +import express from "express"; +import User from "../models/User.js"; +import bcrypt from "bcrypt"; + +const router = express.Router(); + +// Middleware to protect routes — checks the Authorization header for a valid token +export const authenticateUser = async (req, res, next) => { + const token = req.headers.authorization?.replace("Bearer ", ""); + if (!token) { + return res.status(401).json({ message: "Ingen token angiven" }); + } + const user = await User.findOne({ accessToken: token }); + if (!user) { + return res.status(401).json({ message: "Ogiltig token" }); + } + req.user = user; + next(); +}; + +// async - means the route contains code that takes time (talking to the database) +// without async/await we cannot wait for a response from MongoDB +router.post("/register", async (req, res) => { + + // body contains the data the user submitted from the form + // destructuring = we extract email and password directly instead of req.body.email + const { email, password } = req.body; + + // Checks if the user has actually filled in the fields + // ! means "not" - so if email is missing + if (!email || !password) { + + // stop the router here so the rest of the code does not run + // status 400 + return res.status(400).json({ message: "Email och lösenord krävs"}); + } + +// checks if the email already exists in the database, findOne looks for a matching document, returns null if nothing found +const existingUser = await User.findOne({ email }); + +if (existingUser) { + return res.status(400).json({ message: "Email redan registrerad"}); +} +// 10 means the password is hashed 10 times +// await wait until encryption is complete before proceeding +const hashedPassword = await bcrypt.hash(password, 10); + +// newUser creates a new user object with our schema +// we pass in email and the hashed password +const newUser = new User({ + email: email, + password: hashedPassword, +}); +// saves the user to MongoDB +// await - wait until saving is complete +await newUser.save(); + +res.status(201).json({ message: "Konto skapat!" }); +}); + +router.post("/login", async (req, res) => { + + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ message: 'Email och lösenord krävs' }); + } + + // looks for the user's email in the database + const user = await User.findOne({ email }); + // if null is returned the login is denied + if (!user) { + return res.status(401).json({ message: "Fel email eller lösenord"}); + } + + // compare compares the password with the hashed one, await wait until compared, returns true if they match false otherwise + const isMatch = await bcrypt.compare(password, user.password); + // if the password does not match login is denied + if(!isMatch){ + return res.status(401).json({ message: "Fel email eller lösenord"}) + } + res.status(200).json({ + message: 'Inloggad!', + email: user.email, + id: user._id, + accessToken: user.accessToken + }); +}) + +export default router; diff --git a/backend/server.js b/backend/server.js index 070c87518..50d4b3380 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,22 +1,32 @@ import express from "express"; import cors from "cors"; import mongoose from "mongoose"; +import dotenv from 'dotenv'; +import authRoutes from "./routes/auth"; -const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/final-project"; -mongoose.connect(mongoUrl); -mongoose.Promise = Promise; +dotenv.config(); -const port = process.env.PORT || 8080; +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/final-project"; +/* mongoose.Promise = Promise; */ +const port = process.env.PORT || 8081; const app = express(); +/* Middleware */ app.use(cors()); app.use(express.json()); -app.get("/", (req, res) => { - res.send("Hello Technigo!"); -}); +// tells express that all routes in auth.js should start with /api +// register becomes /api/register +app.use("/api", authRoutes); // Start the server -app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`); +mongoose.connect(mongoUrl) + .then(() => { + console.log("✅ Succéss! We are conected to MongoDB Atlas."); + app.listen(port, () => { + console.log(`Server running on http://localhost:${port}`); }); + }) + .catch((err) => { + console.error("❌ Could not connect to the database:", err); + }); diff --git a/frontend/index.html b/frontend/index.html index 664410b5b..d4e87152f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,10 @@ - Technigo React Vite Boiler Plate + Stoppa Proppen + + +
diff --git a/frontend/package.json b/frontend/package.json index 7b2747e94..e593e22f5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,8 +10,15 @@ "preview": "vite preview" }, "dependencies": { + "date-fns": "^4.1.0", + "framer-motion": "^12.34.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^7.13.0", + "react-toastify": "^11.0.5", + "styled-components": "^6.3.9", + "usehooks-ts": "^3.1.1", + "zustand": "^5.0.11" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/frontend/public/_redirects b/frontend/public/_redirects new file mode 100644 index 000000000..50a463356 --- /dev/null +++ b/frontend/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 \ No newline at end of file diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg deleted file mode 100644 index e7b8dfb1b..000000000 --- a/frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0a24275e6..bdafdb9e9 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,8 +1,36 @@ +import LandingPage from "./pages/LandingPage"; +import { ThemeProvider } from "styled-components"; +import GlobalStyles from "./styles/Globalstyles"; +import { theme } from "./styles/theme"; +import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; +import LoginPage from "./pages/LoginPage"; +import MedicinPage from "./pages/MedicinPage"; +import FaqPage from "./pages/FaqPage"; +import OmOssPage from "./pages/OmossPage"; +import HurdetFungerar from "./pages/Hurdetfungerarpage" +import ProtectedRoute from "./components/ProtectedRoute"; +import ProfilPage from "./pages/ProfilPage"; + export const App = () => { return ( - <> -

Welcome to Final Project!

- + + + + + } /> + } /> + + + } /> + } /> + } /> + } /> + } /> + } /> + + + ); }; diff --git a/frontend/src/assets/boiler-plate.svg b/frontend/src/assets/boiler-plate.svg deleted file mode 100644 index c9252833b..000000000 --- a/frontend/src/assets/boiler-plate.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9bb..000000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/technigo-logo.svg b/frontend/src/assets/technigo-logo.svg deleted file mode 100644 index 3f0da3e57..000000000 --- a/frontend/src/assets/technigo-logo.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/src/components/BottomNav.jsx b/frontend/src/components/BottomNav.jsx new file mode 100644 index 000000000..25568751c --- /dev/null +++ b/frontend/src/components/BottomNav.jsx @@ -0,0 +1,121 @@ +// ───────────────────────────────────────────────────────────────────────────── +// BottomNav.jsx +// +// Bottom navigation for app pages on mobile. +// Shows three large buttons with icon + text at the bottom of the screen. +// Hidden on desktop (768px+) since navigation is done via the top menu/back button. +// Props: +// active = string with active page: 'hem' | 'medicin' | 'symptomkoll' +// ───────────────────────────────────────────────────────────────────────────── + +import React from 'react'; +import styled from 'styled-components'; +import { Link } from 'react-router-dom'; +import { theme } from '../styles/theme'; + +// ─── Wrapper ────────────────────────────────────────────────────────────────── +// Fixed at the bottom of the screen (position: fixed). +// Hidden entirely on desktop — navigation is done via the navbar instead. +const Bar = styled.nav` + /* MOBIL: synlig, fast längst ner */ + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 100; + + display: flex; + align-items: stretch; + + background: rgba(8, 35, 40, 0.97); + backdrop-filter: blur(16px); + border-top: 1px solid rgba(255,255,255,0.1); + + /* Extra padding at the bottom for iPhone notch/home indicator */ + padding-bottom: env(safe-area-inset-bottom, 0px); + + /* DESKTOP 768+: döljs */ + @media (min-width: 768px) { + display: none; + } +`; + +// ─── Individual nav button ──────────────────────────────────────────────────── +// $active controls whether the button is highlighted (light green) or not (grey). +const NavItem = styled(Link)` + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.3rem; + padding: 0.75rem 0.5rem; + text-decoration: none; + color: ${({ $active }) => $active ? theme.colors.mint : 'rgba(255,255,255,0.45)'}; + transition: color 0.2s; + + /* Small highlight line at the top of the active button */ + border-top: 2px solid ${({ $active }) => $active ? theme.colors.mint : 'transparent'}; + + &:active { opacity: 0.7; } +`; + +// Icon wrapper +const Icon = styled.div` + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; +`; + +// Text below the icon +const Label = styled.span` + font-size: 0.68rem; + font-weight: ${({ $active }) => $active ? '500' : '400'}; + letter-spacing: 0.02em; +`; + +// ─── Component ──────────────────────────────────────────────────────────────── +const BottomNav = ({ active = '' }) => ( + + + {/* Hem */} + + + + + + + + + + + {/* Medicinlåda */} + + + + + + + + + + + {/* Symtomkoll */} + + + + + + + + + + +); + +export default BottomNav; \ No newline at end of file diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx new file mode 100644 index 000000000..0292720ac --- /dev/null +++ b/frontend/src/components/Layout.jsx @@ -0,0 +1,162 @@ +import styled, { keyframes } from 'styled-components'; +import { theme } from '../styles/theme'; + +// ─── Animations ─────────────────────────────────────────────── +export const fadeUp = keyframes` + from { opacity: 0; transform: translateY(18px); } + to { opacity: 1; transform: translateY(0); } +`; + +export const fadeLeft = keyframes` + from { opacity: 0; transform: translateX(14px); } + to { opacity: 1; transform: translateX(0); } +`; + +export const pulse = keyframes` + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.6; transform: scale(0.85); } +`; + +export const growBar = keyframes` + from { width: 0; } +`; + +// ─── Page frame ─────────────────────────────────────────────── +export const Frame = styled.div` + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + + /* mobile: no padding, the card fills the entire screen */ + padding: 0; + background: ${theme.colors.dark}; + + @media (min-width: 768px) { + padding: 2rem; + } +`; + +// ─── Main card ──────────────────────────────────────────────── +export const Card = styled.main` + width: 100%; + min-height: 100vh; + position: relative; + background: ${theme.gradients.card}; + display: flex; + flex-direction: column; + + /* mobile: no rounded corners, fills the entire screen */ + border-radius: 0; + box-shadow: none; + + @media (min-width: 768px) { + max-width: ${({ maxWidth }) => maxWidth || '1100px'}; + min-height: ${({ minHeight }) => minHeight || '620px'}; + border-radius: 24px; + box-shadow: ${theme.shadow.card}, inset 0 1px 0 rgba(255,255,255,0.15); + } + + /* dark overlay */ + &::after { + content: ''; + position: absolute; + inset: 0; + background: rgba(8, 35, 40, 0.45); + pointer-events: none; + z-index: 2; + } + + /* stripe texture */ + &::before { + content: ''; + position: absolute; + inset: 0; + background-image: repeating-linear-gradient( + 90deg, + transparent, transparent 3px, + rgba(255,255,255,0.025) 3px, rgba(255,255,255,0.025) 4px + ); + pointer-events: none; + z-index: 1; + } +`; + +// ─── Corner labels — only visible on desktop ────────────────── +export const CornerLabel = styled.span` + display: none; + + @media (min-width: 768px) { + display: block; + position: absolute; + color: rgba(255,255,255,0.5); + font-size: 0.62rem; + letter-spacing: 0.05em; + z-index: 15; + + ${({ pos }) => ({ + 'tl': 'top: 1rem; left: 1.2rem;', + 'tr': 'top: 1rem; right: 1.2rem;', + 'bl': 'bottom: 1rem; left: 1.2rem;', + 'br': 'bottom: 1rem; right: 1.2rem;', + }[pos])} + } +`; + +// ─── Shared buttons ─────────────────────────────────────────── +export const BtnPrimary = styled.button` + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + background: rgba(255,255,255,0.92); + color: ${theme.colors.tealDeep}; + border: none; + border-radius: ${theme.radius.pill}; + padding: 0.9rem 2rem; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + letter-spacing: 0.01em; + box-shadow: ${theme.shadow.btn}; + transition: transform 0.2s, box-shadow 0.2s, background 0.2s; + text-decoration: none; + width: 100%; + + @media (min-width: 480px) { + width: auto; + } + + &:hover { + background: #fff; + transform: translateY(-2px); + box-shadow: 0 12px 40px rgba(0,0,0,0.25); + } +`; + +export const BtnGhost = styled.button` + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + background: transparent; + border: 1px solid rgba(255,255,255,0.25); + border-radius: ${theme.radius.pill}; + padding: 0.9rem 2rem; + font-size: 0.88rem; + font-weight: 400; + color: rgba(255,255,255,0.8); + cursor: pointer; + letter-spacing: 0.01em; + transition: border-color 0.2s, color 0.2s; + width: 100%; + + @media (min-width: 480px) { + width: auto; + } + + &:hover { + border-color: rgba(255,255,255,0.5); + color: #fff; + } +`; diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx new file mode 100644 index 000000000..aa7f30756 --- /dev/null +++ b/frontend/src/components/Navbar.jsx @@ -0,0 +1,273 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { Link } from 'react-router-dom'; +import { theme } from '../styles/theme'; +import { useWindowSize } from "usehooks-ts"; +import useUserStore from '../store/userStore'; + +// ─── Styles ─────────────────────────────────────────────────── +const Nav = styled.nav` + position: relative; + z-index: 10; + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.2rem 1.4rem; + + @media (min-width: 768px) { + padding: 1.8rem 2.5rem; + } +`; + +const LogoWrap = styled(Link)` + display: flex; + align-items: center; + gap: 0.6rem; + text-decoration: none; + z-index: 200; +`; + +const LogoIcon = styled.div` + width: 30px; + height: 30px; + border-radius: 50%; + background: rgba(255,255,255,0.2); + border: 1.5px solid rgba(255,255,255,0.35); + display: flex; + align-items: center; + justify-content: center; +`; + +const LogoText = styled.span` + font-size: 0.88rem; + font-weight: 500; + color: rgba(255,255,255,0.9); +`; + +// ─── Desktop nav links ──────────────────────────────────────── +const NavLinks = styled.ul` + display: none; + + @media (min-width: 768px) { + display: flex; + align-items: center; + gap: 2.5rem; + list-style: none; + } +`; + +const NavLink = styled(Link)` + font-size: 0.85rem; + font-weight: 400; + color: rgba(255,255,255,0.88); + text-decoration: none; + transition: color 0.2s; + &:hover { color: #fff; } +`; + +const NavCta = styled(Link)` + display: none; + + @media (min-width: 768px) { + display: inline-block; + font-size: 0.82rem; + font-weight: 500; + color: ${theme.colors.tealDeep}; + background: rgba(255,255,255,0.92); + border: none; + border-radius: 100px; + padding: 0.55rem 1.3rem; + cursor: pointer; + letter-spacing: 0.01em; + transition: background 0.2s, transform 0.15s; + text-decoration: none; + &:hover { background: #fff; transform: translateY(-1px); } + } +`; + +// ─── Hamburger button (mobile only) ─────────────────────────── +const HamburgerBtn = styled.button` + display: flex; + flex-direction: column; + justify-content: center; + gap: 5px; + background: transparent; + border: none; + cursor: pointer; + padding: 0.4rem; + position: fixed; + top: 1.1rem; + right: 1.2rem; + z-index: 999; + + @media (min-width: 768px) { + display: none; + } +`; + +const HamburgerLine = styled.span` + display: block; + width: 22px; + height: 1.5px; + background: rgba(255,255,255,0.9); + border-radius: 2px; + transition: transform 0.25s, opacity 0.25s; + + &:nth-child(1) { + transform: ${({ $open }) => $open ? 'translateY(6.5px) rotate(45deg)' : 'none'}; + } + &:nth-child(2) { + opacity: ${({ $open }) => $open ? 0 : 1}; + } + &:nth-child(3) { + transform: ${({ $open }) => $open ? 'translateY(-6.5px) rotate(-45deg)' : 'none'}; + } +`; + +// ─── Mobile menu drawer ─────────────────────────────────────── +const MobileMenu = styled.div` + position: fixed; + inset: 0; + background: rgba(10,40,46,0.97); + backdrop-filter: blur(16px); + z-index: 500; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.5rem; + transform: ${({ $open }) => $open ? 'translateX(0)' : 'translateX(100%)'}; + transition: transform 0.3s ease; + + @media (min-width: 768px) { + display: none; + } +`; + +const MobileLink = styled(Link)` + font-family: ${theme.fonts.serif}; + font-size: 2rem; + font-weight: 300; + color: rgba(255,255,255,0.85); + text-decoration: none; + padding: 0.6rem 2rem; + transition: color 0.2s; + &:hover { color: #fff; } +`; + +const MobileCta = styled(Link)` + margin-top: 1.5rem; + font-size: 0.9rem; + font-weight: 500; + color: ${theme.colors.tealDeep}; + background: rgba(255,255,255,0.92); + border-radius: 100px; + padding: 0.8rem 2.5rem; + text-decoration: none; + transition: background 0.2s; + &:hover { background: #fff; } +`; + +// ─── User pill ──────────────────────────────────────────────── +const UserPill = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; + background: rgba(0,0,0,0.25); + border: 1px solid rgba(255,255,255,0.25); + border-radius: 100px; + padding: 0.3rem 0.8rem; + font-size: 0.78rem; + color: rgba(255,255,255,0.9); +`; + +const Avatar = styled.div` + width: 22px; + height: 22px; + border-radius: 50%; + background: rgba(125,255,212,0.25); + border: 1px solid rgba(125,255,212,0.5); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.62rem; + color: ${theme.colors.mint}; + font-weight: 500; +`; + +// ─── Component ──────────────────────────────────────────────── +const Navbar = ({ variant = 'default', logoHref = '/' }) => { + const [menuOpen, setMenuOpen] = useState(false); + + const toggleMenu = () => setMenuOpen(prev => !prev); + + const { width } = useWindowSize(); + const isMobile = width < 768; + + const isLoggedIn = useUserStore(state => state.isLoggedIn); + const storeUser = useUserStore(state => state.user); + + return ( + <> + + + {isMobile && variant === 'default' && ( + + + + + + )} + + + {/* Mobile drawer */} + {isMobile && variant === 'default' && ( + + setMenuOpen(false)}>Hem + setMenuOpen(false)}>Min Profil + setMenuOpen(false)}>Om oss + setMenuOpen(false)}>Hur det fungerar + setMenuOpen(false)}>FAQ + setMenuOpen(false)}>Medicinkoll + {!isLoggedIn && ( + setMenuOpen(false)}>Kom igång gratis + )} + + )} + + ); +}; + +export default Navbar; diff --git a/frontend/src/components/ProtectedRoute.jsx b/frontend/src/components/ProtectedRoute.jsx new file mode 100644 index 000000000..b31d75e4f --- /dev/null +++ b/frontend/src/components/ProtectedRoute.jsx @@ -0,0 +1,19 @@ +import { Navigate } from 'react-router-dom'; //checks if the user is logged in, otherwise sends you to login +import useUserStore from '../store/userStore'; // checks isLoggedIn from Zustand + +/* isLoggedIn = true → show children (show whats inside the component) +isLoggedIn = false → */ + +const ProtectedRoute = ({ children }) => { + + const isLoggedIn = useUserStore(state => state.isLoggedIn); + + if (isLoggedIn){ + return children //show the site (component) + } else { + return + } +}; + + +export default ProtectedRoute; \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 51294f399..bc52a5633 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,7 +1,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { App } from "./App.jsx"; -import "./index.css"; + ReactDOM.createRoot(document.getElementById("root")).render( diff --git a/frontend/src/pages/FaqPage.jsx b/frontend/src/pages/FaqPage.jsx new file mode 100644 index 000000000..2b0a347f4 --- /dev/null +++ b/frontend/src/pages/FaqPage.jsx @@ -0,0 +1,365 @@ +import React, { useState } from 'react'; +import styled, { keyframes } from 'styled-components'; +import { Frame, Card, CornerLabel, fadeUp } from '../components/Layout.jsx'; +import Navbar from '../components/Navbar.jsx'; +import { theme } from '../styles/theme.js'; + +// ─── ANIMATIONS ────────────────────────────────────────────── +const slideDown = keyframes` + from { opacity: 0; transform: translateY(-6px); } + to { opacity: 1; transform: translateY(0); } +`; + +// ─── SCROLL-AREA ────────────────────────────────────────────── +const ScrollArea = styled.div` + position: relative; + z-index: 10; + flex: 1; + overflow-y: auto; + padding: 1.5rem 1.4rem 4rem; + scrollbar-width: none; + &::-webkit-scrollbar { display: none; } + + @media (min-width: 768px) { + padding: 2rem 2.5rem 4rem; + max-width: 720px; + margin: 0 auto; + width: 100%; + } +`; + +// ─── HERO ───────────────────────────────────────────────────── +const Hero = styled.div` + margin-bottom: 2.5rem; + animation: ${fadeUp} 0.7s ease both; +`; + +const Badge = styled.div` + display: inline-flex; + align-items: center; + gap: 0.5rem; + background: rgba(0,0,0,0.25); + border: 1px solid rgba(255,255,255,0.2); + border-radius: 100px; + padding: 0.3rem 0.9rem; + font-size: 0.68rem; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + color: rgba(255,255,255,0.75); + margin-bottom: 1rem; +`; + +const BadgeDot = styled.span` + width: 5px; height: 5px; + border-radius: 50%; + background: ${theme.colors.amber}; +`; + +const H1 = styled.h1` + font-family: ${theme.fonts.serif}; + font-size: clamp(2rem, 6vw, 3.2rem); + font-weight: 300; + color: #fff; + line-height: 1.2; + margin-bottom: 0.9rem; + em { font-style: italic; color: rgba(255,255,255,0.6); } +`; + +const HeroText = styled.p` + font-size: 0.9rem; + color: rgba(255,255,255,0.65); + line-height: 1.75; + max-width: 480px; +`; + +// ─── CATEGORY LABEL ──────────────────────────────────────────── +const CategoryLabel = styled.div` + font-size: 0.65rem; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + color: rgba(255,255,255,0.35); + margin: 2rem 0 0.8rem; + display: flex; + align-items: center; + gap: 0.8rem; + + &::after { + content: ''; + flex: 1; + height: 1px; + background: rgba(255,255,255,0.08); + } +`; + +// ─── FAQ ACCORDION CARD ─────────────────────────────────────── +// $open controls whether the answer is visible or not. +const FaqCard = styled.div` + background: rgba(0,0,0,0.22); + border: 1px solid ${({ $open }) => $open ? 'rgba(255,255,255,0.2)' : 'rgba(255,255,255,0.1)'}; + border-radius: 14px; + margin-bottom: 0.6rem; + overflow: hidden; + transition: border-color 0.2s; + animation: ${fadeUp} 0.5s ${({ $delay }) => $delay || '0s'} ease both; +`; + +// Clickable header row +const FaqQuestion = styled.button` + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1.1rem 1.2rem; + background: transparent; + border: none; + cursor: pointer; + text-align: left; +`; + +const QuestionText = styled.span` + font-size: 0.9rem; + font-weight: 500; + color: #fff; + line-height: 1.4; + + @media (min-width: 768px) { + font-size: 0.95rem; + } +`; + +// Rotating arrow icon +const Chevron = styled.span` + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 24px; height: 24px; + color: rgba(255,255,255,0.4); + transform: ${({ $open }) => $open ? 'rotate(180deg)' : 'rotate(0deg)'}; + transition: transform 0.25s ease; +`; + +// Answer text — hidden/shown with a height animation via max-height +const FaqAnswer = styled.div` + /* max-height trick: 0 = hidden, large value = visible */ + max-height: ${({ $open }) => $open ? '400px' : '0'}; + overflow: hidden; + transition: max-height 0.35s ease; +`; + +const AnswerInner = styled.div` + padding: 0 1.2rem 1.2rem; + font-size: 0.84rem; + font-weight: 300; + color: rgba(255,255,255,0.65); + line-height: 1.8; + border-top: 1px solid rgba(255,255,255,0.08); + padding-top: 0.9rem; + animation: ${({ $open }) => $open ? slideDown : 'none'} 0.3s ease both; +`; + +// ─── CONTACT CARD ───────────────────────────────────────────── +const ContactCard = styled.div` + background: rgba(255,217,125,0.07); + border: 1px solid rgba(255,217,125,0.2); + border-radius: 16px; + padding: 1.6rem 1.4rem; + margin-top: 2.5rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + animation: ${fadeUp} 0.6s 0.3s ease both; + + @media (min-width: 600px) { + flex-direction: row; + align-items: center; + justify-content: space-between; + } +`; + +const ContactText = styled.div``; + +const ContactTitle = styled.div` + font-family: ${theme.fonts.serif}; + font-size: 1.2rem; + font-weight: 300; + color: #fff; + margin-bottom: 0.25rem; +`; + +const ContactSub = styled.div` + font-size: 0.78rem; + color: rgba(255,255,255,0.5); +`; + +const ContactBtn = styled.a` + display: inline-flex; + align-items: center; + gap: 0.4rem; + background: rgba(255,217,125,0.15); + border: 1px solid rgba(255,217,125,0.35); + color: ${theme.colors.amber}; + border-radius: 100px; + padding: 0.6rem 1.3rem; + font-size: 0.82rem; + font-weight: 500; + text-decoration: none; + white-space: nowrap; + transition: background 0.2s; + + &:hover { background: rgba(255,217,125,0.25); } + &:active { transform: scale(0.97); } +`; + +// ─── FAQ DATA ───────────────────────────────────────────────── +// Categories with questions and answers. +// No data is fetched from an API — everything is hardcoded in this file. +const FAQ_DATA = [ + { + category: 'Om appen', + items: [ + { + q: 'Vad är Stoppa Proppen?', + a: 'Stoppa Proppen är en gratis hälsoapp som hjälper dig förstå, förebygga och hantera blodpropp. Appen erbjuder en symtomkoll, medicinpåminnelser och ett community där du kan möta andra med liknande erfarenheter.', + }, + { + q: 'Kostar det något att använda appen?', + a: 'Nej, Stoppa Proppen är helt gratis att använda. Vi tar inget betalt för grundfunktionerna och visar inga annonser.', + }, + { + q: 'Är appen godkänd av sjukvården?', + a: 'Appen är inte ett medicintekniskt hjälpmedel och ersätter inte sjukvård. All medicinsk information är baserad på Socialstyrelsens och 1177:s riktlinjer. Vid akuta symtom ska du alltid ringa 112 eller kontakta sjukvården.', + }, + ], + }, + { + category: 'Symtomkollen', + items: [ + { + q: 'Hur fungerar symtomkollen?', + a: 'Du svarar på ett antal frågor om dina symtom och riskfaktorer. Baserat på dina svar ger appen en preliminär riskbedömning. Det är viktigt att förstå att detta inte är en medicinsk diagnos — kontakta alltid sjukvården om du är orolig.', + }, + { + q: 'Vilka symtom på blodpropp bör jag känna till?', + a: 'Vanliga symtom på djup ventrombos (DVT) inkluderar svullnad, värme och ömhet i ett ben. Lungembolism kan ge plötslig andnöd, bröstsmärta och hosta. Vid dessa symtom — ring 112 omedelbart.', + }, + { + q: 'Sparas mina svar från symtomkollen?', + a: 'Just nu sparas inga svar permanet mellan sessioner. I kommande versioner av appen planerar vi att låta inloggade användare se sin historik.', + }, + ], + }, + { + category: 'Medicinpåminnelser', + items: [ + { + q: 'Hur lägger jag till en medicin?', + a: 'Gå till Medicinlådan via menyn och tryck på "Lägg till medicin". Fyll i namn, dos, tid och vilka dagar du tar medicinen. Appen påminner dig när det är dags.', + }, + { + q: 'Vad händer om jag missar en påminnelse?', + a: 'Påminnelsen visas som en notis om appen är öppen. I en framtida version planerar vi push-notiser som fungerar även när appen är stängd. Kontakta alltid din läkare vid frågor om missade doser.', + }, + { + q: 'Kan jag lägga till flera mediciner?', + a: 'Ja, du kan lägga till hur många mediciner som helst. Varje medicin kan ha sin egen tid och sina egna veckodagar.', + }, + ], + }, + { + category: 'Integritet & säkerhet', + items: [ + { + q: 'Hur hanteras mina personuppgifter?', + a: 'Vi samlar bara in den information som krävs för att appen ska fungera. Vi säljer aldrig dina uppgifter till tredje part. Läs vår integritetspolicy för fullständig information.', + }, + { + q: 'Kan jag ta bort mitt konto?', + a: 'Ja, du kan när som helst begära att ditt konto och alla dina uppgifter raderas. Kontakta oss via e-post så hanterar vi det inom 30 dagar.', + }, + ], + }, +]; + +// ─── ACCORDION COMPONENT ────────────────────────────────────── +// A simple accordion component that manages its own open/closed state. +// Separated from FaqPage to keep the code clean. +const FaqItem = ({ question, answer, delay }) => { + // open state: controls whether the answer is visible + const [open, setOpen] = useState(false); + + return ( + + setOpen(o => !o)}> + {question} + + + + + + + + {answer} + + + ); +}; + +// ─── PAGE ───────────────────────────────────────────────────── +const FaqPage = () => ( + + + © 2025 + + + + + + {/* Hero */} + + Vanliga frågor +

+ Svar på dina
+ frågor +

+ + Här hittar du svar på de vanligaste frågorna om appen, + symtomkollen och medicinpåminnelser. Hittar du inte svar? + Hör gärna av dig till oss. + +
+ + {/* Accordion per category */} + {FAQ_DATA.map((cat, ci) => ( +
+ {cat.category} + {cat.items.map((item, ii) => ( + + ))} +
+ ))} + + + + Hittade du inte svar? + Vi svarar vanligtvis inom ett par dagar. + + + Kontakta oss → + + + +
+
+ +); + +export default FaqPage; \ No newline at end of file diff --git a/frontend/src/pages/Hurdetfungerarpage.jsx b/frontend/src/pages/Hurdetfungerarpage.jsx new file mode 100644 index 000000000..5dae9b339 --- /dev/null +++ b/frontend/src/pages/Hurdetfungerarpage.jsx @@ -0,0 +1,447 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Link } from 'react-router-dom'; +import { Frame, Card, CornerLabel, fadeUp } from '../components/Layout.jsx'; +import Navbar from '../components/Navbar.jsx'; +import { theme } from '../styles/theme'; + + +// ─── SCROLL-AREA ────────────────────────────────────────────────────────────── +const ScrollArea = styled.div` + position: relative; + z-index: 10; + flex: 1; + overflow-y: auto; + padding: 1.5rem 1.4rem 4rem; + scrollbar-width: none; + &::-webkit-scrollbar { display: none; } + + @media (min-width: 768px) { + padding: 2rem 2.5rem 4rem; + max-width: 720px; + margin: 0 auto; + width: 100%; + } +`; + + +// ─── HERO ───────────────────────────────────────────────────────────────────── +const Hero = styled.div` + margin-bottom: 2.5rem; + animation: ${fadeUp} 0.7s ease both; +`; + +const Badge = styled.div` + display: inline-flex; + align-items: center; + gap: 0.5rem; + background: rgba(0,0,0,0.25); + border: 1px solid rgba(255,255,255,0.2); + border-radius: 100px; + padding: 0.3rem 0.9rem; + font-size: 0.68rem; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + color: rgba(255,255,255,0.75); + margin-bottom: 1rem; +`; + +const BadgeDot = styled.span` + width: 5px; height: 5px; + border-radius: 50%; + background: ${theme.colors.mint}; +`; + +const H1 = styled.h1` + font-family: ${theme.fonts.serif}; + font-size: clamp(2rem, 6vw, 3.2rem); + font-weight: 300; + color: #fff; + line-height: 1.2; + margin-bottom: 0.9rem; + em { font-style: italic; color: rgba(255,255,255,0.6); } +`; + +const HeroText = styled.p` + font-size: 0.9rem; + color: rgba(255,255,255,0.65); + line-height: 1.75; + max-width: 480px; +`; + + +// ─── DIVIDER ────────────────────────────────────────────────────────────────── +const Divider = styled.div` + height: 1px; + background: rgba(255,255,255,0.1); + margin: 2.5rem 0; +`; + + +// ─── SECTION LABEL ──────────────────────────────────────────────────────────── +const SectionLabel = styled.div` + font-size: 0.65rem; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + color: rgba(255,255,255,0.35); + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.8rem; + + &::after { + content: ''; + flex: 1; + height: 1px; + background: rgba(255,255,255,0.08); + } +`; + +const SectionTitle = styled.h2` + font-family: ${theme.fonts.serif}; + font-size: clamp(1.5rem, 4vw, 2rem); + font-weight: 300; + color: #fff; + line-height: 1.2; + margin-bottom: 1.2rem; + em { font-style: italic; color: rgba(255,255,255,0.6); } +`; + + +// ─── STEP CARDS ─────────────────────────────────────────────────────────────── +const StepList = styled.div` + display: flex; + flex-direction: column; + gap: 0.8rem; + margin-bottom: 2rem; +`; + +const StepCard = styled.div` + background: rgba(0,0,0,0.22); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 16px; + padding: 1.2rem 1.4rem; + display: flex; + gap: 1.1rem; + align-items: flex-start; + animation: ${fadeUp} 0.6s ${({ $delay }) => $delay || '0s'} ease both; +`; + +const StepNumber = styled.div` + font-family: ${theme.fonts.serif}; + font-size: 1.6rem; + font-weight: 300; + color: ${theme.colors.mint}; + line-height: 1; + flex-shrink: 0; + width: 28px; + margin-top: 0.1rem; +`; + +const StepContent = styled.div``; + +const StepTitle = styled.div` + font-size: 0.95rem; + font-weight: 500; + color: #fff; + margin-bottom: 0.3rem; +`; + +const StepText = styled.div` + font-size: 0.82rem; + font-weight: 300; + color: rgba(255,255,255,0.6); + line-height: 1.7; +`; + + +// ─── FEATURE CARDS ──────────────────────────────────────────────────────────── +const FeatureGrid = styled.div` + display: flex; + flex-direction: column; + gap: 0.8rem; + margin-bottom: 2rem; + + @media (min-width: 600px) { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 1rem; + } +`; + +const FeatureCard = styled.div` + background: rgba(0,0,0,0.22); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 16px; + padding: 1.4rem; + animation: ${fadeUp} 0.6s ${({ $delay }) => $delay || '0s'} ease both; +`; + +const FeatureIcon = styled.div` + width: 38px; height: 38px; + border-radius: 10px; + background: rgba(125,255,212,0.1); + border: 1px solid rgba(125,255,212,0.2); + display: flex; + align-items: center; + justify-content: center; + color: ${theme.colors.mint}; + margin-bottom: 0.9rem; +`; + +const FeatureTitle = styled.div` + font-family: ${theme.fonts.serif}; + font-size: 1.05rem; + font-weight: 400; + color: #fff; + margin-bottom: 0.4rem; +`; + +const FeatureText = styled.div` + font-size: 0.78rem; + color: rgba(255,255,255,0.55); + line-height: 1.65; +`; + + +// ─── INFO BOX ───────────────────────────────────────────────────────────────── +const InfoBox = styled.div` + background: rgba(255,217,125,0.07); + border: 1px solid rgba(255,217,125,0.25); + border-radius: 14px; + padding: 1.2rem 1.4rem; + display: flex; + gap: 0.9rem; + align-items: flex-start; + margin-bottom: 2rem; + animation: ${fadeUp} 0.6s 0.2s ease both; +`; + +const InfoIcon = styled.div` + font-size: 1.1rem; + flex-shrink: 0; + margin-top: 0.1rem; +`; + +const InfoText = styled.div` + font-size: 0.82rem; + color: rgba(255,217,125,0.85); + line-height: 1.7; + + strong { + color: rgba(255,217,125,1); + font-weight: 500; + } +`; + + +// ─── CTA CARD ───────────────────────────────────────────────────────────────── +const CtaCard = styled.div` + background: rgba(125,255,212,0.07); + border: 1px solid rgba(125,255,212,0.2); + border-radius: 18px; + padding: 1.8rem 1.4rem; + text-align: center; + animation: ${fadeUp} 0.6s 0.3s ease both; + + @media (min-width: 768px) { + padding: 2.5rem 2rem; + } +`; + +const CtaTitle = styled.h3` + font-family: ${theme.fonts.serif}; + font-size: 1.6rem; + font-weight: 300; + color: #fff; + margin-bottom: 0.6rem; +`; + +const CtaText = styled.p` + font-size: 0.82rem; + color: rgba(255,255,255,0.6); + margin-bottom: 1.4rem; + line-height: 1.7; +`; + +const CtaBtn = styled(Link)` + display: inline-flex; + align-items: center; + gap: 0.5rem; + background: rgba(255,255,255,0.92); + color: ${theme.colors.tealDeep}; + border-radius: 100px; + padding: 0.85rem 2rem; + font-size: 0.88rem; + font-weight: 500; + text-decoration: none; + transition: background 0.2s, transform 0.15s; + + @media (min-width: 768px) { + &:hover { background: #fff; transform: translateY(-1px); } + } + &:active { transform: scale(0.98); } +`; + + +// ─── PAGE ───────────────────────────────────────────────────────────────────── +const HurDetFungerarPage = () => ( + + + © 2025 + + + + + + {/* Hero */} + + Guide +

+ Kom igång på
+ tre minuter +

+ + Stoppa Proppen är byggt för att vara enkelt att använda — + oavsett om du är van vid appar eller inte. Här går vi igenom + hur allt fungerar, steg för steg. + +
+ + + {/* Step-by-step */} + Steg-för-steg + Så här börjar du + + + + 1 + + Skapa ett gratis konto + + Tryck på "Kom igång gratis" på startsidan. Ange din + e-postadress och välj ett lösenord. Det tar mindre än en minut. + + + + + + 2 + + Gör en symtomkoll + + Svara på några enkla frågor om hur du mår. Appen ger dig + en preliminär riskbedömning och råd om vad du bör göra härnäst. + + + + + + 3 + + Lägg till dina mediciner + + Gå till Medicinlådan och lägg till dina mediciner med namn, + dos och tid. Appen påminner dig varje dag när det är dags. + + + + + + 4 + + Följ din hälsa över tid + + Markera mediciner som tagna, gör nya symtomkollar och håll + koll på ditt välmående — allt på ett och samma ställe. + + + + + + + + Funktioner + Vad du kan göra + + + + + + + + + Symtomkoll + + Besvara frågor om dina symtom och få en riskbedömning + baserad på medicinska riktlinjer. + + + + + + + + + + + Medicinlådan + + Håll koll på dina dagliga mediciner med påminnelser + och en tydlig checklista. + + + + + + + + + + + + + Community + + Möt andra med liknande erfarenheter och dela tips + i en trygg och stöttande miljö. + + + + + + + + ⚠️ + + Viktigt att veta: + Stoppa Proppen ersätter inte sjukvård och ställer inga diagnoser. + Vid akuta symtom som plötslig andnöd, bröstsmärta eller kraftig + svullnad i ett ben — ring 112 omedelbart. + + + + {/* CTA */} + + Redo att testa? + + Skapa ett gratis konto och kom igång direkt. + Det tar mindre än en minut. + + Kom igång gratis → + + +
+
+ +); + +export default HurDetFungerarPage; diff --git a/frontend/src/pages/LandingPage.jsx b/frontend/src/pages/LandingPage.jsx new file mode 100644 index 000000000..043694df8 --- /dev/null +++ b/frontend/src/pages/LandingPage.jsx @@ -0,0 +1,290 @@ +import React from 'react'; +import styled, { keyframes } from 'styled-components'; +import { Link } from 'react-router-dom'; +import { Frame, Card, CornerLabel, fadeUp, fadeLeft } from '../components/Layout.jsx'; +import Navbar from '../components/Navbar.jsx'; +import { theme } from '../styles/theme'; + +// ─── ANIMATIONS ────────────────────────────────────────────── +// Grows the progress bar from 0 to its final width +const growBar = keyframes`from { width: 0; }`; + + +// ─── HERO ───────────────────────────────────────────────────── +// MOBILE (base): centered content, less padding +// DESKTOP 768+: more padding all around +const Hero = styled.div` + /* MOBIL */ + position: relative; + z-index: 10; + flex: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 1.8rem; + gap: 1rem; + + /* DESKTOP 768+ */ + @media (min-width: 768px) { + padding: 3rem 3rem 5rem; + } +`; + +// Large heading +// clamp() makes the size responsive without media queries: +// min 2.4rem, grows with screen width, max 5.2rem +const H1 = styled.h1` + font-family: ${theme.fonts.serif}; + font-size: clamp(2.4rem, 6vw, 5.2rem); + font-weight: 300; + line-height: 1.1; + letter-spacing: -0.01em; + color: #fff; + max-width: 700px; + animation: ${fadeUp} 0.8s 0.1s ease both; + text-shadow: 0 2px 12px rgba(0,0,0,0.3); + + em { + font-style: italic; + color: rgba(255,255,255,0.75); + } +`; + +// Subtitle / description text +const HeroSub = styled.p` + /* MOBIL */ + font-size: 0.92rem; + font-weight: 300; + color: rgba(255,255,255,0.88); + max-width: 440px; + line-height: 1.7; + animation: ${fadeUp} 0.8s 0.2s ease both; + text-shadow: 0 1px 4px rgba(0,0,0,0.3); + + /* DESKTOP 768+ */ + @media (min-width: 768px) { + font-size: 1rem; + } +`; + +// Wrapper for buttons/links below the text +const HeroActions = styled.div` + display: flex; + gap: 1rem; + align-items: center; + animation: ${fadeUp} 0.8s 0.3s ease both; + flex-wrap: wrap; + justify-content: center; +`; + +// Subtle ghost link, e.g. "Read more →" +const BtnGhostLink = styled(Link)` + font-size: 0.85rem; + font-weight: 400; + color: rgba(255,255,255,0.88); + cursor: pointer; + letter-spacing: 0.01em; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 0.4rem; + transition: color 0.2s; + text-shadow: 0 1px 3px rgba(0,0,0,0.3); + &:hover { color: #fff; } +`; + + +// ─── STATS PILLS ────────────────────────────────────────────── +// Three pills at the bottom of the hero section. +// MOBILE (base): hidden — takes too much space on small screen +// DESKTOP 768+: shown at the bottom of the card +const Stats = styled.div` + /* MOBIL: hidden */ + /* display: none; */ + display: flex; + justify-content: center; + gap: 0.6rem; + padding: 0 1.4rem 2rem; + flex-wrap: wrap; + animation: ${fadeUp} 0.8s 0.4s ease both; + + /* DESKTOP 768+: shown */ + @media (min-width: 768px) { + position: absolute; + bottom: 2.5rem; + left: 0; + right: 0; + z-index: 10; + justify-content: center; + gap: 1rem; + padding: 0 3rem; + animation: ${fadeUp} 0.8s 0.4s ease both; + flex-wrap: wrap; + } +`; + +const StatPill = styled.div` + background: rgba(0,0,0,0.3); + backdrop-filter: blur(12px); + border: 1px solid rgba(255,255,255,0.2); + border-radius: 100px; + padding: 0.5rem 1.2rem; + display: flex; + align-items: center; + gap: 0.6rem; + font-size: 0.78rem; + color: rgba(255,255,255,0.9); + + strong { font-weight: 500; color: #fff; } +`; + +const StatSep = styled.span` + width: 1px; + height: 14px; + background: rgba(255,255,255,0.25); +`; + + +// ─── FLOAT CARD ─────────────────────────────────────────────── +// The floating card with the "risk assessment" animation. +// MOBILE (base): hidden — takes too much space +// DESKTOP 900+: shown to the right of the hero text +const FloatCard = styled.div` + /* MOBILE: hidden */ + display: none; + + /* DESKTOP 900+: shown as a floating card */ + @media (min-width: 900px) { + display: block; + position: absolute; + top: 50%; + right: 3.5rem; + transform: translateY(-60%); + z-index: 20; + background: rgba(10,40,46,0.8); + backdrop-filter: blur(20px); + border: 1px solid rgba(255,255,255,0.2); + border-radius: 18px; + padding: 1.4rem 1.6rem; + min-width: 220px; + box-shadow: ${theme.shadow.float}; + animation: ${fadeLeft} 0.8s 0.5s ease both; + } +`; + +const FloatLabel = styled.div` + font-size: 0.7rem; + font-weight: 500; + letter-spacing: 0.08em; + text-transform: uppercase; + color: rgba(255,255,255,0.65); + margin-bottom: 0.6rem; +`; + +const FloatTitle = styled.div` + font-family: ${theme.fonts.serif}; + font-size: 1.3rem; + font-weight: 400; + color: #fff; + line-height: 1.3; + margin-bottom: 0.5rem; +`; + +const FloatStatus = styled.div` + display: flex; + align-items: center; + gap: 0.4rem; + font-size: 0.75rem; + color: rgba(255,255,255,0.75); +`; + +const StatusDot = styled.span` + width: 6px; + height: 6px; + border-radius: 50%; + background: ${theme.colors.mint}; +`; + +const ProgressTrack = styled.div` + margin-top: 1rem; + height: 4px; + background: rgba(255,255,255,0.15); + border-radius: 2px; + overflow: hidden; +`; + +const ProgressFill = styled.div` + height: 100%; + width: 68%; + background: linear-gradient(90deg, rgba(255,255,255,0.5), rgba(255,255,255,0.9)); + border-radius: 2px; + animation: ${growBar} 1.5s 1s ease both; +`; + +const FloatMeta = styled.div` + display: flex; + justify-content: space-between; + margin-top: 0.5rem; + font-size: 0.7rem; + color: rgba(255,255,255,0.65); +`; + + +// ─── PAGE ───────────────────────────────────────────────────── +const LandingPage = () => ( + + + 2025 + + + + +

+ Skydda dig mot
+ blodproppar +

+ + + Enkel, medicinsk vägledning för att förstå, förebygga och + agera vid tecken på blodpropp — alltid i din ficka. + + + + + Läs mer + + +
+ + {/* Stats hidden on mobile, shown on desktop */} + + Medicinsk vägledningGratis + Community & stödAlltid tillgänglig + Snabb hjälpSymtomkoll på under 60 sek + + + {/* FloatCard hidden on mobile, shown on desktop */} + + Din riskbedömning + Symtomanalys
pågår…
+ 3 av 5 steg klara + + Genomfört68% +
+
+ +); + + +// ─── SVG ICONS ──────────────────────────────────────────────── +const ArrowIcon = () => ( + + + +); + +export default LandingPage; \ No newline at end of file diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx new file mode 100644 index 000000000..117e4b38c --- /dev/null +++ b/frontend/src/pages/LoginPage.jsx @@ -0,0 +1,568 @@ +import React, { useState } from 'react'; +import styled, { keyframes } from 'styled-components'; +import { Frame, fadeUp, fadeLeft } from '../components/Layout.jsx'; +import { theme } from '../styles/theme'; +import useUserStore from '../store/userStore.js'; +import { useNavigate, Link } from 'react-router-dom'; + +const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8081'; //if the production doesnt work, use an env variabel + +// ─── Split card ─────────────────────────────────────────────── +const SplitCard = styled.div` + width: 100%; + min-height: 100vh; + position: relative; + display: flex; + flex-direction: column; + border-radius: 24px; + overflow: hidden; + + /* mobile: clean gradient, no split */ + background: linear-gradient(160deg, #1a6b76 0%, #0d4a52 100%); + + @media (min-width: 700px) { + min-height: 620px; + border-radius: 24px; + max-width: 1050px; + display: grid; + grid-template-columns: 1fr 1fr; + box-shadow: ${theme.shadow.card}; + } +`; + +// ─── Left panel — desktop only ─────────────────────────────── +const LeftPanel = styled.div` + display: none; + + @media (min-width: 700px) { + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 2.5rem; + background: linear-gradient(160deg, #1a6b76 0%, #1a6b76 20%, #4a9fa8 55%, #1a6b76 80%, #0d4a52 100%); + position: relative; + + &::after { + content: ''; + position: absolute; + inset: 0; + background: rgba(8,35,40,0.42); + pointer-events: none; + z-index: 1; + } + + &::before { + content: ''; + position: absolute; + inset: 0; + background-image: repeating-linear-gradient( + 90deg, transparent, transparent 3px, + rgba(255,255,255,0.025) 3px, rgba(255,255,255,0.025) 4px + ); + pointer-events: none; + z-index: 0; + } + } +`; + +const LeftInner = styled.div` + position: relative; + z-index: 5; +`; + +const LogoWrap = styled(Link)` + display: flex; + align-items: center; + gap: 0.6rem; + text-decoration: none; + position: relative; + z-index: 5; +`; + +const LogoIcon = styled.div` + width: 32px; height: 32px; + border-radius: 50%; + background: rgba(255,255,255,0.2); + border: 1.5px solid rgba(255,255,255,0.35); + display: flex; align-items: center; justify-content: center; +`; + +const LogoText = styled.span` + font-size: 0.9rem; + font-weight: 500; + color: rgba(255,255,255,0.9); +`; + +const LeftHeading = styled.h2` + font-family: ${theme.fonts.serif}; + font-size: 3rem; + font-weight: 300; + line-height: 1.15; + color: #fff; + margin-bottom: 1rem; + animation: ${fadeUp} 0.8s ease both; + + em { font-style: italic; color: rgba(255,255,255,0.78); } +`; + +const LeftSubtext = styled.p` + font-size: 0.88rem; + font-weight: 300; + color: rgba(255,255,255,0.85); + line-height: 1.7; + max-width: 280px; +`; + +const TrustBadges = styled.div` + position: relative; + z-index: 5; + display: flex; + flex-direction: column; + gap: 0.6rem; +`; + +const TrustItem = styled.div` + display: flex; + align-items: center; + gap: 0.7rem; + font-size: 0.75rem; + color: rgba(255,255,255,0.85); +`; + +const TrustIcon = styled.div` + width: 28px; height: 28px; + border-radius: 50%; + background: rgba(255,255,255,0.15); + border: 1px solid rgba(255,255,255,0.28); + display: flex; align-items: center; justify-content: center; + flex-shrink: 0; +`; + +// ─── Right form panel ───────────────────────────────────────── +const RightPanel = styled.div` + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + padding: 2.5rem 1.4rem; + + @media (min-width: 700px) { + background: rgba(10,40,46,0.88); + backdrop-filter: blur(20px); + border-left: 1px solid rgba(255,255,255,0.1); + padding: 3rem; + animation: ${fadeLeft} 0.8s 0.2s ease both; + } +`; + +const CloseBtn = styled(Link)` + position: absolute; + top: 1.2rem; + right: 1.2rem; + z-index: 20; + width: 36px; + height: 36px; + border-radius: 50%; + background: rgba(255,255,255,0.1); + border: 1px solid rgba(255,255,255,0.2); + display: flex; + align-items: center; + justify-content: center; + color: rgba(255,255,255,0.75); + text-decoration: none; + transition: background 0.2s, color 0.2s; + + &:hover { background: rgba(255,255,255,0.18); color: #fff; } + &:active { transform: scale(0.95); } +`; + +// Mobile logo — only visible on mobile ──────────────────────── +const MobileLogo = styled.div` + display: flex; + align-items: center; + gap: 0.6rem; + margin-bottom: 1rem; + z-index: 10; + position: relative; + + @media (min-width: 700px) { + display: none; + } +`; + +const FormHeading = styled.h3` + font-family: ${theme.fonts.serif}; + font-size: 2rem; + font-weight: 300; + color: #fff; + margin-bottom: 0.2rem; + +`; + +const FormSubtext = styled.p` + font-size: 0.82rem; + color: rgba(255,255,255,0.65); + margin-bottom: 2rem; +`; + +const FormGroup = styled.div` + margin-bottom: 1.2rem; +`; + +const Label = styled.label` + display: block; + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + color: rgba(255,255,255,0.65); + margin-bottom: 0.5rem; +`; + +const Input = styled.input` + width: 100%; + background: rgba(255,255,255,0.1); + border: 1px solid rgba(255,255,255,0.2); + border-radius: 10px; + padding: 0.9rem 1.1rem; + font-size: 1rem; /* 16px — prevents zoom on iOS */ + font-weight: 400; + color: #fff; + outline: none; + transition: border-color 0.2s, background 0.2s; + -webkit-appearance: none; + + &::placeholder { color: rgba(255,255,255,0.35); } + &:focus { + border-color: rgba(125,255,212,0.5); + background: rgba(255,255,255,0.14); + } +`; + +const ForgotRow = styled.div` + display: flex; + justify-content: flex-end; + margin-top: -0.5rem; + margin-bottom: 1rem; +`; + +const ForgotLink = styled(Link)` + font-size: 0.75rem; + color: rgba(255,255,255,0.6); + text-decoration: none; + transition: color 0.2s; + &:hover { color: rgba(255,255,255,0.9); } +`; + +const SubmitBtn = styled.button` + width: 100%; + background: rgba(255,255,255,0.92); + color: ${theme.colors.tealDeep}; + border: none; + border-radius: 100px; + padding: 1rem; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s, transform 0.15s; + box-shadow: 0 8px 24px rgba(0,0,0,0.2); + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + + &:hover:not(:disabled) { background: #fff; transform: translateY(-1px); } + &:active:not(:disabled) { transform: scale(0.98); } + &:disabled { opacity: 0.7; cursor: not-allowed; } +`; + +const spin = keyframes` + to { transform: rotate(360deg); } +`; + +const Spinner = styled.span` + display: inline-block; + width: 1rem; + height: 1rem; + border: 2px solid ${theme.colors.tealMid}; + border-top-color: transparent; + border-radius: 50%; + animation: ${spin} 0.7s linear infinite; +`; + +const Divider = styled.div` + display: flex; + align-items: center; + gap: 1rem; + margin: 0.8rem 0; +`; + +const DividerLine = styled.div` + flex: 1; + height: 1px; + background: rgba(255,255,255,0.15); + margin: 0; +`; + +const DividerText = styled.span` + font-size: 0.72rem; + color: rgba(255,255,255,0.55); + letter-spacing: 0.04em; +`; + +const GhostBtn = styled.button` + width: 100%; + background: transparent; + color: rgba(255,255,255,0.8); + border: 1px solid rgba(255,255,255,0.25); + border-radius: 100px; + padding: 1rem; + font-size: 0.9rem; + font-weight: 400; + cursor: pointer; + transition: border-color 0.2s, color 0.2s; + &:hover { border-color: rgba(255,255,255,0.45); color: #fff; } + &:active { transform: scale(0.98); } +`; + +const RegisterLink = styled.p` + text-align: center; + margin-top: 1rem; + font-size: 0.8rem; + color: rgba(255,255,255,0.6); + + button { + background: none; + border: none; + padding: 0; + color: ${theme.colors.mint}; + text-decoration: none; + font-size: inherit; + &:hover { color: #fff; } + cursor: pointer; + } +`; + +const CornerLabel = styled.span` + display: none; + + @media (min-width: 700px) { + display: block; + position: absolute; + color: rgba(255,255,255,0.45); + font-size: 0.62rem; + letter-spacing: 0.05em; + z-index: 10; + + ${({ pos }) => ({ + 'tl': 'top: 1rem; left: 1.2rem;', + 'tr': 'top: 1rem; right: 1.2rem;', + 'bl': 'bottom: 1rem; left: 1.2rem;', + 'br': 'bottom: 1rem; right: 1.2rem;', + }[pos])} + } +`; + +// ─── Page ───────────────────────────────────────────────────── +const LoginPage = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + + const [isLogin, setIsLogin] = useState(true); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const navigate = useNavigate(); + const login = useUserStore(state => state.login); + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + setError(''); + + const response = await fetch(`${BASE_URL}/api/login`, { + + method: "POST", + headers: { + "Content-Type": "application/json" //we sending json + }, + body: JSON.stringify({ + email: email, + password: password + }) + }); + + const data = await response.json() + + if (response.ok) { + login(data) + navigate('/profil'); + } else { + setError(data.message) + setLoading(false); + } + }; + + const handleRegister = async (e) => { + e.preventDefault(); + setLoading(true); + setError(''); + + const response = await fetch(`${BASE_URL}/api/register`, { + + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + email: email, + password: password + }) + }); + + const data = await response.json() + + if (response.ok){ + setLoading(false); + setIsLogin(true); + } else { + setError(data.message) + setLoading(false); + } + } + + return ( + + + + + + + + + Säker anslutning + v0.1 + + {/* Left — desktop only */} + + + + + + + + Stoppa Proppen + + + Välkommen
tillbaka
+ Din hälsojournal och symtomhistorik väntar. Logga in för att fortsätta din riskbedömning. +
+ + Evidensbaserad medicinsk information + Krypterad och säker datahantering + GDPR-säker hantering av persondata + +
+ + {/* Right — form */} + + {/* Logo only visible on mobile */} + + + + + + + Stoppa Proppen + + + {isLogin ? ( + <> + Logga in + Ange dina uppgifter för att fortsätta + +
+ + + setEmail(e.target.value)} required /> + + + + setPassword(e.target.value)} required /> + + + Glömt lösenordet? + + {error &&

{error}

} + + {loading ? <>Laddar... : 'Logga in'} + +
+ + + ELLER + + + window.location.href = '/medicin'}> + Fortsätt som gäst + + + + Inget konto? + + + ) : ( + <> + Skapa Konto + Ange dina uppgifter för att fortsätta + +
+ + + setEmail(e.target.value)} required /> + + + + setPassword(e.target.value)} required /> + + + Har du redan ett lösenord? + + {error &&

{error}

} + + {loading ? <>Laddar... : 'Registrera'} + +
+ + )} +
+
+ + + ); +}; + +const StarIcon = () => ( + + + +); +const LockIcon = () => ( + + + + +); +const CheckCircleIcon = () => ( + + + + +); + +export default LoginPage; diff --git a/frontend/src/pages/MedicinPage.jsx b/frontend/src/pages/MedicinPage.jsx new file mode 100644 index 000000000..a280664bf --- /dev/null +++ b/frontend/src/pages/MedicinPage.jsx @@ -0,0 +1,970 @@ +import React, { useState } from 'react'; +import styled, { keyframes, css } from 'styled-components'; +import { toast, ToastContainer } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; +import { Frame, Card, fadeUp } from '../components/Layout.jsx'; +import Navbar from '../components/Navbar.jsx'; +import { theme } from '../styles/theme'; +import BottomNav from "../components/BottomNav.jsx" + +// react-toastify: toast = triggers a notification, ToastContainer = must exist in the tree +// ─── ANIMATIONS ────────────────────────────────────────────────────────────── + +// Slides up from below — used on the medicine cards and modal sheet +const slideUp = keyframes` + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +`; + +// Bounce animation when a medicine is marked as taken +const checkPop = keyframes` + 0% { transform: scale(0.8); } + 60% { transform: scale(1.2); } + 100% { transform: scale(1); } +`; + +// Simple fade-in for the modal background +const fadeIn = keyframes` + from { opacity: 0; } + to { opacity: 1; } +`; + + +// ─── SCROLL AREA ───────────────────────────────────────────────────────────── +// Contains the entire medicine list and is scrollable. +// padding-bottom is large (7rem) so content is not hidden behind the fixed button. +// The scrollbar is hidden visually for a cleaner mobile appearance. +const ScrollArea = styled.div` + /* MOBILE: full width, some padding on the sides */ + position: relative; + z-index: 10; + flex: 1; + overflow-y: auto; + padding: 0.5rem 1.4rem 7rem; /* large bottom padding due to the fixed button */ + + /* Hides the scrollbar in Firefox */ + scrollbar-width: none; + /* Hides the scrollbar in Chrome/Safari */ + &::-webkit-scrollbar { display: none; } + + /* DESKTOP 768+: limit width and center */ + @media (min-width: 768px) { + padding: 0.5rem 2.5rem 5rem; + max-width: 640px; + margin: 0 auto; + width: 100%; + } +`; + + +// ─── DAY STATUS SECTION ─────────────────────────────────────────────────────── +// Shows date, weekday name and how many medicines are left. +// No responsive differences here — works equally well on all screens. +const DayHeader = styled.div` + margin-bottom: 1.5rem; + animation: ${fadeUp} 0.6s ease both; +`; + +// Small label above, e.g. "26 February" +const DayLabel = styled.div` + font-size: 0.68rem; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + color: rgba(255,255,255,0.55); + margin-bottom: 0.3rem; +`; + +// Large serif heading, e.g. "Monday — 2 left" +// clamp() makes the font size responsive without media queries: +// minimum 1.6rem, grows by 5% of the viewport width, max 2.2rem +const DayTitle = styled.h2` + font-family: ${theme.fonts.serif}; + font-size: clamp(1.6rem, 5vw, 2.2rem); + font-weight: 300; + color: #fff; + line-height: 1.2; + em { font-style: italic; color: rgba(255,255,255,0.65); } +`; + +// Subtext, e.g. "1 of 2 medicines taken today" +const DaySubtext = styled.p` + font-size: 0.82rem; + color: rgba(255,255,255,0.6); + margin-top: 0.4rem; +`; + + +// ─── SECTION HEADING ─────────────────────────────────────────────────────────── +// The headings "Today" and "Tomorrow". +// ::after creates the horizontal line to the right of the text. +const SectionTitle = styled.div` + font-size: 0.65rem; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + color: rgba(255,255,255,0.45); + margin: 1.4rem 0 0.7rem; + display: flex; + align-items: center; + gap: 0.8rem; + + &::after { + content: ''; + flex: 1; + height: 1px; + background: rgba(255,255,255,0.1); + } +`; + + +// ─── MEDICINE CARD ───────────────────────────────────────────────────────────── +// $taken (boolean): controls green or neutral colour +// $delay (string): animation delay per card for sequential slide-in + +const MedCard = styled.div` + /* MOBILE: compact padding */ + background: ${({ $taken }) => $taken ? 'rgba(125,255,212,0.08)' : 'rgba(0,0,0,0.25)'}; + border: 1px solid ${({ $taken }) => $taken ? 'rgba(125,255,212,0.3)' : 'rgba(255,255,255,0.15)'}; + border-radius: 14px; + padding: 0.85rem 1rem; + margin-bottom: 0.6rem; + display: flex; + align-items: center; + gap: 0.75rem; + transition: border-color 0.3s, background 0.3s; + animation: ${slideUp} 0.5s ${({ $delay }) => $delay || '0s'} ease both; + + /* DESKTOP 768+: more space and rounder corners */ + @media (min-width: 768px) { + border-radius: 16px; + padding: 1rem 1.2rem; + gap: 0.9rem; + margin-bottom: 0.7rem; + } +`; + +// Round check button on the left. +// $taken controls colour. The checkPop animation only plays when $taken is true. +const CheckBtn = styled.button` + /* MOBILE: slightly larger for easier touch interaction */ + width: 40px; + height: 40px; + border-radius: 50%; + border: 1.5px solid ${({ $taken }) => $taken ? 'rgba(125,255,212,0.8)' : 'rgba(255,255,255,0.3)'}; + background: ${({ $taken }) => $taken ? 'rgba(125,255,212,0.2)' : 'transparent'}; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + flex-shrink: 0; + transition: all 0.2s; + + ${({ $taken }) => $taken && css`animation: ${checkPop} 0.3s ease;`} + + &:active { transform: scale(0.92); } + + /* DESKTOP 768+: standard size */ + @media (min-width: 768px) { + width: 36px; + height: 36px; + } +`; + +// Middle column with name and dose — clickable to open the edit modal +const MedInfo = styled.div` + flex: 1; + min-width: 0; /* prevents text overflow outside the card */ + cursor: pointer; +`; + +// Medicine name with strikethrough and faded colour when taken +const MedName = styled.div` + /* MOBILE: standard size */ + font-size: 0.92rem; + font-weight: 500; + color: ${({ $taken }) => $taken ? 'rgba(255,255,255,0.45)' : '#fff'}; + text-decoration: ${({ $taken }) => $taken ? 'line-through' : 'none'}; + transition: color 0.3s; + + /* DESKTOP 768+: slightly larger */ + @media (min-width: 768px) { + font-size: 0.95rem; + } +`; + +// Dose text, e.g. "5 mg" +const MedDose = styled.div` + font-size: 0.75rem; + color: rgba(255,255,255,0.4); + margin-top: 0.15rem; +`; + +// Right column: time, taken-time and action buttons +const MedRight = styled.div` + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.3rem; +`; + +// Time text, e.g. "kl 20:00" (amber) or "✓ Tagen" (green) +const MedTime = styled.div` + font-size: 0.72rem; + font-weight: 500; + color: ${({ $taken }) => $taken ? 'rgba(125,255,212,0.7)' : 'rgba(255,217,125,0.9)'}; + white-space: nowrap; +`; + +// Timestamp for when the medicine was actually taken, e.g. "08:14" +const TakenTime = styled.div` + font-size: 0.68rem; + color: rgba(125,255,212,0.55); +`; + + +// ─── ACTION BUTTONS (EDIT / DELETE) ───────────────────────────────────────── +// $danger (boolean): false = neutral (pencil), true = red (trash) +const ActionRow = styled.div` + display: flex; + gap: 0.35rem; + flex-shrink: 0; +`; + +const IconBtn = styled.button` + /* MOBILE: touch-friendly size (44px recommended by Apple/Google) */ + width: 34px; + height: 34px; + border-radius: 8px; + border: 1px solid ${({ $danger }) => $danger ? 'rgba(255,138,125,0.3)' : 'rgba(255,255,255,0.15)'}; + background: ${({ $danger }) => $danger ? 'rgba(255,138,125,0.1)' : 'rgba(255,255,255,0.06)'}; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: ${({ $danger }) => $danger ? '#ff8a7d' : 'rgba(255,255,255,0.6)'}; + transition: all 0.2s; + + &:active { transform: scale(0.92); } + + /* DESKTOP 768+: more compact size + hover effects (hover does not exist on touch) */ + @media (min-width: 768px) { + width: 30px; + height: 30px; + + &:hover { + background: ${({ $danger }) => $danger ? 'rgba(255,138,125,0.2)' : 'rgba(255,255,255,0.12)'}; + color: ${({ $danger }) => $danger ? '#ff8a7d' : '#fff'}; + border-color: ${({ $danger }) => $danger ? 'rgba(255,138,125,0.5)' : 'rgba(255,255,255,0.3)'}; + } + } +`; + +// ─── TOMORROW CARD ───────────────────────────────────────────────────────────── +// Dimmed, informative cards without action buttons. +const TomorrowCard = styled.div` + background: rgba(0,0,0,0.15); + border: 1px solid rgba(255,255,255,0.08); + border-radius: 14px; + padding: 0.9rem 1.2rem; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + gap: 0.8rem; + opacity: 0.6; +`; + +const TomorrowDot = styled.div` + width: 8px; height: 8px; + border-radius: 50%; + border: 1.5px solid rgba(255,255,255,0.35); + flex-shrink: 0; +`; + +const TomorrowName = styled.div` + font-size: 0.88rem; + color: rgba(255,255,255,0.65); + flex: 1; +`; + +const TomorrowTime = styled.div` + font-size: 0.72rem; + color: rgba(255,255,255,0.35); +`; + +// The button is fixed at the bottom. +const AddBtnWrap = styled.div` + position: fixed; + bottom: 4rem; + left: 0; right: 0; + z-index: 50; + padding: 1rem 1.4rem 1.6rem; + + @media (min-width: 768px) { + bottom: 2rem; + max-width: 640px; + left: 50%; + transform: translateX(-50%); + padding: 1rem 2.5rem 1.4rem; + } +`; + +const AddBtn = styled.button` + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 0.6rem; + background: rgba(255,255,255,0.92); + color: ${theme.colors.tealDeep}; + border: none; + border-radius: 100px; + padding: 0.95rem; + font-size: 0.92rem; + font-weight: 500; + cursor: pointer; + box-shadow: 0 8px 30px rgba(0,0,0,0.3); + transition: background 0.2s, transform 0.15s; + + /* Hover only exists on desktop — on mobile :active is used instead */ + @media (min-width: 768px) { + &:hover { background: #fff; transform: translateY(-1px); } + } + &:active { transform: scale(0.98); } +`; + +// ─── MODAL BACKDROP ─────────────────────────────────────────────────────────── +// Covers the entire screen behind the modal. + +const Backdrop = styled.div` + /* MOBILE: modal slides up from below */ + position: fixed; + inset: 0; + background: rgba(0,0,0,0.65); + backdrop-filter: blur(6px); + z-index: 200; + display: flex; + align-items: flex-end; /* bottom sheet on mobile */ + justify-content: center; + animation: ${fadeIn} 0.2s ease both; + + /* DESKTOP 768+: centered modal */ + @media (min-width: 768px) { + align-items: center; + } +`; + +// ─── CONFIRM DIALOG (DELETE) ───────────────────────────────────────────────── +// Shown when the user presses the trash icon. +// Requires an extra click to confirm — prevents mistakes. +const ConfirmSheet = styled.div` + /* MOBILE: bottom sheet style */ + background: #0d3d45; + border: 1px solid rgba(255,138,125,0.25); + border-radius: 20px 20px 0 0; /* rounded corners only at the top */ + padding: 1.6rem 1.4rem 2.2rem; + width: 100%; + max-width: 400px; + animation: ${slideUp} 0.3s ease both; + text-align: center; + + /* DESKTOP 768+: dialog centered with rounded corners all around */ + @media (min-width: 768px) { + border-radius: 20px; + margin: 1rem; + padding: 2rem; + } +`; + +const ConfirmTitle = styled.h3` + font-family: ${theme.fonts.serif}; + font-size: 1.4rem; + font-weight: 300; + color: #fff; + margin-bottom: 0.5rem; +`; + +const ConfirmSub = styled.p` + font-size: 0.82rem; + color: rgba(255,255,255,0.5); + margin-bottom: 1.5rem; +`; + +const DeleteConfirmBtn = styled.button` + width: 100%; + background: rgba(255,138,125,0.2); + color: #ff8a7d; + border: 1px solid rgba(255,138,125,0.4); + border-radius: 100px; + padding: 0.9rem; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + margin-bottom: 0.5rem; + transition: background 0.2s; + + @media (min-width: 768px) { + &:hover { background: rgba(255,138,125,0.3); } + } + &:active { transform: scale(0.98); } +`; + + +// ─── BOTTOM SHEET / MODAL (ADD & EDIT) ─────────────────────────────────────── +// The same Sheet component is used for both modes. +// modalMode ('add' | 'edit') controls heading and button text. + +const Sheet = styled.div` + /* MOBILE: full width, bottom sheet */ + background: #0d3d45; + border: 1px solid rgba(255,255,255,0.15); + border-radius: 20px 20px 0 0; + padding: 1.6rem 1.4rem 2.2rem; + width: 100%; + max-width: 500px; + animation: ${slideUp} 0.3s ease both; + + /* DESKTOP 768+: centered modal */ + @media (min-width: 768px) { + border-radius: 24px; + margin: 1rem; + padding: 2rem; + } +`; + +// Small handle at the top — convention for bottom sheets on mobile. +// Hidden on desktop since it is not a bottom sheet there. +const SheetHandle = styled.div` + /* MOBILE: visible handle */ + width: 36px; + height: 3px; + background: rgba(255,255,255,0.2); + border-radius: 2px; + margin: 0 auto 1.5rem; + + /* DESKTOP 768+: hidden */ + @media (min-width: 768px) { + display: none; + } +`; + +const SheetTitle = styled.h3` + font-family: ${theme.fonts.serif}; + font-size: 1.6rem; + font-weight: 300; + color: #fff; + margin-bottom: 1.5rem; +`; + +const FieldGroup = styled.div` + margin-bottom: 1.1rem; +`; + +const FieldLabel = styled.label` + display: block; + font-size: 0.7rem; + font-weight: 500; + letter-spacing: 0.07em; + text-transform: uppercase; + color: rgba(255,255,255,0.5); + margin-bottom: 0.45rem; +`; + +// Text fields and time fields in the form. +// automatically on focus — below 16px auto-zoom is triggered. +// -webkit-appearance: none removes iOS default styles (border, shadow etc.) +const FieldInput = styled.input` + /* MOBILE: large enough to avoid iOS auto-zoom */ + width: 100%; + background: rgba(255,255,255,0.08); + border: 1px solid rgba(255,255,255,0.15); + border-radius: 10px; + padding: 0.85rem 1rem; /* touch-friendly height */ + font-size: 1rem; /* 16px = iOS threshold for auto-zoom */ + color: #fff; + outline: none; + transition: border-color 0.2s; + -webkit-appearance: none; /* removes iOS default styles */ + + &::placeholder { color: rgba(255,255,255,0.22); } + &:focus { border-color: rgba(125,255,212,0.5); } +`; + +// Row of day buttons (M T W T F S S) +const DayRow = styled.div` + display: flex; + gap: 0.4rem; +`; + +// Day button. $active controls green (selected) or grey (unselected) style. +// flex: 1 makes all seven buttons take equal space. +// Slightly larger padding on mobile for easier touch. +const DayToggle = styled.button` + /* MOBILE: generous touch area */ + flex: 1; + padding: 0.65rem 0; + border-radius: 8px; + font-size: 0.72rem; + font-weight: 500; + border: 1px solid ${({ $active }) => $active ? 'rgba(125,255,212,0.5)' : 'rgba(255,255,255,0.15)'}; + background: ${({ $active }) => $active ? 'rgba(125,255,212,0.15)' : 'transparent'}; + color: ${({ $active }) => $active ? 'rgba(125,255,212,0.95)' : 'rgba(255,255,255,0.45)'}; + cursor: pointer; + transition: all 0.15s; + + &:active { transform: scale(0.95); } + + /* DESKTOP 768+: more compact padding */ + @media (min-width: 768px) { + padding: 0.55rem 0; + } +`; + +// Save button at the bottom of the form +const SaveBtn = styled.button` + width: 100%; + background: rgba(255,255,255,0.92); + color: ${theme.colors.tealDeep}; + border: none; + border-radius: 100px; + padding: 0.95rem; + font-size: 0.92rem; + font-weight: 500; + cursor: pointer; + margin-top: 1.4rem; + transition: background 0.2s; + + @media (min-width: 768px) { + &:hover { background: #fff; } + } + &:active { transform: scale(0.98); } +`; + +// Cancel button — subtle text style +const CancelBtn = styled.button` + width: 100%; + background: transparent; + color: rgba(255,255,255,0.45); + border: none; + padding: 0.75rem; + font-size: 0.85rem; + cursor: pointer; + margin-top: 0.3rem; + + @media (min-width: 768px) { + &:hover { color: rgba(255,255,255,0.75); } + } +`; + +// ─── EMPTY STATE ────────────────────────────────────────────────────────────── +// Shown when the medicines list is empty. +const EmptyState = styled.div` + text-align: center; + padding: 3rem 1rem; + animation: ${fadeUp} 0.6s 0.2s ease both; +`; + +const EmptyIcon = styled.div` + width: 56px; height: 56px; + border-radius: 50%; + background: rgba(255,255,255,0.06); + border: 1px solid rgba(255,255,255,0.1); + display: flex; align-items: center; justify-content: center; + margin: 0 auto 1rem; +`; + +const EmptyText = styled.p` + font-family: ${theme.fonts.serif}; + font-size: 1.3rem; + font-weight: 300; + color: rgba(255,255,255,0.55); + margin-bottom: 0.4rem; +`; + +const EmptySub = styled.p` + font-size: 0.8rem; + color: rgba(255,255,255,0.3); +`; + +// ─── CONSTANTS AND HELPER FUNCTIONS ────────────────────────────────────────── + +// Swedish abbreviations for the day toggle buttons +const DAY_NAMES = ['Mån', 'Tis', 'Ons', 'Tor', 'Fre', 'Lör', 'Sön']; + +// Returns today's name in Swedish. getDay() returns 0 (Sun) – 6 (Sat). +const getTodayName = () => { + const names = ['Söndag', 'Måndag', 'Tisdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lördag']; + return names[new Date().getDay()]; +}; + +// Returns the date formatted in Swedish, e.g. "26 februari" +const getDateString = () => + new Date().toLocaleDateString('sv-SE', { day: 'numeric', month: 'long' }); + +// Empty form — used to reset on open/close +const EMPTY_FORM = { name: '', dose: '', time: '08:00' }; + +// Reusable helper function for toast styles. +// Accepts an optional border colour and text colour so each type looks different. +const toastStyle = ( + border = 'rgba(255,255,255,0.15)', + color = 'rgba(255,255,255,0.9)' +) => ({ + position: 'top-center', + autoClose: 3000, + style: { + background: '#0d3d45', + color, + border: `1px solid ${border}`, + borderRadius: '12px', + fontSize: '0.88rem', + }, +}); + + +// ─── MAIN COMPONENT ─────────────────────────────────────────────────────────── +const MedicinPage = () => { + + // ── STATE ──────────────────────────────────────────────────────────────── + // The list of medicines. Each object: + // id, name, dose, time ("HH:MM"), days ([0-6]), taken (bool), takenAt (str|null) + const [medicines, setMedicines] = useState([ + /* { id: 1, name: 'Waran', dose: '5 mg', time: '08:00', days: [0,1,2,3,4,5,6], taken: true, takenAt: '08:14' }, + { id: 2, name: 'Xarelto', dose: '20 mg', time: '20:00', days: [0,1,2,3,4,5,6], taken: false, takenAt: null }, */ + ]); + + // null = closed, 'add' = add, 'edit' = edit + const [modalMode, setModalMode] = useState(null); + + // ID of the medicine being edited (null if none being edited) + const [editingId, setEditingId] = useState(null); + + // ID of the medicine whose delete dialog is shown (null if none) + const [confirmId, setConfirmId] = useState(null); + + // Selected weekdays in the form (array with indices 0-6) + const [activeDays, setActiveDays] = useState([0,1,2,3,4,5,6]); + + // Form data + const [form, setForm] = useState(EMPTY_FORM); + + // ── COMPUTED VALUES ───────────────────────────────────────────────────── + const takenCount = medicines.filter(m => m.taken).length; + const remainingCount = medicines.filter(m => !m.taken).length; + + + // ── OPEN "ADD" MODAL ─────────────────────────────────────────────────── + const openAdd = () => { + setForm(EMPTY_FORM); + setActiveDays([0,1,2,3,4,5,6]); + setEditingId(null); + setModalMode('add'); + }; + + // ── OPEN "EDIT" MODAL ────────────────────────────────────────────────── + // Fills the form with existing values and sets editingId + const openEdit = (med) => { + setForm({ name: med.name, dose: med.dose, time: med.time }); + setActiveDays(med.days); + setEditingId(med.id); + setModalMode('edit'); + }; + + // ── CLOSE MODAL ─────────────────────────────────────────────────────────── + const closeModal = () => { + setModalMode(null); + setEditingId(null); + }; + + + // ── SAVE (NEW OR UPDATE) ───────────────────────────────────────────────── + const saveMedicine = () => { + if (!form.name.trim()) return; + + if (modalMode === 'edit') { + // map() returns a new array where only the right medicine is changed + setMedicines(prev => prev.map(m => + m.id === editingId + ? { ...m, name: form.name, dose: form.dose, time: form.time, days: activeDays } + : m + )); + toast(`✏️ ${form.name} uppdaterad!`, + toastStyle('rgba(125,255,212,0.3)', '#7dffd4')); + + } else { + const newMed = { + id: Date.now(), // unique ID via timestamp + name: form.name, dose: form.dose, time: form.time, + days: activeDays, taken: false, takenAt: null, + }; + setMedicines(prev => [...prev, newMed]); + + // Schedule toast reminder if selected time is later today + const [h, min] = form.time.split(':').map(Number); + const triggerTime = new Date(); + triggerTime.setHours(h, min, 0, 0); + const msUntil = triggerTime - Date.now(); + + if (msUntil > 0 && msUntil < 24 * 60 * 60 * 1000) { + setTimeout(() => { + toast(`🔔 Dags att ta ${form.name}! (${form.dose})`, + toastStyle('rgba(255,217,125,0.4)', '#fff')); + }, msUntil); + } + + toast(`💊 ${form.name} tillagd! Påminnelse kl ${form.time}.`, toastStyle()); + } + + closeModal(); + }; + + + // ── MARK AS TAKEN / UNTAKEN ────────────────────────────────────────────── + const toggleTaken = (id) => { + setMedicines(prev => prev.map(m => { + if (m.id !== id) return m; + if (!m.taken) { + const timeStr = new Date().toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit' }); + toast(`✓ ${m.name} markerad som tagen!`, + toastStyle('rgba(125,255,212,0.3)', '#7dffd4')); + return { ...m, taken: true, takenAt: timeStr }; + } + return { ...m, taken: false, takenAt: null }; + })); + }; + + + // ── DELETE ─────────────────────────────────────────────────────────────── + // filter() returns all medicines EXCEPT the one with confirmId + const deleteMedicine = () => { + const med = medicines.find(m => m.id === confirmId); + setMedicines(prev => prev.filter(m => m.id !== confirmId)); + setConfirmId(null); + toast(`🗑 ${med?.name} borttagen.`, + toastStyle('rgba(255,138,125,0.3)', '#ff8a7d')); + }; + + + // ── TOGGLE WEEKDAY ──────────────────────────────────────────────────────── + const toggleDay = (i) => + setActiveDays(prev => + prev.includes(i) ? prev.filter(d => d !== i) : [...prev, i] + ); + + return ( + + + {/* Medicinlåda */} + {/* Stoppa Proppen */} + + + + + + {/* Day status */} + + {getDateString()} + + {getTodayName()}{' '} + {remainingCount === 0 ? '— allt klart! 🎉' : `— ${remainingCount} kvar`} + + {medicines.length > 0 && ( + {takenCount} av {medicines.length} mediciner tagna idag + )} + + + Idag + + {/* Empty state or medicine list */} + {medicines.length === 0 ? ( + + + Inga mediciner tillagda + Tryck på knappen nedan för att lägga till din första medicin + + ) : ( + // $delay staggers the animation per card → sequential slide-in + medicines.map((med, i) => ( + + + {/* Check button — toggles taken/untaken */} + toggleTaken(med.id)}> + {med.taken + ? + :
+ } + + + {/* Name + dose — click opens the edit modal */} + openEdit(med)}> + {med.name} + {med.dose} + + + {/* Right column */} + + + {med.taken ? '✓ Tagen' : `kl ${med.time}`} + + {med.takenAt && {med.takenAt}} + + + openEdit(med)} title="Redigera"> + + + setConfirmId(med.id)} title="Ta bort"> + + + + + + + )) + )} + + {/* Tomorrow section */} + {medicines.length > 0 && ( + <> + Imorgon + {medicines.map(med => ( + + + {med.name} — {med.dose} + kl {med.time} + + ))} + + )} + + + + {/* Fixed Add button */} + + + + Lägg till medicin + + + + + + + {/* ── ADD / EDIT MODAL ── */} + {modalMode && ( + e.target === e.currentTarget && closeModal()}> + + + + {modalMode === 'edit' ? 'Redigera medicin' : 'Lägg till medicin'} + + + + Namn på medicin + setForm(f => ({ ...f, name: e.target.value }))} + autoFocus + /> + + + + Dos + setForm(f => ({ ...f, dose: e.target.value }))} + /> + + + + Påminnelsetid + setForm(f => ({ ...f, time: e.target.value }))} + /> + + + + Dagar + + {DAY_NAMES.map((day, i) => ( + toggleDay(i)} + > + {day.charAt(0)} + + ))} + + + + + {modalMode === 'edit' ? 'Spara ändringar' : 'Spara medicin'} + + Avbryt + + + )} + + {/* ── CONFIRM DIALOG (DELETE) ── */} + {confirmId && ( + e.target === e.currentTarget && setConfirmId(null)}> + + + Ta bort medicin? + + {medicines.find(m => m.id === confirmId)?.name} kommer att tas bort permanent. + + Ja, ta bort + setConfirmId(null)}>Avbryt + + + )} + + + ); +}; + + +// ─── SVG ICONS ─────────────────────────────────────────────────────────────── + +const CheckIcon = ({ color = 'white' }) => ( + + + +); + +const PlusIcon = () => ( + + + +); + +const EditIcon = () => ( + + + +); + +const TrashIcon = () => ( + + + +); + +const PillIcon = () => ( + + + + +); + +export default MedicinPage; diff --git a/frontend/src/pages/OmossPage.jsx b/frontend/src/pages/OmossPage.jsx new file mode 100644 index 000000000..1afb11786 --- /dev/null +++ b/frontend/src/pages/OmossPage.jsx @@ -0,0 +1,402 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Link } from 'react-router-dom'; +import { Frame, Card, CornerLabel, fadeUp } from '../components/Layout.jsx'; +import Navbar from '../components/Navbar.jsx'; +import { theme } from '../styles/theme'; + +// ─── SCROLL-AREA ────────────────────────────────────────────── +const ScrollArea = styled.div` + position: relative; + z-index: 10; + flex: 1; + overflow-y: auto; + padding: 1.5rem 1.4rem 4rem; + scrollbar-width: none; + &::-webkit-scrollbar { display: none; } + + @media (min-width: 768px) { + padding: 2rem 2.5rem 4rem; + max-width: 720px; + margin: 0 auto; + width: 100%; + } +`; + +// ─── HERO SECTION ───────────────────────────────────────────── +const Hero = styled.div` + margin-bottom: 3rem; + animation: ${fadeUp} 0.7s ease both; +`; + +const Badge = styled.div` + display: inline-flex; + align-items: center; + gap: 0.5rem; + background: rgba(0,0,0,0.25); + border: 1px solid rgba(255,255,255,0.2); + border-radius: 100px; + padding: 0.3rem 0.9rem; + font-size: 0.68rem; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + color: rgba(255,255,255,0.75); + margin-bottom: 1rem; +`; + +const BadgeDot = styled.span` + width: 5px; height: 5px; + border-radius: 50%; + background: ${theme.colors.mint}; +`; + +const H1 = styled.h1` + font-family: ${theme.fonts.serif}; + font-size: clamp(2.2rem, 7vw, 3.8rem); + font-weight: 300; + line-height: 1.15; + color: #fff; + margin-bottom: 1.2rem; + + em { + font-style: italic; + color: rgba(255,255,255,0.65); + } +`; + +const HeroText = styled.p` + font-size: 0.95rem; + font-weight: 300; + color: rgba(255,255,255,0.75); + line-height: 1.8; + max-width: 520px; +`; + +// ─── DIVIDER ────────────────────────────────────────────────── +const Divider = styled.div` + height: 1px; + background: rgba(255,255,255,0.1); + margin: 2.5rem 0; +`; + +// ─── SECTION HEADING ────────────────────────────────────────── +const SectionLabel = styled.div` + font-size: 0.65rem; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + color: rgba(255,255,255,0.4); + margin-bottom: 1rem; +`; + +// ─── VALUE CARDS (the three card columns) ──────────────────── +const CardGrid = styled.div` + display: flex; + flex-direction: column; + gap: 0.8rem; + margin-bottom: 2.5rem; + + @media (min-width: 600px) { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 1rem; + } +`; + +const ValueCard = styled.div` + background: rgba(0,0,0,0.25); + border: 1px solid rgba(255,255,255,0.12); + border-radius: 16px; + padding: 1.4rem; + animation: ${fadeUp} 0.6s ${({ $delay }) => $delay || '0s'} ease both; +`; + +const ValueIcon = styled.div` + width: 36px; height: 36px; + border-radius: 10px; + background: rgba(125,255,212,0.12); + border: 1px solid rgba(125,255,212,0.25); + display: flex; align-items: center; justify-content: center; + margin-bottom: 0.9rem; + color: ${theme.colors.mint}; +`; + +const ValueTitle = styled.div` + font-family: ${theme.fonts.serif}; + font-size: 1.1rem; + font-weight: 400; + color: #fff; + margin-bottom: 0.4rem; +`; + +const ValueText = styled.div` + font-size: 0.78rem; + color: rgba(255,255,255,0.55); + line-height: 1.65; +`; + +// ─── BODY TEXT SECTION ──────────────────────────────────────── +const TextSection = styled.div` + margin-bottom: 2.5rem; + animation: ${fadeUp} 0.6s 0.2s ease both; +`; + +const SectionTitle = styled.h2` + font-family: ${theme.fonts.serif}; + font-size: clamp(1.5rem, 4vw, 2rem); + font-weight: 300; + color: #fff; + line-height: 1.2; + margin-bottom: 0.9rem; + + em { font-style: italic; color: rgba(255,255,255,0.6); } +`; + +const BodyText = styled.p` + font-size: 0.88rem; + font-weight: 300; + color: rgba(255,255,255,0.7); + line-height: 1.85; + margin-bottom: 0.9rem; +`; + +// ─── TEAM SECTION ───────────────────────────────────────────── +const TeamGrid = styled.div` + display: flex; + flex-direction: column; + gap: 0.8rem; + margin-bottom: 2.5rem; + + @media (min-width: 480px) { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + } +`; + +const TeamCard = styled.div` + background: rgba(0,0,0,0.2); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 14px; + padding: 1.2rem; + display: flex; + align-items: center; + gap: 1rem; + animation: ${fadeUp} 0.6s ${({ $delay }) => $delay || '0s'} ease both; +`; + +const TeamAvatar = styled.div` + width: 44px; height: 44px; + border-radius: 50%; + background: ${({ $color }) => $color || 'rgba(125,255,212,0.15)'}; + border: 1px solid rgba(255,255,255,0.2); + display: flex; align-items: center; justify-content: center; + font-family: ${theme.fonts.serif}; + font-size: 1.1rem; + font-weight: 300; + color: #fff; + flex-shrink: 0; +`; + +const TeamInfo = styled.div``; + +const TeamName = styled.div` + font-size: 0.9rem; + font-weight: 500; + color: #fff; + margin-bottom: 0.15rem; +`; + +const TeamRole = styled.div` + font-size: 0.72rem; + color: rgba(255,255,255,0.45); +`; + +// ─── CTA CARD ───────────────────────────────────────────────── +const CtaCard = styled.div` + background: rgba(125,255,212,0.07); + border: 1px solid rgba(125,255,212,0.2); + border-radius: 18px; + padding: 1.8rem 1.4rem; + text-align: center; + animation: ${fadeUp} 0.6s 0.3s ease both; + + @media (min-width: 768px) { + padding: 2.5rem 2rem; + } +`; + +const CtaTitle = styled.h3` + font-family: ${theme.fonts.serif}; + font-size: 1.6rem; + font-weight: 300; + color: #fff; + margin-bottom: 0.6rem; +`; + +const CtaText = styled.p` + font-size: 0.82rem; + color: rgba(255,255,255,0.6); + margin-bottom: 1.4rem; + line-height: 1.7; +`; + +const CtaBtn = styled(Link)` + display: inline-flex; + align-items: center; + gap: 0.5rem; + background: rgba(255,255,255,0.92); + color: ${theme.colors.tealDeep}; + border-radius: 100px; + padding: 0.85rem 2rem; + font-size: 0.88rem; + font-weight: 500; + text-decoration: none; + transition: background 0.2s, transform 0.15s; + + &:hover { background: #fff; transform: translateY(-1px); } + &:active { transform: scale(0.98); } +`; + +// ─── PAGE ───────────────────────────────────────────────────── +const OmOssPage = () => ( + + + © 2025 + + + + + + {/* Hero */} + + Vårt uppdrag +

+ Medicinsk trygghet
+ för alla — alltid +

+ + Stoppa Proppen är en svensk hälsoapp skapad för att hjälpa + människor förstå, förebygga och hantera blodpropp. Vi kombinerar + evidensbaserad medicinsk information med ett varmt community + där ingen behöver känna sig ensam. + +
+ + + + {/* What we offer */} + + Vad vi erbjuder + Tre pelare + + + + + + + + + Symtomkoll + Svara på några frågor och få en preliminär riskbedömning baserad på kliniska riktlinjer. + + + + + + + + Community + Möt andra med liknande erfarenheter. Dela, stödja och lära av varandra i en trygg miljö. + + + + + + + + + Påminnelser + Håll koll på dina mediciner med dagliga påminnelser direkt i appen — enkelt och pålitligt. + + + + + + {/* Vår historia */} + + Bakgrund + Varför vi byggde detta + + Blodpropp drabbar varje år tusentals svenskar — och många av dem + beskriver känslan av att stå ensam med sin oro och sina frågor. + Sjukvården gör ett fantastiskt jobb, men det finns ett gap mellan + läkarbesöken som Stoppa Proppen vill fylla. + + + Appen startade som ett examensarbete på Technigo bootcamp + och har växt till ett riktigt projekt med målet att bli ett + komplement till den svenska vården — aldrig en ersättning. + + + Vi är alltid transparenta med vad appen kan + och inte kan — den ställer inga diagnoser. + + + + + + {/* Teamet */} + Teamet + + Människorna bakom + + + + R + + Rebecca + Grundare & utvecklare + + + + M + + Medicinsk rådgivare + + + + UX + + Design & tillgänglighet + UX-specialist + + + + LTU + + Luleå tekniska univ. + Akademisk handledning + + + + + + + {/* CTA */} + + Redo att komma igång? + + Skapa ett gratis konto och få tillgång till symtomkoll, + medicinpåminnelser och vårt community idag. + + Kom igång gratis → + + +
+
+ +); + +export default OmOssPage; \ No newline at end of file diff --git a/frontend/src/pages/ProfilPage.jsx b/frontend/src/pages/ProfilPage.jsx new file mode 100644 index 000000000..d1122a9f0 --- /dev/null +++ b/frontend/src/pages/ProfilPage.jsx @@ -0,0 +1,395 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Frame, Card, CornerLabel, fadeUp } from '../components/Layout.jsx'; +import Navbar from '../components/Navbar.jsx'; +import { theme } from '../styles/theme'; +import { Link } from 'react-router-dom'; +import useUserStore from '../store/userStore.js'; +import { useNavigate } from "react-router-dom"; + + +// ─── SCROLL-AREA ────────────────────────────────────────────────────────────── +const ScrollArea = styled.div` + position: relative; + z-index: 10; + flex: 1; + overflow-y: auto; + padding: 1.5rem 1.4rem 4rem; + scrollbar-width: none; + &::-webkit-scrollbar { display: none; } + + @media (min-width: 768px) { + padding: 2rem 2.5rem 4rem; + max-width: 720px; + margin: 0 auto; + width: 100%; + } +`; + + +// ─── HERO ───────────────────────────────────────────────────────────────────── +const Hero = styled.div` + margin-bottom: 2rem; + animation: ${fadeUp} 0.7s ease both; +`; + +const Badge = styled.div` + display: inline-flex; + align-items: center; + gap: 0.5rem; + background: rgba(0,0,0,0.25); + border: 1px solid rgba(255,255,255,0.2); + border-radius: 100px; + padding: 0.3rem 0.9rem; + font-size: 0.68rem; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + color: rgba(255,255,255,0.75); + margin-bottom: 1rem; +`; + +const BadgeDot = styled.span` + width: 5px; height: 5px; + border-radius: 50%; + background: ${theme.colors.mint}; +`; + +const H1 = styled.h1` + font-family: ${theme.fonts.serif}; + font-size: clamp(2rem, 6vw, 3.2rem); + font-weight: 300; + color: #fff; + line-height: 1.2; + margin-bottom: 0.4rem; + em { font-style: italic; color: rgba(255,255,255,0.6); } +`; + +const HeroSub = styled.p` + font-size: 0.88rem; + color: rgba(255,255,255,0.55); +`; + + +const Divider = styled.div` + height: 1px; + background: rgba(255,255,255,0.1); + margin: 2rem 0; +`; + + +const SectionLabel = styled.div` + font-size: 0.65rem; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + color: rgba(255,255,255,0.35); + margin-bottom: 0.8rem; + display: flex; + align-items: center; + gap: 0.8rem; + + &::after { + content: ''; + flex: 1; + height: 1px; + background: rgba(255,255,255,0.08); + } +`; + +const QuickGrid = styled.div` + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.8rem; + margin-bottom: 2rem; +`; + +const QuickCard = styled(Link)` + background: rgba(0,0,0,0.22); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 16px; + padding: 1.2rem; + text-decoration: none; + display: flex; + flex-direction: column; + gap: 0.6rem; + transition: border-color 0.2s, background 0.2s; + animation: ${fadeUp} 0.6s ${({ $delay }) => $delay || '0s'} ease both; + + &:hover { + border-color: rgba(255,255,255,0.2); + background: rgba(0,0,0,0.3); + } + &:active { transform: scale(0.98); } +`; + +const QuickIcon = styled.div` + width: 36px; height: 36px; + border-radius: 10px; + background: rgba(125,255,212,0.1); + border: 1px solid rgba(125,255,212,0.2); + display: flex; + align-items: center; + justify-content: center; + color: ${theme.colors.mint}; +`; + +const QuickTitle = styled.div` + font-size: 0.9rem; + font-weight: 500; + color: #fff; +`; + +const QuickSub = styled.div` + font-size: 0.72rem; + color: rgba(255,255,255,0.45); +`; + + +const AccountCard = styled.div` + background: rgba(0,0,0,0.22); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 16px; + padding: 1.2rem 1.4rem; + margin-bottom: 0.8rem; + animation: ${fadeUp} 0.6s 0.1s ease both; +`; + +const AccountRow = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +`; + +const AccountLabel = styled.div` + font-size: 0.7rem; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + color: rgba(255,255,255,0.4); + margin-bottom: 0.3rem; +`; + +const AccountValue = styled.div` + font-size: 0.92rem; + color: #fff; +`; + +const ActionList = styled.div` + display: flex; + flex-direction: column; + gap: 0.6rem; + margin-bottom: 2rem; +`; + +const ActionBtn = styled.button` + width: 100%; + background: rgba(0,0,0,0.22); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 14px; + padding: 1rem 1.4rem; + display: flex; + align-items: center; + gap: 0.9rem; + cursor: pointer; + text-align: left; + transition: background 0.2s, border-color 0.2s; + animation: ${fadeUp} 0.6s ${({ $delay }) => $delay || '0s'} ease both; + + &:hover { + background: rgba(0,0,0,0.3); + border-color: rgba(255,255,255,0.2); + } + &:active { transform: scale(0.98); } +`; + +const ActionIcon = styled.div` + width: 34px; height: 34px; + border-radius: 9px; + background: ${({ $color }) => $color || 'rgba(255,255,255,0.08)'}; + display: flex; + align-items: center; + justify-content: center; + color: ${({ $iconColor }) => $iconColor || 'rgba(255,255,255,0.6)'}; + flex-shrink: 0; +`; + +const ActionText = styled.div` + flex: 1; +`; + +const ActionTitle = styled.div` + font-size: 0.88rem; + font-weight: 500; + color: #fff; +`; + +const ActionSub = styled.div` + font-size: 0.72rem; + color: rgba(255,255,255,0.4); + margin-top: 0.1rem; +`; + +const ActionArrow = styled.div` + color: rgba(255,255,255,0.25); + font-size: 0.8rem; +`; + + +const DeleteBtn = styled.button` + width: 100%; + background: rgba(255,80,80,0.06); + border: 1px solid rgba(255,80,80,0.2); + border-radius: 14px; + padding: 1rem 1.4rem; + display: flex; + align-items: center; + gap: 0.9rem; + cursor: pointer; + text-align: left; + transition: background 0.2s, border-color 0.2s; + animation: ${fadeUp} 0.6s 0.3s ease both; + + &:hover { + background: rgba(255,80,80,0.1); + border-color: rgba(255,80,80,0.35); + } + &:active { transform: scale(0.98); } +`; + +const DeleteTitle = styled.div` + font-size: 0.88rem; + font-weight: 500; + color: rgba(255,120,120,0.9); +`; + +const DeleteSub = styled.div` + font-size: 0.72rem; + color: rgba(255,100,100,0.5); + margin-top: 0.1rem; +`; + + +// ─── PAGE ───────────────────────────────────────────────────────────────────── +const ProfilPage = () => { + const user = useUserStore(state => state.user); + const logout = useUserStore(state => state.logout); + const navigate = useNavigate(); + + return ( + + + Profil + © 2025 + + + + + + {/* Hero */} + + Inloggad +

+ Mitt konto +

+ Hantera ditt konto och dina inställningar +
+ + + Genvägar + + + + + + + + + Medicinlådan + Dina dagliga mediciner + + + + + + + + + Symtomkoll + Gör en ny bedömning + + + + + + Kontoinformation + + +
+ E-postadress + {/* email from Zustand-store */} + {user?.email} +
+
+
+ + + + Inställningar + + + {logout(); navigate("/login")}}> + + + + + + + + Logga ut + Avslutar din session + + + + + {}}> + + + + + + + + Byt lösenord + Uppdatera ditt lösenord + + + + + + + Farlig zon + {}}> + + + + + + + Radera konto + Tar bort all din data permanent + + + + +
+
+ +)}; + +export default ProfilPage; \ No newline at end of file diff --git a/frontend/src/store/userStore.js b/frontend/src/store/userStore.js new file mode 100644 index 000000000..21beda5cd --- /dev/null +++ b/frontend/src/store/userStore.js @@ -0,0 +1,24 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware" //saves the zustand storen in localstorage + +const useUserStore = create( + persist( + (set) => ({ + + user: null, + accessToken: null, + isLoggedIn: false, + + login: (userData) => set({ + user: { email: userData.email, id: userData.id }, + accessToken: userData.accessToken, + isLoggedIn: true + }), + + logout: () => set({ user: null, accessToken: null, isLoggedIn: false }) + }), + { name: "user-store" } + ) +); + +export default useUserStore \ No newline at end of file diff --git a/frontend/src/styles/Globalstyles.js b/frontend/src/styles/Globalstyles.js new file mode 100644 index 000000000..96403ac1e --- /dev/null +++ b/frontend/src/styles/Globalstyles.js @@ -0,0 +1,29 @@ +import { createGlobalStyle } from 'styled-components'; + +const GlobalStyles = createGlobalStyle` + *, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; + } + + html, body { + min-height: 100%; + font-family: 'DM Sans', sans-serif; + background: #0a2e33; + color: #ffffff; + overflow-x: hidden; + -webkit-font-smoothing: antialiased; + + font-size: 18px; + + @media (min-width: 768px) { + font-size: 17px; + } + } + + a { text-decoration: none; color: inherit; } + button { font-family: 'DM Sans', sans-serif; } +`; + +export default GlobalStyles; diff --git a/frontend/src/styles/theme.js b/frontend/src/styles/theme.js new file mode 100644 index 000000000..5c45f200c --- /dev/null +++ b/frontend/src/styles/theme.js @@ -0,0 +1,32 @@ +export const theme = { + colors: { + tealDeep: '#0d4a52', + tealMid: '#1a6b76', + tealLight: '#4a9fa8', + mint: '#7dffd4', + amber: '#ffd97d', + coral: '#ff8a7d', + white: '#ffffff', + dark: '#0a2e33', + }, + gradients: { + card: 'linear-gradient(160deg, #1a6b76 0%, #1a6b76 20%, #4a9fa8 55%, #1a6b76 80%, #0d4a52 100%)', + }, + overlay: 'rgba(8, 35, 40, 0.45)', + fonts: { + serif: "'Cormorant Garamond', Georgia, serif", + sans: "'DM Sans', sans-serif", + }, + radius: { + sm: '10px', + md: '16px', + lg: '18px', + xl: '24px', + pill: '100px', + }, + shadow: { + card: '0 40px 100px rgba(0,0,0,0.5)', + btn: '0 8px 24px rgba(0,0,0,0.2)', + float: '0 20px 60px rgba(0,0,0,0.35)', + }, +}; diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 000000000..8658d775f --- /dev/null +++ b/netlify.toml @@ -0,0 +1,9 @@ +[build] + base = "frontend" + command = "npm run build" + publish = "dist" + +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 diff --git a/package.json b/package.json deleted file mode 100644 index 680d19077..000000000 --- a/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "project-final-parent", - "version": "1.0.0", - "scripts": { - "postinstall": "npm install --prefix backend" - } -} \ No newline at end of file