diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d942c2d5..fb2c88af 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -3,7 +3,6 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', - 'plugin:@tanstack/eslint-plugin-query/recommended', 'prettier', ], parser: '@typescript-eslint/parser', @@ -28,11 +27,7 @@ module.exports = { // Warn to encourage type safety, could be upgraded to 'error' for stricter enforcement '@typescript-eslint/no-explicit-any': 'warn', - // 5. '@tanstack/query/exhaustive-deps': Ensure exhaustive dependency arrays in React hooks - // Warn to highlight potential issues, but doesn't necessarily break the build - '@tanstack/query/exhaustive-deps': 'warn', - - // 6. '@typescript-eslint/no-unused-vars': Avoid unused variables, enforcing cleaner code + // 5. '@typescript-eslint/no-unused-vars': Avoid unused variables, enforcing cleaner code // Error to ensure unused variables are caught during linting '@typescript-eslint/no-unused-vars': [ 'error', diff --git a/.prettierrc.cjs b/.prettierrc.cjs index c920e99b..7b8b2d7f 100644 --- a/.prettierrc.cjs +++ b/.prettierrc.cjs @@ -3,4 +3,5 @@ module.exports = { singleQuote: true, quoteProps: 'consistent', printWidth: 100, -}; \ No newline at end of file + plugins: ['prettier-plugin-tailwindcss'], +}; diff --git a/package.json b/package.json index 860d2d4b..ad1e00e1 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,11 @@ { "name": "lexicon-frontend", "private": true, - "version": "1.0.0", "type": "module", "scripts": { - "berlin:dev": "pnpm -r --filter berlin dev", - "berlin:build": "pnpm -r --filter berlin build", - "berlin:preview": "pnpm -r --filter berlin preview", + "dev": "pnpm -r --filter berlin dev", + "build": "pnpm -r --filter berlin build", + "preview": "pnpm -r --filter berlin preview", "format": "pnpm exec prettier --check \"packages/**/*.{ts,tsx,json,md}\"", "format:fix": "pnpm exec prettier --write \"packages/**/*.{ts,tsx,json,md}\"", "lint": "pnpm exec eslint ." @@ -14,50 +13,17 @@ "keywords": [], "author": "", "license": "ISC", - "engines": { - "node": "^20" - }, - "dependencies": { - "@hookform/resolvers": "^3.3.4", - "@pcd/passport-interface": "^0.8.0", - "@pcd/pcd-types": "^0.8.0", - "@pcd/semaphore-identity-pcd": "^0.8.0", - "@pcd/semaphore-signature-pcd": "^0.9.0", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-icons": "^1.3.0", - "@tanstack/react-query": "^5.13.4", - "@tanstack/react-query-devtools": "^5.13.5", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.0", - "lucide-react": "^0.397.0", - "react": "^18.2.0", - "react-content-loader": "^7.0.0", - "react-dom": "^18.2.0", - "react-hook-form": "^7.49.3", - "react-hot-toast": "^2.4.1", - "react-joyride": "^2.8.2", - "react-markdown": "^9.0.1", - "react-router-dom": "^6.20.1", - "styled-components": "^6.1.1", - "zod": "^3.22.4", - "zustand": "^4.4.7" - }, "devDependencies": { - "@hookform/devtools": "^4.3.1", - "@tanstack/eslint-plugin-query": "^5.18.0", - "@types/node": "^20.12.4", - "@types/react": "^18.2.37", - "@types/react-dom": "^18.2.15", - "@typescript-eslint/eslint-plugin": "^6.19.0", - "@typescript-eslint/parser": "^6.19.0", - "@vitejs/plugin-react": "^4.2.0", - "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.4", + "prettier-plugin-tailwindcss": "^0.6.5", "prettier": "^3.1.1", - "typescript": "^5.5.2", - "vite": "^5.0.0", - "vite-plugin-node-polyfills": "^0.17.0" + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "@typescript-eslint/parser": "^7.2.0", + "typescript": "^5.5.2" + }, + "engines": { + "node": "^20" } -} \ No newline at end of file +} diff --git a/packages/api/package.json b/packages/api/package.json index 03f62343..05c9fc26 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "api", - "version": "2.4.0", + "version": "2.6.0", "type": "module", "main": "./src/index.ts" } diff --git a/packages/api/src/deleteComment.ts b/packages/api/src/deleteComment.ts index 9d53e897..a52844a1 100644 --- a/packages/api/src/deleteComment.ts +++ b/packages/api/src/deleteComment.ts @@ -1,10 +1,11 @@ -import { DeleteCommentRequest, DeleteCommentResponse } from './types'; +import { ApiRequest, DeleteCommentRequest, DeleteCommentResponse } from './types'; -async function deleteComment({ +export async function deleteComment({ commentId, -}: DeleteCommentRequest): Promise { + serverUrl, +}: ApiRequest): Promise { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/comments/${commentId}`, { + const response = await fetch(`${serverUrl}/api/comments/${commentId}`, { method: 'DELETE', credentials: 'include', headers: { @@ -23,5 +24,3 @@ async function deleteComment({ return null; } } - -export default deleteComment; diff --git a/packages/api/src/deleteLike.ts b/packages/api/src/deleteLike.ts index 2d4d9ac4..2d0a202f 100644 --- a/packages/api/src/deleteLike.ts +++ b/packages/api/src/deleteLike.ts @@ -1,17 +1,17 @@ -import { DeleteLikeRequest, DeleteLikeResponse } from './types'; +import { ApiRequest, DeleteLikeRequest, DeleteLikeResponse } from './types'; -async function deleteLike({ commentId }: DeleteLikeRequest): Promise { +export async function deleteLike({ + commentId, + serverUrl, +}: ApiRequest): Promise { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/comments/${commentId}/likes`, - { - method: 'DELETE', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, + const response = await fetch(`${serverUrl}/api/comments/${commentId}/likes`, { + method: 'DELETE', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', }, - ); + }); if (!response.ok) { throw new Error(`HTTP Error! Status: ${response.status}`); @@ -24,5 +24,3 @@ async function deleteLike({ commentId }: DeleteLikeRequest): Promise { + serverUrl, +}: ApiRequest): Promise< + DeleteUsersToGroupsResponse | { errors: string[] } | null +> { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/users-to-groups/${userToGroupId}`, - { - method: 'DELETE', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, + const response = await fetch(`${serverUrl}/api/users-to-groups/${userToGroupId}`, { + method: 'DELETE', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', }, - ); + }); if (!response.ok) { if (response.status < 500) { @@ -33,5 +33,3 @@ async function deleteUsersToGroups({ return null; } } - -export default deleteUsersToGroups; diff --git a/packages/api/src/fetchAlerts.ts b/packages/api/src/fetchAlerts.ts index 3a22dafc..444ee160 100644 --- a/packages/api/src/fetchAlerts.ts +++ b/packages/api/src/fetchAlerts.ts @@ -1,8 +1,10 @@ -import { GetAlertsResponse } from './types'; +import { ApiRequest, GetAlertsResponse } from './types'; -async function fetchAlerts(): Promise { +export async function fetchAlerts({ + serverUrl, +}: ApiRequest): Promise { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/alerts`, { + const response = await fetch(`${serverUrl}/api/alerts`, { credentials: 'include', headers: { 'Content-Type': 'application/json', @@ -20,5 +22,3 @@ async function fetchAlerts(): Promise { return null; } } - -export default fetchAlerts; diff --git a/packages/api/src/fetchCommentLikes.ts b/packages/api/src/fetchCommentLikes.ts index 7a1e09d7..c73a0031 100644 --- a/packages/api/src/fetchCommentLikes.ts +++ b/packages/api/src/fetchCommentLikes.ts @@ -1,16 +1,16 @@ -import { GetLikesRequest, GetLikesResponse } from './types'; +import { ApiRequest, GetLikesRequest, GetLikesResponse } from './types'; -async function fetchLikes({ commentId }: GetLikesRequest): Promise { +export async function fetchCommentLikes({ + commentId, + serverUrl, +}: ApiRequest): Promise { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/comments/${commentId}/likes`, - { - credentials: 'include', - headers: { - 'Content-type': 'application/json', - }, + const response = await fetch(`${serverUrl}/api/comments/${commentId}/likes`, { + credentials: 'include', + headers: { + 'Content-type': 'application/json', }, - ); + }); if (!response.ok) { throw new Error(`HTTP Error! Status: ${response.status}`); @@ -23,5 +23,3 @@ async function fetchLikes({ commentId }: GetLikesRequest): Promise { + serverUrl, +}: ApiRequest): Promise { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/options/${optionId}/comments`, - { - credentials: 'include', - headers: { - 'Content-type': 'application/json', - }, + const response = await fetch(`${serverUrl}/api/options/${optionId}/comments`, { + credentials: 'include', + headers: { + 'Content-type': 'application/json', }, - ); + }); if (!response.ok) { throw new Error(`HTTP Error! Status: ${response.status}`); @@ -25,5 +23,3 @@ async function fetchComments({ return null; } } - -export default fetchComments; diff --git a/packages/api/src/fetchCycle.ts b/packages/api/src/fetchCycle.ts index 788d776b..392ff4f4 100644 --- a/packages/api/src/fetchCycle.ts +++ b/packages/api/src/fetchCycle.ts @@ -1,8 +1,11 @@ -import { GetCyclesResponse } from './types'; +import { ApiRequest, GetCyclesResponse } from './types'; -async function fetchCycle(cycleId: string): Promise { +export async function fetchCycle({ + cycleId, + serverUrl, +}: ApiRequest<{ cycleId: string }>): Promise { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/cycles/${cycleId}`, { + const response = await fetch(`${serverUrl}/api/cycles/${cycleId}`, { credentials: 'include', headers: { 'Content-Type': 'application/json', @@ -20,5 +23,3 @@ async function fetchCycle(cycleId: string): Promise { +export async function fetchCycles({ + serverUrl, +}: ApiRequest): Promise { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/cycles`, { + const response = await fetch(`${serverUrl}/api/cycles`, { credentials: 'include', headers: { 'Content-Type': 'application/json', @@ -20,5 +22,3 @@ async function fetchCycles(): Promise { return null; } } - -export default fetchCycles; diff --git a/packages/api/src/fetchEvent.ts b/packages/api/src/fetchEvent.ts index 3a8d1843..a6beaa22 100644 --- a/packages/api/src/fetchEvent.ts +++ b/packages/api/src/fetchEvent.ts @@ -1,8 +1,11 @@ -import { GetEventResponse } from './types'; +import { ApiRequest, GetEventResponse } from './types'; -async function fetchEvent(eventId: string): Promise { +export async function fetchEvent({ + serverUrl, + eventId, +}: ApiRequest<{ eventId: string }>): Promise { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/events/${eventId}`, { + const response = await fetch(`${serverUrl}/api/events/${eventId}`, { credentials: 'include', headers: { 'Content-Type': 'application/json', @@ -20,5 +23,3 @@ async function fetchEvent(eventId: string): Promise { return null; } } - -export default fetchEvent; diff --git a/packages/api/src/fetchEventCycles.ts b/packages/api/src/fetchEventCycles.ts index 086f865c..7efdcb39 100644 --- a/packages/api/src/fetchEventCycles.ts +++ b/packages/api/src/fetchEventCycles.ts @@ -1,16 +1,16 @@ -import { GetCyclesResponse } from './types'; +import { ApiRequest, GetCyclesResponse } from './types'; -async function fetchEventCycles(eventId: string): Promise { +export async function fetchEventCycles({ + eventId, + serverUrl, +}: ApiRequest<{ eventId: string }>): Promise { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/events/${eventId}/cycles`, - { - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, + const response = await fetch(`${serverUrl}/api/events/${eventId}/cycles`, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', }, - ); + }); if (!response.ok) { throw new Error(`HTTP Error! Status: ${response.status}`); @@ -23,5 +23,3 @@ async function fetchEventCycles(eventId: string): Promise { +export async function fetchEventGroupCategories({ + eventId, + serverUrl, +}: ApiRequest<{ eventId: string }>): Promise { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/events/${eventId}/group-categories`, - { - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, + const response = await fetch(`${serverUrl}/api/events/${eventId}/group-categories`, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', }, - ); + }); if (!response.ok) { throw new Error(`HTTP Error! Status: ${response.status}`); @@ -25,5 +23,3 @@ async function fetchEventGroupCategories( return null; } } - -export default fetchEventGroupCategories; diff --git a/packages/api/src/fetchEvents.ts b/packages/api/src/fetchEvents.ts index ebe15c99..0df4240f 100644 --- a/packages/api/src/fetchEvents.ts +++ b/packages/api/src/fetchEvents.ts @@ -1,8 +1,10 @@ -import { GetEventsResponse } from './types'; +import { ApiRequest, GetEventsResponse } from './types'; -async function fetchEvents(): Promise { +export async function fetchEvents({ + serverUrl, +}: ApiRequest): Promise { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/events`, { + const response = await fetch(`${serverUrl}/api/events`, { credentials: 'include', headers: { 'Content-Type': 'application/json', @@ -20,5 +22,3 @@ async function fetchEvents(): Promise { return null; } } - -export default fetchEvents; diff --git a/packages/api/src/fetchForumQuestionFunding.ts b/packages/api/src/fetchForumQuestionFunding.ts deleted file mode 100644 index e8ab651f..00000000 --- a/packages/api/src/fetchForumQuestionFunding.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { GetFundingResponse } from './types'; - -async function fetchForumQuestionFunding(questionId: string): Promise { - try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/forum-questions/${questionId}/funding`, - { - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - if (!response.ok) { - throw new Error(`HTTP Error! Status: ${response.status}`); - } - - const stats = (await response.json()) as { data: GetFundingResponse }; - return stats.data; - } catch (error) { - console.error('Error fetching forum question funding:', error); - return null; - } -} - -export default fetchForumQuestionFunding; diff --git a/packages/api/src/fetchForumQuestionStatistics.ts b/packages/api/src/fetchForumQuestionStatistics.ts deleted file mode 100644 index fe14e18d..00000000 --- a/packages/api/src/fetchForumQuestionStatistics.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { GetForumQuestionStatisticsResponse } from './types'; - -async function fetchForumQuestionStatistics( - questionId: string, -): Promise { - try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/forum-questions/${questionId}/statistics`, - { - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - if (!response.ok) { - throw new Error(`HTTP Error! Status: ${response.status}`); - } - - const stats = (await response.json()) as { data: GetForumQuestionStatisticsResponse }; - return stats.data; - } catch (error) { - console.error('Error fetching forum question statistics:', error); - return null; - } -} - -export default fetchForumQuestionStatistics; diff --git a/packages/api/src/fetchGroupCategories.ts b/packages/api/src/fetchGroupCategories.ts index f9c4d49d..7fdc3a82 100644 --- a/packages/api/src/fetchGroupCategories.ts +++ b/packages/api/src/fetchGroupCategories.ts @@ -1,8 +1,10 @@ -import { GetGroupCategoriesResponse } from './types'; +import { ApiRequest, GetGroupCategoriesResponse } from './types'; -async function fetchGroupCategories(): Promise { +export async function fetchGroupCategories({ + serverUrl, +}: ApiRequest): Promise { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/group-categories`, { + const response = await fetch(`${serverUrl}/api/group-categories`, { credentials: 'include', headers: { 'Content-type': 'application/json', @@ -20,5 +22,3 @@ async function fetchGroupCategories(): Promise { +export async function fetchGroupMembers({ + serverUrl, + groupId, +}: ApiRequest<{ groupId: string }>): Promise { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/groups/${groupId}/users-to-groups`, - { - credentials: 'include', - headers: { - 'Content-type': 'application/json', - }, + const response = await fetch(`${serverUrl}/api/groups/${groupId}/users-to-groups`, { + credentials: 'include', + headers: { + 'Content-type': 'application/json', }, - ); + }); if (!response.ok) { throw new Error(`HTTP Error! Status: ${response.status}`); @@ -23,5 +23,3 @@ async function fetchGroupMembers(groupId: string): Promise { +export async function fetchGroupRegistrations({ + serverUrl, + groupId, +}: ApiRequest<{ groupId: string }>): Promise { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/groups/${groupId}/registrations`, - { - credentials: 'include', - headers: { - 'Content-type': 'application/json', - }, + const response = await fetch(`${serverUrl}/api/groups/${groupId}/registrations`, { + credentials: 'include', + headers: { + 'Content-type': 'application/json', }, - ); + }); if (!response.ok) { throw new Error(`HTTP Error! Status: ${response.status}`); @@ -23,5 +23,3 @@ async function fetchGroupRegistrations(groupId: string): Promise { +}: ApiRequest<{ groupCategoryId: string }>): Promise { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/group-categories/${groupCategoryId}/groups`, - { - credentials: 'include', - headers: { - 'Content-type': 'application/json', - }, + const response = await fetch(`${serverUrl}/api/group-categories/${groupCategoryId}/groups`, { + credentials: 'include', + headers: { + 'Content-type': 'application/json', }, - ); + }); if (!response.ok) { throw new Error(`HTTP Error! Status: ${response.status}`); @@ -27,5 +23,3 @@ async function fetchGroups({ return null; } } - -export default fetchGroups; diff --git a/packages/api/src/fetchOption.ts b/packages/api/src/fetchOption.ts index 4b483e5b..9feccc95 100644 --- a/packages/api/src/fetchOption.ts +++ b/packages/api/src/fetchOption.ts @@ -1,8 +1,11 @@ -import { GetQuestionOptionResponse } from './types'; +import { ApiRequest, GetOptionResponse } from './types'; -async function fetchOption(optionId: string): Promise { +export async function fetchOption({ + serverUrl, + optionId, +}: ApiRequest<{ optionId: string }>): Promise { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/options/${optionId}`, { + const response = await fetch(`${serverUrl}/api/options/${optionId}`, { credentials: 'include', headers: { 'Content-type': 'application/json', @@ -12,12 +15,10 @@ async function fetchOption(optionId: string): Promise { +export async function fetchOptionUsers({ + serverUrl, + optionId, +}: ApiRequest<{ optionId: string }>): Promise { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/options/${optionId}/users`, - { - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, + const response = await fetch(`${serverUrl}/api/options/${optionId}/users`, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', }, - ); + }); if (!response.ok) { throw new Error(`HTTP Error! Status: ${response.status}`); @@ -23,5 +23,3 @@ async function fetchOptionUsers(optionId: string): Promise): Promise { + try { + const response = await fetch(`${serverUrl}/api/questions/${questionId}/funding`, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (!response.ok) { + throw new Error(`HTTP Error! Status: ${response.status}`); + } + + const stats = (await response.json()) as { data: GetFundingResponse }; + return stats.data; + } catch (error) { + console.error('Error fetching forum question funding:', error); + return null; + } +} diff --git a/packages/api/src/fetchQuestionStatistics.ts b/packages/api/src/fetchQuestionStatistics.ts new file mode 100644 index 00000000..aeeb3578 --- /dev/null +++ b/packages/api/src/fetchQuestionStatistics.ts @@ -0,0 +1,24 @@ +import { ApiRequest, GetQuestionStatisticsResponse } from './types'; + +export async function fetchQuestionStatistics({ + serverUrl, + questionId, +}: ApiRequest<{ questionId: string }>): Promise { + try { + const response = await fetch(`${serverUrl}/api/questions/${questionId}/statistics`, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (!response.ok) { + throw new Error(`HTTP Error! Status: ${response.status}`); + } + + const stats = (await response.json()) as { data: GetQuestionStatisticsResponse }; + return stats.data; + } catch (error) { + console.error('Error fetching forum question statistics:', error); + return null; + } +} diff --git a/packages/api/src/fetchRegistrationData.ts b/packages/api/src/fetchRegistrationData.ts index a896fa7d..01a7267a 100644 --- a/packages/api/src/fetchRegistrationData.ts +++ b/packages/api/src/fetchRegistrationData.ts @@ -1,11 +1,12 @@ -import { GetRegistrationDataResponse } from './types'; +import { ApiRequest, GetRegistrationDataResponse } from './types'; -async function fetchRegistrationData( - registrationId: string, -): Promise { +export async function fetchRegistrationData({ + serverUrl, + registrationId, +}: ApiRequest<{ registrationId: string }>): Promise { try { const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/registrations/${registrationId}/registration-data`, + `${serverUrl}/api/registrations/${registrationId}/registration-data`, { credentials: 'include', headers: { @@ -25,5 +26,3 @@ async function fetchRegistrationData( return null; } } - -export default fetchRegistrationData; diff --git a/packages/api/src/fetchRegistrationFields.ts b/packages/api/src/fetchRegistrationFields.ts index 6b92592e..56840ea2 100644 --- a/packages/api/src/fetchRegistrationFields.ts +++ b/packages/api/src/fetchRegistrationFields.ts @@ -1,18 +1,16 @@ -import { GetRegistrationFieldsResponse } from './types'; +import { ApiRequest, GetRegistrationFieldsResponse } from './types'; -async function fetchRegistrationFields( - eventId: string, -): Promise { +export async function fetchRegistrationFields({ + serverUrl, + eventId, +}: ApiRequest<{ eventId: string }>): Promise { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/events/${eventId}/registration-fields`, - { - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, + const response = await fetch(`${serverUrl}/api/events/${eventId}/registration-fields`, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', }, - ); + }); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); @@ -25,5 +23,3 @@ async function fetchRegistrationFields( return null; } } - -export default fetchRegistrationFields; diff --git a/packages/api/src/fetchRegistrations.ts b/packages/api/src/fetchRegistrations.ts index 48ec2aac..364f8a23 100644 --- a/packages/api/src/fetchRegistrations.ts +++ b/packages/api/src/fetchRegistrations.ts @@ -1,16 +1,16 @@ -import { GetRegistrationsResponseType } from './types'; +import { ApiRequest, GetRegistrationsResponseType } from './types'; -async function fetchRegistrations(eventId: string): Promise { +export async function fetchRegistrations({ + serverUrl, + eventId, +}: ApiRequest<{ eventId: string }>): Promise { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/events/${eventId}/registrations`, - { - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, + const response = await fetch(`${serverUrl}/api/events/${eventId}/registrations`, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', }, - ); + }); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); @@ -27,5 +27,3 @@ async function fetchRegistrations(eventId: string): Promise { +export async function fetchUser({ + serverUrl, +}: ApiRequest): Promise { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/users`, { + const response = await fetch(`${serverUrl}/api/users`, { credentials: 'include', headers: { 'Content-Type': 'application/json', @@ -20,5 +22,3 @@ async function fetchUserData(): Promise { return null; } } - -export default fetchUserData; diff --git a/packages/api/src/fetchUserAttributes.ts b/packages/api/src/fetchUserAttributes.ts index cf718a2f..7b5948e2 100644 --- a/packages/api/src/fetchUserAttributes.ts +++ b/packages/api/src/fetchUserAttributes.ts @@ -1,16 +1,16 @@ -import { GetUserAttributesResponse } from './types'; +import { ApiRequest, GetUserAttributesResponse } from './types'; -async function fetchUserAttributes(userId: string): Promise { +export async function fetchUserAttributes({ + serverUrl, + userId, +}: ApiRequest<{ userId: string }>): Promise { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/users/${userId}/attributes`, - { - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, + const response = await fetch(`${serverUrl}/api/users/${userId}/attributes`, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', }, - ); + }); if (!response.ok) { throw new Error(`HTTP Error! Status: ${response.status}`); @@ -23,5 +23,3 @@ async function fetchUserAttributes(userId: string): Promise): Promise { + try { + const response = await fetch(`${serverUrl}/api/users/${userId}/options`, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP Error! Status: ${response.status}`); + } + + const userOptions = (await response.json()) as { data: GetUserOptionsResponse }; + return userOptions.data; + } catch (error) { + console.error('Error fetching user options:', error); + return null; + } +} diff --git a/packages/api/src/fetchUserRegistrations.ts b/packages/api/src/fetchUserRegistrations.ts index 0ee8786f..ed741920 100644 --- a/packages/api/src/fetchUserRegistrations.ts +++ b/packages/api/src/fetchUserRegistrations.ts @@ -1,18 +1,16 @@ -import { GetRegistrationsResponseType } from './types'; +import { ApiRequest, GetRegistrationsResponseType } from './types'; -async function fetchUserRegistrations( - userId: string, -): Promise { +export async function fetchUserRegistrations({ + serverUrl, + userId, +}: ApiRequest<{ userId: string }>): Promise { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/users/${userId}/registrations`, - { - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, + const response = await fetch(`${serverUrl}/api/users/${userId}/registrations`, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', }, - ); + }); if (!response.ok) { throw new Error(`HTTP Error! Status: ${response.status}`); @@ -25,5 +23,3 @@ async function fetchUserRegistrations( return null; } } - -export default fetchUserRegistrations; diff --git a/packages/api/src/fetchUserVotes.ts b/packages/api/src/fetchUserVotes.ts index d957578a..fab3d056 100644 --- a/packages/api/src/fetchUserVotes.ts +++ b/packages/api/src/fetchUserVotes.ts @@ -1,8 +1,11 @@ -import { GetUserVotesResponse } from './types'; +import { ApiRequest, GetUserVotesResponse } from './types'; -async function fetchUserVotes(cycleId: string): Promise { +export async function fetchUserVotes({ + serverUrl, + cycleId, +}: ApiRequest<{ cycleId: string }>): Promise { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/cycles/${cycleId}/votes`, { + const response = await fetch(`${serverUrl}/api/cycles/${cycleId}/votes`, { credentials: 'include', headers: { 'Content-Type': 'application/json', @@ -19,5 +22,3 @@ async function fetchUserVotes(cycleId: string): Promise { +export async function fetchUsersToGroups({ + serverUrl, + userId, +}: ApiRequest<{ userId: string }>): Promise { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/users/${userId}/users-to-groups`, - { - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, + const response = await fetch(`${serverUrl}/api/users/${userId}/users-to-groups`, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', }, - ); + }); if (!response.ok) { throw new Error(`HTTP Error! Status: ${response.status}`); @@ -23,5 +23,3 @@ async function fetchUsersToGroups(userId: string): Promise) { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/auth/logout`, { + const response = await fetch(`${serverUrl}/api/auth/logout`, { method: 'POST', credentials: 'include', headers: { @@ -18,5 +20,3 @@ async function logout() { throw new Error('Logout failed'); } } - -export default logout; diff --git a/packages/api/src/postComment.ts b/packages/api/src/postComment.ts index 35a8c0ed..e7693bc5 100644 --- a/packages/api/src/postComment.ts +++ b/packages/api/src/postComment.ts @@ -1,11 +1,12 @@ -import { PostCommentRequest, PostCommentResponse } from './types'; +import { ApiRequest, PostCommentRequest, PostCommentResponse } from './types'; -async function postComment({ +export async function postComment({ questionOptionId, value, -}: PostCommentRequest): Promise { + serverUrl, +}: ApiRequest): Promise { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/comments`, { + const response = await fetch(`${serverUrl}/api/comments`, { method: 'POST', credentials: 'include', headers: { @@ -25,5 +26,3 @@ async function postComment({ return null; } } - -export default postComment; diff --git a/packages/api/src/postGroup.ts b/packages/api/src/postGroup.ts index 53bd46c2..61d90694 100644 --- a/packages/api/src/postGroup.ts +++ b/packages/api/src/postGroup.ts @@ -1,11 +1,12 @@ -import { PostGroupRequest, PostGroupResponse } from './types'; +import { ApiRequest, PostGroupRequest, PostGroupResponse } from './types'; -async function postGroup({ +export async function postGroup({ name, groupCategoryId, -}: PostGroupRequest): Promise { + serverUrl, +}: ApiRequest): Promise { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/groups`, { + const response = await fetch(`${serverUrl}/api/groups`, { method: 'POST', credentials: 'include', headers: { @@ -28,5 +29,3 @@ async function postGroup({ return null; } } - -export default postGroup; diff --git a/packages/api/src/postLike.ts b/packages/api/src/postLike.ts index 5b1e7099..75d3ed85 100644 --- a/packages/api/src/postLike.ts +++ b/packages/api/src/postLike.ts @@ -1,17 +1,17 @@ -import { PostLikeRequest, PostLikeResponse } from './types'; +import { ApiRequest, PostLikeRequest, PostLikeResponse } from './types'; -async function postLike({ commentId }: PostLikeRequest): Promise { +export async function postLike({ + commentId, + serverUrl, +}: ApiRequest): Promise { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/comments/${commentId}/likes`, - { - method: 'POST', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, + const response = await fetch(`${serverUrl}/api/comments/${commentId}/likes`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', }, - ); + }); if (!response.ok) { throw new Error(`HTTP Error! Status: ${response.status}`); @@ -24,5 +24,3 @@ async function postLike({ commentId }: PostLikeRequest): Promise { +}>): Promise { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/registrations`, { + const response = await fetch(`${serverUrl}/api/registrations`, { method: 'POST', credentials: 'include', headers: { @@ -16,6 +17,10 @@ async function postRegistration({ }); if (!response.ok) { + if (response.status < 500) { + const errors = (await response.json()) as { errors: string[] }; + return errors; + } throw new Error(`HTTP error! Status: ${response.status}`); } @@ -26,5 +31,3 @@ async function postRegistration({ return null; } } - -export default postRegistration; diff --git a/packages/api/src/postUsersToGroups.ts b/packages/api/src/postUsersToGroups.ts index a4357b9b..f39f3877 100644 --- a/packages/api/src/postUsersToGroups.ts +++ b/packages/api/src/postUsersToGroups.ts @@ -1,11 +1,14 @@ -import { PostUsersToGroupsRequest, PostUsersToGroupsResponse } from './types'; +import { ApiRequest, PostUsersToGroupsRequest, PostUsersToGroupsResponse } from './types'; -async function postUserToGroups({ +export async function postUsersToGroups({ secret, groupId, -}: PostUsersToGroupsRequest): Promise { + serverUrl, +}: ApiRequest): Promise< + PostUsersToGroupsResponse | { errors: string[] } | null +> { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/users-to-groups`, { + const response = await fetch(`${serverUrl}/api/users-to-groups`, { method: 'POST', credentials: 'include', headers: { @@ -32,5 +35,3 @@ async function postUserToGroups({ return null; } } - -export default postUserToGroups; diff --git a/packages/api/src/postVerify.ts b/packages/api/src/postVerify.ts index 9b68549d..d3753ce8 100644 --- a/packages/api/src/postVerify.ts +++ b/packages/api/src/postVerify.ts @@ -1,18 +1,23 @@ -import { GetUserResponse } from './types'; +import { ApiRequest, GetUserResponse } from './types'; -async function postVerify(body: { +export async function postVerify({ + email, + pcdStr, + serverUrl, + uuid, +}: ApiRequest<{ pcdStr: string; email: string; uuid: string; -}): Promise { +}>): Promise { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/auth/zupass/verify`, { + const response = await fetch(`${serverUrl}/api/auth/zupass/verify`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ pcd: body.pcdStr, email: body.email, uuid: body.uuid }), + body: JSON.stringify({ pcd: pcdStr, email: email, uuid: uuid }), }); if (!response.ok) { @@ -26,5 +31,3 @@ async function postVerify(body: { return null; } } - -export default postVerify; diff --git a/packages/api/src/postVotes.ts b/packages/api/src/postVotes.ts index 2f660a9b..b486afc4 100644 --- a/packages/api/src/postVotes.ts +++ b/packages/api/src/postVotes.ts @@ -1,8 +1,11 @@ -import { PostVotesRequest, PostVotesResponse } from './types'; +import { ApiRequest, PostVotesRequest, PostVotesResponse } from './types'; -async function postVotes({ votes }: PostVotesRequest): Promise { +export async function postVotes({ + votes, + serverUrl, +}: ApiRequest): Promise { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/votes`, { + const response = await fetch(`${serverUrl}/api/votes`, { method: 'POST', credentials: 'include', headers: { @@ -22,5 +25,3 @@ async function postVotes({ votes }: PostVotesRequest): Promise { +}>): Promise { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/registrations/${registrationId}`, - { - method: 'PUT', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), + const response = await fetch(`${serverUrl}/api/registrations/${registrationId}`, { + method: 'PUT', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', }, - ); + body: JSON.stringify(body), + }); if (!response.ok) { + if (response.status < 500) { + const errors = (await response.json()) as { errors: string[] }; + return errors; + } throw new Error(`HTTP error! Status: ${response.status}`); } @@ -31,5 +33,3 @@ async function putRegistration({ return null; } } - -export default putRegistration; diff --git a/packages/api/src/putUser.ts b/packages/api/src/putUser.ts index e492ee00..cffd455f 100644 --- a/packages/api/src/putUser.ts +++ b/packages/api/src/putUser.ts @@ -1,16 +1,16 @@ -import { PutUserRequest, GetUserResponse } from './types'; +import { PutUserRequest, GetUserResponse, ApiRequest } from './types'; -async function updateUserData({ +export async function putUser({ email, firstName, lastName, telegram, - userAttributes, userId, username, -}: PutUserRequest): Promise<{ data: GetUserResponse } | { errors: string[] } | null> { + serverUrl, +}: ApiRequest): Promise<{ data: GetUserResponse } | { errors: string[] } | null> { try { - const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/api/users/${userId}`, { + const response = await fetch(`${serverUrl}/api/users/${userId}`, { method: 'PUT', credentials: 'include', headers: { @@ -21,7 +21,6 @@ async function updateUserData({ firstName, lastName, telegram, - userAttributes, username, }), }); @@ -41,5 +40,3 @@ async function updateUserData({ return null; } } - -export default updateUserData; diff --git a/packages/api/src/putUsersToGroups.ts b/packages/api/src/putUsersToGroups.ts index 94e3fd08..96f1abcb 100644 --- a/packages/api/src/putUsersToGroups.ts +++ b/packages/api/src/putUsersToGroups.ts @@ -1,21 +1,21 @@ -import { PostUsersToGroupsResponse, PutUsersToGroupsRequest } from './types'; +import { ApiRequest, PostUsersToGroupsResponse, PutUsersToGroupsRequest } from './types'; -async function postUsersToGroups({ +export async function putUsersToGroups({ groupId, userToGroupId, -}: PutUsersToGroupsRequest): Promise { + serverUrl, +}: ApiRequest): Promise< + PostUsersToGroupsResponse | { errors: string[] } | null +> { try { - const response = await fetch( - `${import.meta.env.VITE_SERVER_URL}/api/users-to-groups/${userToGroupId}`, - { - method: 'PUT', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ groupId }), + const response = await fetch(`${serverUrl}/api/users-to-groups/${userToGroupId}`, { + method: 'PUT', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', }, - ); + body: JSON.stringify({ groupId }), + }); if (!response.ok) { if (response.status < 500) { @@ -35,5 +35,3 @@ async function postUsersToGroups({ return null; } } - -export default postUsersToGroups; diff --git a/packages/api/src/types/AlertType.ts b/packages/api/src/types/Alerts.ts similarity index 100% rename from packages/api/src/types/AlertType.ts rename to packages/api/src/types/Alerts.ts diff --git a/packages/api/src/types/ApiRequest.ts b/packages/api/src/types/ApiRequest.ts new file mode 100644 index 00000000..9186407e --- /dev/null +++ b/packages/api/src/types/ApiRequest.ts @@ -0,0 +1,3 @@ +export type ApiRequest = { + serverUrl: string; +} & T; diff --git a/packages/api/src/types/CommentType.ts b/packages/api/src/types/Comments.ts similarity index 100% rename from packages/api/src/types/CommentType.ts rename to packages/api/src/types/Comments.ts diff --git a/packages/api/src/types/CycleType.ts b/packages/api/src/types/Cycles.ts similarity index 89% rename from packages/api/src/types/CycleType.ts rename to packages/api/src/types/Cycles.ts index b95e5612..f9952921 100644 --- a/packages/api/src/types/CycleType.ts +++ b/packages/api/src/types/Cycles.ts @@ -1,25 +1,26 @@ export type GetCycleResponse = { id: string; + eventId: string; createdAt: string; updatedAt: string; status: 'OPEN' | 'CLOSED' | 'UPCOMING' | null; startAt: string; endAt: string; - forumQuestions: { + questions: { id: string; showScore: boolean; createdAt: string; updatedAt: string; questionSubTitle: string | null; cycleId: string; - questionTitle: string; - questionOptions: { + title: string; + options: { id: string; createdAt: string; updatedAt: string; questionId: string; - optionTitle: string; - optionSubTitle?: string; + title: string; + subTitle?: string; accepted: boolean; voteScore?: number; fundingRequest: string; diff --git a/packages/api/src/types/EventType.ts b/packages/api/src/types/Events.ts similarity index 80% rename from packages/api/src/types/EventType.ts rename to packages/api/src/types/Events.ts index 7cd41f4e..2ea07899 100644 --- a/packages/api/src/types/EventType.ts +++ b/packages/api/src/types/Events.ts @@ -7,6 +7,8 @@ export type GetEventResponse = { createdAt: string; updatedAt: string; description: string | null; + fields: unknown; + status: 'OPEN' | 'CLOSED' | 'UPCOMING' | null; }; export type GetEventsResponse = GetEventResponse[]; diff --git a/packages/api/src/types/FundingType.ts b/packages/api/src/types/Funding.ts similarity index 100% rename from packages/api/src/types/FundingType.ts rename to packages/api/src/types/Funding.ts diff --git a/packages/api/src/types/GroupCategoryType.ts b/packages/api/src/types/GroupCategories.ts similarity index 100% rename from packages/api/src/types/GroupCategoryType.ts rename to packages/api/src/types/GroupCategories.ts diff --git a/packages/api/src/types/GroupMembersType.ts b/packages/api/src/types/GroupMembers.ts similarity index 100% rename from packages/api/src/types/GroupMembersType.ts rename to packages/api/src/types/GroupMembers.ts diff --git a/packages/api/src/types/GroupRegistrationsType.ts b/packages/api/src/types/GroupRegistrations.ts similarity index 100% rename from packages/api/src/types/GroupRegistrationsType.ts rename to packages/api/src/types/GroupRegistrations.ts diff --git a/packages/api/src/types/GroupType.ts b/packages/api/src/types/Groups.ts similarity index 100% rename from packages/api/src/types/GroupType.ts rename to packages/api/src/types/Groups.ts diff --git a/packages/api/src/types/LikeType.ts b/packages/api/src/types/Likes.ts similarity index 100% rename from packages/api/src/types/LikeType.ts rename to packages/api/src/types/Likes.ts diff --git a/packages/api/src/types/OptionUsersType.ts b/packages/api/src/types/OptionUsers.ts similarity index 100% rename from packages/api/src/types/OptionUsersType.ts rename to packages/api/src/types/OptionUsers.ts diff --git a/packages/api/src/types/QuestionOptionType.ts b/packages/api/src/types/Options.ts similarity index 63% rename from packages/api/src/types/QuestionOptionType.ts rename to packages/api/src/types/Options.ts index 659b519a..8c0dbfe6 100644 --- a/packages/api/src/types/QuestionOptionType.ts +++ b/packages/api/src/types/Options.ts @@ -1,4 +1,4 @@ -export type QuestionOption = { +export type Option = { id: string; createdAt: string; updatedAt: string; @@ -7,11 +7,12 @@ export type QuestionOption = { userId?: string; optionSubTitle?: string; accepted: boolean; + data: unknown; fundingRequest: string; }; -export type GetQuestionOptionRequest = { +export type GetOptionRequest = { optionId: string; }; -export type GetQuestionOptionResponse = QuestionOption; +export type GetOptionResponse = Option; diff --git a/packages/api/src/types/ForumQuestionType.ts b/packages/api/src/types/Questions.ts similarity index 85% rename from packages/api/src/types/ForumQuestionType.ts rename to packages/api/src/types/Questions.ts index c8cd09ee..3e9b1805 100644 --- a/packages/api/src/types/ForumQuestionType.ts +++ b/packages/api/src/types/Questions.ts @@ -1,4 +1,4 @@ -export type GetForumQuestionStatisticsResponse = { +export type GetQuestionStatisticsResponse = { numProposals: number; sumNumOfHearts: number; numOfParticipants: number; @@ -6,7 +6,7 @@ export type GetForumQuestionStatisticsResponse = { optionStats: Record< string, { - optionTitle: string; + title: string; pluralityScore: string; distinctUsers: string; allocatedHearts: string; @@ -25,4 +25,5 @@ export type Question = { description: string | null; cycleId: string; title: string; + fields: unknown; }; diff --git a/packages/api/src/types/RegistrationDataType.ts b/packages/api/src/types/RegistrationData.ts similarity index 53% rename from packages/api/src/types/RegistrationDataType.ts rename to packages/api/src/types/RegistrationData.ts index 83c7e198..033f17c7 100644 --- a/packages/api/src/types/RegistrationDataType.ts +++ b/packages/api/src/types/RegistrationData.ts @@ -1,4 +1,4 @@ -import { RegistrationStatus } from './RegistrationType'; +import { RegistrationStatus } from './Registrations'; export type GetRegistrationDataResponse = { id: string; @@ -13,33 +13,32 @@ export type PostRegistrationRequest = { eventId: string; groupId: string | null; status: RegistrationStatus; - registrationData: { - registrationFieldId: string; - value: string; - }[]; + data: Record< + string, + { + value: string | number | boolean | string[] | null; + type: 'TEXT' | 'TEXTAREA' | 'SELECT' | 'CHECKBOX' | 'MULTI_SELECT' | 'NUMBER'; + fieldId: string; + } + >; }; export type PutRegistrationRequest = { eventId: string; groupId: string | null; status: RegistrationStatus; - registrationData: { - registrationFieldId: string; - value: string; - }[]; + data: Record< + string, + { + value: string | number | boolean | string[] | null; + type: 'TEXT' | 'TEXTAREA' | 'SELECT' | 'CHECKBOX' | 'MULTI_SELECT' | 'NUMBER'; + fieldId: string; + } + >; }; export type PostRegistrationResponse = { - registrationData: - | { - id: string; - createdAt: string; - updatedAt: string; - registrationId: string; - registrationFieldId: string; - value: string; - }[] - | null; + data: unknown | null; id: string; createdAt: string; updatedAt: string; @@ -49,16 +48,7 @@ export type PostRegistrationResponse = { }; export type PutRegistrationResponse = { - registrationData: - | { - id: string; - createdAt: string; - updatedAt: string; - registrationId: string; - registrationFieldId: string; - value: string; - }[] - | null; + registrationData: unknown | null; id: string; createdAt: string; updatedAt: string; diff --git a/packages/api/src/types/RegistrationFieldOptionType.ts b/packages/api/src/types/RegistrationFieldOptions.ts similarity index 100% rename from packages/api/src/types/RegistrationFieldOptionType.ts rename to packages/api/src/types/RegistrationFieldOptions.ts diff --git a/packages/api/src/types/RegistrationFieldType.ts b/packages/api/src/types/RegistrationFields.ts similarity index 98% rename from packages/api/src/types/RegistrationFieldType.ts rename to packages/api/src/types/RegistrationFields.ts index 506308cb..1a707107 100644 --- a/packages/api/src/types/RegistrationFieldType.ts +++ b/packages/api/src/types/RegistrationFields.ts @@ -1,4 +1,4 @@ -import { RegistrationFieldOption } from './RegistrationFieldOptionType'; +import { RegistrationFieldOption } from './RegistrationFieldOptions'; export type GetRegistrationFieldsResponse = { id: string; diff --git a/packages/api/src/types/RegistrationType.ts b/packages/api/src/types/RegistrationType.ts deleted file mode 100644 index da9e0855..00000000 --- a/packages/api/src/types/RegistrationType.ts +++ /dev/null @@ -1,13 +0,0 @@ -export type RegistrationStatus = 'DRAFT' | 'APPROVED' | 'PUBLISHED' | null; - -export type GetRegistrationResponseType = { - id?: string | undefined; - status: RegistrationStatus; - userId: string; - groupId: string | null; - eventId?: string | undefined; - createdAt: string; - updatedAt: string; -}; - -export type GetRegistrationsResponseType = GetRegistrationResponseType[]; diff --git a/packages/api/src/types/Registrations.ts b/packages/api/src/types/Registrations.ts new file mode 100644 index 00000000..c3495ba6 --- /dev/null +++ b/packages/api/src/types/Registrations.ts @@ -0,0 +1,24 @@ +export type RegistrationStatus = 'DRAFT' | 'APPROVED' | 'REJECTED' | null; + +export type GetRegistrationResponseType = { + id?: string | undefined; + status: RegistrationStatus; + userId: string; + groupId: string | null; + eventId?: string | undefined; + createdAt: string; + data: unknown; + updatedAt: string; + event?: { + id: string; + name: string; + imageUrl: string; + link: string | null; + registrationDescription: string | null; + createdAt: string; + updatedAt: string; + description: string | null; + }; +}; + +export type GetRegistrationsResponseType = GetRegistrationResponseType[]; diff --git a/packages/api/src/types/UserAttributesType.ts b/packages/api/src/types/UserAttributes.ts similarity index 100% rename from packages/api/src/types/UserAttributesType.ts rename to packages/api/src/types/UserAttributes.ts diff --git a/packages/api/src/types/UserOptions.ts b/packages/api/src/types/UserOptions.ts new file mode 100644 index 00000000..6208f635 --- /dev/null +++ b/packages/api/src/types/UserOptions.ts @@ -0,0 +1,21 @@ +export type GetUserOptionsResponse = { + id: string; + createdAt: Date; + updatedAt: Date; + userId: string | null; + registrationId: string | null; + questionId: string; + optionTitle: string; + optionSubTitle: string | null; + accepted: boolean | null; + voteScore: string; + fundingRequest: string | null; + question: { + id: string; + createdAt: string; + updatedAt: string; + description: string | null; + cycleId: string; + title: string; + }; +}[]; diff --git a/packages/api/src/types/UserVotesType.ts b/packages/api/src/types/UserVotes.ts similarity index 100% rename from packages/api/src/types/UserVotesType.ts rename to packages/api/src/types/UserVotes.ts diff --git a/packages/api/src/types/UserType.ts b/packages/api/src/types/Users.ts similarity index 100% rename from packages/api/src/types/UserType.ts rename to packages/api/src/types/Users.ts diff --git a/packages/api/src/types/UsersToGroupsType.ts b/packages/api/src/types/UsersToGroups.ts similarity index 100% rename from packages/api/src/types/UsersToGroupsType.ts rename to packages/api/src/types/UsersToGroups.ts diff --git a/packages/api/src/types/index.ts b/packages/api/src/types/index.ts index 7f94de9e..b0aab253 100644 --- a/packages/api/src/types/index.ts +++ b/packages/api/src/types/index.ts @@ -1,21 +1,22 @@ -export * from './AlertType'; -export * from './CommentType'; -export * from './CycleType'; -export * from './EventType'; -export * from './ForumQuestionType'; -export * from './FundingType'; -export * from './GroupCategoryType'; -export * from './GroupMembersType'; -export * from './GroupRegistrationsType'; -export * from './GroupType'; -export * from './LikeType'; -export * from './OptionUsersType'; -export * from './QuestionOptionType'; -export * from './RegistrationDataType'; -export * from './RegistrationFieldOptionType'; -export * from './RegistrationFieldType'; -export * from './RegistrationType'; -export * from './UserAttributesType'; -export * from './UsersToGroupsType'; -export * from './UserType'; -export * from './UserVotesType'; +export * from './Alerts'; +export * from './Comments'; +export * from './Cycles'; +export * from './Events'; +export * from './Questions'; +export * from './Funding'; +export * from './GroupCategories'; +export * from './GroupMembers'; +export * from './GroupRegistrations'; +export * from './Groups'; +export * from './Likes'; +export * from './OptionUsers'; +export * from './Options'; +export * from './RegistrationData'; +export * from './RegistrationFieldOptions'; +export * from './RegistrationFields'; +export * from './Registrations'; +export * from './UserAttributes'; +export * from './UsersToGroups'; +export * from './Users'; +export * from './UserVotes'; +export * from './ApiRequest'; diff --git a/packages/berlin/package.json b/packages/berlin/package.json index 57c77548..879af783 100644 --- a/packages/berlin/package.json +++ b/packages/berlin/package.json @@ -4,44 +4,63 @@ "type": "module", "main": "./src/main.ts", "scripts": { - "dev": "vite", - "build": "vite build", + "dev": "pnpm exec vite", + "build": "pnpm exec vite build", "lint": "pnpm exec eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "format": "pnpm exec prettier --check \"src/**/*.{ts,md}\"", - "format:fix": "pnpm exec prettier --write \"src/**/*.{ts,md}\"", - "preview": "vite preview" + "format": "pnpm exec prettier . --write", + "preview": "pnpm exec vite preview" }, "dependencies": { + "@hookform/resolvers": "^3.3.4", + "@pcd/passport-interface": "^0.8.0", + "@pcd/semaphore-identity-pcd": "^0.8.0", + "@pcd/semaphore-signature-pcd": "^0.9.0", "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-navigation-menu": "^1.2.0", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.0.2", + "@tanstack/react-query": "^5.13.4", "api": "workspace:*", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "cmdk": "^1.0.0", "lucide-react": "^0.397.0", + "react": "^18.2.0", + "react-content-loader": "^7.0.0", + "react-dom": "^18.2.0", + "react-hook-form": "^7.49.3", + "react-hot-toast": "^2.4.1", + "react-joyride": "^2.8.2", + "react-markdown": "^9.0.1", + "react-router-dom": "^6.20.1", + "styled-components": "^6.1.1", "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7", - "ui": "workspace:*" + "ui": "workspace:*", + "zod": "^3.22.4", + "zustand": "^4.4.7" }, "devDependencies": { + "@hookform/devtools": "^4.3.1", + "@pcd/pcd-types": "^0.8.0", + "@tanstack/react-query-devtools": "^5.13.5", "@types/node": "^20.12.4", - "@types/react-dom": "^18.2.22", "@types/react": "^18.3.3", + "@types/react-dom": "^18.2.22", "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.19", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.6", - "eslint": "^8.57.0", - "postcss-cli": "^11.0.0", "postcss": "^8.4.38", + "postcss-cli": "^11.0.0", "tailwindcss": "^3.4.3", - "vite-plugin-dts": "^3.8.1", - "vite": "^5.2.0" + "typescript": "^5.5.2", + "vite": "^5.3.5", + "vite-plugin-node-polyfills": "^0.17.0" + }, + "engines": { + "node": "^20" } } diff --git a/packages/berlin/src/App.tsx b/packages/berlin/src/App.tsx index b517473d..29a6e2da 100644 --- a/packages/berlin/src/App.tsx +++ b/packages/berlin/src/App.tsx @@ -1,9 +1,6 @@ // React and third-party libraries -import { QueryClient } from '@tanstack/react-query'; -import { RouterProvider, createBrowserRouter, redirect } from 'react-router-dom'; - -// Store -import { useAppStore } from './store'; +import { CancelledError, QueryClient } from '@tanstack/react-query'; +import { RouterProvider, createBrowserRouter, redirect, useRouteError } from 'react-router-dom'; // API import { fetchCycle, fetchEvents, fetchRegistrations, fetchUser } from 'api'; @@ -28,90 +25,103 @@ import SecretGroupRegistration from './pages/SecretGroupRegistration.tsx'; * Redirects the user to the landing page if they are not logged in */ async function redirectToLandingLoader(queryClient: QueryClient) { - const user = await queryClient.fetchQuery({ - queryKey: ['user'], - queryFn: fetchUser, - staleTime: 10000, - }); - - if (!user) { - return redirect('/'); - } - return null; -} + try { + const user = await queryClient.fetchQuery({ + queryKey: ['user'], + queryFn: () => fetchUser({ serverUrl: import.meta.env.VITE_SERVER_URL }), + staleTime: 10000, + }); -/** - * Redirects the user to the account page if they have not completed their profile - */ -async function redirectToAccount(queryClient: QueryClient) { - const user = await queryClient.fetchQuery({ - queryKey: ['user'], - queryFn: fetchUser, - }); - - if (user?.username) { - useAppStore.setState({ userStatus: 'COMPLETE' }); + if (!user) { + return redirect('/'); + } return null; - } + } catch (e) { + if (e instanceof CancelledError) { + console.log('Tanstack cancelled error:', e); + } - useAppStore.setState({ userStatus: 'INCOMPLETE' }); - return redirect('/account'); + return redirect('/'); + } } /** - * Redirects the user to the landing page to cycles or account page + * Redirects the user after successful login */ -async function redirectOnLandingLoader(queryClient: QueryClient) { - const user = await queryClient.fetchQuery({ - queryKey: ['user'], - queryFn: fetchUser, - }); - - if (!user) { - return null; - } - - const events = await queryClient.fetchQuery({ - queryKey: ['events'], - queryFn: fetchEvents, - }); +async function redirectAfterLogin(queryClient: QueryClient) { + try { + const user = await queryClient.fetchQuery({ + queryKey: ['user'], + queryFn: () => fetchUser({ serverUrl: import.meta.env.VITE_SERVER_URL }), + }); - const userIsComplete = await redirectToAccount(queryClient); + if (!user) { + return null; + } - if (userIsComplete) { - return userIsComplete; - } + if (!user.username) { + return redirect('/account'); + } - if (events?.length === 1) { - const registrations = await queryClient.fetchQuery({ - queryKey: ['event', events?.[0].id, 'registrations'], - queryFn: () => fetchRegistrations(events?.[0].id || ''), + const events = await queryClient.fetchQuery({ + queryKey: ['events'], + queryFn: () => fetchEvents({ serverUrl: import.meta.env.VITE_SERVER_URL }), }); - if (registrations && registrations.some((registration) => registration.status === 'APPROVED')) { - return redirect(`/events/${events?.[0].id}/register`); + // if there is only one event, redirect to the cycles page + if (events?.length === 1) { + const registrations = await queryClient.fetchQuery({ + queryKey: ['event', events?.[0].id, 'registrations'], + queryFn: () => + fetchRegistrations({ + eventId: events?.[0].id || '', + serverUrl: import.meta.env.VITE_SERVER_URL, + }), + }); + + if ( + registrations && + registrations.every((registration) => registration.status !== 'APPROVED') + ) { + return redirect(`/events/${events?.[0].id}/register`); + } + + return redirect(`/events/${events?.[0].id}/cycles`); } - return redirect(`/events/${events?.[0].id}/cycles`); - } + // if there are multiple events, redirect to the events page + return redirect('/events'); + } catch (e) { + if (e instanceof CancelledError) { + console.log('Tanstack cancelled error:', e); + } - return null; + return null; + } } /** * Redirects the user to the only event if there is only one event */ async function redirectToOnlyOneEventLoader(queryClient: QueryClient) { - const events = await queryClient.fetchQuery({ - queryKey: ['events'], - queryFn: fetchEvents, - }); + try { + const events = await queryClient.fetchQuery({ + queryKey: ['events'], + queryFn: () => fetchEvents({ serverUrl: import.meta.env.VITE_SERVER_URL }), + }); - if (events?.length === 1) { - return redirect(`/events/${events?.[0].id}/cycles`); - } + if (events?.length === 1) { + return redirect(`/events/${events?.[0].id}/cycles`); + } + + return null; + } catch (e) { + if (e instanceof CancelledError) { + console.log('Tanstack cancelled error:', e); + } - return null; + return redirect('/'); + } } /** @@ -119,20 +129,28 @@ async function redirectToOnlyOneEventLoader(queryClient: QueryClient) { * Redirects the user to the holding page if they don't have any approved registrations */ async function redirectToEventHoldingOrRegister(queryClient: QueryClient, eventId?: string) { - const registrations = await queryClient.fetchQuery({ - queryKey: ['event', eventId, 'registrations'], - queryFn: () => fetchRegistrations(eventId || ''), - }); + try { + const registrations = await queryClient.fetchQuery({ + queryKey: ['event', eventId, 'registrations'], + queryFn: () => + fetchRegistrations({ eventId: eventId || '', serverUrl: import.meta.env.VITE_SERVER_URL }), + }); - if (!registrations || !registrations.length) { - return redirect(`/events/${eventId}/register`); - } + if (!registrations || !registrations.length) { + return redirect(`/events/${eventId}/register`); + } - if (!registrations.some((registration) => registration.status === 'APPROVED')) { - return redirect(`/events/${eventId}/holding`); - } + if (!registrations.some((registration) => registration.status === 'APPROVED')) { + return redirect(`/events/${eventId}/holding`); + } - return null; + return null; + } catch (e) { + if (e instanceof CancelledError) { + console.log('Tanstack cancelled error:', e); + } + return redirect('/'); + } } /** @@ -143,32 +161,48 @@ async function redirectToCycleResultsLoader( eventId?: string, cycleId?: string, ) { - const cycle = await queryClient.fetchQuery({ - queryKey: ['cycles', cycleId], - queryFn: () => fetchCycle(cycleId || ''), - }); + try { + const cycle = await queryClient.fetchQuery({ + queryKey: ['cycles', cycleId], + queryFn: () => + fetchCycle({ cycleId: cycleId || '', serverUrl: import.meta.env.VITE_SERVER_URL }), + }); - if (cycle?.status === 'CLOSED') { - return redirect(`/events/${eventId}/cycles/${cycleId}/results`); - } + if (cycle?.status === 'CLOSED') { + return redirect(`/events/${eventId}/cycles/${cycleId}/results`); + } - return null; + return null; + } catch (e) { + if (e instanceof CancelledError) { + console.log('Tanstack cancelled error:', e); + } + return redirect('/'); + } } /** * Redirects the user to the cycle page if the cycle is open */ async function redirectToCycleIfOpen(queryClient: QueryClient, eventId?: string, cycleId?: string) { - const cycle = await queryClient.fetchQuery({ - queryKey: ['cycles', cycleId], - queryFn: () => fetchCycle(cycleId || ''), - }); + try { + const cycle = await queryClient.fetchQuery({ + queryKey: ['cycles', cycleId], + queryFn: () => + fetchCycle({ cycleId: cycleId || '', serverUrl: import.meta.env.VITE_SERVER_URL }), + }); - if (cycle?.status === 'OPEN') { - return redirect(`/events/${eventId}/cycles/${cycleId}`); - } + if (cycle?.status === 'OPEN') { + return redirect(`/events/${eventId}/cycles/${cycleId}`); + } - return null; + return null; + } catch (e) { + if (e instanceof CancelledError) { + console.log('Tanstack cancelled error:', e); + } + return redirect('/'); + } } const router = (queryClient: QueryClient) => @@ -179,10 +213,11 @@ const router = (queryClient: QueryClient) => }, { element: , + errorElement: , children: [ { path: '/', - loader: () => redirectOnLandingLoader(queryClient), + loader: () => redirectAfterLogin(queryClient), element: , }, { @@ -205,7 +240,6 @@ const router = (queryClient: QueryClient) => Component: PublicGroupRegistration, }, { - loader: () => redirectToAccount(queryClient), path: '/events', children: [ { @@ -256,6 +290,13 @@ const router = (queryClient: QueryClient) => }, ]); +function ErrorBoundary() { + const error = useRouteError(); + console.log('@ErrorBoundary error', error); + // Uncaught ReferenceError: path is not defined + return
Dang!
; +} + function App({ queryClient }: { queryClient: QueryClient }) { return ; } diff --git a/packages/berlin/src/_components/ui/command.tsx b/packages/berlin/src/_components/ui/command.tsx index 265ce7c6..f19ed548 100644 --- a/packages/berlin/src/_components/ui/command.tsx +++ b/packages/berlin/src/_components/ui/command.tsx @@ -15,7 +15,7 @@ const Command = React.forwardRef< { return ( - + {children} @@ -46,7 +46,7 @@ const CommandInput = React.forwardRef< (({ className, ...props }, ref) => ( )); @@ -113,7 +113,7 @@ const CommandItem = React.forwardRef< ) => { return ( ); diff --git a/packages/berlin/src/_components/ui/dialog.tsx b/packages/berlin/src/_components/ui/dialog.tsx index 828ac9de..44994afe 100644 --- a/packages/berlin/src/_components/ui/dialog.tsx +++ b/packages/berlin/src/_components/ui/dialog.tsx @@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef< {children} - + Close @@ -84,7 +84,7 @@ const DialogDescription = React.forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/packages/berlin/src/_components/ui/dropdown-menu.tsx b/packages/berlin/src/_components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..01a23898 --- /dev/null +++ b/packages/berlin/src/_components/ui/dropdown-menu.tsx @@ -0,0 +1,188 @@ +'use client'; + +import * as React from 'react'; +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { CheckIcon, ChevronRightIcon, DotFilledIcon } from '@radix-ui/react-icons'; + +import { cn } from '@/lib/utils'; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { + return ( + + ); +}; +DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}; diff --git a/packages/berlin/src/_components/ui/navigation-menu.tsx b/packages/berlin/src/_components/ui/navigation-menu.tsx new file mode 100644 index 00000000..18024242 --- /dev/null +++ b/packages/berlin/src/_components/ui/navigation-menu.tsx @@ -0,0 +1,117 @@ +import * as React from 'react'; +import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu'; +import { cva } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const NavigationMenu = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + +)); +NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName; + +const NavigationMenuList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName; + +const NavigationMenuItem = NavigationMenuPrimitive.Item; + +const navigationMenuTriggerStyle = cva(); +// "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50" + +const NavigationMenuTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + +)); +NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName; + +const NavigationMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName; + +const NavigationMenuLink = NavigationMenuPrimitive.Link; + +const NavigationMenuViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ +
+)); +NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName; + +const NavigationMenuIndicator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +
+ +)); +NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName; + +export { + navigationMenuTriggerStyle, + NavigationMenu, + NavigationMenuList, + NavigationMenuItem, + NavigationMenuContent, + NavigationMenuTrigger, + NavigationMenuLink, + NavigationMenuIndicator, + NavigationMenuViewport, +}; diff --git a/packages/berlin/src/_components/ui/popover.tsx b/packages/berlin/src/_components/ui/popover.tsx index ebbaafc5..6783ddf7 100644 --- a/packages/berlin/src/_components/ui/popover.tsx +++ b/packages/berlin/src/_components/ui/popover.tsx @@ -21,7 +21,7 @@ const PopoverContent = React.forwardRef< align={align} sideOffset={sideOffset} className={cn( - 'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', + 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-none', className, )} {...props} diff --git a/packages/berlin/src/_components/ui/separator.tsx b/packages/berlin/src/_components/ui/separator.tsx index 41062d43..97d71cfe 100644 --- a/packages/berlin/src/_components/ui/separator.tsx +++ b/packages/berlin/src/_components/ui/separator.tsx @@ -14,7 +14,7 @@ const Separator = React.forwardRef< decorative={decorative} orientation={orientation} className={cn( - 'shrink-0 bg-border', + 'bg-border shrink-0', orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]', className, )} diff --git a/packages/berlin/src/components/button/Button.styled.tsx b/packages/berlin/src/components/button/Button.styled.tsx index 3d5fa449..ad00dbbb 100644 --- a/packages/berlin/src/components/button/Button.styled.tsx +++ b/packages/berlin/src/components/button/Button.styled.tsx @@ -2,7 +2,6 @@ import styled, { css } from 'styled-components'; import { StyledButtonProps } from './Button.types'; export const StyledButton = styled.button` - border-radius: 0.5rem; border: none; font-family: var(--font-family-button); font-size: 1rem; diff --git a/packages/berlin/src/components/columns/cycle-columns/CycleColumns.styled.tsx b/packages/berlin/src/components/columns/cycle-columns/CycleColumns.styled.tsx deleted file mode 100644 index 9ddf80a7..00000000 --- a/packages/berlin/src/components/columns/cycle-columns/CycleColumns.styled.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import styled from 'styled-components'; -import { FlexRow } from '../../containers/FlexRow.styled'; - -export const Card = styled(FlexRow)` - display: none; - @media (min-width: 600px) { - display: flex; - border-bottom: 2px solid var(--color-black); - gap: 0; - width: 100%; - } -`; - -export const Proposal = styled(FlexRow)` - flex: 1; - font-weight: bold; - min-width: 11rem; - padding: 1.5rem; - p { - cursor: pointer; - } -`; - -export const Lead = styled(FlexRow)` - display: none; - @media (min-width: 600px) { - display: flex; - font-weight: bold; - max-width: 10rem; - min-width: 8rem; - padding: 1.5rem; - - p { - cursor: pointer; - } - } -`; - -export const Affiliation = styled(FlexRow)` - display: none; - @media (min-width: 600px) { - display: flex; - font-weight: bold; - max-width: 10rem; - min-width: 8rem; - padding: 1.5rem; - - p { - cursor: pointer; - } - } -`; - -export const Hearts = styled(FlexRow)` - gap: 0.5rem; - max-width: 5rem; - padding: 1.5rem; -`; - -export const Plurality = styled(FlexRow)` - max-width: 5rem; - padding: 1.5rem; -`; diff --git a/packages/berlin/src/components/columns/cycle-columns/CycleColumns.tsx b/packages/berlin/src/components/columns/cycle-columns/CycleColumns.tsx deleted file mode 100644 index c1161717..00000000 --- a/packages/berlin/src/components/columns/cycle-columns/CycleColumns.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Heart } from 'lucide-react'; -import IconButton from '../../icon-button'; -import { Body } from '../../typography/Body.styled'; -import { Affiliation, Lead, Card, Hearts, Proposal, Plurality } from './CycleColumns.styled'; -import Icon from '../../icon'; - -type CycleColumnsProps = { - onColumnClick: (column: string) => void; - showScore?: boolean; -}; - -function CycleColumns({ onColumnClick, showScore }: CycleColumnsProps) { - return ( - - - Proposal - - onColumnClick('lead')}> - Lead - - onColumnClick('affiliation')}> - Affiliation - - onColumnClick('numOfVotes')}> - - - - - {showScore && ( - onColumnClick('voteScore')}> - - - )} - - ); -} - -export default CycleColumns; diff --git a/packages/berlin/src/components/columns/cycle-columns/index.ts b/packages/berlin/src/components/columns/cycle-columns/index.ts deleted file mode 100644 index d1ec6baa..00000000 --- a/packages/berlin/src/components/columns/cycle-columns/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './CycleColumns'; diff --git a/packages/berlin/src/components/cycles/Cycles.tsx b/packages/berlin/src/components/cycles/Cycles.tsx new file mode 100644 index 00000000..a8b138b1 --- /dev/null +++ b/packages/berlin/src/components/cycles/Cycles.tsx @@ -0,0 +1,43 @@ +import { GetCycleResponse } from 'api'; +import { Body } from '../typography/Body.styled'; +import { useNavigate } from 'react-router-dom'; + +type CyclesProps = { + cycles: GetCycleResponse[] | undefined; + errorMessage: string; + eventId: string | undefined; +}; + +function Cycles({ cycles, errorMessage, eventId }: CyclesProps) { + const navigate = useNavigate(); + + const formatDate = (date: string) => { + const eventEndDate = new Date(date); + return new Intl.DateTimeFormat('en-US', { month: 'long', day: 'numeric' }).format(eventEndDate); + }; + + const handleCycleClick = (cycleId: string) => { + navigate(`/events/${eventId}/cycles/${cycleId}`); + }; + + return ( + <> + {cycles?.length ? ( + cycles.map((cycle) => ( +
handleCycleClick(cycle.id)} + > + {cycle?.questions[0]?.title} + {formatDate(cycle.endAt)} +
+ )) + ) : ( + {errorMessage} + )} + + ); +} + +export default Cycles; diff --git a/packages/berlin/src/components/cycles/index.ts b/packages/berlin/src/components/cycles/index.ts new file mode 100644 index 00000000..7e9e6dfc --- /dev/null +++ b/packages/berlin/src/components/cycles/index.ts @@ -0,0 +1 @@ +export { default } from './Cycles'; diff --git a/packages/berlin/src/components/event-card/EventCard.styled.tsx b/packages/berlin/src/components/event-card/EventCard.styled.tsx index a3742b19..85457c22 100644 --- a/packages/berlin/src/components/event-card/EventCard.styled.tsx +++ b/packages/berlin/src/components/event-card/EventCard.styled.tsx @@ -1,9 +1,7 @@ import styled from 'styled-components'; import { FlexColumn } from '../containers/FlexColumn.styled'; -import { FlexRowToColumn } from '../containers/FlexRowToColumn.styled'; -export const Card = styled(FlexRowToColumn)` - border-radius: 1rem; +export const Card = styled(FlexColumn)` border: 1px solid var(--color-black); overflow: hidden; width: 100%; @@ -12,7 +10,7 @@ export const Card = styled(FlexRowToColumn)` export const ImageContainer = styled.div` background-color: var(--color-gray); width: 100%; - height: 300px; + height: 160px; img { height: 100%; @@ -24,4 +22,5 @@ export const ImageContainer = styled.div` export const CardContent = styled(FlexColumn)` padding: 2rem; + max-height: 170px; `; diff --git a/packages/berlin/src/components/event-card/EventCard.tsx b/packages/berlin/src/components/event-card/EventCard.tsx index 5c6d1878..a9513aca 100644 --- a/packages/berlin/src/components/event-card/EventCard.tsx +++ b/packages/berlin/src/components/event-card/EventCard.tsx @@ -2,7 +2,6 @@ import { GetEventResponse } from 'api'; import { Subtitle } from '../typography/Subtitle.styled'; import { Body } from '../typography/Body.styled'; import { Card, CardContent, ImageContainer } from './EventCard.styled'; -import Button from '../button'; import Link from '../link'; // Third-party libraries @@ -15,14 +14,12 @@ type EventCardProps = { function EventCard({ event, onClick }: EventCardProps) { return ( - - - {`${event.name} - + {event.name} {event.description && ( {props.children}, p: ({ node, ...props }) => {props.children}, @@ -31,8 +28,10 @@ function EventCard({ event, onClick }: EventCardProps) { {event.description} )} - {onClick && } + + {`${event.name} + ); } diff --git a/packages/berlin/src/components/events/Events.tsx b/packages/berlin/src/components/events/Events.tsx new file mode 100644 index 00000000..953c53b7 --- /dev/null +++ b/packages/berlin/src/components/events/Events.tsx @@ -0,0 +1,60 @@ +import Markdown from 'react-markdown'; + +import { GetEventResponse } from 'api'; + +import { Body } from '../typography/Body.styled'; +import { Subtitle } from '../typography/Subtitle.styled'; +import Link from '../link'; +import { useNavigate } from 'react-router-dom'; + +type EventsProps = { + events: GetEventResponse[] | undefined; + errorMessage: string; +}; + +export default function Events({ events, errorMessage }: EventsProps) { + const navigate = useNavigate(); + + const handleClick = (eventId: string) => { + navigate(`/events/${eventId}/cycles`); + }; + + return events?.length ? ( + events.map((event) => { + return ( +
handleClick(event.id)} + > +
+ {event.name} + {event.description && ( + {props.children}, + p: ({ node, ...props }) => ( + + {props.children} + + ), + }} + > + {event.description} + + )} +
+
+ {`${event.name} +
+
+ ); + }) + ) : ( + {errorMessage} + ); +} diff --git a/packages/berlin/src/components/events/index.ts b/packages/berlin/src/components/events/index.ts new file mode 100644 index 00000000..92337cbd --- /dev/null +++ b/packages/berlin/src/components/events/index.ts @@ -0,0 +1 @@ +export { default } from './Events'; diff --git a/packages/berlin/src/components/footer/Footer.styled.tsx b/packages/berlin/src/components/footer/Footer.styled.tsx deleted file mode 100644 index 0d4958d8..00000000 --- a/packages/berlin/src/components/footer/Footer.styled.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import styled from 'styled-components'; -import { FlexColumn } from '../containers/FlexColumn.styled'; - -export const SyledFooter = styled.footer` - align-items: center; - display: flex; - justify-content: center; - min-height: 7.5rem; - padding-block: 2rem; -`; - -export const FooterContainer = styled(FlexColumn)` - background-color: var(--color-white); - width: min(90%, 1080px); - text-align: center; -`; - -export const Copy = styled.p` - font-size: 0.875rem; - line-height: 1.25rem; -`; diff --git a/packages/berlin/src/components/footer/Footer.tsx b/packages/berlin/src/components/footer/Footer.tsx index 36a3c203..7474c447 100644 --- a/packages/berlin/src/components/footer/Footer.tsx +++ b/packages/berlin/src/components/footer/Footer.tsx @@ -1,27 +1,25 @@ import { useAppStore } from '../../store'; -import { FlexRow } from '../containers/FlexRow.styled'; import { Link as RouterLink } from 'react-router-dom'; -import { Copy, FooterContainer, SyledFooter } from './Footer.styled'; import footerData from '../../data/footer'; function Footer() { const theme = useAppStore((state) => state.theme); return ( - - +
+
{footerData.copy.map(({ id, text }) => ( - {text} +

{text}

))} - +
{footerData.logos.map((logo) => ( {logo.alt} ))} - - - +
+
+
); } diff --git a/packages/berlin/src/components/form/FormInput.tsx b/packages/berlin/src/components/form-input/FormInput.tsx similarity index 77% rename from packages/berlin/src/components/form/FormInput.tsx rename to packages/berlin/src/components/form-input/FormInput.tsx index 2f5bc69f..706e87f3 100644 --- a/packages/berlin/src/components/form/FormInput.tsx +++ b/packages/berlin/src/components/form-input/FormInput.tsx @@ -1,8 +1,8 @@ import { FieldValues, Path, UseFormReturn } from 'react-hook-form'; -import { SelectInput } from './SelectInput'; -import { TextAreaInput } from './TextAreaInput'; -import { TextInput } from './TextInput'; -import { NumberInput } from './NumberInput'; +import { FormSelectInput } from './FormSelectInput'; +import { FormTextAreaInput } from './FormTextAreaInput'; +import { FormTextInput } from './FormTextInput'; +import { FormNumberInput } from './FormNumberInput'; export function FormInput(props: { form: UseFormReturn; @@ -15,7 +15,7 @@ export function FormInput(props: { switch (props.type) { case 'TEXT': return ( - (props: { ); case 'TEXTAREA': return ( - (props: { ); case 'SELECT': return ( - (props: { ); case 'NUMBER': return ( - (props: { +export function FormNumberInput(props: { form: UseFormReturn; name: Path; label: string; diff --git a/packages/berlin/src/components/form/SelectInput.tsx b/packages/berlin/src/components/form-input/FormSelectInput.tsx similarity index 94% rename from packages/berlin/src/components/form/SelectInput.tsx rename to packages/berlin/src/components/form-input/FormSelectInput.tsx index 102beaed..a3263ac2 100644 --- a/packages/berlin/src/components/form/SelectInput.tsx +++ b/packages/berlin/src/components/form-input/FormSelectInput.tsx @@ -1,7 +1,7 @@ import { Controller, FieldValues, Path, UseFormReturn } from 'react-hook-form'; import Select from '../select'; -export function SelectInput(props: { +export function FormSelectInput(props: { form: UseFormReturn; name: Path; label: string; diff --git a/packages/berlin/src/components/form/TextAreaInput.tsx b/packages/berlin/src/components/form-input/FormTextAreaInput.tsx similarity index 93% rename from packages/berlin/src/components/form/TextAreaInput.tsx rename to packages/berlin/src/components/form-input/FormTextAreaInput.tsx index e08198bc..6d02feb7 100644 --- a/packages/berlin/src/components/form/TextAreaInput.tsx +++ b/packages/berlin/src/components/form-input/FormTextAreaInput.tsx @@ -1,7 +1,7 @@ import { Controller, FieldValues, Path, UseFormReturn } from 'react-hook-form'; import Textarea from '../textarea'; -export function TextAreaInput(props: { +export function FormTextAreaInput(props: { form: UseFormReturn; name: Path; label: string; diff --git a/packages/berlin/src/components/form/TextInput.tsx b/packages/berlin/src/components/form-input/FormTextInput.tsx similarity index 94% rename from packages/berlin/src/components/form/TextInput.tsx rename to packages/berlin/src/components/form-input/FormTextInput.tsx index 9861f6e9..6a50fcda 100644 --- a/packages/berlin/src/components/form/TextInput.tsx +++ b/packages/berlin/src/components/form-input/FormTextInput.tsx @@ -1,7 +1,7 @@ import { Controller, FieldValues, Path, UseFormReturn } from 'react-hook-form'; import Input from '../input'; -export function TextInput(props: { +export function FormTextInput(props: { form: UseFormReturn; name: Path; label: string; diff --git a/packages/berlin/src/components/form-input/index.ts b/packages/berlin/src/components/form-input/index.ts new file mode 100644 index 00000000..5f31fbd5 --- /dev/null +++ b/packages/berlin/src/components/form-input/index.ts @@ -0,0 +1,5 @@ +export * from './FormNumberInput'; +export * from './FormSelectInput'; +export * from './FormTextAreaInput'; +export * from './FormTextInput'; +export * from './FormInput'; diff --git a/packages/berlin/src/components/form/AccountForm.tsx b/packages/berlin/src/components/form/AccountForm.tsx new file mode 100644 index 00000000..2174fe68 --- /dev/null +++ b/packages/berlin/src/components/form/AccountForm.tsx @@ -0,0 +1,131 @@ +// React and third-party libraries +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useForm } from 'react-hook-form'; +import toast from 'react-hot-toast'; + +// API Calls +import { putUser, type GetUserResponse } from 'api'; + +// Components +import Button from '../button'; +import { FlexColumn } from '../containers/FlexColumn.styled'; +import { Subtitle } from '../typography/Subtitle.styled'; + +// Store +import { FlexRowToColumn } from '../containers/FlexRowToColumn.styled'; +import { FormTextInput } from '../form-input'; +import { z } from 'zod'; +import { returnZodError } from '../../utils/zod-error-handler'; + +type InitialUser = { + username: string; + firstName: string; + lastName: string; + email: string; +}; + +export function AccountForm({ + initialUser, + user, + title, + afterSubmit, +}: { + initialUser: InitialUser; + user: GetUserResponse | null | undefined; + afterSubmit?: () => void; + title: string; +}) { + const queryClient = useQueryClient(); + + const { mutate: mutateUserData } = useMutation({ + mutationFn: putUser, + onSuccess: async (body) => { + if (!body) { + return; + } + + if ('errors' in body) { + toast.error(`There was an error: ${body.errors.join(', ')}`); + return; + } + + await queryClient.invalidateQueries({ queryKey: ['user'] }); + + toast.success('User data updated!'); + + if (afterSubmit) { + afterSubmit(); + } + }, + onError: () => { + toast.error('There was an error, please try again.'); + }, + }); + + const form = useForm({ + defaultValues: initialUser, + mode: 'all', + }); + + const { + handleSubmit, + formState: { isValid, isSubmitting }, + } = form; + + const onSubmit = async (value: typeof initialUser) => { + if (isValid && user && user.id) { + await mutateUserData({ + userId: user.id, + username: value.username, + email: value.email, + firstName: value.firstName, + lastName: value.lastName, + serverUrl: import.meta.env.VITE_SERVER_URL, + }); + } + }; + + return ( + + {title} +
+ + + + + + + returnZodError(() => z.string().email().parse(value))} + /> + + +
+
+ ); +} diff --git a/packages/berlin/src/components/research-group-form/ResearchGroupForm.tsx b/packages/berlin/src/components/form/ResearchGroupForm.tsx similarity index 92% rename from packages/berlin/src/components/research-group-form/ResearchGroupForm.tsx rename to packages/berlin/src/components/form/ResearchGroupForm.tsx index 8591cf20..32272121 100644 --- a/packages/berlin/src/components/research-group-form/ResearchGroupForm.tsx +++ b/packages/berlin/src/components/form/ResearchGroupForm.tsx @@ -21,7 +21,11 @@ type ResearchGroupFormProps = { setGroupName: React.Dispatch>; }; -function ResearchGroupForm({ formData, handleCreateGroup, setGroupName }: ResearchGroupFormProps) { +export function ResearchGroupForm({ + formData, + handleCreateGroup, + setGroupName, +}: ResearchGroupFormProps) { const researchGroupSchema = z.object({ name: z.string().min(2, { message: formData.input.requiredMessage }), }); @@ -61,5 +65,3 @@ function ResearchGroupForm({ formData, handleCreateGroup, setGroupName }: Resear ); } - -export default ResearchGroupForm; diff --git a/packages/berlin/src/components/form/index.ts b/packages/berlin/src/components/form/index.ts index 038dc54e..42c7ef9c 100644 --- a/packages/berlin/src/components/form/index.ts +++ b/packages/berlin/src/components/form/index.ts @@ -1,5 +1,2 @@ -export * from './NumberInput'; -export * from './SelectInput'; -export * from './TextAreaInput'; -export * from './TextInput'; -export * from './FormInput'; +export * from './AccountForm'; +export * from './ResearchGroupForm'; diff --git a/packages/berlin/src/components/header/Header.styled.tsx b/packages/berlin/src/components/header/Header.styled.tsx deleted file mode 100644 index 36e34cce..00000000 --- a/packages/berlin/src/components/header/Header.styled.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import styled, { keyframes } from 'styled-components'; -import Button from '../button/Button'; - -export const SyledHeader = styled.header` - align-items: center; - display: flex; - justify-content: center; - min-height: 10rem; - padding-block: 2rem; -`; - -export const HeaderContainer = styled.div` - align-items: center; - background-color: var(--color-white); - display: flex; - gap: 1rem; - justify-content: space-between; - margin-inline: auto; - width: min(90%, 1080px); -`; - -export const LogoContainer = styled.div` - align-items: center; - cursor: pointer; - display: flex; - gap: 0.75rem; -`; - -export const LogoImage = styled.img` - height: 3.5rem; - max-width: 3.5rem; - - @media (min-width: 430px) { - height: 5rem; - max-width: 5rem; - } -`; - -export const LogoTextContainer = styled.div` - display: flex; - flex-direction: column; - gap: 0.5rem; -`; - -export const LogoTitle = styled.h1` - font-size: 1.5rem; - line-height: 1.65rem; - - @media (min-width: 430px) { - display: block; - font-family: var(--font-family-title); - font-size: 1.75rem; - font-weight: 600; - line-height: 1.75rem; - } - - @media (min-width: 600px) { - max-width: none; - } -`; - -export const LogoSubtitle = styled.h2` - display: block; - font-family: var(--font-family-body); - font-size: 0.65rem; - font-style: italic; - font-weight: 600; - line-height: 0.75rem; - max-width: 360px; - @media (min-width: 600px) { - font-size: 1rem; - } -`; - -export const NavContainer = styled.nav` - align-items: center; - display: flex; -`; - -export const NavButtons = styled.ul` - display: flex; - gap: 0.75rem; - list-style: none; -`; - -export const DesktopButtons = styled.div` - display: none; - @media (min-width: 1080px) { - display: flex; - gap: 0.75rem; - } -`; - -export const MobileButtons = styled.div` - display: flex; - flex-direction: column; - gap: 1.5rem; - height: 100%; - text-align: center; - @media (min-width: 1080px) { - display: none; - } -`; - -export const ThemeButton = styled(Button)` - img { - min-width: 20px; - } -`; - -export const MenuButton = styled.li` - align-items: center; - cursor: pointer; - display: flex; - flex-direction: column; - height: 2.25rem; - justify-content: center; - width: 2.25rem; - - @media (min-width: 1080px) { - display: none; - } -`; - -export const Bar = styled.div<{ $isOpen: boolean }>` - background-color: var(--color-black); - border-radius: 8px; - height: 3px; - margin: 2px 0; - transition: 0.4s; - width: 27px; - - &:first-child { - transform: ${({ $isOpen }) => ($isOpen ? 'rotate(-45deg) translateY(10px)' : '')}; - } - - &:nth-child(2) { - opacity: ${({ $isOpen }) => ($isOpen ? '0' : '1')}; - transition: 0.2s; - } - - &:nth-child(3) { - transform: ${({ $isOpen }) => ($isOpen ? 'rotate(45deg) translateY(-10px)' : '')}; - } -`; - -const fadeIn = keyframes` - from { - opacity: 0; - } - to { - opacity: 1; - } -`; - -export const BurgerMenuContainer = styled.nav<{ $$isOpen: boolean }>` - align-items: center; - background-color: var(--color-white); - bottom: 0; - display: flex; - height: calc(100% - 160px); - justify-content: center; - left: 0; - position: fixed; - width: 100%; - z-index: 999; - - display: ${(props) => (props.$$isOpen ? 'flex' : 'none')}; - animation: ${fadeIn} 0.3s ease-out; -`; diff --git a/packages/berlin/src/components/header/Header.tsx b/packages/berlin/src/components/header/Header.tsx index e532001d..85ac3353 100644 --- a/packages/berlin/src/components/header/Header.tsx +++ b/packages/berlin/src/components/header/Header.tsx @@ -1,199 +1,195 @@ -// React and third-party libraries -import { useState } from 'react'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { - // useLocation, - useNavigate, -} from 'react-router-dom'; -import { SunMoon, User } from 'lucide-react'; - -// Store -import { useAppStore } from '../../store'; - -// Data -import header from '../../data/header'; - -// API -import { fetchAlerts, fetchEvents, fetchUserRegistrations, logout } from 'api'; - -// Hooks -import useUser from '../../hooks/useUser'; + NavigationMenu, + NavigationMenuContent, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, +} from '@/_components/ui/navigation-menu'; +import useUser from '@/hooks/useUser'; +import { useAppStore } from '@/store'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { fetchAlerts, fetchEvents, fetchUserRegistrations, GetUserResponse, logout } from 'api'; +import { Menu, User } from 'lucide-react'; +import { useMemo, useState } from 'react'; +import { NavLink } from 'react-router-dom'; +import Icon from '../icon'; +import ThemeToggler from '../theme-toggler'; +import { useNavigate } from 'react-router-dom'; +import ZupassLoginButton from '../zupass-button'; -// Components -import Button from '../button'; -import NavButton from '../nav-button'; -import ZupassLoginButton from '../zupass-button/ZupassLoginButton'; +export default function NewHeader() { + const theme = useAppStore((state) => state.theme); + const { user } = useUser(); + const [isMenuOpen, setIsMenuOpen] = useState(false); -// Styled components -import { - Bar, - BurgerMenuContainer, - DesktopButtons, - HeaderContainer, - LogoContainer, - LogoSubtitle, - LogoTextContainer, - LogoTitle, - MenuButton, - MobileButtons, - NavButtons, - NavContainer, - SyledHeader, -} from './Header.styled'; + return ( +
+ {isMenuOpen && ( + setIsMenuOpen(!isMenuOpen)} + > + + {user && } + + + + )} +
+
+ Lexicon Logo +

Lexicon

+
+ + + {user ? ( + <> +
+ + +
+
+ setIsMenuOpen(!isMenuOpen)}> + + +
+ + ) : ( + Login + )} + + + + + +
+
+
+
+ ); +} -function Header() { - const queryClient = useQueryClient(); - const { user } = useUser(); - const theme = useAppStore((state) => state.theme); - const toggleTheme = useAppStore((state) => state.toggleTheme); - const navigate = useNavigate(); - const resetState = useAppStore((state) => state.reset); - const { mutate: mutateLogout } = useMutation({ - mutationFn: logout, - onSuccess: async () => { - resetState(); - await queryClient.invalidateQueries(); - await queryClient.removeQueries(); - navigate('/'); - }, +const HeaderLinks = ({ user }: { user: GetUserResponse }) => { + const { data: events } = useQuery({ + queryKey: ['events'], + queryFn: () => fetchEvents({ serverUrl: import.meta.env.VITE_SERVER_URL }), + enabled: !!user, }); const { data: registrationsData } = useQuery({ queryKey: [user?.id, 'registrations'], - queryFn: () => fetchUserRegistrations(user?.id ?? ''), + queryFn: () => + fetchUserRegistrations({ + userId: user?.id ?? '', + serverUrl: import.meta.env.VITE_SERVER_URL, + }), enabled: !!user, }); const { data: alerts } = useQuery({ queryKey: ['alerts'], - queryFn: () => fetchAlerts(), + queryFn: () => fetchAlerts({ serverUrl: import.meta.env.VITE_SERVER_URL }), enabled: !!user, - refetchInterval: 10000, // Poll every 10 seconds + refetchInterval: 10000, }); - const { data: events } = useQuery({ - queryKey: ['events'], - queryFn: () => fetchEvents(), - enabled: !!user, - }); + const links = useMemo(() => { + const baseLinks = [ + { + title: 'My Proposals', + link: events ? `/events/${events?.[0].id}/register` : '', + }, + { + title: 'Agenda', + link: events ? `/events/${events?.[0].id}/cycles` : '', + }, + ]; - const [isBurgerMenuOpen, setIsBurgerMenuOpen] = useState(false); + if ( + registrationsData?.some((registration) => registration.status === 'APPROVED') && + alerts && + alerts.length > 0 + ) { + const alertsLinks = alerts.map((alert) => ({ + title: alert.title, + link: alert.link || '', + })); + return [...baseLinks, ...alertsLinks]; + } - return ( - - - navigate('/')}> - Lexicon Logo - - {header.title} - {header.subtitle} - - - - - - {user ? ( - <> - {registrationsData?.some( - (registration) => registration.status === 'APPROVED', - ) && ( - <> - {alerts && - alerts.length > 0 && - alerts - // newest alerts first - .sort( - (a, b) => - new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(), - ) - ?.map((alert) => { - return ( - alert.link && - alert.title && ( - - {alert.title} - - ) - ); - })} - - My proposals - - - Agenda - - - )} - - - - ) : ( - Login with Zupass - )} - - setIsBurgerMenuOpen(!isBurgerMenuOpen)}> - - - - -
  • - -
  • -
    -
    - setIsBurgerMenuOpen(false)}> - - - {user ? ( - <> - {registrationsData?.some( - (registration) => registration.status === 'APPROVED', - ) && ( - <> - {alerts && - alerts.length > 0 && - alerts?.map((alert) => { - return ( - alert.link && - alert.title && ( - - {alert.title} - - ) - ); - })} - - My proposals - - - Agenda - - - )} + return baseLinks; + }, [events, registrationsData, alerts]); + + return links.map(({ title, link }) => ( + + + + {title} + + + + )); +}; - - Account - - - - ) : ( - Login with Zupass - )} - - - -
    -
    +const UserMenu = () => { + return ( + + + + + + + + + + + + ); -} +}; + +const UserMenuLinks = () => { + const queryClient = useQueryClient(); + const resetState = useAppStore((state) => state.reset); + const navigate = useNavigate(); + + const { mutate: mutateLogout } = useMutation({ + mutationFn: logout, + onSuccess: async () => { + resetState(); + await queryClient.invalidateQueries(); + await queryClient.removeQueries(); + navigate('/'); + }, + }); + const links = useMemo(() => { + return [ + { + title: 'Account', + link: '/account', + }, + { + title: 'Log out', + onClick: () => mutateLogout({ serverUrl: import.meta.env.VITE_SERVER_URL }), + }, + ]; + }, [mutateLogout]); -export default Header; + return links.map(({ title, link, onClick }) => ( + + + + {title} + + + + )); +}; diff --git a/packages/berlin/src/components/icon/Icon.tsx b/packages/berlin/src/components/icon/Icon.tsx index ba3d553c..5c21c6d1 100644 --- a/packages/berlin/src/components/icon/Icon.tsx +++ b/packages/berlin/src/components/icon/Icon.tsx @@ -3,10 +3,13 @@ import { StyledIcon } from './Icon.styled'; type IconProps = { children: React.ReactNode; + onClick?: () => void; }; -const Icon = forwardRef(({ children }, ref) => ( - {children} +const Icon = forwardRef(({ children, onClick }, ref) => ( + + {children} + )); export default Icon; diff --git a/packages/berlin/src/components/onboarding/Onboarding.tsx b/packages/berlin/src/components/onboarding/Onboarding.tsx index 33c9c808..5df2a97c 100644 --- a/packages/berlin/src/components/onboarding/Onboarding.tsx +++ b/packages/berlin/src/components/onboarding/Onboarding.tsx @@ -70,4 +70,9 @@ function Onboarding({ steps, type }: OnboardingProps) { ); } +function OnboardingCard({ children }: { children: ReactNode }) { + return
    {children}
    ; +} + +export { OnboardingCard }; export default Onboarding; diff --git a/packages/berlin/src/components/onboarding/Steps.tsx b/packages/berlin/src/components/onboarding/Steps.tsx new file mode 100644 index 00000000..1f524160 --- /dev/null +++ b/packages/berlin/src/components/onboarding/Steps.tsx @@ -0,0 +1,215 @@ +import { Body } from '../typography/Body.styled'; +import { FlexColumn } from '../containers/FlexColumn.styled'; +import { FlexRow } from '../containers/FlexRow.styled'; +import { Heart, Radical } from 'lucide-react'; +import { OnboardingCard } from './Onboarding'; +import { ReactNode } from 'react'; +import { Subtitle } from '../typography/Subtitle.styled'; +import Icon from '../icon'; +import IconButton from '../icon-button'; +import { useAppStore } from '@/store'; + +export const eventSteps = [ + createStep({ + target: 'event', + placement: 'center', + title: 'Welcome', + children: ( + <> + Welcome to our tool! + Would you like to take a tour to see how it works? + + ), + }), + createStep({ + target: 'cycles', + title: 'Open Votes', + children: Explore current vote items, the vote deadline, and cast your vote., + }), + createStep({ + target: 'tabs', + title: 'Closed Votes', + children: Review past votes and see the results., + }), +]; + +export const cycleSteps = [ + createStep({ + target: 'welcome', + title: 'Voting Page', + placement: 'center', + children: View vote items and allocate your hearts., + }), + createStep({ + target: 'votes', + title: 'Vote', + + children: ( + + + + + + Upvote or downvote a vote item. + + ), + }), + createStep({ + target: 'save', + title: 'Save Your Votes', + children: You must click this button or your votes will not be recorded., + }), + createStep({ + target: 'hearts', + title: 'Information', + children: ( + <> + View vote item. + + + + + Current number of hearts allocated to this vote item. + + + ), + }), + createStep({ + target: 'plurality', + title: 'Voting Mechanisms', + placement: 'center', + children: ( + + + + Plurality score, unlike quadratic score, considers pre-existing participant relationships + + + ), + }), + createStep({ + target: 'expand', + title: 'Expand a vote item', + + children: ( + + + Click to view the vote item description and other useful information. + + ), + }), +]; + +export const resultsSteps = [ + createStep({ + target: 'welcome', + placement: 'center', + title: 'Results Page', + children: See community decisions., + }), + createStep({ + target: 'icons', + placement: 'center', + title: 'Icons', + children: ( + <> + + + + + Quadratic score + + + + + + Hearts received by a vote item + + + + Plurality score + + + ), + }), + createStep({ + target: 'expand', + placement: 'center', + title: 'Expand a vote item', + children: ( + <> + + + + Clicking this icon will display the vote item description and other useful information. + + + + ), + }), +]; + +type StepProps = { + target: string; + placement?: string; + title: string; + children: ReactNode; +}; + +function createStep({ target, placement, title, children }: StepProps) { + return { + target: `.${target}`, // Prefix the target with a dot to use it as a className selector + placement, + content: ( + + {title} + {children} + + ), + }; +} diff --git a/packages/berlin/src/components/option-card/OptionCard.styled.tsx b/packages/berlin/src/components/option-card/OptionCard.styled.tsx deleted file mode 100644 index 5a48ff92..00000000 --- a/packages/berlin/src/components/option-card/OptionCard.styled.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import styled, { css } from 'styled-components'; -import { FlexRow } from '../containers/FlexRow.styled'; -import { FlexRowToColumn } from '../containers/FlexRowToColumn.styled'; -import { FlexColumn } from '../containers/FlexColumn.styled'; -import { Bold } from '../typography/Bold.styled'; - -export const Card = styled.div<{ $expanded: boolean }>` - border-radius: 0.5rem; - border: 1px solid var(--color-black); - display: flex; - flex-direction: column; - gap: 1rem; - margin-block: 0.5rem; - padding: 2rem; - width: 100%; - - @media (min-width: 600px) { - margin: 0; - border: none; - border-radius: 0; - border-bottom: 1px solid var(--color-black); - flex-direction: row; - gap: 0; - padding: 0; - } - - .description { - display: ${(props) => (props.$expanded ? 'flex' : 'none')}; - @media (min-width: 600px) { - padding: 1.5rem; - padding-top: 0; - } - } -`; - -export const Container = styled(FlexRowToColumn)` - gap: 1rem; - @media (min-width: 600px) { - gap: 0; - } -`; - -export const Proposal = styled(FlexRow)` - @media (min-width: 600px) { - flex: 1; - min-width: 11rem; - padding: 1.5rem; - } -`; - -export const Author = styled(FlexRow)` - gap: 0.25rem; - @media (min-width: 600px) { - display: flex; - max-width: 10rem; - min-width: 8rem; - padding: 1.5rem; - } -`; - -export const Affiliation = styled(FlexRow)` - gap: 0.25rem; - @media (min-width: 600px) { - display: flex; - max-width: 10rem; - min-width: 8rem; - padding: 1.5rem; - } -`; - -export const Votes = styled(FlexRow)<{ $showScore: boolean | undefined }>` - @media (min-width: 600px) { - gap: 0.5rem; - max-width: 5rem; - padding: 1.5rem; - } - - ${(props) => - !props.$showScore && - css` - border-bottom: 1px solid var(--color-black); - padding-bottom: 0.75rem; - @media (min-width: 600px) { - border: none; - padding: 1.5rem; - } - `} -`; -export const VotesIcon = styled(FlexColumn)` - width: 1.25rem; - :first-child { - height: 1.25rem; - width: 1.25rem; - } - @media (min-width: 600px) { - width: 1.5rem; - :first-child { - height: 1rem; - width: 1rem; - } - } -`; - -export const Plurality = styled(FlexRow)` - border-bottom: 1px solid var(--color-black); - padding-bottom: 0.75rem; - - @media (min-width: 600px) { - border: none; - gap: 0.5rem; - max-width: 5rem; - padding-bottom: 0; - padding: 1.5rem; - } -`; - -export const PluralityIcon = styled.div` - display: block; - @media (min-width: 600px) { - display: none; - } -`; - -export const ArrowIcon = styled(PluralityIcon)` - margin-left: auto; -`; - -export const Field = styled(Bold)` - display: inline; - font-size: 1.125rem; - @media (min-width: 600px) { - display: none; - } -`; - -export const ArrowDownIcon = styled.div` - display: none; - @media (min-width: 600px) { - display: block; - } -`; diff --git a/packages/berlin/src/components/option-card/OptionCard.tsx b/packages/berlin/src/components/option-card/OptionCard.tsx deleted file mode 100644 index b1ecb8d1..00000000 --- a/packages/berlin/src/components/option-card/OptionCard.tsx +++ /dev/null @@ -1,196 +0,0 @@ -// React and third-party libraries -import { MessageSquareText } from 'lucide-react'; -import { useMemo, useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { useQuery } from '@tanstack/react-query'; -import Markdown from 'react-markdown'; - -// Store -import { useAppStore } from '../../store'; - -// API -import { fetchOptionUsers, GetCycleResponse } from 'api'; - -// Components -import { Body } from '../typography/Body.styled'; -import { Bold } from '../typography/Bold.styled'; -import { FlexColumn } from '../containers/FlexColumn.styled'; -import IconButton from '../icon-button'; -import Link from '../link'; -import Icon from '../icon'; - -// Styled Components -import { - Affiliation, - ArrowDownIcon, - ArrowIcon, - Author, - Card, - Container, - Field, - Plurality, - PluralityIcon, - Proposal, - Votes, - VotesIcon, -} from './OptionCard.styled'; - -type OptionCardProps = { - option: GetCycleResponse['forumQuestions'][number]['questionOptions'][number]; - showFundingRequest?: boolean; - showScore?: boolean; - numOfVotes: number; - onVote: () => void; - onUnVote: () => void; -}; - -function OptionCard({ - option, - numOfVotes, - onVote, - onUnVote, - showFundingRequest = false, - showScore, -}: OptionCardProps) { - const { eventId, cycleId } = useParams(); - const theme = useAppStore((state) => state.theme); - const navigate = useNavigate(); - const { data: optionUsers } = useQuery({ - queryKey: ['option', option.id, 'users'], - queryFn: () => fetchOptionUsers(option.id || ''), - enabled: !!option.id, - }); - - const formattedPluralityScore = useMemo(() => { - const score = parseFloat(String(option.voteScore)); - return score % 1 === 0 ? score.toFixed(0) : score.toFixed(1); - }, [option.voteScore]); - - const [expanded, setExpanded] = useState(false); - - const author = option.user ? `${option.user?.firstName} ${option.user?.lastName}` : 'Anonymous'; - - const handleCommentsClick = () => { - navigate(`/events/${eventId}/cycles/${cycleId}/options/${option.id}`); - }; - - const coauthors = useMemo(() => { - return optionUsers?.group?.users?.filter( - (optionUser) => optionUser.username !== option.user?.username, - ); - }, [optionUsers, option.user?.username]); - - return ( - - - - - - setExpanded((e) => !e)} - icon={{ src: `/icons/arrow-down-${theme}.svg`, alt: 'Arrow down' }} - $flipVertical={expanded} - /> - - {option.optionTitle} - - - Lead: - {author} - - - Affiliation: - - {option.user?.groups?.find((group) => group.groupCategory?.required)?.name ?? - 'No affiliation'} - - - - - - - - {numOfVotes} - {!showScore && ( - - setExpanded((e) => !e)} - icon={{ src: `/icons/arrow-down-${theme}.svg`, alt: 'Arrow down' }} - $flipVertical={expanded} - /> - - )} - - {showScore && ( - - - - - {formattedPluralityScore} - - setExpanded((e) => !e)} - icon={{ src: `/icons/arrow-down-${theme}.svg`, alt: 'Arrow down' }} - $flipVertical={expanded} - /> - - - )} - - - {coauthors && coauthors.length > 0 && ( - - Co-authors:{' '} - {coauthors.map((coauthor) => `${coauthor.firstName} ${coauthor.lastName}`).join(', ')} - - )} - {showFundingRequest && option.fundingRequest && ( - - Funding request: {option.fundingRequest} - - )} - {option.optionSubTitle && ( - {props.children}, - p: ({ node, ...props }) => {props.children}, - }} - > - {option.optionSubTitle} - - )} - - - - - - - ); -} - -export default OptionCard; diff --git a/packages/berlin/src/components/option-card/index.ts b/packages/berlin/src/components/option-card/index.ts deleted file mode 100644 index 271290b3..00000000 --- a/packages/berlin/src/components/option-card/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './OptionCard'; diff --git a/packages/berlin/src/components/option/Option.tsx b/packages/berlin/src/components/option/Option.tsx new file mode 100644 index 00000000..cacd7ead --- /dev/null +++ b/packages/berlin/src/components/option/Option.tsx @@ -0,0 +1,166 @@ +// React and third-party libraries +import { useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; +import Markdown from 'react-markdown'; + +// API +import { fetchOptionUsers, GetCycleResponse } from 'api'; + +// Components +import { Body } from '../typography/Body.styled'; +import { Bold } from '../typography/Bold.styled'; +import { ChevronDown, MessageSquareText, Minus, Plus } from 'lucide-react'; +import Button from '../button'; +import Link from '../link'; + +type OptionProps = { + option: GetCycleResponse['questions'][number]['options'][number]; + showFundingRequest?: boolean; + showScore?: boolean; + numOfVotes: number; + onVote: () => void; + onUnVote: () => void; +}; + +export default function Option({ + option, + showFundingRequest, + showScore, + numOfVotes, + onVote, + onUnVote, +}: OptionProps) { + const [isExpanded, setIsExpanded] = useState(false); + const [expandedHeight, setExpandedHeight] = useState(0); + const expandedRef = useRef(null); + const { eventId, cycleId } = useParams(); + const navigate = useNavigate(); + + const { data: optionUsers } = useQuery({ + queryKey: ['option', option.id, 'users'], + queryFn: () => + fetchOptionUsers({ optionId: option.id || '', serverUrl: import.meta.env.VITE_SERVER_URL }), + enabled: !!option.id, + }); + + useLayoutEffect(() => { + if (expandedRef.current) { + setExpandedHeight(isExpanded ? expandedRef.current.scrollHeight : 0); + } + }, [isExpanded]); + + const handleChevronClick = () => { + setIsExpanded(!isExpanded); + }; + + const handleCommentsClick = () => { + navigate(`/events/${eventId}/cycles/${cycleId}/options/${option.id}`); + }; + + const pluralityScore = useMemo(() => { + const score = parseFloat(String(option.voteScore)); + return score % 1 === 0 ? score.toFixed(0) : score.toFixed(1); + }, [option.voteScore]); + + const author = useMemo(() => { + if (option.user) { + return `${option.user.firstName} ${option.user.lastName}`; + } + return null; + }, [option.user]); + + const affiliation = useMemo(() => { + return option.user?.groups?.find((group) => group.groupCategory?.required)?.name || null; + }, [option.user]); + + const coauthors = useMemo(() => { + return ( + optionUsers?.group?.users?.filter( + (optionUser) => optionUser.username !== option.user?.username, + ) || [] + ); + }, [optionUsers, option.user?.username]); + + const fundingRequest = useMemo(() => { + if (showFundingRequest) { + return option.fundingRequest; + } + return null; + }, [option.fundingRequest, showFundingRequest]); + + return ( +
    +
    + {option.title} + {author && ( + + Creator: + {author} + + )} + {affiliation && ( + + Affiliation: + {affiliation} {/* should render `no affiliation` if its not found? */} + + )} + {showScore && ( + + + {pluralityScore} + + )} +
    +
    +
    + + {numOfVotes} + +
    +
    + +
    +
    +
    + {coauthors.length > 0 && ( + + Co-authors:{' '} + {coauthors.map((coauthor) => `${coauthor.firstName} ${coauthor.lastName}`).join(', ')} + + )} + {fundingRequest && ( + + Funding request: + {fundingRequest} + + )} + {option.subTitle && ( + {props.children}, + p: ({ node, ...props }) => {props.children}, + }} + > + {option.subTitle} + + )} + +
    +
    + ); +} diff --git a/packages/berlin/src/components/option/index.ts b/packages/berlin/src/components/option/index.ts new file mode 100644 index 00000000..0d128782 --- /dev/null +++ b/packages/berlin/src/components/option/index.ts @@ -0,0 +1 @@ +export { default } from './Option'; diff --git a/packages/berlin/src/components/research-group-form/index.ts b/packages/berlin/src/components/research-group-form/index.ts deleted file mode 100644 index 4b4c949a..00000000 --- a/packages/berlin/src/components/research-group-form/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './ResearchGroupForm'; diff --git a/packages/berlin/src/components/tables/comment-table/CommentsTable.tsx b/packages/berlin/src/components/tables/comment-table/CommentsTable.tsx index 4a5801f0..eae28a52 100644 --- a/packages/berlin/src/components/tables/comment-table/CommentsTable.tsx +++ b/packages/berlin/src/components/tables/comment-table/CommentsTable.tsx @@ -39,7 +39,8 @@ function CommentsTable({ comment }: CommentsTableProps) { const { data: commentLikes } = useQuery({ queryKey: ['commentLikes', comment.id], - queryFn: () => fetchCommentLikes({ commentId: comment.id }), + queryFn: () => + fetchCommentLikes({ commentId: comment.id, serverUrl: import.meta.env.VITE_SERVER_URL }), enabled: !!comment.id, refetchInterval: 5000, // Poll every 5 seconds }); @@ -83,15 +84,15 @@ function CommentsTable({ comment }: CommentsTableProps) { const handleLikeClick = () => { if (isCommentLiked) { - deleteLikeMutation({ commentId: comment.id }); + deleteLikeMutation({ commentId: comment.id, serverUrl: import.meta.env.VITE_SERVER_URL }); } else { - postLikeMutation({ commentId: comment.id }); + postLikeMutation({ commentId: comment.id, serverUrl: import.meta.env.VITE_SERVER_URL }); } }; const handleTrashClick = () => { if (optionId) { - deleteCommentMutation({ commentId: comment.id }); + deleteCommentMutation({ commentId: comment.id, serverUrl: import.meta.env.VITE_SERVER_URL }); } }; diff --git a/packages/berlin/src/components/tables/cycle-table/CycleTable.tsx b/packages/berlin/src/components/tables/cycle-table/CycleTable.tsx index a3d12f81..2d8aa9a1 100644 --- a/packages/berlin/src/components/tables/cycle-table/CycleTable.tsx +++ b/packages/berlin/src/components/tables/cycle-table/CycleTable.tsx @@ -34,7 +34,7 @@ function CycleTable({ cycle }: CycleTableProps) { return ( - {cycle.forumQuestions[0].questionTitle} + {cycle.questions[0].questionTitle} {formattedDateText()} {formattedDate} diff --git a/packages/berlin/src/components/tables/groups-table/GroupsTable.tsx b/packages/berlin/src/components/tables/groups-table/GroupsTable.tsx index 64edd8eb..e3611864 100644 --- a/packages/berlin/src/components/tables/groups-table/GroupsTable.tsx +++ b/packages/berlin/src/components/tables/groups-table/GroupsTable.tsx @@ -38,13 +38,21 @@ function GroupCard({ userToGroup, theme, onLeaveGroup }: GroupCardProps) { const { data: groupMembers } = useQuery({ queryKey: ['group', userToGroup.group.id, 'users-to-groups'], - queryFn: () => fetchGroupMembers(userToGroup.group.id), + queryFn: () => + fetchGroupMembers({ + groupId: userToGroup.group.id, + serverUrl: import.meta.env.VITE_SERVER_URL, + }), enabled: !!userToGroup.group.id, }); const { data: groupRegistrations } = useQuery({ queryKey: ['group', userToGroup.group.id, 'group-registrations'], - queryFn: () => fetchGroupRegistrations(userToGroup.group.id || ''), + queryFn: () => + fetchGroupRegistrations({ + groupId: userToGroup.group.id || '', + serverUrl: import.meta.env.VITE_SERVER_URL, + }), enabled: !!userToGroup.group.id, }); @@ -157,7 +165,9 @@ function GroupsTable({ groupsInCategory }: { groupsInCategory?: GetUsersToGroups key={userToGroup.id} userToGroup={userToGroup} theme={theme} - onLeaveGroup={(userToGroupId) => mutate({ userToGroupId })} + onLeaveGroup={(userToGroupId) => + mutate({ userToGroupId, serverUrl: import.meta.env.VITE_SERVER_URL }) + } /> )); } diff --git a/packages/berlin/src/components/tables/results-table/ResultsTable.tsx b/packages/berlin/src/components/tables/results-table/ResultsTable.tsx index 3fa0b86b..67a9012e 100644 --- a/packages/berlin/src/components/tables/results-table/ResultsTable.tsx +++ b/packages/berlin/src/components/tables/results-table/ResultsTable.tsx @@ -1,8 +1,7 @@ // React and third-party libraries -import { Heart, MessageSquareText, Radical } from 'lucide-react'; -import { useMemo } from 'react'; -import { useNavigate } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; +import { Heart, Radical } from 'lucide-react'; +import { useMemo } from 'react'; import Markdown from 'react-markdown'; // API @@ -12,13 +11,12 @@ import { fetchOptionUsers, fetchRegistrationData, fetchRegistrationFields } from import { useAppStore } from '../../../store'; // Components -import { Body } from '../../typography/Body.styled'; -import { Bold } from '../../typography/Bold.styled'; import { FlexColumn } from '../../containers/FlexColumn.styled'; import { FlexRow } from '../../containers/FlexRow.styled'; import IconButton from '../../icon-button'; +import { Body } from '../../typography/Body.styled'; +import { Bold } from '../../typography/Bold.styled'; import Link from '../../link'; -import LucideIcon from '@/components/icon'; // Styled Components import { Card, Funding, Icon, Plurality, TitleContainer } from './ResultsTable.styled'; @@ -28,7 +26,7 @@ type ResultsTableProps = { eventId?: string; cycleId?: string; option: { - optionTitle: string; + title: string; pluralityScore: string; distinctUsers: string; allocatedHearts: string; @@ -42,9 +40,8 @@ type ResultsTableProps = { onClick: () => void; }; -function ResultsTable({ $expanded, option, onClick, cycleId, eventId }: ResultsTableProps) { +function ResultsTable({ $expanded, option, onClick, eventId }: ResultsTableProps) { const theme = useAppStore((state) => state.theme); - const navigate = useNavigate(); const formattedQuadraticScore = useMemo(() => { const score = parseFloat(option.quadraticScore); return score % 1 === 0 ? score.toFixed(0) : score.toFixed(3); @@ -57,19 +54,28 @@ function ResultsTable({ $expanded, option, onClick, cycleId, eventId }: ResultsT const { data: optionUsers } = useQuery({ queryKey: ['option', option.id, 'users'], - queryFn: () => fetchOptionUsers(option.id || ''), + queryFn: () => + fetchOptionUsers({ optionId: option.id || '', serverUrl: import.meta.env.VITE_SERVER_URL }), enabled: !!option.id, }); const { data: registrationFields } = useQuery({ queryKey: ['event', eventId, 'registrations', 'fields'], - queryFn: () => fetchRegistrationFields(eventId || ''), + queryFn: () => + fetchRegistrationFields({ + eventId: eventId || '', + serverUrl: import.meta.env.VITE_SERVER_URL, + }), enabled: !!eventId, }); const { data: registrationData } = useQuery({ queryKey: ['registrations', optionUsers?.registrationId, 'registration-data'], - queryFn: () => fetchRegistrationData(optionUsers?.registrationId || ''), + queryFn: () => + fetchRegistrationData({ + registrationId: optionUsers?.registrationId || '', + serverUrl: import.meta.env.VITE_SERVER_URL, + }), enabled: !!optionUsers?.registrationId, }); @@ -89,10 +95,6 @@ function ResultsTable({ $expanded, option, onClick, cycleId, eventId }: ResultsT ) .map((user) => `${user.firstName} ${user.lastName}`); - const handleCommentsClick = () => { - navigate(`/events/${eventId}/cycles/${cycleId}/options/${option.id}`); - }; - return ( @@ -103,7 +105,7 @@ function ResultsTable({ $expanded, option, onClick, cycleId, eventId }: ResultsT $flipVertical={$expanded} onClick={onClick} /> - {option.optionTitle} + {option.title} @@ -146,7 +148,6 @@ function ResultsTable({ $expanded, option, onClick, cycleId, eventId }: ResultsT /> {option.allocatedFunding} ARB - Collaborators:{' '} {collaborators && collaborators.length > 0 ? collaborators.join(', ') : 'None'} + + Research Output: {researchOutputValue} + + + Lead Author: {optionUsers?.user?.firstName} {optionUsers?.user?.lastName} + + + Collaborators:{' '} + {collaborators && collaborators.length > 0 ? collaborators.join(', ') : 'None'} + Distinct voters: {option.distinctUsers} Voter affiliations: {option.listOfGroupNames.join(', ')} - - - ); diff --git a/packages/berlin/src/components/tabs/TabsHeader.styled.tsx b/packages/berlin/src/components/tabs/TabsHeader.styled.tsx new file mode 100644 index 00000000..ca12c1fe --- /dev/null +++ b/packages/berlin/src/components/tabs/TabsHeader.styled.tsx @@ -0,0 +1,12 @@ +import styled from 'styled-components'; +import { Body } from '../typography/Body.styled'; + +export const Tab = styled(Body)` + cursor: pointer; + text-transform: capitalize; + + &.active { + font-weight: 600; + text-decoration: underline; + } +`; diff --git a/packages/berlin/src/components/tabs/TabsHeader.tsx b/packages/berlin/src/components/tabs/TabsHeader.tsx new file mode 100644 index 00000000..1c256e88 --- /dev/null +++ b/packages/berlin/src/components/tabs/TabsHeader.tsx @@ -0,0 +1,37 @@ +import { Fragment, useState } from 'react'; +import { Body } from '../typography/Body.styled'; +import { Tab } from './TabsHeader.styled'; + +type TabsHeaderProps = { + tabNames: string[]; + initialTab?: string; + className?: string; + onTabChange?: (tab: string) => void; +}; + +export function TabsHeader({ tabNames, initialTab, className, onTabChange }: TabsHeaderProps) { + const [activeTab, setActiveTab] = useState(initialTab || tabNames[0]); + + const handleTabClick = (tab: string) => { + setActiveTab(tab); + if (onTabChange) { + onTabChange(tab); + } + }; + + return ( +
    + {tabNames.map((tabName, index) => ( + + handleTabClick(tabName)} + > + {tabName} + + {index < tabNames.length - 1 && /} + + ))} +
    + ); +} diff --git a/packages/berlin/src/components/tabs/TabsManager.tsx b/packages/berlin/src/components/tabs/TabsManager.tsx new file mode 100644 index 00000000..eba28ca8 --- /dev/null +++ b/packages/berlin/src/components/tabs/TabsManager.tsx @@ -0,0 +1,13 @@ +export function TabsManager({ + tabs, + tab, + fallback, +}: { + tabs: { + [key in T]?: React.ReactNode; + }; + tab: T; + fallback: React.ReactNode; +}) { + return tabs[tab] || fallback; +} diff --git a/packages/berlin/src/components/tabs/index.ts b/packages/berlin/src/components/tabs/index.ts new file mode 100644 index 00000000..c3d97fbe --- /dev/null +++ b/packages/berlin/src/components/tabs/index.ts @@ -0,0 +1,2 @@ +export * from './TabsHeader'; +export * from './TabsManager'; diff --git a/packages/berlin/src/components/theme-toggler/ThemeToggler.tsx b/packages/berlin/src/components/theme-toggler/ThemeToggler.tsx new file mode 100644 index 00000000..55036590 --- /dev/null +++ b/packages/berlin/src/components/theme-toggler/ThemeToggler.tsx @@ -0,0 +1,17 @@ +import { useEffect } from 'react'; +import { useAppStore } from '@/store'; +import Icon from '../icon'; +import { Moon, Sun } from 'lucide-react'; + +export default function ThemeToggler() { + const theme = useAppStore((state) => state.theme); + const toggleTheme = useAppStore((state) => state.toggleTheme); + + useEffect(() => { + const root = window.document.documentElement; + root.classList.remove('light', 'dark'); + root.classList.add(theme); + }, [theme]); + + return {theme === 'dark' ? : }; +} diff --git a/packages/berlin/src/components/theme-toggler/index.ts b/packages/berlin/src/components/theme-toggler/index.ts new file mode 100644 index 00000000..a9f59a23 --- /dev/null +++ b/packages/berlin/src/components/theme-toggler/index.ts @@ -0,0 +1 @@ +export { default } from './ThemeToggler'; diff --git a/packages/berlin/src/components/zupass-button/ZupassLoginButton.tsx b/packages/berlin/src/components/zupass-button/ZupassLoginButton.tsx index 642240a5..e7b6f735 100644 --- a/packages/berlin/src/components/zupass-button/ZupassLoginButton.tsx +++ b/packages/berlin/src/components/zupass-button/ZupassLoginButton.tsx @@ -8,7 +8,7 @@ import { useZupassPopupMessages, } from '@pcd/passport-interface'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { postPcdStr } from 'api'; +import { postVerify } from 'api'; import { useEffect, useState } from 'react'; import Button from '../button'; import { useNavigate } from 'react-router-dom'; @@ -25,7 +25,7 @@ function ZupassLoginButton({ children, $variant, ...props }: ZupassLoginButtonPr const queryClient = useQueryClient(); const { mutate: mutateVerify } = useMutation({ - mutationFn: postPcdStr, + mutationFn: postVerify, onSuccess: (body) => { if (body) { queryClient.invalidateQueries({ queryKey: ['user'] }); @@ -67,6 +67,7 @@ function ZupassLoginButton({ children, $variant, ...props }: ZupassLoginButtonPr email: user.value.email, uuid: user.value.uuid, pcdStr: JSON.parse(pcdStr).pcd, + serverUrl: import.meta.env.VITE_SERVER_URL, }); } }); @@ -80,7 +81,12 @@ function ZupassLoginButton({ children, $variant, ...props }: ZupassLoginButtonPr return ( <> - diff --git a/packages/berlin/src/global.styled.tsx b/packages/berlin/src/global.styled.tsx index dc7a8263..c9b40c50 100644 --- a/packages/berlin/src/global.styled.tsx +++ b/packages/berlin/src/global.styled.tsx @@ -11,6 +11,8 @@ export const GlobalStyle = createGlobalStyle` } :root { + --color-primary: '#222'; + --color-secondary: '#fff'; --color-white: ${(props) => props.theme.backgroundColor}; --color-black: ${(props) => props.theme.textColor}; --color-gray: ${(props) => props.theme.gray}; diff --git a/packages/berlin/src/hooks/useUser.tsx b/packages/berlin/src/hooks/useUser.tsx index 28f17cef..f070e612 100644 --- a/packages/berlin/src/hooks/useUser.tsx +++ b/packages/berlin/src/hooks/useUser.tsx @@ -8,7 +8,7 @@ function useUser() { isError, } = useQuery({ queryKey: ['user'], - queryFn: fetchUser, + queryFn: () => fetchUser({ serverUrl: import.meta.env.VITE_SERVER_URL }), }); return { user, isLoading, isError }; diff --git a/packages/berlin/src/index.css b/packages/berlin/src/index.css index 8abdb15c..c6b2a343 100644 --- a/packages/berlin/src/index.css +++ b/packages/berlin/src/index.css @@ -13,10 +13,10 @@ --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; - --primary: 222.2 47.4% 11.2%; + --primary: 0 0% 100%; --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; + --secondary: 0 0% 13%; --secondary-foreground: 222.2 47.4% 11.2%; --muted: 210 40% 96.1%; @@ -45,10 +45,10 @@ --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; - --primary: 210 40% 98%; + --primary: 0 0% 13%; --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; + --secondary: 0 0% 100%; --secondary-foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; diff --git a/packages/berlin/src/layout/Layout.styled.tsx b/packages/berlin/src/layout/Layout.styled.tsx index afe51b6f..a428a000 100644 --- a/packages/berlin/src/layout/Layout.styled.tsx +++ b/packages/berlin/src/layout/Layout.styled.tsx @@ -2,7 +2,7 @@ import styled from 'styled-components'; export const Main = styled.main` margin-inline: auto; - min-height: calc(100vh - 17.5rem); + min-height: calc(100vh - 9.375rem); padding-block: 4rem; width: min(90%, 1080px); `; diff --git a/packages/berlin/src/layout/Layout.tsx b/packages/berlin/src/layout/Layout.tsx index 9824e5e4..c4f86618 100644 --- a/packages/berlin/src/layout/Layout.tsx +++ b/packages/berlin/src/layout/Layout.tsx @@ -6,8 +6,8 @@ import { Outlet } from 'react-router-dom'; import { Main } from './Layout.styled'; // Components -import Header from '../components/header'; import Footer from '../components/footer'; +import Header from '@/components/header'; function Layout() { return ( diff --git a/packages/berlin/src/pages/Account.tsx b/packages/berlin/src/pages/Account.tsx index 47ea2897..782a522d 100644 --- a/packages/berlin/src/pages/Account.tsx +++ b/packages/berlin/src/pages/Account.tsx @@ -1,159 +1,159 @@ -// React and third-party libraries -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { useForm } from 'react-hook-form'; -import toast from 'react-hot-toast'; -import { useNavigate } from 'react-router-dom'; - -// API Calls -import { GetEventsResponse, fetchEvents, putUser, type GetUserResponse } from 'api'; - -// Components -import Button from '../components/button'; -import { FlexColumn } from '../components/containers/FlexColumn.styled'; -import Input from '../components/input'; -import { Subtitle } from '../components/typography/Subtitle.styled'; +import { useState } from 'react'; +import { AccountForm } from '../components/form/AccountForm'; import { Title } from '../components/typography/Title.styled'; - -// Hooks import useUser from '../hooks/useUser'; - -// Store -import { useEffect, useMemo } from 'react'; -import { FlexRowToColumn } from '../components/containers/FlexRowToColumn.styled'; - -type InitialUser = { - username: string; - firstName: string; - lastName: string; - email: string; -}; +import { FlexColumn } from '../components/containers/FlexColumn.styled'; +import { FlexRow } from '../components/containers/FlexRow.styled'; +import { TabsManager } from '../components/tabs'; +import { Edit, X } from 'lucide-react'; +import { + GetUserResponse, + fetchCycle, + fetchUserOptions, + fetchUserRegistrations, + fetchUsersToGroups, +} from 'api'; +import { Subtitle } from '../components/typography/Subtitle.styled'; +import { Separator } from '../components/separator'; +import { Body } from '../components/typography/Body.styled'; +import { useQueries, useQuery } from '@tanstack/react-query'; +import Link from '../components/link'; +import { Underline } from '../components/typography/Underline.styled'; +import { useNavigate } from 'react-router-dom'; function Account() { - const { user, isLoading: userIsLoading } = useUser(); - - const { data: events } = useQuery({ - queryKey: ['events'], - queryFn: fetchEvents, - enabled: !!user?.id, - }); - - const initialUser: InitialUser = useMemo(() => { - return { - username: user?.username || '', - firstName: user?.firstName || '', - lastName: user?.lastName || '', - email: user?.email || '', - }; - }, [user]); + const { user: initialUser, isLoading: userIsLoading } = useUser(); + const isFirstLogin = !initialUser?.username; + const [tab, setTab] = useState<'view' | 'edit'>(isFirstLogin ? 'edit' : 'view'); + const navigate = useNavigate(); if (userIsLoading) { return Loading...; } - return ; -} - -function AccountForm({ - initialUser, - user, - events, -}: { - initialUser: InitialUser; - user: GetUserResponse | null | undefined; - events: GetEventsResponse | null | undefined; -}) { - const navigate = useNavigate(); - const queryClient = useQueryClient(); - - const { mutate: mutateUserData } = useMutation({ - mutationFn: putUser, - onSuccess: async (body) => { - if (!body) { - return; - } - - if ('errors' in body) { - toast.error(`There was an error: ${body.errors.join(', ')}`); - return; - } - - await queryClient.invalidateQueries({ queryKey: ['user'] }); - await queryClient.invalidateQueries({ queryKey: ['user', user?.id, 'groups'] }); + const tabs = { + edit: ( + { + if (isFirstLogin) { + navigate('/events'); + } + + setTab('view'); + }} + /> + ), + view: , + }; - toast.success('User data updated!'); + return ( + + + {tab === 'view' ? ( + { + setTab('edit'); + }} + /> + ) : ( + { + setTab('view'); + }} + /> + )} + + Tab not found} /> + + ); +} - if (events?.length === 1) { - navigate(`/events/${events?.[0].id}/register`); - } - }, - onError: () => { - toast.error('There was an error, please try again.'); - }, +function AccountHub({ user }: { user: GetUserResponse | null | undefined }) { + const { data: registrations } = useQuery({ + queryKey: ['users', user?.id, 'registrations'], + queryFn: () => + fetchUserRegistrations({ + userId: user?.id ?? '', + serverUrl: import.meta.env.VITE_SERVER_URL, + }), }); - const { - register, - formState: { errors, isValid, isSubmitting }, - handleSubmit, - reset, - } = useForm({ - defaultValues: useMemo(() => initialUser, [initialUser]), - mode: 'all', + const { data: options } = useQuery({ + queryKey: ['users', user?.id, 'options'], + queryFn: () => + fetchUserOptions({ userId: user?.id ?? '', serverUrl: import.meta.env.VITE_SERVER_URL }), }); - useEffect(() => { - reset(initialUser); - }, [initialUser, reset]); + const { data: usersToGroups } = useQuery({ + queryKey: ['users', user?.id, 'groups'], + queryFn: () => + fetchUsersToGroups({ userId: user?.id ?? '', serverUrl: import.meta.env.VITE_SERVER_URL }), + }); - const onSubmit = async (value: typeof initialUser) => { - if (isValid && user && user.id) { - await mutateUserData({ - userId: user.id, - username: value.username, - email: value.email, - firstName: value.firstName, - lastName: value.lastName, - }); - } - }; + const cycles = useQueries({ + queries: + // can be improved by filtering repeated cycles + options?.map((option) => ({ + queryKey: ['cycles', option.question.cycleId], + queryFn: () => + fetchCycle({ + cycleId: option.question.cycleId, + serverUrl: import.meta.env.VITE_SERVER_URL, + }), + })) ?? [], + }); return ( - Complete your registration -
    +
    + + {user?.firstName} {user?.lastName} + + @{user?.username} + {user?.email} +
    + +
    + Events - - - - - - - + {registrations + ?.filter((registrations) => registrations.status !== 'REJECTED') + .map((registration) => ( + + - {registration.event?.name} + + ))} + +
    +
    + Proposals + + {options?.map((option) => ( + c.data?.id === option.question.cycleId)?.data?.eventId}/cycles/${option.question.cycleId}/options/${option.id}`} + > + - {option.optionTitle} + + ))} + +
    +
    + Groups + + {usersToGroups?.map((userToGroup) => ( + - {userToGroup.group?.name} + ))} - +
    ); } diff --git a/packages/berlin/src/pages/Comments.tsx b/packages/berlin/src/pages/Comments.tsx index 1d7e49c3..2b221805 100644 --- a/packages/berlin/src/pages/Comments.tsx +++ b/packages/berlin/src/pages/Comments.tsx @@ -38,32 +38,20 @@ import Textarea from '../components/textarea'; function Comments() { const queryClient = useQueryClient(); const { optionId } = useParams(); - // const { user } = useUser(); - // const [localUserVotes, setLocalUserVotes] = useState([]); - // const [localOptionHearts, setLocalOptionHearts] = useState(0); const [comment, setComment] = useState(''); const [sortOrder, setSortOrder] = useState('desc'); // 'asc' for ascending, 'desc' for descending const { data: option, isLoading } = useQuery({ queryKey: ['option', optionId], - queryFn: () => fetchOption(optionId || ''), + queryFn: () => + fetchOption({ optionId: optionId || '', serverUrl: import.meta.env.VITE_SERVER_URL }), enabled: !!optionId, }); - // const availableHearts = - // useAppStore((state) => state.availableHearts[option?.questionId || '']) ?? INITIAL_HEARTS; - // const setAvailableHearts = useAppStore((state) => state.setAvailableHearts); - - // const { data: userVotes } = useQuery({ - // queryKey: ['votes', cycleId], - // queryFn: () => fetchUserVotes(cycleId || ''), - // enabled: !!user?.id && !!cycleId, - // retry: false, - // }); - const { data: optionUsers } = useQuery({ queryKey: ['option', optionId, 'users'], - queryFn: () => fetchOptionUsers(optionId || ''), + queryFn: () => + fetchOptionUsers({ optionId: optionId || '', serverUrl: import.meta.env.VITE_SERVER_URL }), enabled: !!optionId, }); @@ -73,7 +61,8 @@ function Comments() { const { data: comments } = useQuery({ queryKey: ['option', optionId, 'comments'], - queryFn: () => fetchComments({ optionId: optionId || '' }), + queryFn: () => + fetchComments({ optionId: optionId || '', serverUrl: import.meta.env.VITE_SERVER_URL }), enabled: !!optionId, refetchInterval: 5000, // Poll every 5 seconds }); @@ -88,20 +77,6 @@ function Comments() { }); }, [comments, sortOrder]); - // useEffect(() => { - // if (optionId) { - // const sumOfAllVotes = userVotes?.reduce((acc, option) => acc + option.numOfVotes, 0) || 0; - // const hearts = userVotes?.find((option) => optionId === option.optionId)?.numOfVotes || 0; - // setLocalOptionHearts(hearts); - // setLocalUserVotes([{ optionId: optionId, numOfVotes: hearts }]); - // // update the available hearts - // setAvailableHearts({ - // questionId: option?.questionId ?? '', - // hearts: Math.max(0, INITIAL_HEARTS - sumOfAllVotes), - // }); - // } - // }, [optionId, userVotes, setAvailableHearts, option?.questionId]); - const { mutate: mutateComments } = useMutation({ mutationFn: postComment, onSuccess: (body) => { @@ -111,55 +86,13 @@ function Comments() { }, }); - // const handleVoteWrapper = (optionId: string) => { - // if (availableHearts === 0) { - // toast.error('No hearts left to give'); - // return; - // } - - // setLocalOptionHearts((prevLocalOptionHearts) => prevLocalOptionHearts + 1); - // setLocalUserVotes((prevLocalUserVotes) => handleLocalVote(optionId, prevLocalUserVotes)); - // setAvailableHearts({ - // questionId: option?.questionId ?? '', - // hearts: handleAvailableHearts(availableHearts, 'vote'), - // }); - // }; - - // const handleUnVoteWrapper = (optionId: string) => { - // if (availableHearts === INITIAL_HEARTS) { - // toast.error('No votes to left to remove'); - // return; - // } - - // setLocalOptionHearts((prevLocalOptionHearts) => Math.max(0, prevLocalOptionHearts - 1)); - // setLocalUserVotes((prevLocalUserVotes) => handleLocalUnVote(optionId, prevLocalUserVotes)); - // setAvailableHearts({ - // questionId: option?.questionId ?? '', - // hearts: handleAvailableHearts(availableHearts, 'unVote'), - // }); - // }; - - // const { mutate: mutateVotes } = useMutation({ - // mutationFn: postVotes, - // onSuccess: (body) => { - // if (body?.errors?.length) { - // toast.error(`Failed to save votes, ${body?.errors[0].message}`); - // } else if (body?.data.length) { - // queryClient.invalidateQueries({ queryKey: ['votes', cycleId] }); - // // this is to update the plural scores in each option - // queryClient.invalidateQueries({ queryKey: ['cycles', cycleId] }); - // toast.success('Votes saved successfully!'); - // } - // }, - // }); - - // const handleSaveVoteWrapper = () => { - // handleSaveVotes(userVotes, localUserVotes, mutateVotes); - // }; - const handlePostComment = () => { if (optionId && comment) { - mutateComments({ questionOptionId: optionId, value: comment }); + mutateComments({ + questionOptionId: optionId, + value: comment, + serverUrl: import.meta.env.VITE_SERVER_URL, + }); setComment(''); } }; @@ -172,29 +105,6 @@ function Comments() { - {/* - - handleVoteWrapper(option?.id ?? '')} - $width={16} - $height={16} - disabled={availableHearts === 0} - /> - handleUnVoteWrapper(option?.id ?? '')} - $width={16} - $height={16} - disabled={localOptionHearts === 0} - /> - - {localOptionHearts} - */} {option?.optionTitle} {option?.optionSubTitle} diff --git a/packages/berlin/src/pages/Cycle.tsx b/packages/berlin/src/pages/Cycle.tsx index 8f53c856..109aaea0 100644 --- a/packages/berlin/src/pages/Cycle.tsx +++ b/packages/berlin/src/pages/Cycle.tsx @@ -25,24 +25,27 @@ import { useAppStore } from '../store'; // Components import { Body } from '../components/typography/Body.styled'; import { Bold } from '../components/typography/Bold.styled'; +import { cycleSteps } from '@/components/onboarding/Steps'; +import { FINAL_QUESTION_TITLE, INITIAL_HEARTS } from '../utils/constants'; import { FlexColumn } from '../components/containers/FlexColumn.styled'; import { FlexRow } from '../components/containers/FlexRow.styled'; +import { Heart, SlidersHorizontal } from 'lucide-react'; +import { Subtitle } from '@/components/typography/Subtitle.styled'; import { Title } from '../components/typography/Title.styled'; import BackButton from '../components/back-button'; import Button from '../components/button'; -import CycleColumns from '../components/columns/cycle-columns'; -import OptionCard from '../components/option-card'; -import { FINAL_QUESTION_TITLE, INITIAL_HEARTS } from '../utils/constants'; -import { Heart } from 'lucide-react'; -import { OnboardingCard } from '@/components/onboarding/Onboaring.styled'; -import { Subtitle } from '@/components/typography/Subtitle.styled'; -import IconButton from '@/components/icon-button'; -import Onboarding from '../components/onboarding'; -import Icon from '@/components/icon'; +import Onboarding from '@/components/onboarding'; +import Option from '@/components/option'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/_components/ui/dropdown-menu'; type Order = 'asc' | 'desc'; type LocalUserVotes = { optionId: string; numOfVotes: number }[]; -type QuestionOption = GetCycleResponse['forumQuestions'][number]['questionOptions'][number]; +type QuestionOption = GetCycleResponse['questions'][number]['options'][number]; function Cycle() { const queryClient = useQueryClient(); @@ -51,21 +54,21 @@ function Cycle() { const { eventId, cycleId } = useParams(); const { data: cycle } = useQuery({ queryKey: ['cycles', cycleId], - queryFn: () => fetchCycle(cycleId || ''), + queryFn: () => + fetchCycle({ cycleId: cycleId || '', serverUrl: import.meta.env.VITE_SERVER_URL }), enabled: !!cycleId, refetchInterval: 5000, // Poll every 5 seconds }); const { data: userVotes } = useQuery({ queryKey: ['votes', cycleId], - queryFn: () => fetchUserVotes(cycleId || ''), + queryFn: () => + fetchUserVotes({ cycleId: cycleId || '', serverUrl: import.meta.env.VITE_SERVER_URL }), enabled: !!user?.id && !!cycleId, }); const availableHearts = - useAppStore((state) => state.availableHearts[cycle?.forumQuestions[0].id || '']) ?? - INITIAL_HEARTS; - const theme = useAppStore((state) => state.theme); + useAppStore((state) => state.availableHearts[cycle?.questions[0].id || '']) ?? INITIAL_HEARTS; const setAvailableHearts = useAppStore((state) => state.setAvailableHearts); const [startAt, setStartAt] = useState(null); const [endAt, setEndAt] = useState(null); @@ -96,11 +99,11 @@ function Cycle() { useEffect(() => { // Initial sorting - if (cycle?.forumQuestions[0].questionOptions.length) { + if (cycle?.questions[0].options.length) { setSortedOptions((prev) => ({ ...prev, options: sortOptions({ - options: cycle.forumQuestions[0].questionOptions, + options: cycle.questions[0].options, sorting: prev, votes: userVotes, }), @@ -130,7 +133,7 @@ function Cycle() { votes?.map((option) => option.numOfVotes).reduce((prev, curr) => prev + curr, 0) ?? 0; setAvailableHearts({ - questionId: cycle?.forumQuestions[0].id || '', + questionId: cycle?.questions[0].id || '', hearts: Math.max(0, INITIAL_HEARTS - givenVotes), }); @@ -181,7 +184,7 @@ function Cycle() { setLocalUserVotes((prevLocalUserVotes) => handleLocalVote(optionId, prevLocalUserVotes)); setAvailableHearts({ - questionId: cycle?.forumQuestions[0].id ?? '', + questionId: cycle?.questions[0].id ?? '', hearts: handleAvailableHearts(availableHearts, 'vote'), }); }; @@ -194,20 +197,22 @@ function Cycle() { setLocalUserVotes((prevLocalUserVotes) => handleLocalUnVote(optionId, prevLocalUserVotes)); setAvailableHearts({ - questionId: cycle?.forumQuestions[0].id ?? '', + questionId: cycle?.questions[0].id ?? '', hearts: handleAvailableHearts(availableHearts, 'unVote'), }); }; const handleSaveVotesWrapper = () => { if (cycle?.status === 'OPEN') { - handleSaveVotes(userVotes, localUserVotes, mutateVotes); + handleSaveVotes(userVotes, localUserVotes, ({ votes }) => + mutateVotes({ votes, serverUrl: import.meta.env.VITE_SERVER_URL }), + ); } else { toast.error('Cycle is not open'); } }; - const currentCycle = cycle?.forumQuestions[0]; + const currentCycle = cycle?.questions[0]; const sortId = (a: QuestionOption, b: QuestionOption, order: Order) => { const idA = a.id.toUpperCase(); @@ -298,139 +303,13 @@ function Cycle() { ); }; - const steps = [ - { - target: '.step-1', - content: ( - - Voting Page - View vote items and allocate your hearts. - - ), - placement: 'center', - }, - { - target: '.step-2', - content: ( - - Vote - - - - - - Upvote or downvote a vote item. - - - ), - placement: 'center', - }, - { - target: '.step-3', - content: ( - - Save Your Votes - - You must click the - {' '} - button or your vote will not be recorded. - - - ), - placement: 'center', - }, - { - target: '.step-4', - content: ( - - Information - View vote item. - - - - - Current number of hearts allocated to this vote item. - - - ), - placement: 'center', - }, - { - target: '.step-5', - content: ( - - Voting Mechanisms - - - - Plurality score, unlike quadratic score, considers pre-existing participant - relationships - - - - ), - placement: 'center', - }, - { - target: '.step-6', - content: ( - - Expand a vote item - - - Click to view the vote item description and other useful information. - - {/* - - - Click to view the comments page and start a discussion with other participants. - - */} - - ), - placement: 'center', - }, - ]; - return ( <> - - + + - {currentCycle?.questionTitle} + {currentCycle?.title} {voteInfo} You have {availableHearts} hearts left to give away: @@ -440,26 +319,54 @@ function Cycle() { ))} - - {currentCycle?.questionOptions.length ? ( - - + {currentCycle?.options.length ? ( + +
    + Vote items +
    + Sort + + + + + + handleColumnClick('lead')}> + + + handleColumnClick('affiliation')}> + + + handleColumnClick('numOfVotes')}> + + + {currentCycle.showScore && ( + handleColumnClick('voteScore')}> + + + )} + + +
    +
    {sortedOptions.options.map((option) => { const userVote = localUserVotes.find((vote) => vote.optionId === option.id); const numOfVotes = userVote ? userVote.numOfVotes : 0; return ( - handleVoteWrapper(option.id)} - onUnVote={() => handleUnVoteWrapper(option.id)} - /> + <> +
    diff --git a/packages/berlin/src/pages/Event.tsx b/packages/berlin/src/pages/Event.tsx index 7add90e4..0179b01c 100644 --- a/packages/berlin/src/pages/Event.tsx +++ b/packages/berlin/src/pages/Event.tsx @@ -1,74 +1,36 @@ // React and third-party libraries -import { useMemo } from 'react'; +import { useState, useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; // API -import { GetCycleResponse, fetchEvent, fetchEventCycles } from 'api'; +import { fetchEvent, fetchEventCycles } from 'api'; // Components import { Body } from '../components/typography/Body.styled'; +import { eventSteps } from '@/components/onboarding/Steps'; import { FlexColumn } from '../components/containers/FlexColumn.styled'; -import { Table } from '../components/table'; -import Button from '../components/button'; -import EventCard from '../components/event-card'; +import { Subtitle } from '../components/typography/Subtitle.styled'; +import * as Tabs from '../components/tabs'; +import BackButton from '@/components/back-button'; +import Cycles from '../components/cycles'; import Link from '../components/link'; -import { OnboardingCard } from '@/components/onboarding/Onboaring.styled'; -import { Subtitle } from '@/components/typography/Subtitle.styled'; +import Markdown from 'react-markdown'; import Onboarding from '@/components/onboarding'; -const steps = [ - { - target: '.step-1', - content: ( - - Welcome - Welcome to our tool! - Would you like to take a tour to see how it works? - - ), - placement: 'center', - }, - { - target: '.step-2', - content: ( - - Open Votes - Explore current vote items, the vote deadline, and cast your vote. - - ), - placement: 'center', - }, - { - target: '.step-3', - content: ( - - Closed Votes - - Review past votes and see results by clicking the - {' '} - button. - - - ), - placement: 'center', - }, -]; - function Event() { - const navigate = useNavigate(); const { eventId } = useParams(); const { data: event } = useQuery({ queryKey: ['event', eventId], - queryFn: () => fetchEvent(eventId || ''), + queryFn: () => + fetchEvent({ eventId: eventId || '', serverUrl: import.meta.env.VITE_SERVER_URL }), enabled: !!eventId, }); const { data: eventCycles } = useQuery({ queryKey: ['events', eventId, 'cycles'], - queryFn: () => fetchEventCycles(eventId || ''), + queryFn: () => + fetchEventCycles({ eventId: eventId || '', serverUrl: import.meta.env.VITE_SERVER_URL }), enabled: !!eventId, refetchInterval: 5000, // Poll every 5 seconds }); @@ -82,87 +44,42 @@ function Event() { [eventCycles], ); - const handleDataPolicyClick = () => { - navigate(`/data-policy`); - }; - - // TODO: Create functions to navigate to onboarding slides + const tabNames = ['upcoming', 'past']; + const [activeTab, setActiveTab] = useState('upcoming'); - const handleOnboardingClick = () => { - navigate(`/onboarding`); + const tabs = { + upcoming: , + past: , }; return ( <> - - - {/* */} - {!!openCycles?.length && } - {!!closedCycles?.length && } - {event && } - - Click to revisit the{' '} - - event rules - - ,{' '} - - trust assumptions - - , and the community’s{' '} - - data policy - - . - + + + +
    + {event?.name} + {event?.description && ( + {props.children}, + p: ({ node, ...props }) => {props.children}, + }} + > + {event.description} + + )} +
    +
    + Questions + +
    + + +
    ); } -function CycleTable({ cycles, status }: { cycles: GetCycleResponse[]; status: 'open' | 'closed' }) { - const { eventId } = useParams(); - const navigate = useNavigate(); - const formatDate = (date: string) => { - const eventEndDate = new Date(date); - return eventEndDate.toLocaleDateString(); - }; - const handleClick = (cycleId: string) => { - navigate(`/events/${eventId}/cycles/${cycleId}`); - }; - - const formattedColumnText = () => { - if (status === 'open') { - return 'Closes on'; - } else { - return 'Closed on'; - } - }; - - return ( - [ - cycle.forumQuestions?.[0]?.questionTitle, - formatDate(cycle.endAt), - , - ])} - /> - ); -} - export default Event; diff --git a/packages/berlin/src/pages/Events.tsx b/packages/berlin/src/pages/Events.tsx index 31a8afba..685ca579 100644 --- a/packages/berlin/src/pages/Events.tsx +++ b/packages/berlin/src/pages/Events.tsx @@ -1,5 +1,5 @@ // React and third-party libraries -import { useNavigate } from 'react-router-dom'; +import { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; // API @@ -11,27 +11,39 @@ import useUser from '../hooks/useUser'; // Components import { FlexColumn } from '../components/containers/FlexColumn.styled'; import { Title } from '../components/typography/Title.styled'; -import EventCard from '../components/event-card'; +import * as Tabs from '../components/tabs'; +import EventsCards from '../components/events'; function Events() { - const navigate = useNavigate(); + const [activeTab, setActiveTab] = useState('upcoming'); const { user } = useUser(); const { data: events } = useQuery({ queryKey: ['events'], - queryFn: fetchEvents, + queryFn: () => fetchEvents({ serverUrl: import.meta.env.VITE_SERVER_URL }), enabled: !!user?.id, }); - const handleClick = (eventId: string) => { - navigate(`/events/${eventId}/cycles`); + const openEvents = useMemo(() => events?.filter((event) => event.status === 'OPEN'), [events]); + const closedEvents = useMemo( + () => events?.filter((events) => events.status === 'CLOSED'), + [events], + ); + + const tabNames = ['upcoming', 'past']; + const tabs = { + upcoming: , + past: , }; return ( - Welcome, {user?.username ?? 'User'} - {events?.map((event) => { - return handleClick(event.id)} />; - })} +
    + Events + +
    +
    + +
    ); } diff --git a/packages/berlin/src/pages/PublicGroupRegistration.tsx b/packages/berlin/src/pages/PublicGroupRegistration.tsx index 428681b9..db12851b 100644 --- a/packages/berlin/src/pages/PublicGroupRegistration.tsx +++ b/packages/berlin/src/pages/PublicGroupRegistration.tsx @@ -36,13 +36,18 @@ function PublicGroupRegistration() { const { data: groups } = useQuery({ queryKey: ['group-categories', groupCategoryIdParam, 'groups'], - queryFn: () => fetchGroups({ groupCategoryId: groupCategoryIdParam || '' }), + queryFn: () => + fetchGroups({ + groupCategoryId: groupCategoryIdParam || '', + serverUrl: import.meta.env.VITE_SERVER_URL, + }), enabled: !!user?.id && !!groupCategoryIdParam, }); const { data: usersToGroups } = useQuery({ queryKey: ['user', user?.id, 'users-to-groups'], - queryFn: () => fetchUsersToGroups(user?.id || ''), + queryFn: () => + fetchUsersToGroups({ userId: user?.id || '', serverUrl: import.meta.env.VITE_SERVER_URL }), enabled: !!user?.id, }); @@ -109,6 +114,7 @@ function PublicGroupRegistration() { putUsersToGroupsMutation({ userToGroupId: prevUserToGroup.id, groupId: getValues('group'), + serverUrl: import.meta.env.VITE_SERVER_URL, }); setValue('group', ''); reset(); @@ -116,7 +122,10 @@ function PublicGroupRegistration() { } // If the user is not in the category group, create a new userToGroup - postUsersToGroupsMutation({ groupId: getValues('group') }); + postUsersToGroupsMutation({ + groupId: getValues('group'), + serverUrl: import.meta.env.VITE_SERVER_URL, + }); setValue('group', ''); reset(); } diff --git a/packages/berlin/src/pages/Register.tsx b/packages/berlin/src/pages/Register.tsx index 0b0c140e..6ecd7b7b 100644 --- a/packages/berlin/src/pages/Register.tsx +++ b/packages/berlin/src/pages/Register.tsx @@ -1,7 +1,6 @@ // React and third-party libraries -import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query'; -import { useEffect, useMemo, useState } from 'react'; -import ContentLoader from 'react-content-loader'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useMemo, useState } from 'react'; import { UseFormReturn, useForm } from 'react-hook-form'; import toast from 'react-hot-toast'; import { useNavigate, useParams } from 'react-router-dom'; @@ -16,16 +15,11 @@ import { fetchEvent, fetchEventGroupCategories, fetchGroups, - fetchRegistrationData, - fetchRegistrationFields, fetchRegistrations, fetchUsersToGroups, postRegistration, postUsersToGroups, putRegistration, - type GetRegistrationDataResponse, - type GetRegistrationFieldsResponse, - type GetRegistrationResponseType, type GetUserResponse, } from 'api'; @@ -33,50 +27,50 @@ import { import useUser from '../hooks/useUser'; // Components +import { dataSchema, fieldsSchema } from '@/utils/form-validation'; +import { z } from 'zod'; import Button from '../components/button'; +import { Carousel } from '../components/carousel'; import { FlexColumn } from '../components/containers/FlexColumn.styled'; import { Form } from '../components/containers/Form.styled'; -import { FormInput, SelectInput } from '../components/form'; +import { FormInput, FormSelectInput } from '../components/form-input'; import Select from '../components/select'; import Label from '../components/typography/Label'; import { Subtitle } from '../components/typography/Subtitle.styled'; import { SafeArea } from '../layout/Layout.styled'; -import { Carousel } from '../components/carousel'; function Register() { const { user, isLoading } = useUser(); const { eventId } = useParams(); - const [selectedRegistrationFormKey, setSelectedRegistrationFormKey] = useState< - string | undefined - >(); const { data: event } = useQuery({ queryKey: ['event', eventId], - queryFn: () => fetchEvent(eventId || ''), + queryFn: () => + fetchEvent({ eventId: eventId || '', serverUrl: import.meta.env.VITE_SERVER_URL }), enabled: !!eventId, }); const { data: registrations } = useQuery({ queryKey: ['event', eventId, 'registrations'], - queryFn: () => fetchRegistrations(eventId || ''), - enabled: !!eventId, - }); - - const { data: registrationFields } = useQuery({ - queryKey: ['event', eventId, 'registrations', 'fields'], - queryFn: () => fetchRegistrationFields(eventId || ''), + queryFn: () => + fetchRegistrations({ eventId: eventId || '', serverUrl: import.meta.env.VITE_SERVER_URL }), enabled: !!eventId, }); const { data: usersToGroups } = useQuery({ queryKey: ['user', user?.id, 'users-to-groups'], - queryFn: () => fetchUsersToGroups(user?.id || ''), + queryFn: () => + fetchUsersToGroups({ userId: user?.id || '', serverUrl: import.meta.env.VITE_SERVER_URL }), enabled: !!user?.id, }); const { data: groupCategories } = useQuery({ queryKey: ['event', eventId, 'group-categories'], - queryFn: () => fetchEventGroupCategories(eventId || ''), + queryFn: () => + fetchEventGroupCategories({ + eventId: eventId || '', + serverUrl: import.meta.env.VITE_SERVER_URL, + }), enabled: !!eventId, }); @@ -96,38 +90,6 @@ function Register() { return 0; }, [groupCategories, usersToGroups]); - const multipleRegistrationData = useQueries({ - queries: - registrations?.map((registration) => ({ - queryKey: ['registrations', registration.id, 'registration-data'], - queryFn: () => fetchRegistrationData(registration.id || ''), - enabled: !!registration.id, - })) ?? [], - combine: (results) => { - // return a map of registration id to { data, loading } - return results.reduce( - (acc, result, idx) => { - if (registrations && registrations[idx] && result.data) { - acc[registrations[idx].id || ''] = { - data: result.data, - loading: result.isLoading, - }; - } - return acc; - }, - {} as Record< - string, - { data: GetRegistrationDataResponse | null | undefined; loading: boolean } - >, - ); - }, - }); - - const onRegistrationFormCreate = (newRegistrationId: string) => { - // select the newly created registration form - setSelectedRegistrationFormKey(newRegistrationId); - }; - if (isLoading) { return Loading...; } @@ -140,12 +102,7 @@ function Register() { usersToGroups={usersToGroups} user={user} registrations={registrations} - selectedRegistrationFormKey={selectedRegistrationFormKey} - multipleRegistrationData={multipleRegistrationData} - registrationFields={registrationFields} event={event} - onRegistrationFormCreate={onRegistrationFormCreate} - setSelectedRegistrationFormKey={setSelectedRegistrationFormKey} /> ); @@ -156,30 +113,15 @@ const CarouselWrapper = ({ usersToGroups, user, registrations, - selectedRegistrationFormKey, - multipleRegistrationData, - registrationFields, event, - onRegistrationFormCreate, - setSelectedRegistrationFormKey, + defaultStep, }: { defaultStep: number; groupCategories: GetGroupCategoriesResponse | null | undefined; usersToGroups: GetUsersToGroupsResponse | null | undefined; user: GetUserResponse | null | undefined; registrations: GetRegistrationsResponseType | undefined | null; - selectedRegistrationFormKey: string | undefined; - multipleRegistrationData: Record< - string, - { - data: GetRegistrationDataResponse | null | undefined; - loading: boolean; - } - >; - registrationFields: GetRegistrationFieldsResponse | null | undefined; event: GetEventResponse | null | undefined; - onRegistrationFormCreate: (newRegistrationId: string) => void; - setSelectedRegistrationFormKey: (key: string) => void; }) => { const navigate = useNavigate(); @@ -197,20 +139,26 @@ const CarouselWrapper = ({ useMutation({ mutationFn: postRegistration, onSuccess: async (body) => { - if (body) { - toast.success('Registration saved successfully!'); - await queryClient.invalidateQueries({ - queryKey: ['event', body.eventId, 'registrations'], - }); - - // invalidate user registrations, this is for the 1 event use case - // where the authentication is because you are approved to the event - await queryClient.invalidateQueries({ - queryKey: [user?.id, 'registrations'], - }); - } else { + if (!body) { toast.error('Failed to save registration, please try again'); + return; + } + + if ('errors' in body) { + toast.error(body.errors[0]); + return; } + + toast.success('Registration saved successfully!'); + await queryClient.invalidateQueries({ + queryKey: ['event', body.eventId, 'registrations'], + }); + + // invalidate user registrations, this is for the 1 event use case + // where the authentication is because you are approved to the event + await queryClient.invalidateQueries({ + queryKey: [user?.id, 'registrations'], + }); }, onError: (error) => { console.error('Error saving registration:', error); @@ -248,8 +196,9 @@ const CarouselWrapper = ({ eventId: event?.id || '', groupId: null, status: 'DRAFT', - registrationData: [], + data: {}, }, + serverUrl: import.meta.env.VITE_SERVER_URL, }); } else { await postRegistrationMutation({ @@ -257,19 +206,25 @@ const CarouselWrapper = ({ eventId: event?.id || '', groupId: null, status: 'DRAFT', - registrationData: [], + data: {}, }, + serverUrl: import.meta.env.VITE_SERVER_URL, }); } }; return ( { // query registration to check if it is approved const registrations = await queryClient.fetchQuery({ queryKey: ['event', event?.id, 'registrations'], - queryFn: () => fetchRegistrations(event?.id || ''), + queryFn: () => + fetchRegistrations({ + eventId: event?.id || '', + serverUrl: import.meta.env.VITE_SERVER_URL, + }), }); redirectToNextPage( @@ -302,18 +257,19 @@ const CarouselWrapper = ({ ), }, { - isEnabled: (registrationFields?.length ?? 0) > 0, + isEnabled: + (fieldsSchema.safeParse(event?.fields).success + ? Object.values(fieldsSchema.parse(event?.fields)).length + : 0) > 0, render: ({ handleStepComplete }) => ( ), @@ -323,210 +279,6 @@ const CarouselWrapper = ({ ); }; -const sortRegistrationsByCreationDate = (registrations: GetRegistrationResponseType[]) => { - return [ - ...registrations.sort((a, b) => { - return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); - }), - ]; -}; - -const createRegistrationForms = ({ - registrations, - usersToGroups, -}: { - registrations: GetRegistrationsResponseType | undefined | null; - usersToGroups: GetUsersToGroupsResponse | undefined | null; -}) => { - const sortedRegistrationsByCreationDate = sortRegistrationsByCreationDate(registrations || []); - - const registrationForms: { - key: string | 'create'; - registration?: GetRegistrationResponseType; - group?: GetUsersToGroupsResponse[number]['group']; - mode: 'edit' | 'create'; - }[] = sortedRegistrationsByCreationDate.map((reg) => { - return { - key: reg.id || '', - registration: reg, - group: usersToGroups?.find((userToGroup) => userToGroup.group.id === reg.groupId)?.group, - mode: 'edit', - }; - }); - - registrationForms.push({ - key: 'create', - mode: 'create', - }); - - return registrationForms; -}; - -function SelectRegistrationDropdown({ - usersToGroups, - selectedRegistrationFormKey, - registrations, - onSelectedRegistrationFormKeyChange, - multipleRegistrationData, - registrationFields, -}: { - selectedRegistrationFormKey: string | undefined; - registrations: GetRegistrationsResponseType | undefined | null; - usersToGroups: GetUsersToGroupsResponse | undefined | null; - onSelectedRegistrationFormKeyChange: (key: string) => void; - multipleRegistrationData: Record< - string, - { - data: GetRegistrationDataResponse | null | undefined; - loading: boolean; - } - >; - registrationFields: GetRegistrationFieldsResponse | null | undefined; -}) { - useEffect(() => { - // select the first registration if it exists - // and no registration form is selected - if ( - registrations && - registrations.length && - registrations[0].id && - !selectedRegistrationFormKey - ) { - const firstRegistrationId = sortRegistrationsByCreationDate(registrations)[0].id; - - if (firstRegistrationId) { - onSelectedRegistrationFormKeyChange(firstRegistrationId); - } - } - }, [onSelectedRegistrationFormKeyChange, registrations, selectedRegistrationFormKey]); - - const showRegistrationsSelect = ( - registrations: GetRegistrationsResponseType | null | undefined, - ): boolean => { - // only show select when user has previously registered - return !!registrations && registrations.length > 0; - }; - - const getRegistrationTitle = ({ - multipleRegistrationData, - registration, - registrationFields, - }: { - multipleRegistrationData: Record< - string, - { - data: GetRegistrationDataResponse | null | undefined; - loading: boolean; - } - >; - registration: GetRegistrationResponseType | null | undefined; - registrationFields: GetRegistrationFieldsResponse | null | undefined; - }) => { - const firstField = filterRegistrationFields( - registrationFields, - registration?.groupId ? 'group' : 'user', - )?.sort((a, b) => (a.fieldDisplayRank ?? 0) - (b.fieldDisplayRank ?? 0))[0]; - - if (!firstField) { - return ''; - } - - const registrationData = multipleRegistrationData[registration?.id || '']?.data; - - if (!registrationData) { - return ''; - } - - return ( - registrationData.find((data) => data.registrationFieldId === firstField.id)?.value ?? - 'Untitled' - ); - }; - - const createOptionName = ({ - index, - mode, - groupName, - multipleRegistrationData, - registration, - registrationFields, - }: { - multipleRegistrationData: Record< - string, - { - data: GetRegistrationDataResponse | null | undefined; - loading: boolean; - } - >; - registration: GetRegistrationResponseType | null | undefined; - registrationFields: GetRegistrationFieldsResponse | null | undefined; - mode: 'edit' | 'create'; - index: number; - groupName?: string; - }) => { - if (mode === 'create') { - return 'Create a new proposal'; - } - - return `${index}. ${getRegistrationTitle({ - multipleRegistrationData, - registration, - registrationFields, - })} ${groupName ? `[${groupName}]` : ''}`; - }; - - return ( - showRegistrationsSelect(registrations) && ( - - -