diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml new file mode 100644 index 00000000..c6a544d1 --- /dev/null +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -0,0 +1,25 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on PR +on: + pull_request: + push: + branches: + - master +permissions: + checks: write + contents: read + pull-requests: write +jobs: + build_and_preview: + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm ci && npm run build + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_MEDIETEKNIK }} + projectId: medieteknik diff --git a/backend/Dockerfile b/backend/Dockerfile index c8a27a1b..d0808efa 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -13,8 +13,6 @@ WORKDIR /app COPY . /app COPY env/service-account-file.json /app/service-account-file.json -# Set up the relevant environment variable -ENV GOOGLE_APPLICATION_CREDENTIALS="/app/service-account-file.json" # Install any needed packages specified in requirements.txt RUN pip install --trusted-host pypi.python.org -r requirements.txt diff --git a/backend/models/committees/committee.py b/backend/models/committees/committee.py index 4c14f125..54dd6f93 100644 --- a/backend/models/committees/committee.py +++ b/backend/models/committees/committee.py @@ -1,6 +1,6 @@ import uuid from typing import Any, Dict, List -from sqlalchemy import DateTime, String, Integer, Column, ForeignKey, inspect, text +from sqlalchemy import String, Integer, Column, ForeignKey, inspect, text from sqlalchemy.dialects.postgresql import UUID from utility.database import db from utility.constants import AVAILABLE_LANGUAGES @@ -91,7 +91,7 @@ class CommitteeTranslation(db.Model): ) title = Column(String(125)) - description = Column(String(500)) + description = Column(String(512)) # Foreign keys committee_id = Column(UUID(as_uuid=True), ForeignKey("committee.committee_id")) diff --git a/backend/models/committees/committee_category.py b/backend/models/committees/committee_category.py index d16c3d65..c7bcba5e 100644 --- a/backend/models/committees/committee_category.py +++ b/backend/models/committees/committee_category.py @@ -1,6 +1,6 @@ import uuid from typing import List -from sqlalchemy import String, Integer, Column, ForeignKey, inspect, text +from sqlalchemy import String, Column, ForeignKey, inspect, text from sqlalchemy.dialects.postgresql import UUID from utility.database import db from utility.constants import AVAILABLE_LANGUAGES diff --git a/backend/models/committees/committee_position.py b/backend/models/committees/committee_position.py index 2e2de987..5219b13e 100644 --- a/backend/models/committees/committee_position.py +++ b/backend/models/committees/committee_position.py @@ -31,6 +31,7 @@ class CommitteePositionCategory(enum.Enum): NΓ„RINGSLIV_OCH_KOMMUNIKATION = "NΓ„RINGSLIV OCH KOMMUNIKATION" STUDIESOCIALT = "STUDIESOCIALT" FANBORGEN = "FANBORGEN" + UTBILDNING = "UTBILDNING" class CommitteePosition(db.Model): @@ -43,7 +44,7 @@ class CommitteePosition(db.Model): server_default=text("gen_random_uuid()"), ) - email = Column(String(255), unique=True) + email = Column(String(255)) weight = Column(Integer, default=1_000) role = Column( Enum(CommitteePositionsRole), @@ -118,18 +119,19 @@ def to_dict( ] if is_public_route: - del data["weight"] del data["role"] - if include_parent: + if include_parent and self.committee_id: parent_committee = Committee.query.filter_by( committee_id=self.committee_id ).first() - if parent_committee and isinstance(parent_committee, Committee): - data["committee"] = parent_committee.to_dict( - provided_languages=provided_languages, - ) + if not parent_committee or not isinstance(parent_committee, Committee): + return data + + data["committee"] = parent_committee.to_dict( + provided_languages=provided_languages, + ) return data diff --git a/backend/routes/__init__.py b/backend/routes/__init__.py index 27a17d54..f6cdfda5 100644 --- a/backend/routes/__init__.py +++ b/backend/routes/__init__.py @@ -203,9 +203,13 @@ def index(): @app.route("/api/v1/csrf-token") def get_csrf_token(): - if "csrf_token" not in session: - session["csrf_token"] = generate_csrf() - return jsonify({"token": session["csrf_token"]}) + token = session.get("csrf_token") + print(token) + if not token: + new_token = generate_csrf() + session["csrf_token"] = new_token + return jsonify({"token": new_token}) + return jsonify({"token": token}) @app.route("/oauth/kth/login") def kth_login(): diff --git a/backend/routes/committee_position_routes.py b/backend/routes/committee_position_routes.py index 499b8c92..fac29819 100644 --- a/backend/routes/committee_position_routes.py +++ b/backend/routes/committee_position_routes.py @@ -38,7 +38,7 @@ def create_committee_position(): data: Dict[str, Any] = json.loads(json.dumps(data)) email = data.get("email") - category: CommitteePositionCategory = data.get("category") + category: str = data.get("category") weight = data.get("weight") translations: List[Dict[str, Any]] = data.get("translations") committee_title = data.get("committee_title") @@ -53,7 +53,7 @@ def create_committee_position(): new_position = CommitteePosition( email=email if email else None, - category=category, + category=None if category == "NONE" else category.replace(" ", "_").upper(), weight=weight, active=True, role=CommitteePositionsRole.COMMITTEE.value, diff --git a/backend/routes/student_routes.py b/backend/routes/student_routes.py index 4654d7d8..ab4c89be 100644 --- a/backend/routes/student_routes.py +++ b/backend/routes/student_routes.py @@ -9,7 +9,6 @@ from flask import Blueprint, jsonify, make_response, request from flask_jwt_extended import ( create_access_token, - get_jwt, get_jwt_identity, jwt_required, current_user, @@ -21,7 +20,9 @@ from models.committees.committee_position import CommitteePosition from models.core.student import Student, StudentMembership from services.core.student import login, assign_password, get_permissions, update +from utility.gc import delete_file, upload_file from utility.translation import retrieve_languages +from utility.database import db student_bp = Blueprint("student", __name__) @@ -57,6 +58,46 @@ def update_student(): ) +@student_bp.route("/reception", methods=["PUT"]) +@csrf_protected +@jwt_required() +def update_reception(): + student_id = get_jwt_identity() + student = Student.query.filter_by(student_id=student_id).one_or_none() + + if not student or not isinstance(student, Student): + return jsonify({"error": "Invalid credentials"}), 401 + + reception_image = request.files.get("reception_image") + reception_name = request.form.get("reception_name") + + if not reception_image: + return jsonify({"error": "Invalid data"}), 400 + + file_extension = reception_image.filename.split(".")[-1] + + if getattr(student, "reception_profile_picture_url"): + delete_file( + getattr(student, "reception_profile_picture_url"), + ) + + result = upload_file( + file=reception_image, + file_name=f"{student.student_id}.{file_extension}", + path="profile/reception", + ) + + if not result: + return jsonify({"error": "Failed to upload"}), 400 + + setattr(student, "reception_profile_picture_url", result) + setattr(student, "reception_name", reception_name) + + db.session.commit() + + return jsonify({"url": result}), 201 + + @student_bp.route("/refresh", methods=["POST"]) @jwt_required(refresh=True) def refresh_token(): diff --git a/backend/services/committees/public/committee_position.py b/backend/services/committees/public/committee_position.py index e3aff72a..573849a7 100644 --- a/backend/services/committees/public/committee_position.py +++ b/backend/services/committees/public/committee_position.py @@ -150,6 +150,10 @@ def get_all_committee_members( .filter_by( committee_id=committee.committee_id, ) + .filter( + CommitteePosition.active.is_(True), + StudentMembership.termination_date.is_(None), + ) .join( Student, StudentMembership.student_id == Student.student_id, diff --git a/backend/services/content/item.py b/backend/services/content/item.py index 58a1d9b1..5c6f3404 100644 --- a/backend/services/content/item.py +++ b/backend/services/content/item.py @@ -189,16 +189,16 @@ def create_item( authors_items_ids = [] translation_table = None - if isinstance(item_table, News): + if isinstance(item_table, News) or item_table is News: authors_items_ids = [a.news_id for a in all_authors_items] translation_table = NewsTranslation - elif isinstance(item_table, Event): + elif isinstance(item_table, Event) or item_table is Event: authors_items_ids = [a.event_id for a in all_authors_items] translation_table = EventTranslation - elif isinstance(item_table, Album): + elif isinstance(item_table, Album) or item_table is Album: authors_items_ids = [a.album_id for a in all_authors_items] translation_table = AlbumTranslation - elif isinstance(item_table, Document): + elif isinstance(item_table, Document) or item_table is Document: authors_items_ids = [a.document_id for a in all_authors_items] translation_table = DocumentTranslation else: diff --git a/backend/utility/csrf.py b/backend/utility/csrf.py index 2621ef94..974425dd 100644 --- a/backend/utility/csrf.py +++ b/backend/utility/csrf.py @@ -33,12 +33,28 @@ def validate_csrf(csrf_token) -> Response | bool: return response if csrf_token != header_csrf_token: - response = make_response(jsonify({"message": "Invalid CSRF Token"})) + response = make_response( + jsonify( + { + "message": "Invalid Header CSRF Token", + "csrf_token": csrf_token, + "header_csrf_token": header_csrf_token, + } + ) + ) response.status_code = HTTPStatus.FORBIDDEN return response if csrf_token != session_csrf_token: - response = make_response(jsonify({"message": "Invalid CSRF Token"})) + response = make_response( + jsonify( + { + "message": "Invalid Session CSRF Token", + "csrf_token": csrf_token, + "session_csrf_token": session_csrf_token, + } + ) + ) response.status_code = HTTPStatus.FORBIDDEN return response diff --git a/frontend/.firebase/hosting.cHVibGlj.cache b/frontend/.firebase/hosting.cHVibGlj.cache new file mode 100644 index 00000000..38f5eb55 --- /dev/null +++ b/frontend/.firebase/hosting.cHVibGlj.cache @@ -0,0 +1,32 @@ +index.html,1725154496799,d9d7bc4ab8a92b15cc2009280fe4aea0d497560fe7b84d597ebcd2ad04f7c317 +404.html,1725154496712,762bf484ba67404bd1a3b181546ea28d60dfddf18e9dd4795d8d25bcf3c1a890 +images/logo.webp,1720431006580,2e2ed372391f67e94e94c4e0fecb24320da557538daa7fb4eabf4b7f411e9681 +images/svg/youtube.svg,1710785650591,e9016d78170c690d49d935a91bd81e02a734d4abafc67b0747ae8e803c827aab +images/svg/ths.svg,1717953182261,5b1b5af7e08278a04437bfe6030dfce7ea53232079ad7388f70ee247c90dac8b +images/svg/linkedin.svg,1710787637900,d01bff426eb95b311bccd560e257c1a426d0789b3721b80b8aa6e2223e363a3f +images/svg/mbd.svg,1711051608799,6d486107a644d05cc58f7ecc67eeb6f433540450d21835b17a1cecbf7b972e35 +images/svg/medieteknik.svg,1717953208687,20a606c6b0f7ab0fbb9092d9f70c6a76c33a366768dd5ce7cd1a4aade30a10c5 +images/svg/instagram.svg,1710785600065,a093432cf165a98aa1a6b49122fe951860ca71dd9d4727a45ca9ec388b39544c +images/cs.jpg,1708855444172,3a8f31249f57a708100ed5e597bbd003e8d706d9a6318436299ccef0934da9ce +images/svg/datateknik.svg,1717954310511,7e222db520ffa91ed3eadfd3fa85812a277a5bf45ae8630d5a98f4306f332c26 +images/logobig_light.jpg,1713814110573,25b0591ae761e535a9ef043dd0740647fa78b3df140442cd4390656040e45665 +images/logobig_dark.jpg,1714179224566,9d5e1feb5cd3ca7b6a70f0118bf9c159e4f73e6c245dd1c942bf8788bad2d903 +images/logobig.png,1710782675272,7bc0f40f5bf714f1b70ccb4fbc1f0a6627afcb3136b2aad436b8ca678294a15a +images/svg/facebook.svg,1710784995921,410040bc378c6c86fd75695f1f2a4eb749ad0ad1c53dfbe451d7f4981960086b +images/ict.jpg,1708856955078,8a840e4b6ca2f83901c1e34ec8abd8fd9802343d39c5fb7dc94443d79ea65ee3 +images/sd.jpg,1709014945099,fa0727975b29d57738a268da4e7e262cdcb75b061285bc8ef1b0d39bed9322c7 +screenshots/phone.webp,1720428750813,09a5b82e9868b09f5a93ff1398bb8212ec54b74cdd8a64a3db8723de953016c5 +images/ml.jpg,1708856962239,8138b0b058ca7752ee40cc32cc4aa25d9d854d5165e4880d5d32e82aafaa6be7 +images/svg/kth.svg,1711063787967,04db06aa8f5073b9693efb4ffa236e1df2fca8776a31265bde377c07fb8950d7 +images/imt.jpg,1708856946101,503fec3af341c92bfb964b5f7332cd773429e63dfbd059599dcd79a7de1becae +images/KTH.jpg,1711828165385,4f497bda7e0c7f3b997214c01c0dcc389a5546218ff43275ec7a40a75b0bd93f +screenshots/desktop.webp,1720428118145,fb766eed3c7cb6c53358353f3c0d83346d55c386f8b8a543926266ec71872a5b +images/testbg.jpg,1709696850461,02ac934a8ce6cb7efa6917f3c7853e084a969d34d4cd4cf635f53fe983b068c9 +images/ths_placeholder.jpg,1709687564492,af2c7227a1247730687e733f1daff25129f7f3ddd4497d2dab6c3afd2f6e2356 +images/kth-landskap.jpg,1711819019756,b28a8cb7a0e430bf961ecf7870100d1b83ab8660476a1fca0b36f73aa313d20a +images/kth-landskap.webp,1721805296540,5c9b68ff00d4ea5981d7776bc57f0bb5acca568e57175c974c54993d0934d8d3 +images/international_placeholder.jpg,1709687695136,ca11b7b0dc9562294ad4b1e0d778243b1b4cdb1d7daf8bb5f6f7749f6ffc3976 +images/bg.webp,1720432387556,56af6239eed0080c917231b4e1e2fabfea36de843c5004b92e9941461e81c11f +images/testbg2.jpg,1696497351214,ba0e6b61f35bde9dd9949426da569529ec7b33d63fbdbab307b99a0b14217046 +images/bg2.webp,1723934109217,5848c87093a6345d24e8891469b68ab4916aba33add37dd0faa4b5d1d9491119 +images/bg2.jpg,1723589234023,b05c4b16cfe029c4a04ec98a3d92dd767efc0ce149d46fc31f46b48902d1feb9 diff --git a/frontend/.firebaserc b/frontend/.firebaserc new file mode 100644 index 00000000..aac32933 --- /dev/null +++ b/frontend/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "medieteknik" + } +} diff --git a/frontend/firebase-debug.log b/frontend/firebase-debug.log new file mode 100644 index 00000000..a8c41515 --- /dev/null +++ b/frontend/firebase-debug.log @@ -0,0 +1,25 @@ +[debug] [2024-09-01T01:41:52.373Z] ---------------------------------------------------------------------- +[debug] [2024-09-01T01:41:52.375Z] Command: C:\Program Files\nodejs\node.exe C:\Users\andre\AppData\Roaming\npm\node_modules\firebase-tools\lib\bin\firebase.js init hosting:github +[debug] [2024-09-01T01:41:52.375Z] CLI Version: 13.16.0 +[debug] [2024-09-01T01:41:52.375Z] Platform: win32 +[debug] [2024-09-01T01:41:52.376Z] Node Version: v21.5.0 +[debug] [2024-09-01T01:41:52.376Z] Time: Sun Sep 01 2024 03:41:52 GMT+0200 (Central European Summer Time) +[debug] [2024-09-01T01:41:52.376Z] ---------------------------------------------------------------------- +[debug] +[debug] [2024-09-01T01:41:52.432Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"] +[debug] [2024-09-01T01:41:52.433Z] > authorizing via signed-in user (webmaster@medieteknik.com) +[info] + ######## #### ######## ######## ######## ### ###### ######## + ## ## ## ## ## ## ## ## ## ## ## + ###### ## ######## ###### ######## ######### ###### ###### + ## ## ## ## ## ## ## ## ## ## ## + ## #### ## ## ######## ######## ## ## ###### ######## + +You're about to initialize a Firebase project in this directory: + + D:\Programming\Medieteknik\medieteknik.com\frontend + +Before we get started, keep in mind: + + * You are initializing within an existing Firebase project directory + diff --git a/frontend/firebase.json b/frontend/firebase.json new file mode 100644 index 00000000..621dfef6 --- /dev/null +++ b/frontend/firebase.json @@ -0,0 +1,14 @@ +{ + "hosting": { + "public": "public", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ] + }, + "remoteconfig": { + "template": "remoteconfig.template.json" + }, + "extensions": {} +} diff --git a/frontend/public/404.html b/frontend/public/404.html new file mode 100644 index 00000000..829eda8f --- /dev/null +++ b/frontend/public/404.html @@ -0,0 +1,33 @@ + + + + + + Page Not Found + + + + +
+

404

+

Page Not Found

+

The specified file was not found on this website. Please check the URL for mistakes and try again.

+

Why am I seeing this?

+

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

+
+ + diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 00000000..1d52c3b0 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,89 @@ + + + + + + Welcome to Firebase Hosting + + + + + + + + + + + + + + + + + + + +
+

Welcome

+

Firebase Hosting Setup Complete

+

You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!

+ Open Hosting Documentation +
+

Firebase SDK Loading…

+ + + + diff --git a/frontend/remoteconfig.template.json b/frontend/remoteconfig.template.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/frontend/remoteconfig.template.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/frontend/src/app/[language]/account/pages/account/accountForm.tsx b/frontend/src/app/[language]/account/pages/account/accountForm.tsx index 39583355..efaf4937 100644 --- a/frontend/src/app/[language]/account/pages/account/accountForm.tsx +++ b/frontend/src/app/[language]/account/pages/account/accountForm.tsx @@ -122,8 +122,6 @@ export default function AccountForm({ }, }) - const { setValue } = accountForm - const profileForm = useForm>({ resolver: zodResolver(ProfileFormSchema), defaultValues: { @@ -291,6 +289,7 @@ export default function AccountForm({ ( @@ -308,6 +307,7 @@ export default function AccountForm({ ( @@ -319,8 +319,7 @@ export default function AccountForm({ /> - Manage your accounts email for external services (e.g. - Notifications) + Manage your accounts email for external services @@ -334,6 +333,9 @@ export default function AccountForm({ render={({ field }) => ( Password + + Can be used to change your password + { - setValue('csrf_token', csrf.token) + accountForm.setValue('csrf_token', csrf.token) }} > Save @@ -381,127 +383,134 @@ export default function AccountForm({ -
-
- -

- Profile Settings -

- ( - - - - Facebook - - - { - e.target.value = 'https://www.facebook.com/' - }} - onBlur={(e) => { - if (e.target.value === 'https://www.facebook.com/') { - e.target.value = '' - } - }} - /> - - - - )} - /> - ( - - - - Instagram - - - { - e.target.value = 'https://www.instagram.com/' - }} - onBlur={(e) => { - if (e.target.value === 'https://www.instagram.com/') { - e.target.value = '' - } - }} - /> - - - - )} - /> - ( - - - - LinkedIn - - - { - e.target.value = 'https://www.linkedin.com/in/' - }} - onBlur={(e) => { - if (e.target.value === 'https://www.linkedin.com/in/') { - e.target.value = '' - } - }} - /> - - - - )} - /> - ( - - - Email Notifications - - -
- - - Receive email notifications - -
-
- -
- )} - /> - - +
+
+

To be Added

- +
+
+ +

+ Profile Settings +

+ ( + + + + Facebook + + + { + e.target.value = 'https://www.facebook.com/' + }} + onBlur={(e) => { + if (e.target.value === 'https://www.facebook.com/') { + e.target.value = '' + } + }} + /> + + + + )} + /> + ( + + + + Instagram + + + { + e.target.value = 'https://www.instagram.com/' + }} + onBlur={(e) => { + if (e.target.value === 'https://www.instagram.com/') { + e.target.value = '' + } + }} + /> + + + + )} + /> + ( + + + + LinkedIn + + + { + e.target.value = 'https://www.linkedin.com/in/' + }} + onBlur={(e) => { + if ( + e.target.value === 'https://www.linkedin.com/in/' + ) { + e.target.value = '' + } + }} + /> + + + + )} + /> + ( + + + Email Notifications + + +
+ + + Receive email notifications + +
+
+ +
+ )} + /> + + +
+ +
) } diff --git a/frontend/src/app/[language]/account/pages/account/receptionForm.tsx b/frontend/src/app/[language]/account/pages/account/receptionForm.tsx index 094c6b89..d18d17dc 100644 --- a/frontend/src/app/[language]/account/pages/account/receptionForm.tsx +++ b/frontend/src/app/[language]/account/pages/account/receptionForm.tsx @@ -1,7 +1,7 @@ 'use client' import Logo from 'public/images/logo.webp' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { zodResolver } from '@hookform/resolvers/zod' import { Input } from '@/components/ui/input' import { @@ -18,19 +18,18 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { z } from 'zod' import { useForm } from 'react-hook-form' import { Button } from '@/components/ui/button' +import { API_BASE_URL } from '@/utility/Constants' +import useSWR from 'swr' +import Loading from '@/components/tooltips/Loading' +import { useAuthentication } from '@/providers/AuthenticationProvider' -const FormSchema = z.object({ - image: z.instanceof(window.File), - receptionName: z.string().optional().or(z.literal('')), -}) - -const MAX_FILE_SIZE = 500 * 1024 -const ACCEPTED_IMAGE_TYPES = [ - 'image/jpeg', - 'image/jpg', - 'image/png', - 'image/webp', -] +const fetcher = (url: string) => + fetch(url).then( + (res) => + res.json() as Promise<{ + token: string + }> + ) export default function ReceptionForm({ params: { language }, @@ -39,6 +38,22 @@ export default function ReceptionForm({ }) { const [receptionPicturePreview, setReceptionPicturePreview] = useState(null) + const { student } = useAuthentication() + const [csrfToken, setCsrfToken] = useState() + + const FormSchema = z.object({ + image: z.instanceof(window.File), + receptionName: z.string().optional().or(z.literal('')), + csrf_token: z.string().optional().or(z.literal('')), + }) + + const MAX_FILE_SIZE = 500 * 1024 + const ACCEPTED_IMAGE_TYPES = [ + 'image/jpeg', + 'image/jpg', + 'image/png', + 'image/webp', + ] const form = useForm>({ resolver: zodResolver(FormSchema), @@ -48,8 +63,49 @@ export default function ReceptionForm({ }, }) - const onSubmit = (data: z.infer) => { - console.log(data) + const { + data: csrf, + error, + isLoading, + } = useSWR(`${API_BASE_URL}/csrf-token`, fetcher) + + useEffect(() => { + if (csrf) { + setCsrfToken(csrf.token) + form.setValue('csrf_token', csrf.token) + } + }) + + if (!student) return null + if (error) return
Failed to load
+ if (isLoading) return + if (!csrf) return null + + const onSubmit = async (data: z.infer) => { + const formData = new FormData() + + formData.append('reception_image', data.image) + formData.append('reception_name', data.receptionName || '') + formData.append('csrf_token', data.csrf_token || csrf.token) + + try { + const response = await fetch(`${API_BASE_URL}/students/reception`, { + method: 'PUT', + headers: { + 'X-CSRF-Token': csrfToken || data.csrf_token || '', + }, + credentials: 'include', + body: formData, + }) + + if (!response.ok) { + alert('Failed to save') + return + } + } catch (error) { + alert('Failed to save') + return + } } return ( @@ -72,7 +128,7 @@ export default function ReceptionForm({ src={ receptionPicturePreview ? URL.createObjectURL(receptionPicturePreview) - : Logo.src + : student.reception_profile_picture_url || Logo.src } /> Profile Icon @@ -144,7 +200,18 @@ export default function ReceptionForm({
)} /> - + } + /> + diff --git a/frontend/src/app/[language]/admin/admin.tsx b/frontend/src/app/[language]/admin/admin.tsx new file mode 100644 index 00000000..2e645274 --- /dev/null +++ b/frontend/src/app/[language]/admin/admin.tsx @@ -0,0 +1,116 @@ +import { Head } from '@/components/static/Static' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from '@/components/ui/breadcrumb' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { + ArrowTrendingUpIcon, + ChevronDownIcon, + Cog8ToothIcon, + NewspaperIcon, + PresentationChartLineIcon, + ServerStackIcon, + UserGroupIcon, + UsersIcon, +} from '@heroicons/react/24/outline' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import Dashboard from './pages/dashboard' +import Link from 'next/link' + +export default function Admin({ + params: { language }, +}: { + params: { language: string } +}) { + return ( +
+
+ +
+ + + + Dashboard + + + + + + Students + + + + Committees + + +
+
+ + + + +
+

+ Student Management +

+
+ +
+
+
+
+

AndrΓ© Eriksson

+ + andree4@kth.se + +
+
+ + +
+
+

Not implemented yet

+
+ + +
+

+ Committee Management +

+
+
+
+
+
+ ) +} diff --git a/frontend/src/app/[language]/bulletin/news/upload/[slug]/commandBar.tsx b/frontend/src/app/[language]/bulletin/news/upload/[slug]/commandBar.tsx index 99dd2955..f5940c93 100644 --- a/frontend/src/app/[language]/bulletin/news/upload/[slug]/commandBar.tsx +++ b/frontend/src/app/[language]/bulletin/news/upload/[slug]/commandBar.tsx @@ -95,6 +95,23 @@ export default function CommandBar({ const postForm = async (data: z.infer) => { await saveCallback(language, true) + /* + const formData = new window.FormData() + + supportedLanguages.forEach((lang, index) => { + formData.append(`translations[${index}][language_code]`, lang) + formData.append(`translations[${index}][title]`, data.title) + formData.append(`translations[${index}][main_image_url]`, data.image) + formData.append( + `translations[${index}][body]`, + content.translations[index].body + ) + formData.append( + `translations[${index}][short_description]`, + data.short_description + ) + })*/ + const json_data = { ...content, translations: [ @@ -140,10 +157,6 @@ export default function CommandBar({
- - Styrelsen - - Articles @@ -175,6 +188,7 @@ export default function CommandBar({ className='ml-4' title='Import/Export' aria-label='Import or Export' + disabled // TODO: Enable when import/export is ready > @@ -183,6 +197,7 @@ export default function CommandBar({ className='ml-4' title='Language' aria-label='Language' + disabled // TODO: Enable when language is ready > diff --git a/frontend/src/app/[language]/bulletin/news/upload/[slug]/page.tsx b/frontend/src/app/[language]/bulletin/news/upload/[slug]/page.tsx index 64b60a60..41ea89a6 100644 --- a/frontend/src/app/[language]/bulletin/news/upload/[slug]/page.tsx +++ b/frontend/src/app/[language]/bulletin/news/upload/[slug]/page.tsx @@ -75,6 +75,7 @@ export default function UploadNews({ value='tags' className='p-2 mb-2 hover:bg-neutral-400/30' title='Select tags' + disabled // TODO: Implement tags page > @@ -82,6 +83,7 @@ export default function UploadNews({ value='engagement' className='p-2 mb-2 hover:bg-neutral-400/30' title='View article engagement' + disabled // TODO: Implement engagement page > @@ -89,6 +91,7 @@ export default function UploadNews({ value='settings' className='p-2 hover:bg-neutral-400/30' title='Article settings' + disabled // TODO: Implement settings page > diff --git a/frontend/src/app/[language]/bulletin/recruiting.tsx b/frontend/src/app/[language]/bulletin/recruiting.tsx index 6fe4bc93..e7dbbd90 100644 --- a/frontend/src/app/[language]/bulletin/recruiting.tsx +++ b/frontend/src/app/[language]/bulletin/recruiting.tsx @@ -97,7 +97,11 @@ export default function Recruitment({
diff --git a/frontend/src/app/[language]/chapter/committees/[committee]/client/manage.tsx b/frontend/src/app/[language]/chapter/committees/[committee]/client/manage.tsx index 0279315d..3c710d10 100644 --- a/frontend/src/app/[language]/chapter/committees/[committee]/client/manage.tsx +++ b/frontend/src/app/[language]/chapter/committees/[committee]/client/manage.tsx @@ -13,22 +13,17 @@ export default function ManageButton({ language, committee }: Props) { const { committees, role } = useAuthentication() return (
- {committees.includes(committee) || - (role === 'ADMIN' && ( - - ))} + + + + )}
) } -// \ No newline at end of file +// diff --git a/frontend/src/app/[language]/chapter/committees/[committee]/committee.tsx b/frontend/src/app/[language]/chapter/committees/[committee]/committee.tsx index 4c0a9623..9ca86f24 100644 --- a/frontend/src/app/[language]/chapter/committees/[committee]/committee.tsx +++ b/frontend/src/app/[language]/chapter/committees/[committee]/committee.tsx @@ -6,12 +6,31 @@ import { fallbackLanguage } from '@/app/i18n/settings' import Image from 'next/image' import FallbackImage from 'public/images/logo.webp' import CommitteeMembers from './members' -import ExploreMore from './client/explore' +//import ExploreMore from './client/explore' import ManageButton from './client/manage' +import Link from 'next/link' export const revalidate = 60 * 60 * 24 * 30 -export async function generateStaticParams() { +interface Params { + language: string + committee: string +} + +interface Props { + params: Params +} + +/** + * @name generateStaticParams + * @description Generates the static paths for the committee pages + * + * @returns {Promise<{ language: string; committee: string }[]>} The generated static paths + * @see {@link https://nextjs.org/docs/app/api-reference/functions/generate-static-params | Next.js Static Generation} + */ +export async function generateStaticParams(): Promise< + { language: string; committee: string }[] +> { try { const response = await fetch( API_BASE_URL + `/public/committees?language=${fallbackLanguage}` @@ -29,14 +48,24 @@ export async function generateStaticParams() { } } catch (error) { console.error(error) + return [] } + + return [] } +/** + * @name Committee + * @description The page for displaying a committee + * + * @param {object} param - The dynamic URL parameters + * @param {string} param.language - The language of the page + * @param {string} param.committee - The committee name to display + * @returns {Promise} The rendered server component + */ export default async function Committee({ params: { language, committee }, -}: { - params: { language: string; committee: string } -}) { +}: Props): Promise { const data: Committee | null = await GetCommitteePublic(committee, language) if (!data || Object.keys(data).length === 0) { @@ -87,17 +116,30 @@ export default async function Committee({ className='w-24 lg:w-[9.5rem] bg-white h-auto absolute left-0 top-0 bottom-0 right-0 m-auto hover:scale-105 duration-300 transition-transform' /> -
-

+
+

= 15 + ? 'text-lg xxs:text-xl md:text-4xl xl:text-6xl desktop:text-7xl' + : 'text-3xl xxs:text-4xl md:text-6xl xl:text-7xl' + } uppercase tracking-wide w-fit text-center lg:text-start flex flex-col-reverse justify-center`} + > {committeeName}

-

+ + {data.email} + +

{data.translations[0].description}

@@ -105,7 +147,7 @@ export default async function Committee({
- + {/**/} ) } diff --git a/frontend/src/app/[language]/chapter/committees/[committee]/manage/edit.tsx b/frontend/src/app/[language]/chapter/committees/[committee]/manage/edit.tsx index db75ee44..c0adbda2 100644 --- a/frontend/src/app/[language]/chapter/committees/[committee]/manage/edit.tsx +++ b/frontend/src/app/[language]/chapter/committees/[committee]/manage/edit.tsx @@ -28,6 +28,9 @@ import { Label } from '@/components/ui/label' import '/node_modules/flag-icons/css/flag-icons.min.css' import { API_BASE_URL, LANGUAGES } from '@/utility/Constants' import { useState } from 'react' +import { useAuthentication } from '@/providers/AuthenticationProvider' +import { Permission } from '@/models/Permission' +import { Textarea } from '@/components/ui/textarea' /** * @name TranslatedInputs @@ -65,7 +68,7 @@ function TranslatedInputs({ [{language}] - +