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({
-
)}
/>
-
+ }
+ />
+
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
+
+
+
+
+
+
+
+
+
+
+
+ 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}]
-
+
)}
@@ -89,9 +92,9 @@ export default function EditCommittee({
language: string
committee: Committee
}): JSX.Element {
- // TODO: Add permissions
const [errorMessage, setErrorMessage] = useState(null)
const [open, setOpen] = useState(false)
+ const { permissions, positions } = useAuthentication()
const EditCommitteeSchema = z.object({
title: z
.string()
@@ -103,7 +106,7 @@ export default function EditCommittee({
description: z
.string()
.min(1, { message: 'Description is required' })
- .max(500, { message: 'Description is too long' }),
+ .max(511, { message: 'Description is too long' }),
})
),
logo: z.instanceof(window.File).optional().or(z.literal('')),
@@ -124,6 +127,20 @@ export default function EditCommittee({
},
})
+ if (
+ !(
+ permissions.student &&
+ permissions.student.includes(Permission.COMMITTEE_EDIT)
+ ) &&
+ !(
+ positions &&
+ positions.length > 0 &&
+ positions.some((position) => position.weight <= 150)
+ )
+ ) {
+ return <>>
+ }
+
const MAX_LOGO_FILE_SIZE = 1 * 1024 * 1024 // 1 MB
const MAX_GROUP_PHOTO_FILE_SIZE = 15 * 1024 * 1024 // 15 MB
diff --git a/frontend/src/app/[language]/chapter/committees/[committee]/manage/forms/memberForm.tsx b/frontend/src/app/[language]/chapter/committees/[committee]/manage/forms/memberForm.tsx
index 844fae20..b34f5ab6 100644
--- a/frontend/src/app/[language]/chapter/committees/[committee]/manage/forms/memberForm.tsx
+++ b/frontend/src/app/[language]/chapter/committees/[committee]/manage/forms/memberForm.tsx
@@ -43,6 +43,7 @@ import {
FormMessage,
} from '@/components/ui/form'
import { API_BASE_URL } from '@/utility/Constants'
+import { useAuthentication } from '@/providers/AuthenticationProvider'
export function RemoveMemberForm({ language }: { language: string }) {
const { committee, members } = useCommitteeManagement()
@@ -140,15 +141,36 @@ export function RemoveMemberForm({ language }: { language: string }) {
)
}
-export function AddMemberForm({ language }: { language: string }) {
+export function AddMemberForm({
+ language,
+ onSuccess,
+}: {
+ language: string
+ onSuccess: () => void
+}) {
const { positions } = useCommitteeManagement()
+ const { positions: studentPositions } = useAuthentication()
const [open, setOpen] = useState(false)
const [value, setValue] = useState('')
- const positionOptions = positions.map((position) => ({
- label: position.translations[0].title,
- value: position.committee_position_id,
- }))
+ const positionOptions = positions
+ .filter((position) => {
+ if (!studentPositions) return false
+ if (studentPositions.length === 0) return true
+
+ // Find students highest position (position with highest weight)
+ const studentHighestPosition = studentPositions.reduce((prev, current) =>
+ prev.weight > current.weight ? prev : current
+ )
+
+ // Remove positions with higher weight than the student's highest position
+ return position.weight > studentHighestPosition.weight
+ })
+ .sort((a, b) => a.weight - b.weight)
+ .map((position) => ({
+ label: position.translations[0].title,
+ value: position.committee_position_id,
+ }))
const Schema = z.object({
students: z
@@ -185,7 +207,7 @@ export function AddMemberForm({ language }: { language: string }) {
)
if (response.ok) {
- console.log('success')
+ onSuccess()
} else {
console.error('error')
}
diff --git a/frontend/src/app/[language]/chapter/committees/[committee]/manage/forms/positionForm.tsx b/frontend/src/app/[language]/chapter/committees/[committee]/manage/forms/positionForm.tsx
index 200bb610..d695b093 100644
--- a/frontend/src/app/[language]/chapter/committees/[committee]/manage/forms/positionForm.tsx
+++ b/frontend/src/app/[language]/chapter/committees/[committee]/manage/forms/positionForm.tsx
@@ -45,6 +45,8 @@ import { useState } from 'react'
import { API_BASE_URL, LANGUAGES } from '@/utility/Constants'
import { title } from 'process'
import { LanguageCode } from '@/models/Language'
+import { useAuthentication } from '@/providers/AuthenticationProvider'
+import { Role } from '@/models/Permission'
function TranslatedInputs({
index,
@@ -116,6 +118,7 @@ export default function PositionForm({
}) {
const [popoverOpen, setPopoverOpen] = useState(false)
const [value, setValue] = useState('NONE')
+ const { role } = useAuthentication()
const PositionFormSchema = z.object({
email: z
.string()
@@ -130,6 +133,7 @@ export default function PositionForm({
'NΓRINGSLIV OCH KOMMUNIKATION',
'STUDIESOCIALT',
'FANBORGEN',
+ 'UTBILDNING',
'NONE',
]),
translations: z.array(
@@ -335,27 +339,28 @@ export default function PositionForm({
)}
/>
-
- (
-
- Weight
-
-
-
-
- Permission Level, leave 1000 if unsure!
-
-
-
- )}
- />
+ {role === Role.ADMIN && (
+ (
+
+ Weight
+
+
+
+
+ Permission Level, leave 1000 if unsure!
+
+
+
+ )}
+ />
+ )}
Submit
diff --git a/frontend/src/app/[language]/chapter/committees/[committee]/manage/forms/recruitmentForm.tsx b/frontend/src/app/[language]/chapter/committees/[committee]/manage/forms/recruitmentForm.tsx
index a3ecd9cb..36e56c29 100644
--- a/frontend/src/app/[language]/chapter/committees/[committee]/manage/forms/recruitmentForm.tsx
+++ b/frontend/src/app/[language]/chapter/committees/[committee]/manage/forms/recruitmentForm.tsx
@@ -41,6 +41,7 @@ import {
import { ChevronDownIcon } from '@heroicons/react/24/outline'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
+import { useAuthentication } from '@/providers/AuthenticationProvider'
function TranslatedInputs({
index,
@@ -104,6 +105,7 @@ function TranslatedInputs({
export default function RecruitmentForm({ language }: { language: string }) {
const [popoverOpen, setPopoverOpen] = useState(false)
const { positions } = useCommitteeManagement()
+ const { positions: studentPositions } = useAuthentication()
if (!positions) {
throw new Error('Positions not found')
@@ -111,20 +113,40 @@ export default function RecruitmentForm({ language }: { language: string }) {
const [value, setValue] = useState(positions[0].translations[0].title)
- const dropdownOptions = positions.map((position) => ({
- value: position.translations[0].title,
- label: position.translations[0].title
- .split('_')
- .map((word) => word.charAt(0) + word.slice(1).toLowerCase())
- .join(' '),
- }))
+ const dropdownOptions = positions
+ .filter((position) => {
+ if (!studentPositions) return false
+ if (studentPositions.length === 0) return true
+
+ // Find students highest position (position with highest weight)
+ const studentHighestPosition = studentPositions.reduce((prev, current) =>
+ prev.weight > current.weight ? prev : current
+ )
+
+ // Remove positions with higher weight than the student's highest position
+ return position.weight > studentHighestPosition.weight
+ })
+ .sort((a, b) => a.weight - b.weight)
+ .map((position) => ({
+ value: position.translations[0].title,
+ label: position.translations[0].title
+ .split('_')
+ .map((word) => word.charAt(0) + word.slice(1).toLowerCase())
+ .join(' '),
+ }))
const RecruitmentSchema = z
.object({
position: z.string(),
- start_date: z.coerce.date().refine((date) => date >= new Date(), {
- message: 'Start date must be today or later',
- }),
+ start_date: z.coerce
+ .date()
+ .refine(
+ (date) =>
+ date.getTime() >= new Date().getTime() - 1000 * 60 * 60 * 24,
+ {
+ message: 'Start date must be today or later',
+ }
+ ),
end_date: z.coerce.date().refine((date) => date >= new Date(), {
message: 'Start date must be today or later',
}),
diff --git a/frontend/src/app/[language]/chapter/committees/[committee]/manage/forms/removePosition.tsx b/frontend/src/app/[language]/chapter/committees/[committee]/manage/forms/removePosition.tsx
index 4fb8e219..096a00ef 100644
--- a/frontend/src/app/[language]/chapter/committees/[committee]/manage/forms/removePosition.tsx
+++ b/frontend/src/app/[language]/chapter/committees/[committee]/manage/forms/removePosition.tsx
@@ -46,10 +46,14 @@ export default function RemovePositionForm({
const [open, setOpen] = useState(false)
const [value, setValue] = useState('')
- const positionOptions = positions.map((position) => ({
- label: position.translations[0].title,
- value: position.committee_position_id,
- }))
+ const positionOptions = positions
+ .filter((position) => {
+ return !position.base
+ })
+ .map((position) => ({
+ label: position.translations[0].title,
+ value: position.committee_position_id,
+ }))
const Schema = z.object({
position_id: z.string().uuid(),
diff --git a/frontend/src/app/[language]/chapter/committees/[committee]/manage/landing.tsx b/frontend/src/app/[language]/chapter/committees/[committee]/manage/landing.tsx
new file mode 100644
index 00000000..f26fdfdd
--- /dev/null
+++ b/frontend/src/app/[language]/chapter/committees/[committee]/manage/landing.tsx
@@ -0,0 +1,149 @@
+import Logo from 'public/images/logo.webp'
+import {
+ HomeIcon,
+ UserGroupIcon,
+ DocumentDuplicateIcon,
+ NewspaperIcon,
+ CalendarDaysIcon,
+} from '@heroicons/react/24/outline'
+import Image from 'next/image'
+import Link from 'next/link'
+import React from 'react'
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+} from '@/components/ui/breadcrumb'
+import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import Content from './content'
+import EditCommittee from './edit'
+import Committee from '@/models/Committee'
+
+interface Props {
+ language: string
+ committeeData: Committee
+ committeeName: string
+}
+
+export default function CommitteeLandingPage({
+ language,
+ committeeData,
+ committeeName,
+}: Props) {
+ return (
+ <>
+
+
+
+
+
+ Chapter
+
+
+
+
+
+ Committees
+
+
+
+
+
+ {committeeName}
+
+
+
+
+ Manage
+
+
+
+
+
+
+
+
+
+ {decodeURIComponent(committeeName)}
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+ Members
+
+
+
+ News
+
+
+
+ Events
+
+
+
+ Documents
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/frontend/src/app/[language]/chapter/committees/[committee]/manage/manage.tsx b/frontend/src/app/[language]/chapter/committees/[committee]/manage/manage.tsx
index 01012a33..0bef360c 100644
--- a/frontend/src/app/[language]/chapter/committees/[committee]/manage/manage.tsx
+++ b/frontend/src/app/[language]/chapter/committees/[committee]/manage/manage.tsx
@@ -1,28 +1,15 @@
-import Logo from 'public/images/logo.webp'
-import {
- HomeIcon,
- UserGroupIcon,
- DocumentDuplicateIcon,
- NewspaperIcon,
- CalendarDaysIcon,
-} from '@heroicons/react/24/outline'
-import Image from 'next/image'
-import Link from 'next/link'
import React from 'react'
-import {
- Breadcrumb,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbList,
- BreadcrumbPage,
- BreadcrumbSeparator,
-} from '@/components/ui/breadcrumb'
-import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { GetCommitteePublic } from '@/api/committee'
-import Content from './content'
-import EditCommittee from './edit'
+import CommitteeRedirect from './redirect'
-export const revalidate = 60 * 60 * 24
+interface Params {
+ language: string
+ committee: string
+}
+
+interface Props {
+ params: Params
+}
/**
* @name CommitteeManage
@@ -35,9 +22,7 @@ export const revalidate = 60 * 60 * 24
*/
export default async function CommitteeManage({
params: { language, committee },
-}: {
- params: { language: string; committee: string }
-}): Promise {
+}: Props): Promise {
const decodedCommittee = decodeURIComponent(committee)
const committeeData = await GetCommitteePublic(decodedCommittee)
@@ -47,115 +32,12 @@ export default async function CommitteeManage({
return (
-
-
-
-
-
- Chapter
-
-
-
-
-
- Committees
-
-
-
-
-
- {decodedCommittee}
-
-
-
-
- Manage
-
-
-
-
-
-
-
-
-
- {decodeURIComponent(committee)}
-
-
-
-
-
-
-
-
-
- Home
-
-
-
-
- Members
-
-
-
- News
-
-
-
- Events
-
-
-
- Documents
-
-
-
-
-
-
-
+
)
}
diff --git a/frontend/src/app/[language]/chapter/committees/[committee]/manage/pages/members.tsx b/frontend/src/app/[language]/chapter/committees/[committee]/manage/pages/members.tsx
index 6a8edd07..02113f47 100644
--- a/frontend/src/app/[language]/chapter/committees/[committee]/manage/pages/members.tsx
+++ b/frontend/src/app/[language]/chapter/committees/[committee]/manage/pages/members.tsx
@@ -36,6 +36,7 @@ import RecruitmentForm from '../forms/recruitmentForm'
import { StudentTag } from '@/components/tags/StudentTag'
import { AddMemberForm, RemoveMemberForm } from '../forms/memberForm'
import RemovePositionForm from '../forms/removePosition'
+import { useAuthentication } from '@/providers/AuthenticationProvider'
/**
* @name MembersPage
@@ -55,6 +56,7 @@ export default function MembersPage({
// TODO: Clean-up the code, separate the components into smaller components?
const [isLoading, setIsLoading] = useState(true)
const [addPositionOpen, setAddPositionOpen] = useState(false)
+ const [addMemberOpen, setAddMemberOpen] = useState(false)
const {
members,
positions,
@@ -62,12 +64,32 @@ export default function MembersPage({
error,
recruitments,
addPosition,
+ addMember,
} = useCommitteeManagement()
+ const { positions: userPostions } = useAuthentication()
const findPosition = (id: string) => {
return positions.find((position) => position.committee_position_id === id)
}
+ const limit = (weight: number) => {
+ const positions = userPostions.map((userPosition) =>
+ findPosition(userPosition.committee_position_id)
+ )
+
+ if (!positions || positions.length === 0) {
+ return false
+ }
+
+ return positions.some((position) => {
+ if (!position) {
+ return false
+ }
+
+ return position.weight >= weight
+ })
+ }
+
useEffect(() => {
if (!isLoadingMembers) {
setIsLoading(false)
@@ -97,24 +119,32 @@ export default function MembersPage({
)}
-