`
+
+Error handling:
+- `400` for invalid request / failed signup validation flow
+- `409` for duplicate email on signup
+- `401` for invalid login or missing/invalid auth token
+- `404` for user not found (`/dashboard/:id`)
+- `500` for internal server errors
+- `502` for Mailchimp integration failure
+
+
+## â¶ïž Run Locally
+### 1. Clone repository
+```bash
+git clone https://github.com/mikaelasturk/womenation-membership
+cd womenation-membership
+```
+
+### 2. Start backend
+```bash
+cd backend
+npm install
+npm run dev
+```
+
+Optional environment variables for backend integrations:
+- `MONGO_URL`
+- `MAILCHIMP_API_KEY`
+- `MAILCHIMP_SERVER_PREFIX`
+- `MAILCHIMP_LIST_ID`
+
+### 3. Start frontend
+```bash
+cd ../frontend
+npm install
+npm run dev
+```
+
+
+## đ Roadmap
+Describe the planning process, implementation phase, and future improvements.
+
+### đ Project Documentation
+This project uses a structured workflow with interconnected documentation:
+
+- **[đ PLANNING.md](PLANNING.md)** - Detailed planning document with design decisions, tech stack, wireframes, and timelines
+- **[â
TODO.md](TODO.md)** - Active task tracking with priorities, status updates, and development progress
+
+**Workflow:**
+1. **Plan** â Use PLANNING.md to define goals, design, and architecture
+2. **Break Down** â Convert plan into actionable tasks in TODO.md
+3. **Execute** â Work through TODO items, updating status as you go
+4. **Document** â Update this README with completed features and learnings
+
+### Planning
+Brief description of initial planning and design decisions.
+Optional link to a planning document.
+[đ PLANNING.md](PLANNING.md)
+
+### Execution
+Describe how the project was implemented, including architectural decisions, state management, and challenges encountered.
+
+### Retrospect
+Summarize reflections, lessons learned, technical insights, and ideas for improvement.
+
+
+## đ§âđ» Authors
+
+### Carolina Oldertz
+- [GitHub](https://www.github.com/carro-barro)
+- [LinkedIn](https://www.linkedin.com/in/carolina-oldertz-a875601b8/)
+
+### Mikaela Sturk
+- [GitHub](https://www.github.com/mikaelasturk)
+- [LinkedIn](https://www.linkedin.com/in/mikaelasturk)
+
+
+## đ License
+This project was created as part of a Technigo course assignment and not to be shared outside of school.
+For educational purposes only.
+
+
+---
+
+
+
+ Made with â€ïž by Carolina Oldertz & Mikaela Sturk
+
\ No newline at end of file
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000000..92b0321ca5
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,78 @@
+> đ **Navigation:** [README](README.md) | [PLANNING](PLANNING.md)
+
+# TODO
+> **Open in preview**
+`Shift + Command + V on Mac`
+`Shift + Control + V on Windows`
+
+> đĄ **Workflow:** Plan â Break Down â Execute â Document
+
+## đ Table of Contents
+- [High Priority](#1)
+- [Medium Priority](#2)
+- [Low Priority](#3)
+- [In Progress](#4)
+- [Blocked](#5)
+- [Completed](#6)
+- [By Category](#7)
+- [Notes](#8)
+
+
+## đŽ High Priority
+- [ ] Feature or critical bug description (Due: YYYY-MM-DD)
+- [ ] Feature or critical bug description
+
+
+## đĄ Medium Priority
+- [ ] Enhancement or improvement description
+- [ ] Enhancement or improvement description
+
+
+## đą Low Priority
+- [ ] Nice-to-have feature or refactor description
+- [ ] Nice-to-have feature or refactor description
+
+
+## đ§ In Progress
+- [ ] Currently working on this feature/task
+- [ ] Currently working on this feature/task
+
+
+## đ« Blocked
+- [ ] Blocked task with reason [#issue-number](link-to-issue)
+- [ ] Waiting for external dependency or decision
+
+
+## â
Completed
+- [x] Completed feature or task description
+- [x] Completed feature or task description
+
+
+## đ By Category
+### Frontend
+- [ ] Component or UI-related task
+- [ ] Component or UI-related task
+
+### Backend
+- [ ] API, database, or server-related task
+- [ ] API, database, or server-related task
+
+### Testing
+- [ ] Unit, integration, or E2E test task
+- [ ] Unit, integration, or E2E test task
+
+### Documentation
+- [ ] README, comments, or docs update
+- [ ] README, comments, or docs update
+
+
+
+## đ Notes
+- Important reminder or consideration for the project
+- Browser compatibility note or technical constraint
+- Design decision or architectural choice to remember
+
+### đ Don't Forget
+- Update [PLANNING](PLANNING.md) when making architectural changes
+- Update [README](README.md) when completing major features
+- Move completed items to â
Completed section regularly
\ No newline at end of file
diff --git a/backend/config/googleOauthConfig.js b/backend/config/googleOauthConfig.js
new file mode 100644
index 0000000000..190820b259
--- /dev/null
+++ b/backend/config/googleOauthConfig.js
@@ -0,0 +1,7 @@
+// [ ] todo: implement Google OAuth configuration and add secrets in env file
+
+// cloudinary.config({
+// cloud_name: process.env.CLOUDINARY_NAME,
+// api_key: process.env.CLOUDINARY_KEY,
+// api_secret: process.env.CLOUDINARY_SECRET
+// })
\ No newline at end of file
diff --git a/backend/config/mailchimpConfig.js b/backend/config/mailchimpConfig.js
new file mode 100644
index 0000000000..4734f61bd8
--- /dev/null
+++ b/backend/config/mailchimpConfig.js
@@ -0,0 +1,55 @@
+import mailchimp from "@mailchimp/mailchimp_marketing"
+
+// [x] set up env
+// [x] hÀmta server prefix
+// [ ] skapa validering/error i ui för om emailadressen redan finns i mailchimp (Mailchimp returnerar 400 med "Member Exists" i body) Ăr detta en sĂ€kerhetssĂ„rbarhet?
+// [x] Kolla om premiumStartDate och premiumEndDate funkar och skickas med i Mailchimp
+// [ ] Skapa logik som taggar ny anvÀndare med "Vill bli medlem" i Mailchimp vid POST
+// [ ] Ta bort required pÄ alla fÀlt i Mailchimp, backend sköter validering sÄ det inte blir krock
+
+mailchimp.setConfig({
+ apiKey: process.env.MAILCHIMP_API_KEY,
+ server: process.env.MAILCHIMP_SERVER_PREFIX
+})
+
+const toMailchimpMember = (user, cityLabel) => ({
+ email_address: user.email,
+ status: "subscribed", //vi har single opt-in, sÄ vi kan ha "subscribed" och inte "pending"
+ merge_fields: {
+ FNAME: user.firstName,
+ LNAME: user.lastName,
+ CITY: cityLabel,
+ MOTIVE: user.justifyMembership,
+ // [ ] WORKSTATUS: user.workStatus,
+ MMERGE15: user.isPremium ? "Premium" : "Gratis",
+ PREM_START: user.premiumStartDate,
+ PREM_END: user.premiumEndDate,
+ CREATEDAT: user.userCreatedAt
+ }
+})
+
+export const addContactToMailchimp = async (user, cityLabel) => {
+
+ // Stoppa tidigt om input saknas.
+ if (!user?.email) {
+ throw new Error("Missing user email for Mailchimp")
+ }
+
+ // SÀkerstÀll att Mailchimp-konfig finns innan API-anrop.
+ if (!process.env.MAILCHIMP_API_KEY || !process.env.MAILCHIMP_SERVER_PREFIX || !process.env.MAILCHIMP_LIST_ID) {
+ throw new Error("Missing Mailchimp environment variables")
+ }
+
+ try {
+ const response = await mailchimp.lists.addListMember(
+ process.env.MAILCHIMP_LIST_ID,
+ toMailchimpMember(user, cityLabel)
+ )
+
+ return response
+
+ } catch (error) {
+ console.error("Mailchimp error:", error.response?.body || error.message)
+ throw error
+ }
+}
\ No newline at end of file
diff --git a/backend/middleware/authMiddleware.js b/backend/middleware/authMiddleware.js
new file mode 100644
index 0000000000..f0cdc9580a
--- /dev/null
+++ b/backend/middleware/authMiddleware.js
@@ -0,0 +1,25 @@
+// [ ] todo: implement middleware for user authentication and for premium content access which is then used in dashboardRoutes
+// [ ] note: do we need to add replace bearer in req.header and test loggedOut: true,
+
+import { User } from '../models/User'
+
+export const authenticateUser = async (request, response, next) => {
+ try {
+ const user = await User.findOne({ accessToken: request.header("Authorization").replace("Bearer ", "") })
+
+ if (user) {
+ request.user = user
+ next()
+ } else {
+ response.status(401).json({
+ message: "Authentication missing or invalid",
+ loggedOut: true
+ })
+ }
+ } catch (error) {
+ response.status(500).json({
+ message: "Internal Server Error",
+ error: error.message
+ })
+ }
+}
\ No newline at end of file
diff --git a/backend/models/User.js b/backend/models/User.js
new file mode 100644
index 0000000000..5426cc5a1c
--- /dev/null
+++ b/backend/models/User.js
@@ -0,0 +1,88 @@
+// [x] todo: implement user schema and model with Mongoose
+// [ ] todo: look into adding enum for city if neccessary
+// [ ] note: do we need ispremium in schema?
+
+import crypto from "crypto"
+import mongoose from "mongoose"
+
+const formatStockholm = (date) => date ? new Intl.DateTimeFormat("sv-SE", {
+ timeZone: "Europe/Stockholm",
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ }).format(date)
+: undefined;
+
+const userSchema = new mongoose.Schema({
+
+ firstName: {
+ type: String,
+ required: true,
+ minlength: 2
+ },
+ lastName: {
+ type: String,
+ required: true,
+ minlength: 2
+ },
+ email: {
+ type: String,
+ required: true,
+ unique: true,
+ lowercase: true
+ },
+ password: {
+ type: String,
+ required: true
+ },
+ accessToken: {
+ type: String,
+ default: () => crypto.randomBytes(128).toString("hex")
+ },
+ city: {
+ type: String,
+ required: true
+ },
+ justifyMembership: {
+ type: String,
+ required: true,
+ minlength: 10
+ },
+ isPremium: {
+ type: Boolean,
+ required: true,
+ default: false
+ },
+ premiumStartDate: {
+ type: Date,
+ default: function() {
+ return this.isPremium === true ? new Date() : undefined
+ }
+ },
+ premiumEndDate: {
+ type: Date,
+ default: function() {
+ return this.isPremium === true ? new Date(new Date().setFullYear(new Date().getFullYear() + 1)) : undefined
+ }
+ },
+ userCreatedAt: {
+ type: Date,
+ default: Date.now
+ }
+},
+
+{
+ toJSON: {
+ transform(doc, ret) {
+ ret.userCreatedAt = formatStockholm(ret.userCreatedAt);
+ ret.premiumStartDate = formatStockholm(ret.premiumStartDate);
+ ret.premiumEndDate = formatStockholm(ret.premiumEndDate);
+ }
+ }
+}
+)
+
+export const User = mongoose.model("User", userSchema)
\ No newline at end of file
diff --git a/backend/package.json b/backend/package.json
index 08f29f2448..9aed807425 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -12,9 +12,13 @@
"@babel/core": "^7.17.9",
"@babel/node": "^7.16.8",
"@babel/preset-env": "^7.16.11",
- "cors": "^2.8.5",
- "express": "^4.17.3",
- "mongoose": "^8.4.0",
- "nodemon": "^3.0.1"
+ "@mailchimp/mailchimp_marketing": "^3.0.80",
+ "bcrypt": "^6.0.0",
+ "cors": "^2.8.6",
+ "express": "^4.22.1",
+ "express-list-endpoints": "^7.1.1",
+ "mongodb": "^7.1.1",
+ "mongoose": "^8.23.0",
+ "nodemon": "^3.1.14"
}
-}
\ No newline at end of file
+}
diff --git a/backend/routes/dashboardRoutes.js b/backend/routes/dashboardRoutes.js
new file mode 100644
index 0000000000..0425aacf1d
--- /dev/null
+++ b/backend/routes/dashboardRoutes.js
@@ -0,0 +1,38 @@
+// [ ] todo: implement dashboard routes and connect middleware for user authentication and for premium content access (is some content visable to free users or is the whole "page" restricted?)
+// [ ] todo: implement patch request and get request to /mina-sidor
+// [ ] Skapa admin route. HĂ€r eller egen route?
+// [ ] Flytta ut controllers?
+
+import express from "express"
+//import { getDashboard } from "../controllers/dashboardController.js"
+import { User } from "../models/User"
+import { seedingUsers } from "../seedingDatabase/seedingUsers"
+
+const router = express.Router()
+seedingUsers()
+
+// Dashboard route - protected, only accessible to authenticated users
+router.get("/:id", async (request, response) => {
+ try {
+ const { id } = request.params
+ const user = await User.findById(id)
+
+ if (user) {
+ response.status(200).json({
+ message: "Hej!!!! VĂ€lkommen till din hemliga dashboard, " + user.firstName + "!!!",
+ user
+ })
+ } else {
+ response.status(404).json({
+ error: "user not found"
+ })
+ }
+
+ } catch (error) {
+ response.status(400).json({
+ error: "invalid request"
+ })
+ }
+})
+
+export default router
\ No newline at end of file
diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js
new file mode 100644
index 0000000000..2677c6bdb0
--- /dev/null
+++ b/backend/routes/userRoutes.js
@@ -0,0 +1,114 @@
+// [x] todo: implement user routes for signup and login locally
+// [ ] todo: make route for admin to view members and filter on premium(query)
+
+import express from "express"
+import bcrypt from "bcrypt"
+import { User } from "../models/User"
+import { addContactToMailchimp } from "../config/mailchimpConfig";
+import { seedingUsers } from "../seedingDatabase/seedingUsers";
+
+const router = express.Router()
+
+seedingUsers()
+
+router.get("/", async (request, response) => {
+
+ const user = await User.find().sort({ userCreatedAt: "desc"})
+ response.json(user)
+})
+
+// Signup route
+router.post("/signup", async (request, response) => {
+ try {
+ const { email, password, firstName, lastName, cityValue, cityLabel, justifyMembership, isPremium } = request.body
+ const existingUser = await User.findOne({ email: email.toLowerCase()})
+
+ if (existingUser) {
+ return response.status(409).json({
+ success: false,
+ message: "Email address already exists"
+ // [ ] Ăr detta en GDPR frĂ„ga? FĂ„r vi sĂ€ga att emailadressen redan finns?
+ })
+ }
+
+ const salt = bcrypt.genSaltSync()
+ const hashedPassword = bcrypt.hashSync(password, salt)
+
+ const user = new User({
+ email,
+ password: hashedPassword,
+ firstName,
+ lastName,
+ city: cityValue,
+ // [ ] lÀgg till cityLabel hÀr ocksÄ? Isf blir det cityLabel och cityValue i User schema istÀllet
+ // [ ] lÀgg till workStatus
+ justifyMembership,
+ isPremium: isPremium || false
+ })
+
+ const savedUser = await user.save()
+
+ try {
+ await addContactToMailchimp(savedUser, cityLabel)
+ } catch (error) {
+ const mailchimpError = error.response?.body || error.message
+
+ console.error("Kunde inte lÀgga till anvÀndare i Mailchimp:", mailchimpError)
+
+ return response.status(502).json({
+ success: false,
+ message: "User created, but Mailchimp sync failed",
+ response: mailchimpError
+ })
+ }
+
+ response.status(201).json({
+ success: true,
+ message: "User created successfully and added to Mailchimp",
+ response: savedUser
+ })
+
+ } catch (error) {
+ response.status(400).json({
+ success: false,
+ message: "Failed to create user",
+ response: error
+ })
+ }
+})
+
+// Login route
+router.post("/login", async (request, response) => {
+
+ try {
+ const { email, password } = request.body
+
+ const user = await User.findOne({email:email.toLowerCase()})
+
+ if (user && bcrypt.compareSync(password, user.password))
+ {
+ response.status(200).json({
+ success: true,
+ message: "Login successful",
+ response: {
+ email: user.email,
+ id: user._id,
+ accessToken: user.accessToken }
+ })
+ } else {
+ response.status(401).json({
+ success: false,
+ message: "Invalid email or password",
+ response: null
+ })
+ }
+ } catch(error) {
+ response.status(500).json({
+ success: false,
+ message: "Something went wrong during login",
+ response: error
+ })
+ }
+})
+
+export default router
\ No newline at end of file
diff --git a/backend/seedingDatabase/seedingUsers.js b/backend/seedingDatabase/seedingUsers.js
new file mode 100644
index 0000000000..f6d79fecab
--- /dev/null
+++ b/backend/seedingDatabase/seedingUsers.js
@@ -0,0 +1,41 @@
+import { User } from "../models/User"
+import bcrypt from "bcrypt"
+
+export const seedingUsers = async () => {
+ const salt = 10
+
+ if (process.env.RESET_SEED_DB === "true") {
+ console.log("Resetting seeding database...");
+ await User.deleteMany()
+ }
+
+ // To check if Carolina exists
+ const carolinaExists = await User.findOne({ email: "carolina.oldertz@gmail.com" })
+ if (!carolinaExists) {
+ await new User ({
+ firstName: "Carolina",
+ lastName: "Oldertz",
+ email: "carolina.oldertz@gmail.com",
+ password: bcrypt.hashSync("carolina", salt),
+ city: "Stockholm",
+ justifyMembership: "Hej pÄ dig!!!!!!!!!!",
+ isPremium: true,
+ premiumStartDate: new Date(),
+ premiumEndDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
+ }).save();
+ }
+
+ // To check if Mikaela exists
+ const mikaelaExists = await User.findOne({ email: "mikaelasturk@gmail.com" })
+ if (!mikaelaExists) {
+ await new User ({
+ firstName: "Mikaela",
+ lastName: "Sturk",
+ email: "mikaelasturk@gmail.com",
+ password: bcrypt.hashSync("mikaela", salt),
+ city: "Stockholm",
+ justifyMembership: "Hej pÄ digSAN!",
+ isPremium: false
+ }).save()
+ }
+}
\ No newline at end of file
diff --git a/backend/server.js b/backend/server.js
index 070c875189..75a23234e5 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -1,8 +1,17 @@
+// [ ] todo: connect user routes, premium user routes, and dashboard routes to server
+// [x] todo: implement listEndpoints from express-list-endpoints to "/"
+
import express from "express";
import cors from "cors";
import mongoose from "mongoose";
+import listEndpoints from "express-list-endpoints";
+import userRoutes from "./routes/userRoutes"
+import { authenticateUser } from "./middleware/authMiddleware";
+import dashboardRoutes from "./routes/dashboardRoutes"
+
-const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/final-project";
+// [ ] Make new connection in compass!
+const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/membership";
mongoose.connect(mongoUrl);
mongoose.Promise = Promise;
@@ -13,10 +22,19 @@ app.use(cors());
app.use(express.json());
app.get("/", (req, res) => {
- res.send("Hello Technigo!");
+ const endpoints = listEndpoints(app)
+ res.json({
+ message: "Welcome to Womenation API",
+ endpoints: endpoints
+ })
});
+
+app.use("/users", userRoutes)
+app.use("/dashboard", authenticateUser, dashboardRoutes)
+
+
// Start the server
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
-});
+});
\ No newline at end of file
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000000..2fd24fdb30
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,33 @@
+import js from '@eslint/js'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import globals from 'globals'
+
+export default [
+ { ignores: ['dist'] },
+ {
+ files: ['**/*.{js,jsx}'],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ parserOptions: {
+ ecmaVersion: 'latest',
+ ecmaFeatures: { jsx: true },
+ sourceType: 'module'
+ }
+ },
+ plugins: {
+ 'react-hooks': reactHooks,
+ 'react-refresh': reactRefresh
+ },
+ rules: {
+ ...js.configs.recommended.rules,
+ ...reactHooks.configs.recommended.rules,
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true }
+ ]
+ }
+ }
+]
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 0000000000..2fd24fdb30
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -0,0 +1,33 @@
+import js from '@eslint/js'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import globals from 'globals'
+
+export default [
+ { ignores: ['dist'] },
+ {
+ files: ['**/*.{js,jsx}'],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ parserOptions: {
+ ecmaVersion: 'latest',
+ ecmaFeatures: { jsx: true },
+ sourceType: 'module'
+ }
+ },
+ plugins: {
+ 'react-hooks': reactHooks,
+ 'react-refresh': reactRefresh
+ },
+ rules: {
+ ...js.configs.recommended.rules,
+ ...reactHooks.configs.recommended.rules,
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true }
+ ]
+ }
+ }
+]
diff --git a/frontend/index.html b/frontend/index.html
index 664410b5b9..6d0820371a 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -2,12 +2,19 @@
-
+
+
+
+
+
- Technigo React Vite Boiler Plate
+ Womenation
-
+
diff --git a/frontend/package.json b/frontend/package.json
index 7b2747e949..297b19a865 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,26 +1,32 @@
{
- "name": "project-final-backend",
- "description": "Client part of final project",
- "version": "1.0.0",
+ "name": "technigo-react-vite-boiler",
+ "private": true,
+ "version": "0.0.0",
"type": "module",
"scripts": {
- "dev": "vite",
+ "dev": "vite --host",
"build": "vite build",
- "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
+ "lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
- "react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-router": "^7.13.2",
+ "react-router-dom": "^7.13.2",
+ "react-select": "^5.10.2",
+ "styled-components": "^6.3.12",
+ "zustand": "^5.0.12"
},
"devDependencies": {
- "@types/react": "^18.2.15",
- "@types/react-dom": "^18.2.7",
- "@vitejs/plugin-react": "^4.0.3",
- "eslint": "^8.45.0",
- "eslint-plugin-react": "^7.32.2",
- "eslint-plugin-react-hooks": "^4.6.0",
- "eslint-plugin-react-refresh": "^0.4.3",
- "vite": "^6.3.5"
+ "@eslint/js": "^9.21.0",
+ "@types/react": "^19.0.10",
+ "@types/react-dom": "^19.0.4",
+ "@vitejs/plugin-react": "^4.3.4",
+ "eslint": "^9.21.0",
+ "eslint-plugin-react-hooks": "^5.1.0",
+ "eslint-plugin-react-refresh": "^0.4.19",
+ "globals": "^15.15.0",
+ "vite": "^6.2.0"
}
}
diff --git a/frontend/public/_redirects b/frontend/public/_redirects
new file mode 100644
index 0000000000..7797f7c6a7
--- /dev/null
+++ b/frontend/public/_redirects
@@ -0,0 +1 @@
+/* /index.html 200
diff --git a/frontend/public/favicon.png b/frontend/public/favicon.png
new file mode 100644
index 0000000000..2f55027fe1
Binary files /dev/null and b/frontend/public/favicon.png differ
diff --git a/frontend/public/media/icons/checkbox.png b/frontend/public/media/icons/checkbox.png
new file mode 100644
index 0000000000..975446c9f2
Binary files /dev/null and b/frontend/public/media/icons/checkbox.png differ
diff --git a/frontend/public/media/icons/checkbox2.png b/frontend/public/media/icons/checkbox2.png
new file mode 100644
index 0000000000..0fc03dcb59
Binary files /dev/null and b/frontend/public/media/icons/checkbox2.png differ
diff --git a/frontend/public/media/images/1 (254).jpg b/frontend/public/media/images/1 (254).jpg
new file mode 100644
index 0000000000..e2caa9a5b1
Binary files /dev/null and b/frontend/public/media/images/1 (254).jpg differ
diff --git a/frontend/public/media/images/1 (258).jpg b/frontend/public/media/images/1 (258).jpg
new file mode 100644
index 0000000000..296a786b6b
Binary files /dev/null and b/frontend/public/media/images/1 (258).jpg differ
diff --git a/frontend/public/media/images/1 (284).jpg b/frontend/public/media/images/1 (284).jpg
new file mode 100644
index 0000000000..e00ee173b0
Binary files /dev/null and b/frontend/public/media/images/1 (284).jpg differ
diff --git a/frontend/public/media/images/1 (285) (1).jpg b/frontend/public/media/images/1 (285) (1).jpg
new file mode 100644
index 0000000000..a317fee20a
Binary files /dev/null and b/frontend/public/media/images/1 (285) (1).jpg differ
diff --git a/frontend/public/media/images/1 (285).jpg b/frontend/public/media/images/1 (285).jpg
new file mode 100644
index 0000000000..a317fee20a
Binary files /dev/null and b/frontend/public/media/images/1 (285).jpg differ
diff --git a/frontend/public/media/images/1 (286).jpg b/frontend/public/media/images/1 (286).jpg
new file mode 100644
index 0000000000..9d4d78a10c
Binary files /dev/null and b/frontend/public/media/images/1 (286).jpg differ
diff --git a/frontend/public/media/images/396725506_266163596405153_7816128484953626918_n.jpg b/frontend/public/media/images/396725506_266163596405153_7816128484953626918_n.jpg
new file mode 100644
index 0000000000..a7b6afcc00
Binary files /dev/null and b/frontend/public/media/images/396725506_266163596405153_7816128484953626918_n.jpg differ
diff --git a/frontend/public/media/images/396727846_266161843071995_5188809347353672283_n.jpg b/frontend/public/media/images/396727846_266161843071995_5188809347353672283_n.jpg
new file mode 100644
index 0000000000..6c1b23da70
Binary files /dev/null and b/frontend/public/media/images/396727846_266161843071995_5188809347353672283_n.jpg differ
diff --git a/frontend/public/media/images/396728014_266161709738675_2522972426941273959_n (1).jpg b/frontend/public/media/images/396728014_266161709738675_2522972426941273959_n (1).jpg
new file mode 100644
index 0000000000..59cc93252e
Binary files /dev/null and b/frontend/public/media/images/396728014_266161709738675_2522972426941273959_n (1).jpg differ
diff --git a/frontend/public/media/images/397877626_266163639738482_4804133712260471633_n.jpg b/frontend/public/media/images/397877626_266163639738482_4804133712260471633_n.jpg
new file mode 100644
index 0000000000..1c40175056
Binary files /dev/null and b/frontend/public/media/images/397877626_266163639738482_4804133712260471633_n.jpg differ
diff --git a/frontend/public/media/images/397969742_266161883071991_1986506717576720906_n.jpg b/frontend/public/media/images/397969742_266161883071991_1986506717576720906_n.jpg
new file mode 100644
index 0000000000..2506eb00cb
Binary files /dev/null and b/frontend/public/media/images/397969742_266161883071991_1986506717576720906_n.jpg differ
diff --git a/frontend/public/media/images/Fotograf-linkoping_SugarCrispPictures_Womenation-12.jpg b/frontend/public/media/images/Fotograf-linkoping_SugarCrispPictures_Womenation-12.jpg
new file mode 100644
index 0000000000..dda185510c
Binary files /dev/null and b/frontend/public/media/images/Fotograf-linkoping_SugarCrispPictures_Womenation-12.jpg differ
diff --git a/frontend/public/media/images/Fotograf-linkoping_SugarCrispPictures_Womenation-15.jpg b/frontend/public/media/images/Fotograf-linkoping_SugarCrispPictures_Womenation-15.jpg
new file mode 100644
index 0000000000..43acafafc4
Binary files /dev/null and b/frontend/public/media/images/Fotograf-linkoping_SugarCrispPictures_Womenation-15.jpg differ
diff --git a/frontend/public/media/images/Fotograf-linkoping_SugarCrispPictures_Womenation-17.jpg b/frontend/public/media/images/Fotograf-linkoping_SugarCrispPictures_Womenation-17.jpg
new file mode 100644
index 0000000000..ab6b4220c4
Binary files /dev/null and b/frontend/public/media/images/Fotograf-linkoping_SugarCrispPictures_Womenation-17.jpg differ
diff --git a/frontend/public/media/images/Fotograf-linkoping_SugarCrispPictures_Womenation-20.jpg b/frontend/public/media/images/Fotograf-linkoping_SugarCrispPictures_Womenation-20.jpg
new file mode 100644
index 0000000000..c8dce72f8a
Binary files /dev/null and b/frontend/public/media/images/Fotograf-linkoping_SugarCrispPictures_Womenation-20.jpg differ
diff --git a/frontend/public/media/images/Gemini_Generated_Image_5znreq5znreq5znr.png b/frontend/public/media/images/Gemini_Generated_Image_5znreq5znreq5znr.png
new file mode 100644
index 0000000000..113e2c6d09
Binary files /dev/null and b/frontend/public/media/images/Gemini_Generated_Image_5znreq5znreq5znr.png differ
diff --git a/frontend/public/media/images/IMG_2162.HEIC b/frontend/public/media/images/IMG_2162.HEIC
new file mode 100644
index 0000000000..b963908e86
Binary files /dev/null and b/frontend/public/media/images/IMG_2162.HEIC differ
diff --git a/frontend/public/media/images/IMG_2874.HEIC b/frontend/public/media/images/IMG_2874.HEIC
new file mode 100644
index 0000000000..6256d20677
Binary files /dev/null and b/frontend/public/media/images/IMG_2874.HEIC differ
diff --git a/frontend/public/media/images/IMG_2877.HEIC b/frontend/public/media/images/IMG_2877.HEIC
new file mode 100644
index 0000000000..5d4107ee98
Binary files /dev/null and b/frontend/public/media/images/IMG_2877.HEIC differ
diff --git a/frontend/public/media/images/IMG_8886.JPG b/frontend/public/media/images/IMG_8886.JPG
new file mode 100644
index 0000000000..34eecac10f
Binary files /dev/null and b/frontend/public/media/images/IMG_8886.JPG differ
diff --git a/frontend/public/media/images/Womenation networking 6 (2).jpg b/frontend/public/media/images/Womenation networking 6 (2).jpg
new file mode 100644
index 0000000000..5c4923c5e5
Binary files /dev/null and b/frontend/public/media/images/Womenation networking 6 (2).jpg differ
diff --git a/frontend/public/media/images/done (44).jpg b/frontend/public/media/images/done (44).jpg
new file mode 100644
index 0000000000..811bf7446b
Binary files /dev/null and b/frontend/public/media/images/done (44).jpg differ
diff --git a/frontend/public/media/images/done (45).jpg b/frontend/public/media/images/done (45).jpg
new file mode 100644
index 0000000000..71b05908e9
Binary files /dev/null and b/frontend/public/media/images/done (45).jpg differ
diff --git a/frontend/public/media/images/done (5).jpg b/frontend/public/media/images/done (5).jpg
new file mode 100644
index 0000000000..44965af778
Binary files /dev/null and b/frontend/public/media/images/done (5).jpg differ
diff --git a/frontend/public/media/images/done (6).jpg b/frontend/public/media/images/done (6).jpg
new file mode 100644
index 0000000000..00aa19bb81
Binary files /dev/null and b/frontend/public/media/images/done (6).jpg differ
diff --git a/frontend/public/media/logo/Black logo - no background.png b/frontend/public/media/logo/Black logo - no background.png
new file mode 100644
index 0000000000..be2926ddfa
Binary files /dev/null and b/frontend/public/media/logo/Black logo - no background.png differ
diff --git a/frontend/public/media/logo/Color logo - no background.png b/frontend/public/media/logo/Color logo - no background.png
new file mode 100644
index 0000000000..7214fb310e
Binary files /dev/null and b/frontend/public/media/logo/Color logo - no background.png differ
diff --git a/frontend/public/media/logo/Color logo with background.png b/frontend/public/media/logo/Color logo with background.png
new file mode 100644
index 0000000000..586c15c5c9
Binary files /dev/null and b/frontend/public/media/logo/Color logo with background.png differ
diff --git a/frontend/public/media/logo/White logo - no background.png b/frontend/public/media/logo/White logo - no background.png
new file mode 100644
index 0000000000..48b16526b8
Binary files /dev/null and b/frontend/public/media/logo/White logo - no background.png differ
diff --git a/frontend/public/media/logo/icon.png b/frontend/public/media/logo/icon.png
new file mode 100644
index 0000000000..2f55027fe1
Binary files /dev/null and b/frontend/public/media/logo/icon.png differ
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 0a24275e6e..cddb8dad16 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,8 +1,46 @@
-export const App = () => {
+// [ ] Skapa premium route
+
+import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"
+import styled, { ThemeProvider } from "styled-components"
+import { Theme } from "./themes/Theme"
+import { GlobalStyle } from "./styles/GlobalStyle"
+import { MainPageLayout, MedlemsportalLayout } from "./layouts"
+import { ProtectedRoute } from "./routes"
+import { OmMedlemskap, LoggaIn, MinaSidor, Medlemskap, Events, BliMedlem, Erbjudanden } from "./pages"
+const AccountHomeTitle = styled.h1`
+ color: ${({ theme }) => theme.konto.txtClrH1};
+ text-transform: uppercase;
+ letter-spacing: 10px;
+ text-align: center;
+ font-size: 28px;
+ font-weight: 400;
+ margin-bottom: 48px;
+`
+
+export const App = () => {
return (
- <>
- Welcome to Final Project!
- >
- );
-};
+
+
+
+
+ }>
+ }/>
+ }/>
+ }/>
+ }/>
+ }>
+ }>
+ VĂ€lkommen till ditt konto!} />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/ComponentTemplate.jsx b/frontend/src/components/ComponentTemplate.jsx
new file mode 100644
index 0000000000..ab42218206
--- /dev/null
+++ b/frontend/src/components/ComponentTemplate.jsx
@@ -0,0 +1,13 @@
+import styled from 'styled-components'
+
+const StyledComponent = styled.div`
+
+`
+
+export const ComponentTemplate = () => {
+ return (
+ <>
+ >
+ )
+}
+
diff --git a/frontend/src/components/navigation/HamburgerMenu.jsx b/frontend/src/components/navigation/HamburgerMenu.jsx
new file mode 100644
index 0000000000..f3b9e80e3c
--- /dev/null
+++ b/frontend/src/components/navigation/HamburgerMenu.jsx
@@ -0,0 +1,108 @@
+// [ ] Ska vi lÀgga auto-stÀngning av hamburgermenyn?
+// [ ] Vill vi att HamburgerMenu ska hantera sin egen state för expanded, eller ska det hanteras i Navbar och skickas ner som prop?
+// [ ] Se till sÄ att contentStore och Theme anvÀnder samma sid-struktur
+
+import styled from "styled-components"
+import { NavLink } from "react-router-dom"
+import { useContentStore } from "../../store"
+
+
+const StyledButton = styled.button`
+ color: ${({ theme }) => theme.navbar.txtClr};
+ background-color: transparent;
+ padding: 12px;
+ border: none;
+ z-index: 4001;
+ position: relative;
+ margin-left: 20px;
+
+ @media (min-width: ${({ theme }) => theme.breakpoints.tablet}) {
+ display: none;
+ }
+`
+
+const StyledNavContent = styled.div`
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 4000;
+ padding: 0 24px 42px 24px;
+ display: flex;
+ flex-direction: column;
+ background: ${({ theme }) => theme.navbar.bgClr};
+ align-items: center;
+ width: 100%;
+ box-sizing: border-box;
+ height: 50vh;
+ transition: transform 0.3s ease-in-out, opacity 0.5s ease;
+ transform: ${({ $expanded }) => ($expanded ? "translateY(0)" : "translateY(-100%)")};
+ opacity: ${({ $expanded }) => ($expanded ? "1" : "0")};
+ pointer-events: ${({ $expanded }) => ($expanded ? "all" : "none")};
+ visibility: ${({ $expanded }) => ($expanded ? "visible" : "hidden")}; // För att förhindra att tabba genom menyn nÀr den Àr stÀngd
+`
+
+const StyledSpan = styled.span`
+ display: block;
+ height: 4px;
+ width: 40px;
+ background: ${({ theme }) => theme.navbar.spanClr};
+ margin: 8px 0;
+ border-radius: 2px;
+ transition: transform 0.4s ease, opacity 0.3s ease;
+`
+
+const StyledFirstSpan = styled(StyledSpan)`
+ transform: ${({ $expanded }) => ($expanded ? "translateY(12px) rotate(45deg)" : "initial")};
+`
+
+const StyledSecondSpan = styled(StyledSpan)`
+ opacity: ${({ $expanded }) => ($expanded ? "0" : "initial")};
+`
+
+const StyledThirdSpan = styled(StyledSpan)`
+ transform: ${({ $expanded }) => ($expanded ? "translateY(-12px) rotate(-45deg)" : "initial")};
+`
+
+const StyledHamList = styled.ul`
+ list-style: none;
+ padding-top: 100px;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+`
+
+const StyledNavLink = styled(NavLink)`
+ text-decoration: none;
+ font-size: 18px;
+ color: ${({ theme }) => theme.navbar.txtClr};
+`
+
+export const HamburgerMenu = ({ expanded, onToggle }) => {
+ const { navbarContent } = useContentStore()
+ const { links } = navbarContent
+ const { omMedlemskap, konto } = links
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/frontend/src/components/navigation/Navbar.jsx b/frontend/src/components/navigation/Navbar.jsx
new file mode 100644
index 0000000000..efc49c8d5f
--- /dev/null
+++ b/frontend/src/components/navigation/Navbar.jsx
@@ -0,0 +1,105 @@
+// [ ] Flytta handleLogout till userStore
+// [ ] Ska vi flytta usestate för hamburger-menu till store?
+// [ ] StÀda upp stylingen hÀr
+
+import styled from "styled-components"
+import { NavLink, useNavigate } from "react-router-dom"
+import { Button } from "../ui"
+import { useContentStore, useUserStore } from "../../store"
+import { useState } from "react"
+import { HamburgerMenu } from "./HamburgerMenu"
+
+const StyledNavbar = styled.nav`
+ background: ${({theme}) => theme.navbar.bgClr};
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+`
+
+const StyledNavContentContainer = styled.div`
+ display: flex;
+ flex-direction: row;
+`
+
+const StyledLogo = styled.img`
+ height: 10vh;
+ display: flex;
+ align-items: center;
+ justify-items: center;
+ padding: 10px 0px 10px 20px;
+`
+
+const StyledUl = styled.ul`
+ display: none;
+
+ @media (min-width: ${({theme})=> theme.breakpoints.tablet}) {
+ display: flex;
+ flex-direction: row;
+ gap: 30px;
+ align-items: center;
+ margin-left: 40px;
+ }
+`
+
+const StyledItems = styled.li`
+ color: ${({theme})=> theme.navbar.txtClr}
+`
+
+const StyledButtonWrapper = styled.div`
+ margin: 35px 35px 30px 0;
+`
+
+export const Navbar = () => {
+ const { navbarContent } = useContentStore()
+ const { buttons, logoHref, links } = navbarContent
+ const { user, resetUserData } = useUserStore()
+ const [expanded, setExpanded] = useState(false)
+ const navigate = useNavigate()
+ const isLoggedIn = !!user?.accessToken
+
+ const handleLogout = () => {
+ resetUserData()
+ navigate("/logga-in")
+ }
+
+ const handleClick = (nextState) => {
+ if (typeof nextState === 'boolean') {
+ setExpanded(nextState)
+ return
+ }
+
+ setExpanded(prev => !prev)
+ }
+
+ return (
+
+
+
+
+
+
+ {links.omMedlemskap}
+ {links.konto}
+
+
+
+
+ {isLoggedIn
+ ?
+ :
+ }
+
+
+ )
+}
+
diff --git a/frontend/src/components/navigation/Sidebar.jsx b/frontend/src/components/navigation/Sidebar.jsx
new file mode 100644
index 0000000000..a935e516ca
--- /dev/null
+++ b/frontend/src/components/navigation/Sidebar.jsx
@@ -0,0 +1,70 @@
+import styled from "styled-components"
+import { NavLink } from "react-router-dom"
+import { useUserStore } from "../../store"
+
+const StyledSidebar = styled.aside`
+ display: none;
+
+ @media (min-width: ${({ theme }) => theme.breakpoints.tablet}) {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 28px 22px;
+ background: ${({ theme }) => theme.konto.sidebar.bgClr};
+ width: clamp(220px, 25%, 320px);
+ min-width: 220px;
+ box-sizing: border-box;
+ }
+`
+
+const AvatarRow = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 24px;
+ padding: 8px 0;
+`
+
+const Avatar = styled.div`
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ background: ${({ theme }) => theme.konto.sidebar.avatar.avatarClr};
+ flex-shrink: 0;
+`
+
+const MemberName = styled.span`
+ color: ${({ theme }) => theme.konto.sidebar.avatar.txtClr};
+ text-transform: uppercase;
+ letter-spacing: 2.5px;
+`
+
+const StyledNavLink = styled(NavLink)`
+ display: block;
+ padding: 14px 14px;
+ background: ${({ theme }) => theme.konto.sidebar.links.bgClr};
+ color: ${({ theme }) => theme.konto.sidebar.links.txtClr};
+ text-transform: uppercase;
+ letter-spacing: 2.5px;
+ border: 1px solid transparent;
+
+ &.active {
+ border-color: ${({ theme }) => theme.konto.sidebar.links.borderClrActive};
+ }
+`
+
+export const Sidebar = () => {
+ const { user } = useUserStore()
+
+ return (
+
+
+
+ {user?.firstName}
+
+ Mina Sidor
+ Medlemskap
+ Events
+ Erbjudanden
+
+ )
+}
diff --git a/frontend/src/components/navigation/SidebarHamMenu.jsx b/frontend/src/components/navigation/SidebarHamMenu.jsx
new file mode 100644
index 0000000000..8326e77222
--- /dev/null
+++ b/frontend/src/components/navigation/SidebarHamMenu.jsx
@@ -0,0 +1,111 @@
+// [ ] LĂ€gg till contentStore
+// [ ] Ăndra till rĂ€tt fĂ€rger i theme
+
+import styled from "styled-components"
+import { NavLink } from "react-router-dom"
+
+
+const StyledButton = styled.button`
+ color: ${({ theme }) => theme.navbar.txtClr};
+ background-color: transparent;
+ border: none;
+ z-index: 4001;
+ position: relative;
+ margin-left: 20px;
+
+ @media (min-width: ${({ theme }) => theme.breakpoints.tablet}) {
+ display: none;
+ }
+`
+
+const StyledNavContent = styled.div`
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 4000;
+ padding: 0 24px 42px 24px;
+ display: flex;
+ flex-direction: column;
+ background: ${({ theme }) => theme.konto.mainPage.bgClr};
+ align-items: center;
+ width: 100%;
+ box-sizing: border-box;
+ height: 50vh;
+ transition: transform 0.3s ease-in-out, opacity 0.5s ease;
+ transform: ${({ $expanded }) => ($expanded ? "translateY(0)" : "translateY(-100%)")};
+ opacity: ${({ $expanded }) => ($expanded ? "1" : "0")};
+ pointer-events: ${({ $expanded }) => ($expanded ? "all" : "none")};
+ visibility: ${({ $expanded }) => ($expanded ? "visible" : "hidden")}; // För att förhindra att tabba genom menyn nÀr den Àr stÀngd
+`
+
+const StyledSpan = styled.span`
+ display: block;
+ height: 4px;
+ width: 40px;
+ background: ${({ theme }) => theme.konto.sidebar.links.borderClrActive};
+ margin: 8px 0;
+ border-radius: 2px;
+ transition: transform 0.4s ease, opacity 0.3s ease;
+`
+
+const StyledFirstSpan = styled(StyledSpan)`
+ transform: ${({ $expanded }) => ($expanded ? "translateY(12px) rotate(45deg)" : "initial")};
+`
+
+const StyledSecondSpan = styled(StyledSpan)`
+ opacity: ${({ $expanded }) => ($expanded ? "0" : "initial")};
+`
+
+const StyledThirdSpan = styled(StyledSpan)`
+ transform: ${({ $expanded }) => ($expanded ? "translateY(-12px) rotate(-45deg)" : "initial")};
+`
+
+const StyledHamList = styled.ul`
+ list-style: none;
+ padding-top: 100px;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+`
+
+const StyledNavLink = styled(NavLink)`
+ display: block;
+ padding: 14px 14px;
+ background: ${({ theme }) => theme.konto.sidebar.links.bgClr};
+ color: ${({ theme }) => theme.konto.sidebar.links.txtClr};
+ text-transform: uppercase;
+ letter-spacing: 2.5px;
+ border: 1px solid transparent;
+
+ &.active {
+ border-color: ${({ theme }) => theme.konto.sidebar.links.borderClrActive};
+ }
+`
+
+export const SidebarHamMenu = ({ expanded, onToggle }) => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/frontend/src/components/navigation/index.js b/frontend/src/components/navigation/index.js
new file mode 100644
index 0000000000..70ba2f0b94
--- /dev/null
+++ b/frontend/src/components/navigation/index.js
@@ -0,0 +1,5 @@
+// [ ] Fixa import paths frÄn denna fil
+export { SidebarHamMenu } from "./SidebarHamMenu"
+export { Navbar } from "./Navbar"
+export { HamburgerMenu } from "./HamburgerMenu"
+export { Sidebar } from "./Sidebar"
\ No newline at end of file
diff --git a/frontend/src/components/pages/bli-medlem/SignUpCard.jsx b/frontend/src/components/pages/bli-medlem/SignUpCard.jsx
new file mode 100644
index 0000000000..b65242aced
--- /dev/null
+++ b/frontend/src/components/pages/bli-medlem/SignUpCard.jsx
@@ -0,0 +1,57 @@
+import styled from 'styled-components'
+import { BodyText, CardTitle } from '../../typography'
+import { useContentStore } from '../../../store'
+import { SignUpForm } from "./SignUpForm"
+import { Link } from 'react-router-dom'
+
+const StyledSignUpCard = styled.div`
+ background: ${({theme}) => theme.loggaIn.form.bgClr};
+ color: ${({theme}) => theme.loggaIn.form.txtClr};
+ width: 80%;
+ max-width: 400px;
+ display: flex;
+ flex-direction: column;
+ gap: 30px;
+ //justify-self: center; // stödjs inte av safari - bytte etill margin auto istÀllet
+ padding: 80px 40px;
+ //margin-top: 60px;
+ margin: 60px auto 0;
+ border-radius: 10px;
+
+ @media (min-width: ${({theme}) => theme.breakpoints.desktop}) {
+ width: 50%;
+ max-width: 450px;
+ }
+`
+
+const StyledTextContainer = styled.div`
+
+`
+
+const StyledSignupContainer = styled.div`
+ text-align: center;
+`
+
+const StyledLink = styled(Link)`
+ text-decoration: underline;
+`
+
+export const SignUpCard = ({ handleSignUp }) => {
+ const { signuUpContent } = useContentStore()
+ const { form } = signuUpContent
+ const { heading, subHeading, navigateToLogIn } = form
+ const { text, linkTo, linkText } = navigateToLogIn
+
+ return (
+
+
+
+
+
+
+
+ {linkText}
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/components/pages/bli-medlem/SignUpForm.jsx b/frontend/src/components/pages/bli-medlem/SignUpForm.jsx
new file mode 100644
index 0000000000..37d54de7ee
--- /dev/null
+++ b/frontend/src/components/pages/bli-medlem/SignUpForm.jsx
@@ -0,0 +1,139 @@
+//[x] todo: fixa setSignUpSubmitting för att knappen ska byta texten vid submitting lÀge, integrera med API fetch
+//[x] todo: fixa resetSignUp för att funka med setSignUpSubmitting, integrera med API fetch
+
+
+import styled from 'styled-components'
+import { useContentStore, useFormStore } from '../../../store'
+import { Button, FormInput, Fieldset, CitySelector } from '../../ui'
+import { API_URL } from '../../../constants/Constants'
+
+const StyledForm = styled.form`
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+`
+
+export const SignUpForm = ({ handleSignUp }) => {
+ const { signuUpContent } = useContentStore()
+ const { form } = signuUpContent
+ const { labels, placeholders, button } = form
+ const { signUpData, setSignUpField, setSignUpSubmitting, resetSignUp, setSignUpError } = useFormStore()
+
+ const handleSubmit = async (event) => {
+ event.preventDefault()
+ console.log("Submit klickad", signUpData)
+
+ const { city, workStatus, ...payload } = signUpData
+
+ const selectedWorkStatusOptions = Object.entries(workStatus)
+ .filter(([key, value]) => value === true && key !== 'otherText')
+ .map(([key]) => key)
+
+ const requestBody = {
+ ...payload,
+ cityValue: city.value,
+ cityLabel: city.label,
+ workStatus: selectedWorkStatusOptions,
+ otherText: workStatus.other ? workStatus.otherText : ''
+ }
+
+ setSignUpError('')
+ setSignUpSubmitting(true)
+
+ try {
+ const response = await fetch(`${API_URL}/users/signup`, {
+ method: 'POST',
+ body: JSON.stringify(requestBody),
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ })
+
+ const data = await response.json()
+ console.log("Sign up response status:", response.status)
+ console.log("Sign up response data:", data)
+
+ if (!response.ok) {
+ throw new Error(data?.message || "Sign up failed")
+ }
+ console.log("Sign up lyckades, anropar handleSignUp")
+
+ if (handleSignUp) {
+ handleSignUp(data.response)
+ resetSignUp()
+ }
+ } catch (error) {
+ setSignUpError(error.message || "Ett fel intrÀffade vid registrering")
+ } finally {
+ setSignUpSubmitting(false)
+ }
+
+ }
+
+
+ return (
+
+ setSignUpField('firstName', event.target.value)}
+ label={labels.firstName} />
+ setSignUpField('lastName', event.target.value)}
+ label={labels.lastName} />
+ setSignUpField('email', event.target.value)}
+ label={labels.email} />
+ setSignUpField('password', event.target.value)}
+ label={labels.password} />
+
+ setSignUpField('justifyMembership', event.target.value)}
+ label={labels.justifyMembership}
+ placeholder={placeholders.justifyMembership} />
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/components/pages/events/EventCard.jsx b/frontend/src/components/pages/events/EventCard.jsx
new file mode 100644
index 0000000000..f0a7c4ca2e
--- /dev/null
+++ b/frontend/src/components/pages/events/EventCard.jsx
@@ -0,0 +1,85 @@
+import styled from "styled-components"
+import { CardTitle, BodyText } from "../../typography"
+import { useContentStore } from "../../../store"
+import { EventDateTag } from "./EventDateTag"
+import { Button } from "../../ui/"
+
+const StyledCardContainer = styled.div`
+ display: grid;
+ grid-template-columns: repeat(1, minmax(300px, 400px));
+ gap: 16px;
+ justify-content: center;
+
+
+ @media (min-width: ${({theme})=> theme.breakpoints.tablet}) {
+ grid-template-columns: repeat(2, minmax(250px, 500px));
+ }
+`
+
+const StyledCard = styled.div`
+ background: ${({ theme }) => theme.konto.mainPage.events.cards.bgClr};
+ border: 1px solid #444;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ position: relative;
+ width: 100%;
+`
+
+const StyledImage = styled.img`
+ width: 100%;
+ height: 150px;
+ object-fit: cover;
+ display: block;
+`
+
+const StyledTextContainer = styled.div`
+ padding: 12px 14px 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ flex: 1;
+`
+
+const StyledBottomRow = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-top: auto;
+ background: ${({ theme }) => theme.konto.mainPage.events.cards.bgClr};
+`
+
+const StyledBottomRight = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 12px;
+`
+
+
+export const EventCard = () => {
+ const {eventContent} = useContentStore()
+ const { events } = eventContent
+
+ return (
+
+ {events.map((event) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ )
+}
diff --git a/frontend/src/components/pages/events/EventDateTag.jsx b/frontend/src/components/pages/events/EventDateTag.jsx
new file mode 100644
index 0000000000..2b3078dfc0
--- /dev/null
+++ b/frontend/src/components/pages/events/EventDateTag.jsx
@@ -0,0 +1,21 @@
+import styled from "styled-components"
+import { BodyText } from "../../typography/BodyText"
+
+const StyledDateTagContainer = styled.div`
+ position: absolute;
+ top: 6px;
+ right: 6px;
+ background: ${({ theme }) => theme.konto.mainPage.events.cards.tag.bgClr};
+ color: ${({ theme }) => theme.konto.mainPage.events.cards.tag.txtClr};
+ font-size: 14px;
+ padding: 4px 8px;
+`
+
+export const EventDateTag = ({ text }) => {
+
+ return (
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/components/pages/logga-in/LogInCard.jsx b/frontend/src/components/pages/logga-in/LogInCard.jsx
new file mode 100644
index 0000000000..11ff4c41d9
--- /dev/null
+++ b/frontend/src/components/pages/logga-in/LogInCard.jsx
@@ -0,0 +1,57 @@
+// [x] TODO make button for going back to the membership page.
+
+import styled from 'styled-components'
+import { BodyText, CardTitle } from '../../typography'
+import { useContentStore } from '../../../store'
+import { LogInForm } from "./LogInForm"
+import { Link } from 'react-router-dom'
+
+const StyledLogInCard = styled.div`
+ background: ${({theme}) => theme.loggaIn.form.bgClr};
+ color: ${({theme}) => theme.loggaIn.form.txtClr};
+ width: 80%;
+ max-width: 400px;
+ display: flex;
+ flex-direction: column;
+ gap: 30px;
+ //justify-self: center; // stödjs inte av safari - bytte etill margin auto istÀllet
+ padding: 80px 40px;
+ //margin-top: 60px;
+ margin: 60px auto 0;
+ border-radius: 10px;
+
+ @media (min-width: ${({theme}) => theme.breakpoints.desktop}) {
+ width: 50%;
+ max-width: 450px;
+ }
+`
+
+const StyledTextContainer = styled.div``
+
+const StyledLoginContainer = styled.div`
+ text-align: center;
+`
+
+const StyledLink = styled(Link)`
+ text-decoration: underline;
+`
+
+export const LogInCard = ({ handleLogin }) => {
+ const { logInContent } = useContentStore()
+ const { form } = logInContent
+ const { heading, subHeading, navigateToSignUp } = form
+ const { text, linkTo, linkText } = navigateToSignUp
+
+ return (
+
+
+
+
+
+
+
+ {linkText}
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/components/pages/logga-in/LogInForm.jsx b/frontend/src/components/pages/logga-in/LogInForm.jsx
new file mode 100644
index 0000000000..d4cb33cd96
--- /dev/null
+++ b/frontend/src/components/pages/logga-in/LogInForm.jsx
@@ -0,0 +1,104 @@
+// [x] Make components for inputfields?
+// [x] Connect form to backend
+// [x] Connect form to content store
+// [ ] Bugg: se till att submit med enter funkar
+
+import styled from 'styled-components'
+import { useContentStore, useFormStore } from '../../../store'
+import { Button, FormInput } from '../../ui'
+import { API_URL } from '../../../constants/Constants'
+
+const StyledForm = styled.form`
+ display: flex;
+ flex-direction: column;
+ gap: 30px;
+`
+
+const StyledContentContainer = styled.div``
+
+export const LogInForm = ({ handleLogin }) => {
+ const { logInContent } = useContentStore()
+ const { form } = logInContent
+ const { labels, placeholders, button } = form
+ const { loginData, setLoginField, setLoginError, setLoginSubmitting, resetLogin } = useFormStore()
+
+ const handleSubmit = async (event) => {
+ event.preventDefault()
+ console.log("Submit klickad", loginData)
+
+ if (!loginData.email || !loginData.password) {
+ setLoginError("Please fill in all fields")
+ // [ ] setError kopplat till backend error response?
+ return
+ }
+
+ setLoginError('')
+ setLoginSubmitting(true)
+
+ try {
+ const response = await fetch(`${API_URL}/users/login`, {
+ method: "POST",
+ body: JSON.stringify({
+ email: loginData.email,
+ password: loginData.password
+ }),
+ headers: {
+ "Content-Type": "application/json"
+ }
+ })
+
+ const data = await response.json()
+ console.log("Login response status:", response.status)
+ console.log("Login response data:", data)
+
+ if (!response.ok) {
+ throw new Error(data?.message || "Login failed")
+ }
+ console.log("Login lyckades, anropar handleLogin")
+
+ if (handleLogin) {
+ handleLogin(data.response)
+ resetLogin()
+ }
+ } catch (error) {
+ // [ ] Error response frÄn backend (?)
+ setLoginError(error.message || "Invalid email or password")
+ } finally {
+ setLoginSubmitting(false)
+ }
+ }
+
+ return (
+
+
+ setLoginField('email', event.target.value)}
+ type="email"
+ id="email"
+ name="email"
+ placeholder={placeholders.email}
+ value={loginData.email}
+ label={labels.email}
+ />
+ setLoginField('password', event.target.value)}
+ type="password"
+ id="password"
+ name="password"
+ placeholder={placeholders.password}
+ value={loginData.password}
+ label={labels.password}
+ />
+
+ {loginData.error && {loginData.error}
}
+
+
+
+ )
+}
+
diff --git a/frontend/src/components/pages/mina-sidor/accountInfoCard.jsx b/frontend/src/components/pages/mina-sidor/accountInfoCard.jsx
new file mode 100644
index 0000000000..99bb781f16
--- /dev/null
+++ b/frontend/src/components/pages/mina-sidor/accountInfoCard.jsx
@@ -0,0 +1,62 @@
+// [ ] TODO Map out user info from context to show in card
+// [ ] TODO Add edit button and functionality to update user info
+
+import styled from "styled-components"
+import { BodyText } from "../../typography/BodyText"
+import { useContentStore } from "../../../store"
+import { useOutletContext } from "react-router-dom"
+
+const StyledCardContainer = styled.div`
+
+`
+
+const StyledCard = styled.div`
+ background: ${({ theme }) => theme.konto.mainPage.minaSidor.form.cardBgClr};
+ width: 80%;
+ max-width: 400px;
+ padding: 80px 40px;
+ border-radius: 10px;
+
+ @media (min-width: ${({theme}) => theme.breakpoints.desktop}) {
+ width: 50%;
+ max-width: 450px;
+ }
+`
+
+const StyledTextContainer = styled.div`
+ background: ${({ theme }) => theme.konto.mainPage.minaSidor.form.bgClr};
+ padding: 12px 16px;
+ border-radius: 3px;
+ height: 20%;
+ width: 100%;
+ margin: 10px 0;
+`
+export const AccountInfoCard = () => {
+ const { logInContent } = useContentStore()
+ const { form } = logInContent
+ const { user } = useOutletContext()
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/pages/mina-sidor/accountInfoForm.jsx b/frontend/src/components/pages/mina-sidor/accountInfoForm.jsx
new file mode 100644
index 0000000000..4fb0cd92fe
--- /dev/null
+++ b/frontend/src/components/pages/mina-sidor/accountInfoForm.jsx
@@ -0,0 +1,73 @@
+// [ ] implementera accountInfoForm nÀr vi skapat patch för userData
+// [ ] Àndra till rÀtt funktioner, Àr nu kopierat frÄn signupform
+
+import styled from "styled-components"
+import { FormInput, CitySelector } from "../../ui"
+
+const StyledForm = styled.form`
+
+`
+export const accountInfoForm = () => {
+ return (
+
+ setSignUpField('firstName', event.target.value)}
+ label={form.firstName} />
+ setSignUpField('lastName', event.target.value)}
+ label={form.lastName} />
+ setSignUpField('email', event.target.value)}
+ label={form.email} />
+ setSignUpField('password', event.target.value)}
+ label={form.password} />
+
+ setSignUpField('justifyMembership', event.target.value)}
+ label={form.justifyMembershipLabel} />
+
+
+)
+}
\ No newline at end of file
diff --git a/frontend/src/components/pages/om-medlemskap/Hero.jsx b/frontend/src/components/pages/om-medlemskap/Hero.jsx
new file mode 100644
index 0000000000..c9c7d028d9
--- /dev/null
+++ b/frontend/src/components/pages/om-medlemskap/Hero.jsx
@@ -0,0 +1,56 @@
+import styled from 'styled-components'
+import { PageTitle } from '../../typography'
+import { useContentStore } from '../../../store'
+
+const HeroWrapper = styled.section`
+ width: 100%;
+`
+
+const Background = styled.div`
+ position: relative;
+ height: 40vh;
+ min-height: 280px;
+ background-image: ${props => `url("${props.bg}")`};
+ background-size: cover;
+ background-position: center 30%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ @media (min-width: ${({theme}) => theme.breakpoints.desktop}) {
+ height: 65vh;
+ }
+`
+
+const Overlay = styled.div`
+ position: absolute;
+ inset: 0;
+ background: rgba(0,0,0,0.45);
+ pointer-events: none;
+`
+
+const Content = styled.div`
+ position: relative;
+ z-index: 2;
+ color: ${({theme}) => theme.omMedlemskap.txtClr};
+ text-align: center;
+ padding: 0 1rem;
+`
+
+export const Hero = () => {
+ const { omMedlemskapContent } = useContentStore()
+ const { hero } = omMedlemskapContent
+ const filename = '1 (286).jpg'
+ const img = `/media/images/${encodeURIComponent(filename)}`
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/components/pages/om-medlemskap/MembershipCard.jsx b/frontend/src/components/pages/om-medlemskap/MembershipCard.jsx
new file mode 100644
index 0000000000..0c49456712
--- /dev/null
+++ b/frontend/src/components/pages/om-medlemskap/MembershipCard.jsx
@@ -0,0 +1,65 @@
+// [x] TODO Make variant for text and button text and pass them as props to the component.
+// [x] TODO Make json from the data and map through it to create the cards instead of hardcoding them. (content store used instead))
+// [x] TODO Make the Button reusable, without hardcoded text. so it can be used for login button and not just for the membership card.
+
+import styled from "styled-components"
+import { Button } from "../../ui"
+import { BodyText, CardTitle } from "../../typography"
+import { useContentStore } from "../../../store"
+
+const StyledCard = styled.div`
+ background: ${({ theme }) => theme.omMedlemskap.cards.bgClr};
+ border-radius: 10px;
+ padding: 30px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ max-height: 450px;
+ width: 90%;
+
+
+ @media (min-width: ${({theme}) => theme.breakpoints.mobile}) {
+ max-width: 400px;
+ }
+
+ @media (min-width: ${({theme}) => theme.breakpoints.tablet}) {
+ min-width: 270px;
+ height: 450px;
+ }
+
+ @media (min-width: ${({theme}) => theme.breakpoints.desktop}) {
+ max-width: 450px;
+ }
+`
+const StyledTextContainer = styled.div`
+ width: 90%;
+ display: flex;
+ flex-direction: column;
+ justify-self: center;
+ color: ${({theme}) => theme.omMedlemskap.cards.txtClr};
+`
+
+const StyledDivider = styled.hr`
+ height: 0.5px;
+ margin: 10px 0;
+`
+
+export const MembershipCard = ({ variant }) => {
+ const { omMedlemskapContent } = useContentStore()
+ const { cards, buttons } = omMedlemskapContent
+ const { type, price, benefit } = cards[variant]
+
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
+
+
diff --git a/frontend/src/components/typography/BodyText.jsx b/frontend/src/components/typography/BodyText.jsx
new file mode 100644
index 0000000000..91a51985e8
--- /dev/null
+++ b/frontend/src/components/typography/BodyText.jsx
@@ -0,0 +1,61 @@
+// [ ] TODO make prop for variants for all typography
+
+import styled from 'styled-components'
+
+const StyledBodyText = styled.div`
+
+ ${({ $variant, $icon }) => $variant === "om-medlemskap-info" && `
+ ul {
+ list-style: none;
+ padding-left: 0;
+ margin: 0.5rem 0;
+ }
+ li {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5rem;
+ margin-bottom: 0.4rem;
+ }
+ li::before {
+ content: '';
+ width: 20px;
+ height: 20px;
+ flex: 0 0 20px;
+ background-image: url("${$icon || '/media/icons/checkbox2.png'}");
+ background-size: contain;
+ background-repeat: no-repeat;
+ margin-top: 0.2rem;
+ }
+ `}
+
+ ${({ $variant, theme }) => $variant === "event" && `
+ color: ${theme.konto.mainPage.events.cards.textClr.p};
+ font-size: 18px;
+ `}
+
+ ${({ $variant, theme}) => $variant === "mina-sidor" && `
+ color: ${theme.konto.mainPage.minaSidor.form.txtClr};
+ font-size: 18px;
+ `}
+`
+
+export const BodyText = ({ text, children, variant, icon }) => {
+ if (variant === "om-medlemskap-info") {
+ const lines = (text || '').split('\n').map(l => l.trim()).filter(Boolean)
+
+ return (
+
+
+ {lines.map((line, i) => - {line}
)}
+
+
+ )
+ }
+
+ return (
+
+ {text}{children}
+
+ )
+}
+
diff --git a/frontend/src/components/typography/CardTitle.jsx b/frontend/src/components/typography/CardTitle.jsx
new file mode 100644
index 0000000000..a7c34677b7
--- /dev/null
+++ b/frontend/src/components/typography/CardTitle.jsx
@@ -0,0 +1,21 @@
+import styled from 'styled-components'
+
+const StyledCardTitle = styled.h2`
+ margin: 0;
+
+ ${({ $variant, theme }) => $variant === "om-medlemskap" && `
+
+ @media(min-width: ${theme.breakpoints.tablet}) {
+ font-size: 45px;
+ }
+ `}
+`
+
+export const CardTitle = ({ text, variant }) => {
+ return (
+
+ {text}
+
+ )
+}
+
diff --git a/frontend/src/components/typography/PageTitle.jsx b/frontend/src/components/typography/PageTitle.jsx
new file mode 100644
index 0000000000..c50abb80fe
--- /dev/null
+++ b/frontend/src/components/typography/PageTitle.jsx
@@ -0,0 +1,28 @@
+import styled from 'styled-components'
+
+const StyledPageTitle = styled.h1`
+ ${({ $variant, theme }) => $variant === "hero" && `
+ @media(min-width: ${theme.breakpoints.desktop}) {
+ font-size: 50px;
+ }
+ `}
+
+ ${({ $variant, theme }) => $variant === "konto" && `
+ color: ${theme.konto.txtClrH1};
+ text-transform: uppercase;
+ letter-spacing: 10px;
+ text-align: center;
+ font-size: 28px;
+ font-weight: 400;
+ margin-bottom: 32px;
+ `}
+`
+
+export const PageTitle = ({ text, variant }) => {
+ return (
+
+ {text}
+
+ )
+}
+
diff --git a/frontend/src/components/typography/index.js b/frontend/src/components/typography/index.js
new file mode 100644
index 0000000000..01860b878f
--- /dev/null
+++ b/frontend/src/components/typography/index.js
@@ -0,0 +1,3 @@
+export { BodyText } from './BodyText' //p tag
+export { CardTitle } from './CardTitle' //h2 tag
+export { PageTitle } from './PageTitle' //h1 tag
\ No newline at end of file
diff --git a/frontend/src/components/ui/Button.jsx b/frontend/src/components/ui/Button.jsx
new file mode 100644
index 0000000000..a7dd0a6310
--- /dev/null
+++ b/frontend/src/components/ui/Button.jsx
@@ -0,0 +1,55 @@
+// Safari overrides the text color of buttons by default, put in color fixes here // check together
+
+import styled from "styled-components"
+
+const StyledButton = styled.button`
+ border-radius: 20px;
+ border: none;
+ display: flex;
+ padding: 8px 30px;
+ cursor: pointer;
+ appearance: none;
+ -webkit-appearance: none;
+ -webkit-text-fill-color: currentColor;
+
+ ${({ $variant, theme }) => $variant === "card" && `
+ width: max-content;
+ margin-top: 50px;
+ align-self: center;
+ justify-self: center;
+ background: ${theme.omMedlemskap.buttons.vÀljMedlemskap.bgClr};
+ color: ${theme.omMedlemskap.buttons.vÀljMedlemskap.txtClr};
+ `}
+
+ ${({ $variant, theme }) => $variant === "omMedlemskap-login" && `
+ background: ${theme.omMedlemskap.buttons.loggaIn.bgClr};
+ justify-self: center;
+ color: ${theme.omMedlemskap.buttons.loggaIn.txtClr};
+ `}
+
+ ${({ $variant, theme }) => $variant === "loggaIn-login" && `
+ background: ${theme.loggaIn.buttons.loggaIn.bgClr};
+ justify-content: center;
+ color: ${theme.loggaIn.buttons.loggaIn.txtClr};
+ `}
+
+ ${({ $variant, theme}) => $variant === "go-back" && `
+ display: inline;
+ background: ${theme.loggaIn.buttons.gÄTillbaka.bgClr};
+ margin-left: 40px;
+ color: ${theme.loggaIn.buttons.gÄTillbaka.txtClr};
+ `}
+
+ ${({ $variant, theme }) => $variant === "event" && `
+ background: ${theme.konto.mainPage.events.cards.button.bgClr};
+ color: ${theme.konto.mainPage.events.cards.button.txtClr};
+ font-size: 15px;
+ padding: 8px 14px;
+ `}
+`
+
+export const Button = ({ text, variant, ...props }) => {
+ return (
+ {text}
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/components/ui/CitySelector.jsx b/frontend/src/components/ui/CitySelector.jsx
new file mode 100644
index 0000000000..5cc5830799
--- /dev/null
+++ b/frontend/src/components/ui/CitySelector.jsx
@@ -0,0 +1,96 @@
+import styled from "styled-components"
+import { useContentStore, useFormStore } from '../../store'
+import Select from "react-select"
+import swedishCities from "../../data/swedishCities.json"
+import { useMemo } from 'react'
+
+// [ ] TODO: styla Select-komponenten sÄ att den matchar resten av formulÀret, t.ex. genom att Àndra bakgrundsfÀrg, kantfÀrg och textfÀrg. AnvÀnd gÀrna props för att göra det enkelt att anpassa stilen. Ljuslila bakgrund pÄ option?
+
+const StyledLabel = styled.label`
+ display: block;
+ color: ${({theme}) => theme.signUp.form.txtClr || 'var(--white)'};
+ font-size: 15px;
+ margin-top: 15px;
+`
+
+// Klass Vad det Àr
+// __placeholder PlatshÄllartexten
+// __single-value Det valda vÀrdet som visas
+// __input-container Wrappern runt textinputen
+// __input SjÀlva input-elementet
+// __dropdown-indicator Pilen/ikonen till höger
+// __indicator-separator Strecket bredvid pilen
+// __clear-indicator X-knappen (om isClearable)
+// __menu-list Listan inuti menyn (scrollbar)
+// __no-options-message Texten nÀr inga alternativ matchar
+// __value-container Wrappern runt det valda vÀrdet
+
+const StyledSelect = styled(Select)`
+ & .select__control {
+ appearance: none;
+ -webkit-appearance: none;
+ background: ${({theme}) => theme.signUp.form.inputBgClr};
+ border: none;
+ padding: 2px;
+ border-radius: 3px;
+ box-shadow: none;
+ }
+
+ & .select__control--is-focused {
+ border-color: red;
+ box-shadow: none;
+ }
+
+ & .select__menu {
+ z-index: 10;
+ background: ${({theme}) => theme.signUp.form.inputBgClr};
+ }
+
+ & .select__option {
+ width: 100%;
+ padding: 8px;
+ padding-left: 14px;
+ border-radius: 3px;
+ border: none;
+ background: ${({theme}) => theme.signUp.form.inputBgClr};
+ color: ${({theme}) => theme.signUp.form.inputTxtClr};
+ }
+
+ & .select__option--is-focused {
+ background: ${({theme}) => theme.signUp.form.inputBgClrHover};
+ color: ${({theme}) => theme.signUp.form.inputTxtClrHover};
+ }
+
+ & .select__option--is-selected {
+ background: ${({theme}) => theme.signUp.form.inputBgColorSelected};
+ color: ${({theme}) => theme.signUp.form.inputTxtColorSelected};
+ }
+`
+
+export const CitySelector = ({ label, id, name }) => {
+ const { signUpData, setSignUpField } = useFormStore()
+ const { signuUpContent } = useContentStore()
+ const { form } = signuUpContent
+ const { placeholders } = form
+ const cityOptions = useMemo(() => swedishCities, [])
+
+
+ return (
+ <>
+ {label}
+ setSignUpField('city', option)}
+ placeholder={placeholders.city}
+ isSearchable={true}
+ isClearable={true}
+ menuPlacement='auto'
+ backspaceRemovesValue={true}
+ />
+ >
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/components/ui/Fieldset.jsx b/frontend/src/components/ui/Fieldset.jsx
new file mode 100644
index 0000000000..fef44cc894
--- /dev/null
+++ b/frontend/src/components/ui/Fieldset.jsx
@@ -0,0 +1,133 @@
+// [ ] TODO - LÀgg till state och onChange för checkboxarna
+// [ ] TODO - LÀgg till props sÄ att Fieldset kan ÄteranvÀndas i EditProfile?
+
+import styled from 'styled-components';
+import { useContentStore, useFormStore } from '../../store';
+
+const StyledFieldset = styled.fieldset`
+ margin-top: 15px;
+ display: flex;
+ flex-direction: column;
+ justify-self: center;
+ align-self: center;
+ border: 1px solid #ccc;
+ padding: 2px 30px 30px 30px;
+
+ legend {
+ font-size: 1.2em;
+ }
+
+ p {
+ font-size: 0.8em; //funkar inte?
+ margin-bottom: 15px;
+ }
+`
+const StyledCheckbox = styled.div`
+ display: flex;
+ gap: 10px;
+ margin-bottom: 10px;
+
+ input[type="checkbox"] {
+ display: flex;
+ align-self: center;
+ background: ${({theme}) => theme.signUp.form.inputBgClr};
+ color: ${({theme}) => theme.loggaIn.form.inputTxtClr};
+ /* width: 20px;
+ height: 20px; */
+ cursor: pointer;
+ }
+
+ label {
+ font-size:;
+ cursor: pointer;
+ }
+`
+
+
+export const Fieldset = () => {
+ const { signUpData, setWorkStatus, setOtherText } = useFormStore()
+ const { signuUpContent } = useContentStore()
+ const { form } = signuUpContent;
+ const { labels, placeholders } = form
+ const { workStatus } = labels
+ const { checkboxLabel } = workStatus
+
+
+
+ return (
+
+
+ {workStatus.desc}
+
+
+ setWorkStatus(event.target.value, event.target.checked)}
+ />
+
+
+
+
+ setWorkStatus(event.target.value, event.target.checked)}
+ />
+
+
+
+
+ setWorkStatus(event.target.value, event.target.checked)}
+ />
+
+
+
+
+ setWorkStatus(event.target.value, event.target.checked)}
+ />
+
+
+
+
+ setWorkStatus(event.target.value, event.target.checked)}
+ />
+
+
+
+ {signUpData.workStatus.other && (
+ setOtherText(event.target.value)}
+ value={signUpData.workStatus.otherText}
+ />
+ )}
+
+
+ );
+};
\ No newline at end of file
diff --git a/frontend/src/components/ui/FormInput.jsx b/frontend/src/components/ui/FormInput.jsx
new file mode 100644
index 0000000000..5395365533
--- /dev/null
+++ b/frontend/src/components/ui/FormInput.jsx
@@ -0,0 +1,71 @@
+import styled from 'styled-components'
+
+const StyledInputContainer = styled.div`
+ margin-top: 15px;
+`
+
+const StyledLabel = styled.label`
+ display: block;
+ color: ${({theme}) => theme.loggaIn.form.txtClr || 'var(--white)'};
+ font-size: 15px;
+ margin-bottom: 6px;
+`
+
+
+const StyledInput = styled.input`
+ width: 100%;
+ padding: 8px;
+ border-radius: 3px;
+ border: 2px solid transparent;
+ appearance: none;
+ -webkit-appearance: none;
+
+ ${({ $variant, theme }) => $variant === "login" && `
+ background: ${theme.loggaIn.form.inputBgClr};
+ color: ${theme.loggaIn.form.inputTxtClr};
+ `}
+
+ ${({ $variant, theme }) => $variant === "signup" && `
+ background: ${theme.signUp.form.inputBgClr};
+ color: ${theme.signUp.form.inputTxtClr};
+ `}
+
+ &:focus,
+ &:focus-visible {
+ border: 2px solid #442E57;
+ outline: 2px solid orange;
+ }
+`
+
+const StyledTextarea = styled.textarea`
+ height: 120px;
+ resize: vertical;
+ padding: 8px;
+ width: 100%;
+ border-radius: 3px;
+ border: 2px solid transparent;
+ appearance: none;
+ -webkit-appearance: none;
+ background: ${({theme}) => theme.signUp.form.inputBgClr};
+ color: ${({theme}) => theme.signUp.form.inputTxtClr};
+
+ &:focus,
+ &:focus-visible {
+ border: 2px solid #442E57;
+ outline: 2px solid orange;
+ }
+
+`
+
+export const FormInput = ({ type, id, name, label, value, onChange, variant, placeholder }) => {
+
+ return (
+
+ {label}
+ {type === 'textarea'
+ ?
+ :
+ }
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/components/ui/index.js b/frontend/src/components/ui/index.js
new file mode 100644
index 0000000000..3ffc5c2dff
--- /dev/null
+++ b/frontend/src/components/ui/index.js
@@ -0,0 +1,4 @@
+export { Button } from './Button'
+export { FormInput } from './FormInput'
+export { Fieldset } from './Fieldset'
+export { CitySelector } from './CitySelector'
diff --git a/frontend/src/constants/Constants.js b/frontend/src/constants/Constants.js
new file mode 100644
index 0000000000..17b7d69549
--- /dev/null
+++ b/frontend/src/constants/Constants.js
@@ -0,0 +1,12 @@
+// [ ] Ska vi skapa constants för app routes? alltsÄ /om-medlemskap, /logga-in osv? Eller Àr det överflödigt? Om vi gör det, ska vi dÄ skapa en separat fil för det eller lÀgga det i contentStore? Det ligger redan nÄgora routes i contentStore
+
+// export const APP_ROUTES = {
+// home: "/",
+// membership: "/om-medlemskap",
+// login: "/logga-in",
+// account: "/konto",
+// events: "/events",
+// erbjudanden: "/erbjudanden"
+// }
+
+export const API_URL="https://womenation-membership.onrender.com"
\ No newline at end of file
diff --git a/frontend/src/data/swedishCities.json b/frontend/src/data/swedishCities.json
new file mode 100644
index 0000000000..1544839ac3
--- /dev/null
+++ b/frontend/src/data/swedishCities.json
@@ -0,0 +1,292 @@
+[
+ { "value": "upplands-vasby", "label": "Upplands VĂ€sby" },
+ { "value": "vallentuna", "label": "Vallentuna" },
+ { "value": "osteraker", "label": "ĂsterĂ„ker" },
+ { "value": "varmdo", "label": "VÀrmdö" },
+ { "value": "jarfalla", "label": "JÀrfÀlla" },
+ { "value": "ekero", "label": "Ekerö" },
+ { "value": "huddinge", "label": "Huddinge" },
+ { "value": "botkyrka", "label": "Botkyrka" },
+ { "value": "salem", "label": "Salem" },
+ { "value": "haninge", "label": "Haninge" },
+ { "value": "tyreso", "label": "Tyresö" },
+ { "value": "upplands-bro", "label": "Upplands-Bro" },
+ { "value": "nykvarn", "label": "Nykvarn" },
+ { "value": "taby", "label": "TĂ€by" },
+ { "value": "danderyd", "label": "Danderyd" },
+ { "value": "sollentuna", "label": "Sollentuna" },
+ { "value": "stockholm", "label": "Stockholm" },
+ { "value": "sodertalje", "label": "SödertÀlje" },
+ { "value": "nacka", "label": "Nacka" },
+ { "value": "sundbyberg", "label": "Sundbyberg" },
+ { "value": "solna", "label": "Solna" },
+ { "value": "lidingo", "label": "Lidingö" },
+ { "value": "vaxholm", "label": "Vaxholm" },
+ { "value": "norrtalje", "label": "NorrtÀlje" },
+ { "value": "sigtuna", "label": "Sigtuna" },
+ { "value": "nynashamn", "label": "NynÀshamn" },
+ { "value": "habo-uppsala", "label": "HÄbo" },
+ { "value": "alvkarleby", "label": "Ălvkarleby" },
+ { "value": "knivsta", "label": "Knivsta" },
+ { "value": "heby", "label": "Heby" },
+ { "value": "tierp", "label": "Tierp" },
+ { "value": "uppsala", "label": "Uppsala" },
+ { "value": "enkoping", "label": "Enköping" },
+ { "value": "osthammar", "label": "Ăsthammar" },
+ { "value": "vingaker", "label": "VingÄker" },
+ { "value": "gnesta", "label": "Gnesta" },
+ { "value": "nykoping", "label": "Nyköping" },
+ { "value": "oxelosund", "label": "Oxelösund" },
+ { "value": "flen", "label": "Flen" },
+ { "value": "katrineholm", "label": "Katrineholm" },
+ { "value": "eskilstuna", "label": "Eskilstuna" },
+ { "value": "strangnas", "label": "StrÀngnÀs" },
+ { "value": "trosa", "label": "Trosa" },
+ { "value": "odeshog", "label": "Ădeshög" },
+ { "value": "ydre", "label": "Ydre" },
+ { "value": "kinda", "label": "Kinda" },
+ { "value": "boxholm", "label": "Boxholm" },
+ { "value": "atvidaberg", "label": "Ă
tvidaberg" },
+ { "value": "finspang", "label": "FinspÄng" },
+ { "value": "valdemarsvik", "label": "Valdemarsvik" },
+ { "value": "linkoping", "label": "Linköping" },
+ { "value": "norrkoping", "label": "Norrköping" },
+ { "value": "soderkoping", "label": "Söderköping" },
+ { "value": "motala", "label": "Motala" },
+ { "value": "vadstena", "label": "Vadstena" },
+ { "value": "mjolby", "label": "Mjölby" },
+ { "value": "aneby", "label": "Aneby" },
+ { "value": "gnosjo", "label": "Gnosjö" },
+ { "value": "mullsjo", "label": "Mullsjö" },
+ { "value": "habo", "label": "Habo" },
+ { "value": "gislaved", "label": "Gislaved" },
+ { "value": "vaggeryd", "label": "Vaggeryd" },
+ { "value": "jonkoping", "label": "Jönköping" },
+ { "value": "nassjo", "label": "NÀssjö" },
+ { "value": "varnamo", "label": "VĂ€rnamo" },
+ { "value": "savsjo", "label": "SÀvsjö" },
+ { "value": "vetlanda", "label": "Vetlanda" },
+ { "value": "eksjo", "label": "Eksjö" },
+ { "value": "tranas", "label": "TranÄs" },
+ { "value": "uppvidinge", "label": "Uppvidinge" },
+ { "value": "lessebo", "label": "Lessebo" },
+ { "value": "tingsryd", "label": "Tingsryd" },
+ { "value": "alvesta", "label": "Alvesta" },
+ { "value": "almhult", "label": "Ălmhult" },
+ { "value": "markaryd", "label": "Markaryd" },
+ { "value": "vaxjo", "label": "VÀxjö" },
+ { "value": "ljungby", "label": "Ljungby" },
+ { "value": "hogsby", "label": "Högsby" },
+ { "value": "torsas", "label": "TorsÄs" },
+ { "value": "morbylanga", "label": "MörbylÄnga" },
+ { "value": "hultsfred", "label": "Hultsfred" },
+ { "value": "monsteras", "label": "MönsterÄs" },
+ { "value": "emmeboda", "label": "Emmaboda" },
+ { "value": "kalmar", "label": "Kalmar" },
+ { "value": "nybro", "label": "Nybro" },
+ { "value": "oskarshamn", "label": "Oskarshamn" },
+ { "value": "vastervik", "label": "VĂ€stervik" },
+ { "value": "vimmerby", "label": "Vimmerby" },
+ { "value": "borgholm", "label": "Borgholm" },
+ { "value": "gotland", "label": "Gotland" },
+ { "value": "olofstrom", "label": "Olofström" },
+ { "value": "karlskrona", "label": "Karlskrona" },
+ { "value": "ronneby", "label": "Ronneby" },
+ { "value": "karlshamn", "label": "Karlshamn" },
+ { "value": "solvesborg", "label": "Sölvesborg" },
+ { "value": "svalov", "label": "Svalöv" },
+ { "value": "staffanstorp", "label": "Staffanstorp" },
+ { "value": "burlov", "label": "Burlöv" },
+ { "value": "vellinge", "label": "Vellinge" },
+ { "value": "ostra-goinge", "label": "Ăstra Göinge" },
+ { "value": "orkelljunga", "label": "Ărkelljunga" },
+ { "value": "bjuv", "label": "Bjuv" },
+ { "value": "kavlinge", "label": "KĂ€vlinge" },
+ { "value": "lomma", "label": "Lomma" },
+ { "value": "svedala", "label": "Svedala" },
+ { "value": "skurup", "label": "Skurup" },
+ { "value": "sjobo", "label": "Sjöbo" },
+ { "value": "horby", "label": "Hörby" },
+ { "value": "hoor", "label": "Höör" },
+ { "value": "tomelilla", "label": "Tomelilla" },
+ { "value": "bromolla", "label": "Bromölla" },
+ { "value": "osby", "label": "Osby" },
+ { "value": "perstorp", "label": "Perstorp" },
+ { "value": "klippan", "label": "Klippan" },
+ { "value": "astorp", "label": "Ă
storp" },
+ { "value": "bastad", "label": "BÄstad" },
+ { "value": "malmo", "label": "Malmö" },
+ { "value": "lund", "label": "Lund" },
+ { "value": "landskrona", "label": "Landskrona" },
+ { "value": "helsingborg", "label": "Helsingborg" },
+ { "value": "hoganas", "label": "HöganÀs" },
+ { "value": "eslov", "label": "Eslöv" },
+ { "value": "ystad", "label": "Ystad" },
+ { "value": "trelleborg", "label": "Trelleborg" },
+ { "value": "kristianstad", "label": "Kristianstad" },
+ { "value": "simrishamn", "label": "Simrishamn" },
+ { "value": "angelholm", "label": "Ăngelholm" },
+ { "value": "hassleholm", "label": "HĂ€ssleholm" },
+ { "value": "hylte", "label": "Hylte" },
+ { "value": "halmstad", "label": "Halmstad" },
+ { "value": "laholm", "label": "Laholm" },
+ { "value": "falkenberg", "label": "Falkenberg" },
+ { "value": "varberg", "label": "Varberg" },
+ { "value": "kungsbacka", "label": "Kungsbacka" },
+ { "value": "harryda", "label": "HĂ€rryda" },
+ { "value": "partille", "label": "Partille" },
+ { "value": "ockero", "label": "Ăckerö" },
+ { "value": "stenungsund", "label": "Stenungsund" },
+ { "value": "tjorn", "label": "Tjörn" },
+ { "value": "orust", "label": "Orust" },
+ { "value": "sotenas", "label": "SotenÀs" },
+ { "value": "munkedal", "label": "Munkedal" },
+ { "value": "tanum", "label": "Tanum" },
+ { "value": "dals-ed", "label": "Dals-Ed" },
+ { "value": "fargelanda", "label": "FĂ€rgelanda" },
+ { "value": "ale", "label": "Ale" },
+ { "value": "lerum", "label": "Lerum" },
+ { "value": "vargarda", "label": "VÄrgÄrda" },
+ { "value": "bollebygd", "label": "Bollebygd" },
+ { "value": "grastorp", "label": "GrÀstorp" },
+ { "value": "essunga", "label": "Essunga" },
+ { "value": "karlsborg", "label": "Karlsborg" },
+ { "value": "gullspang", "label": "GullspÄng" },
+ { "value": "tranemo", "label": "Tranemo" },
+ { "value": "bengtsfors", "label": "Bengtsfors" },
+ { "value": "mellerud", "label": "Mellerud" },
+ { "value": "lilla-edet", "label": "Lilla Edet" },
+ { "value": "mark", "label": "Mark" },
+ { "value": "svenljunga", "label": "Svenljunga" },
+ { "value": "herrljunga", "label": "Herrljunga" },
+ { "value": "vara", "label": "Vara" },
+ { "value": "gotene", "label": "Götene" },
+ { "value": "tibro", "label": "Tibro" },
+ { "value": "toreboda", "label": "Töreboda" },
+ { "value": "goteborg", "label": "Göteborg" },
+ { "value": "molndal", "label": "Mölndal" },
+ { "value": "kungalv", "label": "KungÀlv" },
+ { "value": "lysekil", "label": "Lysekil" },
+ { "value": "uddevalla", "label": "Uddevalla" },
+ { "value": "stromstad", "label": "Strömstad" },
+ { "value": "vanersborg", "label": "VĂ€nersborg" },
+ { "value": "trollhattan", "label": "TrollhÀttan" },
+ { "value": "alingsas", "label": "AlingsÄs" },
+ { "value": "boras", "label": "BorÄs" },
+ { "value": "ulricehamn", "label": "Ulricehamn" },
+ { "value": "amal", "label": "Ă
mÄl" },
+ { "value": "mariestad", "label": "Mariestad" },
+ { "value": "lidkoping", "label": "Lidköping" },
+ { "value": "skara", "label": "Skara" },
+ { "value": "skovde", "label": "Skövde" },
+ { "value": "hjo", "label": "Hjo" },
+ { "value": "tidaholm", "label": "Tidaholm" },
+ { "value": "falkoping", "label": "Falköping" },
+ { "value": "kil", "label": "Kil" },
+ { "value": "eda", "label": "Eda" },
+ { "value": "torsby", "label": "Torsby" },
+ { "value": "storfors", "label": "Storfors" },
+ { "value": "hammaro", "label": "Hammarö" },
+ { "value": "munkfors", "label": "Munkfors" },
+ { "value": "forshaga", "label": "Forshaga" },
+ { "value": "grums", "label": "Grums" },
+ { "value": "arjang", "label": "Ă
rjÀng" },
+ { "value": "sunne", "label": "Sunne" },
+ { "value": "karlstad", "label": "Karlstad" },
+ { "value": "kristinehamn", "label": "Kristinehamn" },
+ { "value": "filipstad", "label": "Filipstad" },
+ { "value": "hagfors", "label": "Hagfors" },
+ { "value": "arvika", "label": "Arvika" },
+ { "value": "saffle", "label": "SĂ€ffle" },
+ { "value": "lekeberg", "label": "Lekeberg" },
+ { "value": "laxa", "label": "LaxÄ" },
+ { "value": "hallsberg", "label": "Hallsberg" },
+ { "value": "degerfors", "label": "Degerfors" },
+ { "value": "hallefors", "label": "HĂ€llefors" },
+ { "value": "ljusnarsberg", "label": "Ljusnarsberg" },
+ { "value": "orebro", "label": "Ărebro" },
+ { "value": "kumla", "label": "Kumla" },
+ { "value": "askersund", "label": "Askersund" },
+ { "value": "karlskoga", "label": "Karlskoga" },
+ { "value": "nora", "label": "Nora" },
+ { "value": "lindesberg", "label": "Lindesberg" },
+ { "value": "skinnskatteberg", "label": "Skinnskatteberg" },
+ { "value": "surahammar", "label": "Surahammar" },
+ { "value": "kungsor", "label": "Kungsör" },
+ { "value": "hallstahammar", "label": "Hallstahammar" },
+ { "value": "norberg", "label": "Norberg" },
+ { "value": "vasteras", "label": "VÀsterÄs" },
+ { "value": "sala", "label": "Sala" },
+ { "value": "fagersta", "label": "Fagersta" },
+ { "value": "koping", "label": "Köping" },
+ { "value": "arboga", "label": "Arboga" },
+ { "value": "vansbro", "label": "Vansbro" },
+ { "value": "malung-salen", "label": "Malung-SĂ€len" },
+ { "value": "gagnef", "label": "Gagnef" },
+ { "value": "leksand", "label": "Leksand" },
+ { "value": "rattvik", "label": "RĂ€ttvik" },
+ { "value": "orsa", "label": "Orsa" },
+ { "value": "alvdalen", "label": "Ălvdalen" },
+ { "value": "smedjebacken", "label": "Smedjebacken" },
+ { "value": "mora", "label": "Mora" },
+ { "value": "falun", "label": "Falun" },
+ { "value": "borlange", "label": "BorlÀnge" },
+ { "value": "sater", "label": "SĂ€ter" },
+ { "value": "hedemora", "label": "Hedemora" },
+ { "value": "avesta", "label": "Avesta" },
+ { "value": "ludvika", "label": "Ludvika" },
+ { "value": "ockelbo", "label": "Ockelbo" },
+ { "value": "hofors", "label": "Hofors" },
+ { "value": "ovanaker", "label": "OvanÄker" },
+ { "value": "nordanstig", "label": "Nordanstig" },
+ { "value": "ljusdal", "label": "Ljusdal" },
+ { "value": "gavle", "label": "GĂ€vle" },
+ { "value": "sandviken", "label": "Sandviken" },
+ { "value": "soderhamn", "label": "Söderhamn" },
+ { "value": "bollnas", "label": "BollnÀs" },
+ { "value": "hudiksvall", "label": "Hudiksvall" },
+ { "value": "ange", "label": "Ă
nge" },
+ { "value": "timra", "label": "TimrÄ" },
+ { "value": "harnosand", "label": "HÀrnösand" },
+ { "value": "sundsvall", "label": "Sundsvall" },
+ { "value": "kramfors", "label": "Kramfors" },
+ { "value": "solleftea", "label": "SollefteÄ" },
+ { "value": "ornskoldsvik", "label": "Ărnsköldsvik" },
+ { "value": "ragunda", "label": "Ragunda" },
+ { "value": "bracke", "label": "BrÀcke" },
+ { "value": "krokom", "label": "Krokom" },
+ { "value": "stromsund", "label": "Strömsund" },
+ { "value": "are", "label": "Ă
re" },
+ { "value": "berg", "label": "Berg" },
+ { "value": "harjedalen", "label": "HĂ€rjedalen" },
+ { "value": "ostersund", "label": "Ăstersund" },
+ { "value": "nordmaling", "label": "Nordmaling" },
+ { "value": "bjurholm", "label": "Bjurholm" },
+ { "value": "vindeln", "label": "Vindeln" },
+ { "value": "robertsfors", "label": "Robertsfors" },
+ { "value": "norsjo", "label": "Norsjö" },
+ { "value": "mala", "label": "MalÄ" },
+ { "value": "storuman", "label": "Storuman" },
+ { "value": "sorsele", "label": "Sorsele" },
+ { "value": "dorotea", "label": "Dorotea" },
+ { "value": "vannas", "label": "VÀnnÀs" },
+ { "value": "vilhelmina", "label": "Vilhelmina" },
+ { "value": "asele", "label": "Ă
sele" },
+ { "value": "umea", "label": "UmeÄ" },
+ { "value": "lycksele", "label": "Lycksele" },
+ { "value": "skelleftea", "label": "SkellefteÄ" },
+ { "value": "arvidsjaur", "label": "Arvidsjaur" },
+ { "value": "arjeplog", "label": "Arjeplog" },
+ { "value": "jokkmokk", "label": "Jokkmokk" },
+ { "value": "overkalix", "label": "Ăverkalix" },
+ { "value": "kalix", "label": "Kalix" },
+ { "value": "overtornea", "label": "ĂvertorneĂ„" },
+ { "value": "pajala", "label": "Pajala" },
+ { "value": "gallivare", "label": "GĂ€llivare" },
+ { "value": "alvsbyn", "label": "Ălvsbyn" },
+ { "value": "lulea", "label": "LuleÄ" },
+ { "value": "pitea", "label": "PiteÄ" },
+ { "value": "boden", "label": "Boden" },
+ { "value": "haparanda", "label": "Haparanda" },
+ { "value": "kiruna", "label": "Kiruna" }
+]
\ No newline at end of file
diff --git a/frontend/src/layouts/MainPageLayout.jsx b/frontend/src/layouts/MainPageLayout.jsx
new file mode 100644
index 0000000000..5998c4d3f1
--- /dev/null
+++ b/frontend/src/layouts/MainPageLayout.jsx
@@ -0,0 +1,23 @@
+import styled from "styled-components"
+import { Outlet } from "react-router-dom"
+import { Navbar } from "../components/navigation/Navbar"
+
+const StyledLayout = styled.div`
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+`
+
+export const MainPageLayout = () => {
+
+ return (
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/layouts/MedlemsportalLayout.jsx b/frontend/src/layouts/MedlemsportalLayout.jsx
new file mode 100644
index 0000000000..02837ecd30
--- /dev/null
+++ b/frontend/src/layouts/MedlemsportalLayout.jsx
@@ -0,0 +1,104 @@
+import { Outlet } from "react-router-dom"
+import styled from "styled-components"
+import { useEffect, useState } from "react"
+import { useUserStore } from "../store"
+import { API_URL } from "../constants/Constants"
+import { SidebarHamMenu, Sidebar } from "../components/navigation/"
+
+
+const PageWrapper = styled.div`
+ display: flex;
+ min-height: 100vh;
+ background: ${({ theme }) => theme.konto.bgClr};
+`
+
+
+//clamp vÀljer ett min vÀrde och ett maxvÀrde och anpassar innehÄllet till, i det hÀr fallet 25%, nÀr det gÄr
+
+const StyledMain = styled.main`
+ flex: 1; // Tar upp ÄterstÄende utrymme bredvid sidomenyn
+ background: ${({ theme }) => theme.konto.mainPage.bgClr};
+ padding: 40px 10px;
+ box-sizing: border-box;
+ overflow-x: hidden;
+
+ // > h1, > h2, > div > h1, > div > h2
+ // De trÀffar rubriker som Àr:
+ // direkta barn till huvudytan
+ // rubriker inne i en direkt underliggande div
+
+ > h1 {
+ display: flex;
+ justify-content: center;
+ font-size: 28px;
+ text-align: center;
+ color: ${({ theme }) => theme.konto.mainPage.headingClr};
+ position: relative;
+ left: -10vw;
+ }
+ > .mina-sidor-page > div {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ position: relative;
+ left: -10vw;
+ }
+
+`
+
+const PortalPageContent = styled.div`
+ > h1,
+ > div > div {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+`
+
+export const MedlemsportalLayout = () => {
+ const { setUserData, user } = useUserStore()
+ const [expanded, setExpanded] = useState(false)
+
+ console.log(user)
+
+ useEffect(() => {
+ const fetchData = async () => {
+ if (!user?.id) return
+ try {
+ const response = await fetch(`${API_URL}/dashboard/${user.id}`, {
+ headers: { "Authorization": `Bearer ${user.accessToken}` }
+ })
+ const data = await response.json()
+
+ console.log("Dashboard response:", data)
+
+ if (data.user) setUserData(data.user)
+ } catch (error) {
+ console.error("Error fetching dashboard data:", error)
+ }
+ }
+ fetchData()
+ }, [user?.id, user?.accessToken, setUserData])
+
+ const handleToggle = (nextState) => {
+ if (typeof nextState === 'boolean') {
+ setExpanded(nextState)
+ return
+ }
+
+ setExpanded(prev => !prev)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/layouts/index.js b/frontend/src/layouts/index.js
new file mode 100644
index 0000000000..bb42255984
--- /dev/null
+++ b/frontend/src/layouts/index.js
@@ -0,0 +1,2 @@
+export { MainPageLayout } from "./MainPageLayout"
+export { MedlemsportalLayout } from "./MedlemsportalLayout"
\ No newline at end of file
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
index 51294f3998..e0b789bf12 100644
--- a/frontend/src/main.jsx
+++ b/frontend/src/main.jsx
@@ -1,10 +1,11 @@
-import React from "react";
-import ReactDOM from "react-dom/client";
-import { App } from "./App.jsx";
-import "./index.css";
+import React from 'react'
+import ReactDOM from 'react-dom/client'
-ReactDOM.createRoot(document.getElementById("root")).render(
+import { App } from './App.jsx'
+
+
+ReactDOM.createRoot(document.getElementById('root')).render(
-);
+)
diff --git a/frontend/src/pages/BliMedlem.jsx b/frontend/src/pages/BliMedlem.jsx
new file mode 100644
index 0000000000..a297d4e87c
--- /dev/null
+++ b/frontend/src/pages/BliMedlem.jsx
@@ -0,0 +1,38 @@
+// [ ] Gör userStore och byt ut useState överallt, sök pÄ useState i hela appen
+
+import styled from 'styled-components'
+import { SignUpCard } from '../components/pages/bli-medlem/SignUpCard'
+import { Button } from "../components/ui"
+import { Link, useNavigate } from 'react-router-dom'
+import { useContentStore, useUserStore } from '../store'
+
+
+const StyledBliMedlem = styled.div`
+ background: ${({theme}) => theme.loggaIn.bgClr};
+ min-height: 90vh;
+`
+
+const StyledContentContainer = styled.div`
+ padding-top: 40px;
+`
+
+export const BliMedlem = () => {
+ const navigate = useNavigate()
+ const { sharedContent } = useContentStore()
+ const { buttons } = sharedContent
+ const { setUserData } = useUserStore()
+
+
+ const handleSignUp = (userData) => {
+ setUserData(userData)
+ navigate('/konto')
+ }
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/pages/LoggaIn.jsx b/frontend/src/pages/LoggaIn.jsx
new file mode 100644
index 0000000000..f38606225a
--- /dev/null
+++ b/frontend/src/pages/LoggaIn.jsx
@@ -0,0 +1,45 @@
+// [x] Connect to contentStore
+// [ ] Gör userStore och byt ut useState
+
+import styled from 'styled-components'
+import { LogInCard } from '../components/pages/logga-in/LogInCard'
+import { Button } from "../components/ui"
+import { Link, useNavigate } from 'react-router-dom'
+import { PageTitle, BodyText } from '../components/typography'
+import { useContentStore, useUserStore } from '../store'
+
+const StyledLoggaIn = styled.div`
+ background: ${({theme}) => theme.loggaIn.bgClr};
+ min-height: 90vh;
+`
+
+const StyledContentContainer = styled.div`
+ padding-top: 40px;
+`
+
+export const LoggaIn = () => {
+ const { logInContent, sharedContent } = useContentStore()
+ const { pageHeading, pageSubHeading } = logInContent
+ const navigate = useNavigate()
+ const { setUserData } = useUserStore()
+
+
+// [ ] ska handleLogin vara pÄ loginCard istÀllet för loginpage??
+ const handleLogin = (userData) => {
+ setUserData(userData)
+ navigate('/konto')
+ }
+
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
+
diff --git a/frontend/src/pages/OmMedlemskap.jsx b/frontend/src/pages/OmMedlemskap.jsx
new file mode 100644
index 0000000000..2692af7f6a
--- /dev/null
+++ b/frontend/src/pages/OmMedlemskap.jsx
@@ -0,0 +1,51 @@
+import styled from 'styled-components'
+import { MembershipCard } from "../components/pages/om-medlemskap/MembershipCard"
+import { Hero } from '../components/pages/om-medlemskap/Hero'
+import { useContentStore } from "../store"
+import { PageTitle, BodyText } from "../components/typography"
+
+const StyledOmMedlemskap = styled.div`
+ background: ${({ theme }) => theme.omMedlemskap.bgClr};
+ min-height: 90vh;
+`
+
+const StyledContent = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-self: center;
+ padding: 40px 20px;
+ color: ${({ theme }) => theme.omMedlemskap.txtClr};
+`
+
+const StyledCardContainer = styled.div`
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 24px;
+ margin-top: 50px;
+
+ @media (min-width: ${({ theme }) => theme.breakpoints.tablet}) {
+ flex-direction: row;
+ justify-content: center;
+ }
+`
+
+export const OmMedlemskap = () => {
+ const { omMedlemskapContent } = useContentStore()
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
diff --git a/frontend/src/pages/index.js b/frontend/src/pages/index.js
new file mode 100644
index 0000000000..aa51bd430e
--- /dev/null
+++ b/frontend/src/pages/index.js
@@ -0,0 +1,7 @@
+export { OmMedlemskap } from './OmMedlemskap'
+export { LoggaIn } from './LoggaIn'
+export { MinaSidor } from './portal-pages/MinaSidor'
+export { Medlemskap } from './portal-pages/Medlemskap'
+export { Events } from './portal-pages/Events'
+export { BliMedlem } from './BliMedlem'
+export { Erbjudanden } from './portal-pages/Erbjudanden'
\ No newline at end of file
diff --git a/frontend/src/pages/portal-pages/Erbjudanden.jsx b/frontend/src/pages/portal-pages/Erbjudanden.jsx
new file mode 100644
index 0000000000..51110a5252
--- /dev/null
+++ b/frontend/src/pages/portal-pages/Erbjudanden.jsx
@@ -0,0 +1,12 @@
+import { PageTitle } from '../../components/typography'
+import { useContentStore } from '../../store'
+
+export const Erbjudanden = () => {
+ const { erbjudandenContent } = useContentStore()
+
+ return (
+ <>
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/pages/portal-pages/Events.jsx b/frontend/src/pages/portal-pages/Events.jsx
new file mode 100644
index 0000000000..19cac3729a
--- /dev/null
+++ b/frontend/src/pages/portal-pages/Events.jsx
@@ -0,0 +1,16 @@
+import { EventCard } from '../../components/pages/events/EventCard'
+import { PageTitle } from '../../components/typography'
+import { useContentStore } from '../../store'
+
+
+export const Events = () => {
+ const { eventContent } = useContentStore()
+
+ return (
+ <>
+
+
+ >
+ )
+}
+
diff --git a/frontend/src/pages/portal-pages/Medlemskap.jsx b/frontend/src/pages/portal-pages/Medlemskap.jsx
new file mode 100644
index 0000000000..ad09cec981
--- /dev/null
+++ b/frontend/src/pages/portal-pages/Medlemskap.jsx
@@ -0,0 +1,35 @@
+import styled from "styled-components"
+import { MembershipCard } from "../../components/pages/om-medlemskap/MembershipCard"
+import { PageTitle } from "../../components/typography"
+import { useContentStore } from "../../store"
+
+
+const CardsWrapper = styled.div`
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 24px;
+ padding: 40px 20px;
+
+
+ @media (min-width: ${({ theme }) => theme.breakpoints.tablet}) {
+ flex-direction: row;
+ justify-content: center;
+ }
+`
+
+export const Medlemskap = () => {
+ const { medlemskapContent } = useContentStore()
+
+ return (
+ <>
+
+
+
+
+
+ >
+ )
+}
+
diff --git a/frontend/src/pages/portal-pages/MinaSidor.jsx b/frontend/src/pages/portal-pages/MinaSidor.jsx
new file mode 100644
index 0000000000..d2dee8006c
--- /dev/null
+++ b/frontend/src/pages/portal-pages/MinaSidor.jsx
@@ -0,0 +1,211 @@
+import styled from "styled-components"
+import { PageTitle } from "../../components/typography"
+import { useContentStore } from "../../store"
+import { AccountInfoCard } from "../../components/pages/mina-sidor/accountInfoCard"
+
+
+const PageContent = styled.div`
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+`
+
+export const MinaSidor = () => {
+ const { minaSidorContent } = useContentStore()
+
+ return (
+
+
+
+
+ )
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+//[ ] hÀr ligger gamla mina sidor för att se hur vi gör nÀr vi kopplar till patch
+
+// import { useState } from "react"
+// import styled from "styled-components"
+// import { FormInput } from "../components/reusable/ui/FormInput"
+// import { PageTitle } from "../components/reusable/typography/PageTitle"
+// import { useContentStore } from "../store/contentStore"
+
+
+// const PageContent = styled.div`
+// width: 100%;
+// display: flex;
+// flex-direction: column;
+// `
+
+// const Card = styled.div`
+// display: flex;
+// flex-direction: column;
+// gap: 30px;
+// padding: 20px 40px;
+// width: 45vw;
+// position: relative;
+
+// label {
+// color: ${({ theme }) => theme.konto.mainPage.txtClr};
+// font-size: 15px;
+// }
+
+// input {
+// font-size: 16px;
+// height: 42px;
+// padding: 0 12px;
+// }
+// `
+
+// const SectionTitle = styled.h3`
+// color: ${({ theme }) => theme.konto.mainPage.sectionTitleClr};
+// font-size: 22px;
+// font-weight: 400;
+// margin-bottom: 8px;
+// `
+
+// const Section = styled.section``
+
+// const SectionHeader = styled.div`
+// display: flex;
+// justify-content: space-between;
+// align-items: center;
+// margin-bottom: 4px;
+// `
+
+// const EditButton = styled.button`
+// background: transparent;
+// border: 1px solid ${({ theme }) => theme.konto.mainPage.minaSidor.form.editButton.borderClr};
+// color: ${({ theme }) => theme.konto.mainPage.minaSidor.form.editButton.txtClr};
+// font-size: 13px;
+// padding: 4px 12px;
+// border-radius: 20px;
+// cursor: pointer;
+// `
+
+// const SaveButton = styled.button`
+// background: ${({ theme }) => theme.konto.mainPage.minaSidor.form.saveButton.bgClr};
+// border: none;
+// color: ${({ theme }) => theme.konto.mainPage.minaSidor.form.saveButton.txtClr};
+// font-size: 13px;
+// padding: 8px 24px;
+// border-radius: 20px;
+// cursor: pointer;
+// align-self: flex-end;
+// `
+
+// const ValueText = styled.p`
+// color: ${({ theme }) => theme.konto.mainPage.minaSidor.form.txtClr};
+// font-size: 16px;
+// padding: 8px 0;
+// border-bottom: 1px solid #444;
+// `
+
+// const FieldLabel = styled.span`
+// color: ${({ theme }) => theme.konto.mainPage.minaSidor.form.txtClr};
+// font-size: 13px;
+// opacity: 0.7;
+// display: block;
+// margin-top: 12px;
+// `
+
+// export const MinaSidor = () => {
+// const { minaSidorContent } = useContentStore()
+// const [editing, setEditing] = useState(false)
+// const [formData, setFormData] = useState({
+// namn: "",
+// email: "",
+// bank: "",
+// xxx: "",
+// })
+// const [savedData, setSavedData] = useState({
+// namn: "Test Testersson",
+// email: "test.testersson@example.com",
+// bank: "Bank of Test",
+// xxx: "",
+// })
+
+// const handleChange = (e) => {
+// setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }))
+// }
+
+// const handleEdit = () => {
+// setFormData(savedData)
+// setEditing(true)
+// }
+
+// const handleSave = (e) => {
+// e.preventDefault()
+// setSavedData(formData)
+// setEditing(false)
+// }
+
+// return (
+//
+//
+//
+//
+//
+//
+// {!editing && Ăndra}
+//
+
+// Kontaktuppgifter
+// {editing ? (
+// <>
+//
+//
+// >
+// ) : (
+// <>
+// Namn:
+// {savedData.namn || "â"}
+// Email:
+// {savedData.email || "â"}
+// >
+// )}
+
+// Betaluppgifter
+// {editing ? (
+// <>
+//
+//
+// >
+// ) : (
+// <>
+// Bank:
+// {savedData.bank || "â"}
+// XXX:
+// {savedData.xxx || "â"}
+// >
+// )}
+//
+
+// {editing && Spara}
+//
+//
+// )
+// }
+
diff --git a/frontend/src/routes/ProtectedRoute.jsx b/frontend/src/routes/ProtectedRoute.jsx
new file mode 100644
index 0000000000..0f0b5f2b90
--- /dev/null
+++ b/frontend/src/routes/ProtectedRoute.jsx
@@ -0,0 +1,15 @@
+import { Navigate, Outlet } from "react-router-dom"
+import { useUserStore } from "../store"
+
+export const ProtectedRoute = () => {
+ const user = useUserStore((state) => state.user)
+
+ const accessToken =
+ user?.accessToken || user?.savedUser?.accessToken
+
+ if (!accessToken) {
+ return
+ }
+
+ return
+}
diff --git a/frontend/src/routes/index.js b/frontend/src/routes/index.js
new file mode 100644
index 0000000000..1025213ee1
--- /dev/null
+++ b/frontend/src/routes/index.js
@@ -0,0 +1 @@
+export { ProtectedRoute } from './ProtectedRoute'
diff --git a/frontend/src/store/contentStore.js b/frontend/src/store/contentStore.js
new file mode 100644
index 0000000000..9dbab968d9
--- /dev/null
+++ b/frontend/src/store/contentStore.js
@@ -0,0 +1,197 @@
+// [ ] Ska vi skriva krav i lösenordsfÀltets placebolder? Eller i en tooltip? Eller i en liten text under fÀltet?
+// [ ] Ska vi lÀgga till validering och error text i en store? I denna eller i en egen contentStore för formulÀrdata? Ev. Skapa formContentStore och pagesContentStore typ?
+// [ ] LÀgg till mer i medlemskapsContent nÀr vi vet mer om hur sidan ska se ut och vi har implementerat Stripe etc...
+
+import { create } from "zustand"
+
+
+const NavbarContent = {
+ logoAltTxt: "Womenation",
+ logoHref: "https://womenation.se",
+ links: {
+ omMedlemskap: "Om Medlemskap",
+ konto: "Konto"
+ },
+ buttons: {
+ logIn: "Logga in",
+ logOut: "Logga ut"
+ }
+}
+
+const sharedContent = {
+ buttons: {
+ backToMembership: "Tillbaka till Om Medlemskap"
+ }
+}
+
+const omMedlemskapContent = {
+ hero: "OM MEDLEMSKAP",
+ heading: "BLI PREMIUM MEDLEM!",
+ text: "FÄ exklusiva rabatter och erbjudande. Förtur pÄ eventbiljetter",
+ cards: {
+ basic: {
+ type: "BASIC",
+ price: "Gratis",
+ benefit: "Köpa biljetter via portalen\nTillgÄng till medlemsportalen",
+ },
+ pro: {
+ type: "PRO",
+ price: "SEK 200",
+ benefit: "FörtrÀde till events\nExklusiva rabatter frÄn vÄra partners\nTillgÄng till Womenation marketplace",
+ },
+ },
+ buttons: {
+ basic: "VĂ€lj Basic (inaktiv)",
+ pro: "VĂ€lj Pro (inaktiv)",
+ }
+}
+
+const signuUpContent = {
+ pageHeading: "",
+ pageSubHeading: "",
+ form: {
+ heading: "BLI MEDLEM",
+ subHeading: "Skapa konto!\nfyll i din information för att fÄ tillgÄng till ditt konto.",
+ labels: {
+ firstName: "Förnamn",
+ lastName: "Efternamn",
+ email: "E-post",
+ password: "Lösenord",
+ city: "I vilken stad bor du?",
+ justifyMembership: "BerÀtta kort om dig sjÀlv och varför du vill vara en del av Womenation!",
+ workStatus: {
+ label: "Vart befinner du dig just nu?",
+ desc: "VÀlj det alternativ som passar din arbetssituation bÀst, du kan vÀlja fler alternativ.",
+ checkboxLabel: {
+ worker: "Jobbar i ett företag eller organisation",
+ owner: "Driver eget företag",
+ startUp: "HÄller pÄ att starta eget",
+ searching: "Söker nytt jobb eller vill byta bana",
+ other: "Annat"
+ },
+ },
+ },
+ placeholders: {
+ firstName: "ex. Anna",
+ lastName: "ex. Andersson",
+ email: "ex. anna@example.com",
+ password: "Ange ditt lösenord (Skriv krav hÀr?)",
+ city: "Sök bland alla svenska stÀder...",
+ justifyMembership: "BerÀtta kort om dig sjÀlv och varför du vill vara en del av Womenation!",
+ workStatusOther: "Skriv din arbetssituation hÀr..."
+ },
+ button: {
+ text: "Bli medlem",
+ textSubmitting: "Skapar konto..."
+ },
+ navigateToLogIn: {
+ text: "Redan medlem? Logga in ",
+ linkText: "hÀr",
+ linkTo: "/logga-in"
+ }
+ }
+}
+
+const logInContent = {
+ pageHeading: "",
+ pageSubHeading: "",
+ form: {
+ heading: "LOGGA IN",
+ subHeading: "VÀlkommen tillbaka!\nLogga in för att fÄ tillgÄng till ditt konto.",
+ labels: {
+ email: "E-post",
+ password: "Lösenord",
+ },
+ placeholders: {
+ email: "ex. anna@example.com",
+ password: "Ange ditt lösenord",
+ },
+ button: {
+ text: "Logga in",
+ textSubmitting: "Loggar in..."
+ },
+ navigateToSignUp: {
+ text: "Inte registrerad? Bli medlem ",
+ linkText: "hÀr",
+ linkTo: "/bli-medlem"
+ }
+ }
+}
+
+const sidebarContent = {
+ links: {
+ //AnvÀnds inte just nu
+ minaSidor: "Mina sidor",
+ medlemskap: "Medlemskap",
+ events: "Events",
+ erbjudanden: "Erbjudanden"
+ }
+}
+
+const minaSidorContent = {
+ heading: "Mina sidor",
+ description: "HĂ€r kan du se dina medlemskap och uppdatera din information.",
+ form: {
+ labels: {
+ firstName: "Förnamn",
+ lastName: "Efternamn",
+ email: "E-post",
+ city: "Stad",
+ },
+ inputData: {
+ firstName: "user.firstName",
+ lastName: "user.lastName",
+ email: "user.email",
+ city: "user.city"
+ },
+ editButton: "Uppdatera information",
+ saveButton: "Spara",
+ textButtonSubmitting: "Uppdaterar..."
+ }
+}
+
+const medlemskapContent = {
+ heading: "Medlemskap",
+ desc: "",
+ // Just nu anvÀnds omMedlemskapsContent hÀr
+}
+
+const eventContent = {
+ heading: "Events",
+ sdsc: "HÀr Àr vÄra kommande event. Som medlem fÄr du förtur pÄ biljetter. HÄll utkik, fler event kommer snart!",
+ // Just nu anvÀnds mockup data. Ska vi hÀr mappa frÄn backend via nya event-GET frÄn event-POST skapat i adminlÀge)
+ events: [
+ { id: 1, image: "media/images/1 (258).jpg", date: "24 mars", title: "Event title", desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", price: "100kr", city: "Stockholm" },
+ { id: 2, date: "7 april", title: "Event title", desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", price: "100kr", city: "Göteborg" },
+ { id: 3, date: "19 maj", title: "Event title", desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", price: "100kr", city: "Malmö" },
+ { id: 4, date: "31 maj", title: "Event title", desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", price: "100kr", city: "Stockholm" }
+ ],
+ button: "AnmÀl dig hÀr (inaktiv)"
+// card: {
+// image: "",
+// heading: "",
+// description: "",
+// price: "",
+// place: "",
+// tag: "", // datum
+// button: ""
+// }
+}
+
+const erbjudandenContent = {
+ heading: "Erbjudanden",
+ description: "HÀr hittar du vÄra aktuella erbjudanden."
+}
+
+export const useContentStore = create(() => ({
+ sharedContent: sharedContent,
+ navbarContent: NavbarContent,
+ omMedlemskapContent: omMedlemskapContent,
+ signuUpContent: signuUpContent,
+ logInContent: logInContent,
+ sidebarContent: sidebarContent,
+ minaSidorContent: minaSidorContent,
+ medlemskapContent: medlemskapContent,
+ eventContent: eventContent,
+ erbjudandenContent: erbjudandenContent
+}))
diff --git a/frontend/src/store/formStore.js b/frontend/src/store/formStore.js
new file mode 100644
index 0000000000..39257daa7a
--- /dev/null
+++ b/frontend/src/store/formStore.js
@@ -0,0 +1,87 @@
+import { create } from 'zustand'
+
+const getInitialSignUpData = () => ({
+ firstName: '',
+ lastName: '',
+ email: '',
+ password: '',
+ city: null,
+ justifyMembership: '',
+ workStatus: {
+ worker: false,
+ owner: false,
+ startUp: false,
+ searching: false,
+ other: false,
+ otherText: ''
+ },
+ error: '',
+ isSubmitting: false
+})
+
+const getInitialLoginData = () => ({
+ email: '',
+ password: '',
+ error: '',
+ isSubmitting: false
+})
+
+export const useFormStore = create((set) => ({
+
+ //signup form state
+ signUpData: getInitialSignUpData(),
+
+ setSignUpField: (field, value) =>
+ set((state) => ({
+ signUpData: { ...state.signUpData, [field]: value}
+ })),
+
+ setWorkStatus: (value, checked) =>
+ set((state) => ({
+ signUpData: {
+ ...state.signUpData,
+ workStatus: {...state.signUpData.workStatus, [value]: checked}
+ }
+ })),
+
+ setOtherText: (text) =>
+ set((state) => ({
+ signUpData: {
+ ...state.signUpData,
+ workStatus: {...state.signUpData.workStatus, otherText: text}
+ }
+ })),
+
+ setSignUpError: (error) =>
+ set((state) => ({
+ signUpData: {...state.signUpData, error}
+ })),
+
+ setSignUpSubmitting: (isSubmitting) =>
+ set((state) => ({
+ signUpData: {...state.signUpData, isSubmitting}
+ })),
+
+ resetSignUp: () => set({ signUpData: getInitialSignUpData() }),
+
+ //login form state
+ loginData: getInitialLoginData(),
+
+ setLoginField: (field, value) =>
+ set((state) => ({
+ loginData: {...state.loginData, [field]: value}
+ })),
+
+ setLoginError: (error) =>
+ set((state) => ({
+ loginData: {...state.loginData, error}
+ })),
+
+ setLoginSubmitting: (isSubmitting) =>
+ set((state) => ({
+ loginData: {...state.loginData, isSubmitting}
+ })),
+
+ resetLogin: () => set({ loginData: getInitialLoginData()} )
+
+}))
\ No newline at end of file
diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js
new file mode 100644
index 0000000000..d209df8db2
--- /dev/null
+++ b/frontend/src/store/index.js
@@ -0,0 +1,3 @@
+export { useUserStore } from './userStore'
+export { useContentStore } from './contentStore'
+export { useFormStore } from './formStore'
diff --git a/frontend/src/store/userStore.js b/frontend/src/store/userStore.js
new file mode 100644
index 0000000000..84ebc4ee5b
--- /dev/null
+++ b/frontend/src/store/userStore.js
@@ -0,0 +1,46 @@
+// [ ] Ta bort persist/localStorage och anvÀnd fetch/httpOnly cookies
+// [ ] AnvÀnd setLoading och error i hela appen dÀr det behövs
+// [ ] LĂ€gg till logout funktion som tar bort user data och localStorage
+// [ ] LĂ€gg till token expiration och refresh token logik
+// [ ] LÀgg till user role/permissions i user data och anvÀnd det för att styra access till olika delar av appen (isAdmin, isPremium osv)
+// [ ] LÀgg till user profile data i user data och anvÀnd det i medlemsportal (avatar, name, email osv) istÀllet för att hÀmta det separat frÄn dashboard endpoint - diskutera om det hÀr behövs?
+// [ ] LÀgg till en global error state i userStore som kan anvÀndas för att visa globala error messages i UI nÀr nÄgot gÄr fel med user auth eller dashboard fetch
+// [ ] Ska vi lÀgga till en global isAuthenticated state (?)
+// [ ] Diskutera om vi behöver en separat authStore för auth relaterad state och logik, eller om det Àr okej att ha allt i userStore för enkelhetens skull. AuthStore skulle kunna hantera allt som har med inloggning, tokenhantering, user roles osv att göra, medan userStore bara hanterar user profile data och dashboard data
+// [ ] Vi har diskuterat att flytta ut fetch frÄn bÄda formulÀren till en authStore, vi kommer inte göra det nu men det kan vara vÀrt för att tunna ut formulÀr komponenterna och det kan förenkla arbetet senare nÀr vi implementerar HTTPonly cookies. Vi gör mer research dÄ
+// [ ] Flytta upp fetchning av user-datan till userstore istÀllet för i medlemsportalslayout
+
+import { create } from 'zustand'
+import { persist } from 'zustand/middleware'
+
+const getInitialUserData = () => ({
+ user: null,
+ isLoading: false,
+})
+
+export const useUserStore = create(
+ persist(
+ (set) => ({
+ ...getInitialUserData(),
+
+ setUserData: (user) =>
+ set({
+ user,
+ isLoading: false
+ }),
+
+ setLoading: (isLoading) =>
+ set({
+ isLoading
+ }),
+
+ resetUserData: () =>
+ set({
+ ...getInitialUserData()
+ })
+ }),
+ {
+ name: "user-storage"
+ }
+ )
+)
\ No newline at end of file
diff --git a/frontend/src/styles/GlobalStyle.jsx b/frontend/src/styles/GlobalStyle.jsx
new file mode 100644
index 0000000000..3c876b4b0b
--- /dev/null
+++ b/frontend/src/styles/GlobalStyle.jsx
@@ -0,0 +1,118 @@
+// [x] TODO Put in correct fonts etc from graphic design
+// [ ] TODO Look through reset
+
+import { createGlobalStyle } from "styled-components"
+
+export const GlobalStyle = createGlobalStyle`
+ :root {
+ /* ------------- */
+ /* Font settings */
+ /* ------------- */
+
+ --font-body-txt: 'Ubuntu', sans-serif; // fontweight 400
+ --font-heading: 'Ubuntu', sans-serif; // fontweight 700
+ --font-highlighted-txt: 'Didot', serif;
+
+ //--h1-font-weight: 600;
+ --p-font-size-mobile: 16px;
+ --h1-font-size-mobile: 32px;
+
+ --p-font-size-desktop:16px;
+ --h1-font-size-desktop: 32px;
+
+ /* ----------- */
+ /* Main colors */
+ /* ----------- */
+ --darkPurple: #442E57; //from graphic design
+ --mediumPurple: #574368; //from website
+ --lightPurple: #CFCCE4; //from graphic design
+ --gold: #CDB56C; //from graphic design
+ --darkGrey: #353333; //from website
+ --lightGrey: #C6C6C7; //made up
+ --black: #000000;
+ --white: #ffffff;
+ }
+
+ /* --------- */
+ /* Reset */
+ /* --------- */
+ *, *::before, *::after {
+ box-sizing: border-box;
+ }
+
+ * {
+ margin: 0;
+ padding: 0;
+ }
+
+ html {
+ scroll-behavior: smooth;
+ //-webkit-text-size-adjust: 100%;
+ //text-size-adjust: 100%;
+ }
+
+ body {
+ min-height: 100vh;
+ line-height: 1.5;
+ font-family: var(--font-body-txt);
+ font-weight: 400;
+ max-width: 100vw;
+ }
+
+ img, picture, video, canvas, svg {
+ display: block;
+ max-width: 100%;
+ }
+
+ input, button, textarea, select {
+ font: inherit;
+ }
+
+ h1, h2, h3, h4, h5, h6 {
+ display: block;
+ font-family: var(--font-heading);
+ font-size: 2em;
+ margin-block-start: 0.67em;
+ margin-block-end: 0.67em;
+ margin-inline-start: 0px;
+ margin-inline-end: 0px;
+ font-weight: 700;
+ unicode-bidi: isolate;
+ }
+
+ ul, ol {
+ list-style: none;
+ }
+
+ h1 {
+ font-size: var(--h1-font-size-mobile);
+ }
+
+ p {
+ overflow-wrap: break-word;
+ font-size: var(--p-font-size-mobile);
+ }
+
+ a {
+ text-decoration: none;
+ color: inherit;
+ }
+
+ @media (min-width: ${({ theme }) => theme.breakpoints.tablet}) {
+
+ }
+
+ @media (min-width: ${({ theme }) => theme.breakpoints.desktop}) {
+ h1 {
+ font-size: var(--h1-font-size-desktop);
+ }
+
+ p {
+ font-size: var(--p-font-size-desktop);
+ }
+ }
+
+ @media (min-width: ${({ theme }) => theme.breakpoints.desktopLarge}) {
+
+ }
+`
\ No newline at end of file
diff --git a/frontend/src/themes/Theme.jsx b/frontend/src/themes/Theme.jsx
new file mode 100644
index 0000000000..31c268f658
--- /dev/null
+++ b/frontend/src/themes/Theme.jsx
@@ -0,0 +1,247 @@
+// [x] Mikaela ska kolla upp hur vi ska strukturera theme colors efter pages.
+// [x] gÄ igenom vad som anvÀnder colors och Àndra till strukturen nedan
+
+
+export const Theme = {
+
+ //Endingbreakpoints for said media querie.
+ breakpoints: {
+ mobile: '576px',
+ tablet: '768px',
+ desktop: '1024px',
+ },
+
+ // --darkPurple: '#442E57', //from graphic design
+ // --mediumPurple: '#574368', //from website
+ // --lightPurple: '#CFCCE4', //from graphic design
+ // --gold: '#CDB56C', //from graphic design
+ // --darkGrey: '#353333', //from website
+ // --lightGrey: '#C6C6C7', //made up
+ // --black: '#000000',
+ // --white: '#ffffff'
+
+ navbar: {
+ bgClr: 'var(--black)',
+ txtClr: 'var(--white)',
+ spanClr: 'var(--white)',
+ },
+
+ //Color theme for "omMedlemskap" page
+ omMedlemskap: {
+ bgClr: 'var(--darkPurple)',
+ txtClr: 'var(--white)',
+
+ cards:{
+ bgClr: 'var(--mediumPurple)',
+ txtClr: 'var(--white)'
+ },
+
+ buttons: {
+ vÀljMedlemskap: {
+ bgClr: 'var(--gold)',
+ txtClr: 'var(--black)',
+ },
+ loggaIn: {
+ bgClr: 'var(--gold)',
+ txtClr: 'var(--black)',
+ },
+ },
+ },
+
+ signUp: {
+ bgClr: 'var(--mediumPurple)',
+ txtClr: '',
+ cards: {
+ bgClr: '',
+ },
+ form: {
+ bgClr: 'var(--darkPurple)',
+ txtClr: 'var(--white)',
+ inputBgClr: 'var(--lightPurple)',
+ inputTxtClr: 'var(--darkGrey)',
+ inputBgClrHover: 'var(--mediumPurple)',
+ inputTxtClrHover: 'var(--white)',
+ inputBgClrSelected: 'var(--darkPurple)',
+ inputTxtClrSelected: 'var(--white)',
+ },
+ buttons: {
+ signUp: {
+ bgClr: 'var(--gold)',
+ txtClr: 'var(--black)',
+ },
+ gÄTillbaka: {
+ bgClr: 'var(--lightGrey)',
+ txtClr: 'var(--black)',
+ },
+ },
+ },
+
+ //Color theme for "loggaIn" page
+ loggaIn: {
+ bgClr: 'var(--mediumPurple)',
+ txtClr: '',
+ cards: {
+ bgClr: '',
+ },
+ form: {
+ bgClr: 'var(--darkPurple)',
+ txtClr: 'var(--white)',
+ inputBgClr: 'var(--lightPurple)',
+ inputTxtClr: 'var(--darkGrey)',
+ },
+ buttons: {
+ loggaIn: {
+ bgClr: 'var(--gold)',
+ txtClr: 'var(--black)',
+ },
+ gÄTillbaka: {
+ bgClr: 'var(--lightGrey)',
+ txtClr: 'var(--black)',
+ },
+ },
+ },
+
+ konto: {
+ bgClr: 'var(--darkGrey)',
+ txtClr: 'var(--white)',
+ txtClrH1: 'var(--gold)',
+
+ sidebar: {
+ bgClr: 'var(--darkGrey)',
+ avatar: {
+ bgClr: 'inherit',
+ avatarClr: 'var(--gold)',
+ txtClr: 'var(--gold)',
+ },
+ links: {
+ bgClr: 'inherit',
+ txtClr: 'var(--gold)',
+ borderClrActive: 'var(--gold)',
+ hoverBgClr: '',
+ hoverTxtClr: '',
+ hoverBorderClr: '',
+ },
+ },
+
+ mainPage: {
+ pageTitleClr: "var(--gold)",
+ sectionTitleClr: "var(--lightGrey)",
+ bgClr: 'var(--darkGrey)',
+ txtClr: 'var(--white)',
+
+ minaSidor: {
+ form: {
+ cardBgClr: 'var(--mediumPurple)',
+ bgClr: 'var(--lightPurple)',
+ txtClr: 'var(--white)',
+
+ editButton: {
+ bgClr: 'var(--darkGrey)',
+ txtClr: 'var(--gold)',
+ borderClr: 'var(--gold)',
+ hoverBgClr: '',
+ hoverTextClr: '',
+ hoverBorderClr: '',
+ },
+
+ saveButton: {
+ bgClr: 'var(--gold)',
+ txtClr: 'var(--darkGrey)',
+ borderClr: '',
+ hoverBgClr: '',
+ hoverTextClr: '',
+ hoverBorderClr: '',
+ },
+ },
+ },
+
+ medlemskap: {
+ // Detta ska uppdateras nÀr vi vet mer om hur sidan ska se ut och har implementerat Stripe etc...
+ cards: {
+ bgClr: 'var(--darkPurple)',
+ textClr: {
+ h2: 'var(--white)',
+ p: 'var(--white)',
+ tag: 'var(--white)',
+ },
+ button: {
+ bgClr: 'var(--gold)',
+ txtClr: 'var(--darkGrey)',
+ borderClr: '',
+ hoverBgClr: '',
+ hoverTextClr: '',
+ hoverBorderClr: '',
+ },
+ checkmark: {
+ bgClr: 'var(--darkGrey)',
+ symbolClr: 'var(--gold)',
+ },
+ },
+ },
+
+ events: {
+ cards: {
+ bgClr: 'var(--black)',
+ textClr: {
+ h2: 'var(--white)',
+ p: 'var(--white)',
+ price: 'var(--white)',
+ },
+ button: {
+ bgClr: 'var(--gold)',
+ txtClr: 'var(--darkGrey)',
+ borderClr: '',
+ hoverBgClr: '',
+ hoverTextClr: '',
+ hoverBorderClr: '',
+ },
+ tag: {
+ bgClr: 'var(--mediumPurple)',
+ txtClr: 'var(--white)',
+ },
+ },
+ },
+
+ erbjudanden: {
+ cards: {
+ bgClr: '',
+ textClr: {
+ h2: '',
+ p: '',
+ },
+ button: {
+ bgClr: '',
+ txtClr: '',
+ borderClr: '',
+ hoverBgClr: '',
+ hoverTextClr: '',
+ hoverBorderClr: '',
+ },
+ tag: {
+ bgClr: '',
+ txtClr: '',
+ },
+ },
+ },
+ },
+ },
+
+ form: {
+ bgClr: 'var(--darkPurple)',
+ txtClr: 'var(--white)',
+ inputBgClr: 'var(--lightPurple)',
+ inputTxtClr: 'var(--darkGrey)',
+ inputBgClrHover: 'var(--mediumPurple)',
+ inputTxtClrHover: 'var(--white)',
+ inputBgClrSelected: 'var(--darkPurple)',
+ inputTxtClrSelected: 'var(--white)',
+ },
+
+ buttons: {
+ loggaIn: {
+ bgClr: 'var(--gold)',
+ txtClr: 'var(--black)',
+ },
+ }
+}
+
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
index 5a33944a9b..ba242447bc 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -1,7 +1,7 @@
-import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
- plugins: [react()],
+ plugins: [react()]
})
diff --git a/netlify.toml b/netlify.toml
new file mode 100644
index 0000000000..f4196f3032
--- /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
index 680d190772..ea5d4af0f0 100644
--- a/package.json
+++ b/package.json
@@ -3,5 +3,8 @@
"version": "1.0.0",
"scripts": {
"postinstall": "npm install --prefix backend"
+ },
+ "dependencies": {
+ "@mailchimp/mailchimp_marketing": "^3.0.80"
}
-}
\ No newline at end of file
+}
diff --git a/src/components/ComponentTemplate.jsx b/src/components/ComponentTemplate.jsx
new file mode 100644
index 0000000000..a9d35cdad2
--- /dev/null
+++ b/src/components/ComponentTemplate.jsx
@@ -0,0 +1,15 @@
+import styled from 'styled-components'
+import { } from ''
+
+
+const StyledComponent = styled.div`
+
+`
+
+export const ComponentTemplate = () => {
+ return (
+ <>
+ >
+ )
+}
+
diff --git a/src/components/SETUP-PREPLANNING.md b/src/components/SETUP-PREPLANNING.md
new file mode 100644
index 0000000000..25118ddb5a
--- /dev/null
+++ b/src/components/SETUP-PREPLANNING.md
@@ -0,0 +1,14 @@
+
+- Pre-planning bulletpoints - Mikaela
+- Make design in figma - Carolina
+
+- Breakdown desgin into components
+- Choose what tools to use
+ - Styled components?
+ - Themes?
+ - Routes?
+ - Layout?
+ - Globalstyling?
+
+
+