├── .npmrc ├── .husky ├── commit-msg └── pre-commit ├── .yarnrc.yml ├── src ├── operations │ ├── chat │ │ ├── ChatOperations.ts │ │ └── ChatSidebarOperations.ts │ ├── notifications │ │ └── NotificationsPopoverOperations.ts │ ├── navbar │ │ └── NavbarOperations.ts │ ├── monitor │ │ └── MonitorOperations.ts │ ├── inbox │ │ └── ToolbarOperations.ts │ ├── home │ │ ├── HomeOperations.tsx │ │ ├── ChatOperations.ts │ │ └── CalendarOperations.ts │ ├── filter-modal │ │ └── FilterModalOperations.ts │ └── toast │ │ └── toastOperations.tsx ├── domain │ ├── entities │ │ ├── User.ts │ │ ├── monitor │ │ │ ├── types.ts │ │ │ └── generated-types.ts │ │ ├── notifications │ │ │ ├── Notification.ts │ │ │ └── generated-types.ts │ │ ├── FilterModalState.ts │ │ ├── inbox-item │ │ │ └── inbox-item.ts │ │ ├── map │ │ │ ├── euCities.ts │ │ │ └── LocationData.ts │ │ ├── navbar │ │ │ └── NavItemType.ts │ │ ├── chat │ │ │ ├── Sidebar.ts │ │ │ └── generated-types.ts │ │ ├── alerts │ │ │ ├── generated-types.ts │ │ │ └── alert.ts │ │ ├── profile │ │ │ └── generated-types.ts │ │ ├── MapIndicator │ │ │ └── MeetingCountByCountry.ts │ │ └── calendar │ │ │ ├── generated-types.ts │ │ │ └── CalendarTypes.ts │ ├── hooks │ │ ├── countriesHook.ts │ │ ├── topicHook.ts │ │ ├── alertHooks.ts │ │ ├── notificationsHooks.ts │ │ ├── use-mobile.ts │ │ ├── useNewsletterDialog.ts │ │ ├── meetingHooks.ts │ │ ├── legislative-hooks.ts │ │ ├── use-debounce.ts │ │ ├── use-incomplete-profile.tsx │ │ └── use-debounced-search.ts │ ├── actions │ │ ├── login-with-google.ts │ │ ├── auth.ts │ │ ├── chat-actions.ts │ │ ├── profile.ts │ │ └── alert-actions.ts │ ├── animations.ts │ └── schemas │ │ └── auth.ts ├── app │ ├── error │ │ └── page.tsx │ ├── chat │ │ ├── loading.tsx │ │ ├── [sessionId] │ │ │ ├── loading.tsx │ │ │ ├── page.tsx │ │ │ └── error.tsx │ │ ├── page.tsx │ │ └── layout.tsx │ ├── inbox │ │ ├── loading.tsx │ │ ├── page.tsx │ │ └── data-table.tsx │ ├── calendar │ │ └── page.tsx │ ├── profile │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── loading.tsx │ ├── map │ │ ├── layout.tsx │ │ └── loading.tsx │ ├── forgot-password │ │ └── page.tsx │ ├── update-password │ │ └── page.tsx │ ├── monitor │ │ ├── loading.tsx │ │ ├── [legisId] │ │ │ └── loading.tsx │ │ └── columns.tsx │ ├── onboarding │ │ ├── loading.tsx │ │ └── page.tsx │ ├── page.tsx │ ├── not-found.tsx │ ├── auth │ │ ├── confirm │ │ │ └── route.ts │ │ └── callback │ │ │ └── route.ts │ ├── register │ │ └── page.tsx │ ├── login │ │ └── page.tsx │ ├── layout.tsx │ └── elements │ │ └── page.tsx ├── components │ ├── Chat │ │ ├── AIResponseSkeleton.tsx │ │ ├── ChatMessage.tsx │ │ ├── ContextBadge.tsx │ │ ├── ChatToolbar.tsx │ │ ├── ChatSkeleton.tsx │ │ ├── ChatFooter.tsx │ │ ├── ChatInputCard.tsx │ │ └── ChatInterface.tsx │ ├── ui │ │ ├── skeleton.tsx │ │ ├── sonner.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── textarea.tsx │ │ ├── progress.tsx │ │ ├── collapsible.tsx │ │ ├── input.tsx │ │ ├── spinner.tsx │ │ ├── switch.tsx │ │ ├── avatar.tsx │ │ ├── checkbox.tsx │ │ ├── toggle.tsx │ │ ├── popover.tsx │ │ ├── scroll-area.tsx │ │ ├── badge.tsx │ │ ├── alert.tsx │ │ ├── tooltip.tsx │ │ ├── button-group.tsx │ │ ├── tabs.tsx │ │ ├── toggle-group.tsx │ │ ├── slider.tsx │ │ ├── button.tsx │ │ └── card.tsx │ ├── monitor │ │ ├── ToolbarSkeleton.tsx │ │ └── KanbanSkeleton.tsx │ ├── inbox │ │ ├── InboxSkeleton.tsx │ │ ├── AlertBulkActions.tsx │ │ ├── ColHeader.tsx │ │ ├── NewsletterDialog.tsx │ │ └── ViewOptions.tsx │ ├── LoadingSpinner.tsx │ ├── PersonalizeSwitch │ │ ├── PersonalizeLegislationSwitch.tsx │ │ └── PersonalizeMeetingSwitch.tsx │ ├── calendar │ │ ├── MonthlyCalendar │ │ │ └── MonthlyCalendar.tsx │ │ ├── MonthViewCalendar │ │ │ ├── EventBullet.tsx │ │ │ ├── DroppableArea.tsx │ │ │ ├── DraggableEvent.tsx │ │ │ └── MonthViewCalendar.tsx │ │ ├── CalendarSkeleton │ │ │ ├── CalendarSkeleton.tsx │ │ │ ├── MonthViewSkeleton.tsx │ │ │ └── WeekViewSkeleton.tsx │ │ ├── TagBadge.tsx │ │ ├── WeekViewCalendar │ │ │ ├── CalendarTimeline.tsx │ │ │ ├── RenderGroupedEvents.tsx │ │ │ └── EventListBlock.tsx │ │ └── CalendarHeader │ │ │ └── TodayButton.tsx │ ├── onboarding │ │ ├── OnboardingProgress.tsx │ │ ├── Step1PathDecision.tsx │ │ ├── Step3FocusArea.tsx │ │ ├── FeaturePreviewCard.tsx │ │ ├── Step2PoliticalRoleDetails.tsx │ │ └── Step2EntrepreneurRoleDetails.tsx │ ├── TooltipMotionButton.tsx │ ├── map │ │ ├── constants.ts │ │ └── Map.tsx │ ├── navigation │ │ ├── AuthAwareNavItems.tsx │ │ ├── MobileNav.tsx │ │ ├── AuthAwareNavContent.tsx │ │ ├── NavItem.tsx │ │ └── SettingsPopover.tsx │ ├── home │ │ ├── features │ │ │ ├── MapFeature.tsx │ │ │ └── FeatureCard.tsx │ │ └── Footer.tsx │ ├── section.tsx │ ├── RelevanceScore.tsx │ ├── profile │ │ ├── forms │ │ │ └── CompletionForm.tsx │ │ ├── NotificationsForm.tsx │ │ └── InterestsForm.tsx │ ├── SampleCard │ │ └── SampleComponent.tsx │ ├── DateRangeFilter.tsx │ ├── auth │ │ └── UpdatePasswordForm.tsx │ └── ExportModal │ │ └── ExportModal.tsx ├── lib │ ├── provider │ │ ├── ThemeProvider.tsx │ │ └── ReactQueryProvider.tsx │ ├── supabase │ │ ├── server.ts │ │ ├── client.ts │ │ └── middleware.ts │ ├── scripts │ │ └── fetch-api.mjs │ ├── dal.ts │ └── formatters.ts ├── middleware.ts └── repositories │ ├── countryRepository.ts │ ├── topicRepository.ts │ ├── notificationRepository.ts │ └── alertRepository.ts ├── commitlint.config.js ├── public ├── favicon.png ├── project-europe.png └── project-europe-no-bg.png ├── postcss.config.mjs ├── .lintstagedrc.json ├── .prettierignore ├── next.config.ts ├── tsconfig.scripts.json ├── .vscode └── settings.json ├── components.json ├── .prettierrc ├── .github └── workflows │ └── lint.yaml ├── tsconfig.json └── .gitignore /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | yarn commitlint --edit $1 -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | yarn lint-staged -------------------------------------------------------------------------------- /src/operations/chat/ChatOperations.ts: -------------------------------------------------------------------------------- 1 | export const SUPPORTED_CONTEXT_TYPES = ['legislation']; 2 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jst-seminar-rostlab-tum/openeu-frontend/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/project-europe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jst-seminar-rostlab-tum/openeu-frontend/HEAD/public/project-europe.png -------------------------------------------------------------------------------- /src/domain/entities/User.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: string; 3 | name: string; 4 | picturePath: string | null; 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ['@tailwindcss/postcss', 'autoprefixer'], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /public/project-europe-no-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jst-seminar-rostlab-tum/openeu-frontend/HEAD/public/project-europe-no-bg.png -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"], 3 | "*.{json,css,md}": ["prettier --write"] 4 | } 5 | -------------------------------------------------------------------------------- /src/app/error/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | export default function ErrorPage() { 4 | return

Sorry, something went wrong

; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/chat/loading.tsx: -------------------------------------------------------------------------------- 1 | import ChatSkeleton from '@/components/Chat/ChatSkeleton'; 2 | 3 | export default function Loading() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/inbox/loading.tsx: -------------------------------------------------------------------------------- 1 | import InboxSkeleton from '@/components/inbox/InboxSkeleton'; 2 | 3 | export default function Loading() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/chat/[sessionId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import ChatSkeleton from '@/components/Chat/ChatSkeleton'; 2 | 3 | export default function Loading() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/calendar/page.tsx: -------------------------------------------------------------------------------- 1 | import Calendar from '@/components/calendar/MonthlyCalendar/MonthlyCalendar'; 2 | 3 | export default function CalendarPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/domain/entities/monitor/types.ts: -------------------------------------------------------------------------------- 1 | import { SUPPORTED_CONTEXT_TYPES } from '@/operations/chat/ChatOperations'; 2 | 3 | export type TContext = (typeof SUPPORTED_CONTEXT_TYPES)[number]; 4 | -------------------------------------------------------------------------------- /src/domain/entities/notifications/Notification.ts: -------------------------------------------------------------------------------- 1 | export interface PopoverNotification { 2 | id: string; 3 | title: string; 4 | isRead: boolean; 5 | type: 'info' | 'warning'; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/profile/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function ProfileLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return
{children}
; 7 | } 8 | -------------------------------------------------------------------------------- /src/domain/entities/FilterModalState.ts: -------------------------------------------------------------------------------- 1 | export interface FilterModalState { 2 | startDate?: Date; 3 | endDate?: Date; 4 | countries?: string[]; 5 | topics: string[]; 6 | institutions?: string[]; 7 | } 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Build output 2 | .next/ 3 | out/ 4 | build/ 5 | dist/ 6 | 7 | # Dependencies 8 | node_modules/ 9 | 10 | # Static assets 11 | public/ 12 | 13 | # UI Components 14 | src/components/ui/** 15 | -------------------------------------------------------------------------------- /src/domain/entities/inbox-item/inbox-item.ts: -------------------------------------------------------------------------------- 1 | export type InboxItem = { 2 | id: string; 3 | title: string; 4 | date: string; 5 | country: string; 6 | relevanceScore: number | undefined; 7 | message: string | null; 8 | }; 9 | -------------------------------------------------------------------------------- /src/domain/entities/map/euCities.ts: -------------------------------------------------------------------------------- 1 | import cities from './euCities.json'; 2 | 3 | export type CityInfo = { 4 | city: string; 5 | country: string; 6 | lat: number; 7 | lng: number; 8 | population: number; 9 | altNames: string[]; 10 | }; 11 | 12 | export const euCities: CityInfo[] = cities; 13 | -------------------------------------------------------------------------------- /src/domain/entities/navbar/NavItemType.ts: -------------------------------------------------------------------------------- 1 | interface NavItemContent { 2 | title: string; 3 | href: string; 4 | description?: string; 5 | } 6 | 7 | interface NavItemWithContent extends NavItemContent { 8 | items?: NavItemContent[]; 9 | } 10 | 11 | export type NavItemType = NavItemContent | NavItemWithContent; 12 | -------------------------------------------------------------------------------- /src/components/Chat/AIResponseSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@/components/ui/skeleton'; 2 | 3 | export function AIResponseSkeleton() { 4 | return ( 5 |
6 | 7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/domain/entities/chat/Sidebar.ts: -------------------------------------------------------------------------------- 1 | import { Home } from 'lucide-react'; 2 | 3 | type IconType = typeof Home; 4 | 5 | interface SidebarItem { 6 | title: string; 7 | icon?: IconType; 8 | onClick: () => void; 9 | } 10 | 11 | export interface SidebarGroupData { 12 | label: string; 13 | items: SidebarItem[]; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/map/layout.tsx: -------------------------------------------------------------------------------- 1 | import { MeetingProvider } from '@/components/calendar/MeetingContext'; 2 | 3 | export default function MapLayout({ children }: { children: React.ReactNode }) { 4 | return ( 5 | 6 | {children} 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | async rewrites() { 6 | return [ 7 | { 8 | source: '/onboarding', 9 | destination: '/onboarding/1', 10 | }, 11 | ]; 12 | }, 13 | }; 14 | 15 | export default nextConfig; 16 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ) 11 | } 12 | 13 | export { Skeleton } 14 | -------------------------------------------------------------------------------- /src/domain/hooks/countriesHook.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import { countryRepository } from '@/repositories/countryRepository'; 4 | 5 | export const useCountries = (enabled = true) => 6 | useQuery({ 7 | queryKey: ['countries'], 8 | queryFn: countryRepository.getCountries, 9 | enabled, 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/monitor/ToolbarSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@/components/ui/skeleton'; 2 | 3 | export function ToolbarSkeleton() { 4 | return ( 5 |
6 | 7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.scripts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "noEmit": false, 7 | "outDir": "./dist", 8 | "allowJs": false, 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true 11 | }, 12 | "include": ["scripts", "src/utils/parseGeoCities.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /src/app/forgot-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { ForgotPasswordForm } from '@/components/auth/ForgotPasswordForm'; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/update-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { UpdatePasswordForm } from '@/components/auth/UpdatePasswordForm'; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/provider/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ThemeProvider as NextThemesProvider } from 'next-themes'; 4 | import { type ThemeProviderProps } from 'next-themes'; 5 | import * as React from 'react'; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/monitor/loading.tsx: -------------------------------------------------------------------------------- 1 | import { KanbanSkeleton } from '@/components/monitor/KanbanSkeleton'; 2 | import { ToolbarSkeleton } from '@/components/monitor/ToolbarSkeleton'; 3 | 4 | export default function MonitorSkeleton() { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/components/monitor/KanbanSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@/components/ui/skeleton'; 2 | 3 | export function KanbanSkeleton() { 4 | return ( 5 |
6 | {Array.from({ length: 5 }).map((_, columnIndex) => ( 7 | 8 | ))} 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/domain/hooks/topicHook.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import { Topic } from '@/domain/entities/calendar/generated-types'; 4 | import { topicRepository } from '@/repositories/topicRepository'; 5 | 6 | export const useTopics = (enabled = true) => 7 | useQuery({ 8 | queryKey: ['topics'], 9 | queryFn: topicRepository.getTopics, 10 | enabled, 11 | }); 12 | -------------------------------------------------------------------------------- /src/domain/entities/alerts/generated-types.ts: -------------------------------------------------------------------------------- 1 | import type { components, operations } from '@/lib/api-types'; 2 | 3 | // === API TYPES (truly generated from backend) === 4 | export type Alert = components['schemas']['AlertResponse']; 5 | 6 | // Extract the response type for the alerts endpoint 7 | export type GetAlertsResponse = 8 | operations['get_alerts_endpoint_alerts_get']['responses']['200']['content']['application/json']; 9 | -------------------------------------------------------------------------------- /src/app/chat/[sessionId]/page.tsx: -------------------------------------------------------------------------------- 1 | import ChatFooter from '@/components/Chat/ChatFooter'; 2 | import ChatInterface from '@/components/Chat/ChatInterface'; 3 | 4 | export default function ChatSessionPage() { 5 | return ( 6 |
7 |
8 | 9 |
10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/domain/entities/map/LocationData.ts: -------------------------------------------------------------------------------- 1 | // import { Meeting } from '../calendar/generated-types'; 2 | import { Meeting } from '@/domain/entities/calendar/CalendarTypes'; 3 | 4 | export interface CityData { 5 | city: string; 6 | lat: number; 7 | lng: number; 8 | totalCount: number; 9 | meetings: Meeting[]; 10 | } 11 | 12 | export interface CountryData { 13 | country: string; 14 | cities: Record; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/inbox/InboxSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Section } from '@/components/section'; 2 | import { Skeleton } from '@/components/ui/skeleton'; 3 | 4 | export default function InboxSkeleton() { 5 | return ( 6 |
7 |
8 | 9 | 10 | 11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.formatOnSave": true, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "editor.tabSize": 2, 6 | "eslint.validate": [ 7 | "javascript", 8 | "javascriptreact", 9 | "typescript", 10 | "typescriptreact" 11 | ], 12 | "files.eol": "\n", 13 | "cSpell.words": ["Choropleth", "nextui"], 14 | "prettier.requireConfig": true, 15 | "prettier.bracketSameLine": false, 16 | "prettier.jsxSingleQuote": false 17 | } 18 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /src/components/LoadingSpinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function LoadingSpinner() { 4 | return ( 5 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/domain/actions/login-with-google.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@/lib/supabase/client'; 2 | 3 | export async function loginWithGoogle() { 4 | const supabase = createClient(); 5 | await supabase.auth.signInWithOAuth({ 6 | provider: 'google', 7 | options: { 8 | redirectTo: `${window.location.origin}/auth/callback`, 9 | queryParams: { 10 | access_type: 'offline', 11 | prompt: 'consent', 12 | }, 13 | scopes: 'https://www.googleapis.com/auth/calendar', 14 | }, 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/domain/hooks/alertHooks.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import { fetchBackendAlerts } from '@/repositories/alertRepository'; 4 | 5 | import { Alert } from '../entities/alerts/generated-types'; 6 | export interface AlertQueryParams { 7 | userId: string; 8 | enabled?: boolean; 9 | } 10 | 11 | export const useAlerts = (props: AlertQueryParams) => { 12 | return useQuery({ 13 | queryKey: ['alerts', props.userId], 14 | queryFn: () => fetchBackendAlerts(props.userId), 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "endOfLine": "lf", 4 | "quoteProps": "as-needed", 5 | "proseWrap": "preserve", 6 | "htmlWhitespaceSensitivity": "css", 7 | "vueIndentScriptAndStyle": false, 8 | "embeddedLanguageFormatting": "auto", 9 | "semi": true, 10 | "singleQuote": true, 11 | "tabWidth": 2, 12 | "useTabs": false, 13 | "trailingComma": "all", 14 | "printWidth": 80, 15 | "bracketSpacing": true, 16 | "bracketSameLine": false, 17 | "arrowParens": "always", 18 | "jsxSingleQuote": false 19 | } 20 | -------------------------------------------------------------------------------- /src/operations/notifications/NotificationsPopoverOperations.ts: -------------------------------------------------------------------------------- 1 | import { PopoverNotification } from '@/domain/entities/notifications/Notification'; 2 | 3 | export default class NotificationsPopoverOperations { 4 | static getMockNotifications(): PopoverNotification[] { 5 | return [ 6 | ...Array.from({ length: 10 }, (_, i) => ({ 7 | id: String(i), 8 | title: `Test notification ${i}`, 9 | isRead: i % 2 === 0, 10 | type: Math.random() > 0.5 ? 'info' : ('warning' as 'info' | 'warning'), 11 | })), 12 | ]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import ProfileContent from '@/components/profile/ProfileContent'; 2 | import { getProfile } from '@/domain/actions/profile'; 3 | import { getUser } from '@/lib/dal'; 4 | 5 | export default async function ProfilePage() { 6 | const user = await getUser(); 7 | 8 | if (!user) { 9 | throw new Error('User does not exist'); 10 | } 11 | 12 | const userProfile = await getProfile(user.id); 13 | 14 | if (!userProfile) { 15 | throw new Error('UserProfile does not exist'); 16 | } 17 | 18 | return ; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/PersonalizeSwitch/PersonalizeLegislationSwitch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import PersonalizeSwitch from './PersonalizeSwitch'; 4 | 5 | interface PersonalizeLegislationSwitchProps { 6 | onUserIdChange?: (userId: string | undefined) => void; 7 | selectedUserId?: string; 8 | } 9 | 10 | export default function PersonalizeLegislationSwitch({ 11 | onUserIdChange, 12 | selectedUserId, 13 | }: PersonalizeLegislationSwitchProps) { 14 | return ( 15 | {})} 17 | selectedUserId={selectedUserId} 18 | /> 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/domain/hooks/notificationsHooks.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import { Notification } from '@/domain/entities/notifications/generated-types'; 4 | import { fetchBackendNotifications } from '@/repositories/notificationRepository'; 5 | 6 | export interface AlertQueryParams { 7 | userId: string; 8 | limit?: number; 9 | } 10 | 11 | export const useNotifications = (props: AlertQueryParams, enabled = true) => 12 | useQuery({ 13 | queryKey: ['notifications', props.userId], 14 | queryFn: () => fetchBackendNotifications(props.userId), 15 | enabled: enabled && !!props.userId, 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/PersonalizeSwitch/PersonalizeMeetingSwitch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { useMeetingContext } from '@/domain/hooks/meetingHooks'; 4 | 5 | import PersonalizeSwitch from './PersonalizeSwitch'; 6 | 7 | export default function PersonalizeMeetingSwitch() { 8 | const { setSelectedUserId, selectedUserId } = useMeetingContext(); 9 | 10 | const handleCheckedChange = (userId: string | undefined) => { 11 | setSelectedUserId(userId || ''); 12 | }; 13 | 14 | return ( 15 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint and Format Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint-and-format: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Node.js 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: '22.15.0' 23 | - name: Install dependencies 24 | run: yarn 25 | 26 | - name: Run ESLint 27 | run: yarn lint 28 | 29 | - name: Run Prettier 30 | run: yarn format 31 | -------------------------------------------------------------------------------- /src/operations/navbar/NavbarOperations.ts: -------------------------------------------------------------------------------- 1 | import { NavItemType } from '@/domain/entities/navbar/NavItemType'; 2 | 3 | export default class NavbarOperations { 4 | static getNavItems(): NavItemType[] { 5 | return [ 6 | { 7 | title: 'Map', 8 | href: '/map', 9 | }, 10 | { 11 | title: 'Calendar', 12 | href: '/calendar', 13 | }, 14 | 15 | { 16 | title: 'Chat', 17 | href: '/chat', 18 | }, 19 | { 20 | title: 'Inbox', 21 | href: '/inbox', 22 | }, 23 | { 24 | title: 'Monitor', 25 | href: '/monitor', 26 | }, 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest } from 'next/server'; 2 | 3 | import { updateSession } from '@/lib/supabase/middleware'; 4 | 5 | export async function middleware(request: NextRequest) { 6 | return await updateSession(request); 7 | } 8 | 9 | export const config = { 10 | matcher: [ 11 | /* 12 | * Match all request paths except for the ones starting with: 13 | * - _next/static (static files) 14 | * - _next/image (image optimization files) 15 | * - favicon.ico (favicon file) 16 | * - api (API routes - handled separately if needed) 17 | */ 18 | '/((?!_next/static|_next/image|favicon.ico|favicon.png|api).*)', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /src/app/onboarding/loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Section } from '@/components/section'; 4 | import { Skeleton } from '@/components/ui/skeleton'; 5 | 6 | export default function Loading() { 7 | return ( 8 |
9 | {/* Progress bar skeleton */} 10 | 11 | {/* Main card skeleton */} 12 | 13 | {/* Navigation buttons skeleton */} 14 |
15 | 16 | 17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner, ToasterProps } from "sonner" 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme() 8 | 9 | return ( 10 | 22 | ) 23 | } 24 | 25 | export { Toaster } 26 | -------------------------------------------------------------------------------- /src/components/calendar/MonthlyCalendar/MonthlyCalendar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | import { CalendarBody } from '@/components/calendar/CalendarBody/CalendarBody'; 6 | import { CalendarHeader } from '@/components/calendar/CalendarHeader/CalendarHeader'; 7 | import { MeetingProvider } from '@/components/calendar/MeetingContext'; 8 | 9 | export default function Calendar() { 10 | return ( 11 |
12 | 13 |
14 | 15 | 16 |
17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/domain/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const MOBILE_BREAKPOINT = 768; 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState( 7 | undefined, 8 | ); 9 | 10 | React.useEffect(() => { 11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); 12 | const onChange = () => { 13 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 14 | }; 15 | mql.addEventListener('change', onChange); 16 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 17 | return () => mql.removeEventListener('change', onChange); 18 | }, []); 19 | 20 | return !!isMobile; 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/provider/ReactQueryProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 4 | import React, { ReactNode } from 'react'; 5 | 6 | const queryClient = new QueryClient({ 7 | defaultOptions: { 8 | queries: { 9 | refetchOnMount: false, 10 | refetchOnReconnect: false, 11 | refetchOnWindowFocus: false, 12 | gcTime: 1000 * 60 * 60, 13 | staleTime: Infinity, 14 | }, 15 | }, 16 | }); 17 | 18 | export default function ReactQueryProvider({ 19 | children, 20 | }: { 21 | children: ReactNode; 22 | }) { 23 | return ( 24 | {children} 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/domain/entities/chat/generated-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Auto-generated chat types extracted from OpenAPI specification 3 | * Run `npm run api:update` to regenerate 4 | */ 5 | 6 | // Import the auto-generated types 7 | import type { components } from '@/lib/api-types'; 8 | 9 | // === API TYPES (truly generated) === 10 | export type Message = components['schemas']['MessagesResponseModel']; 11 | export type ChatSession = components['schemas']['SessionsResponseModel']; 12 | export type CreateSessionRequest = components['schemas']['NewSessionItem']; 13 | export type SendMessageRequest = components['schemas']['ChatMessageItem']; 14 | export type CreateSessionResponse = 15 | components['schemas']['NewChatResponseModel']; 16 | -------------------------------------------------------------------------------- /src/app/profile/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@/components/ui/skeleton'; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 |
7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/calendar/MonthViewCalendar/EventBullet.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | 3 | import { transition } from '@/domain/animations'; 4 | import { cn, COLOR_SCHEMES, ColorSchemeKey } from '@/lib/utils'; 5 | 6 | export function EventBullet({ 7 | color, 8 | className, 9 | }: { 10 | color?: ColorSchemeKey; 11 | className?: string; 12 | }) { 13 | const dotColor = COLOR_SCHEMES[color ?? 'blue'].dot; 14 | 15 | return ( 16 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/operations/monitor/MonitorOperations.ts: -------------------------------------------------------------------------------- 1 | import { LegislativeFile } from '@/domain/entities/monitor/generated-types'; 2 | 3 | export default class MonitorOperations { 4 | static groupLegislationByStatus( 5 | data: LegislativeFile[], 6 | ): Record { 7 | return data.reduce( 8 | (acc, item) => { 9 | const status = item.status || 'Other'; 10 | (acc[status] ??= []).push(item); 11 | return acc; 12 | }, 13 | {} as Record, 14 | ); 15 | } 16 | 17 | static extractYearFromId(id: string): number | null { 18 | const match = id.match(/^(\d{4})\//); 19 | return match ? parseInt(match[1], 10) : null; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # bun 14 | .bun 15 | bun.lockb 16 | 17 | # testing 18 | /coverage 19 | 20 | # next.js 21 | /.next/ 22 | /out/ 23 | 24 | # production 25 | /build 26 | 27 | # misc 28 | .DS_Store 29 | *.pem 30 | 31 | # debug 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | .pnpm-debug.log* 36 | 37 | # env files (can opt-in for committing if needed) 38 | .env* 39 | 40 | # vercel 41 | .vercel 42 | 43 | # typescript 44 | *.tsbuildinfo 45 | next-env.d.ts 46 | 47 | .idea/ 48 | -------------------------------------------------------------------------------- /src/components/onboarding/OnboardingProgress.tsx: -------------------------------------------------------------------------------- 1 | import { Progress } from '@/components/ui/progress'; 2 | 3 | interface OnboardingProgressProps { 4 | currentStep: number; 5 | } 6 | 7 | export default function OnboardingProgress({ 8 | currentStep, 9 | }: OnboardingProgressProps) { 10 | const TOTAL_STEPS = 5; 11 | return ( 12 |
13 |
14 |

15 | Step {currentStep} of {TOTAL_STEPS} 16 |

17 |

{Math.round((currentStep / TOTAL_STEPS) * 100)}% Complete

18 |
19 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as LabelPrimitive from '@radix-ui/react-label'; 4 | import * as React from 'react'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ); 22 | } 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /src/domain/entities/profile/generated-types.ts: -------------------------------------------------------------------------------- 1 | import type { components } from '@/lib/api-types'; 2 | 3 | export type PoliticianCreate = components['schemas']['PoliticianCreate']; 4 | export type PoliticianUpdate = components['schemas']['PoliticianUpdate']; 5 | export type Politician = components['schemas']['PoliticianReturn']; 6 | 7 | export type CompanyCreate = components['schemas']['CompanyCreate']; 8 | export type CompanyUpdate = components['schemas']['CompanyUpdate']; 9 | export type Company = components['schemas']['CompanyReturn']; 10 | 11 | export type ProfileCreate = components['schemas']['ProfileCreate']; 12 | export type ProfileUpdate = components['schemas']['ProfileUpdate']; 13 | export type Profile = components['schemas']['ProfileReturn']; 14 | -------------------------------------------------------------------------------- /src/domain/entities/MapIndicator/MeetingCountByCountry.ts: -------------------------------------------------------------------------------- 1 | export const meetingsPerCountry: Map = new Map([ 2 | ['Austria', 0], 3 | ['Belgium', 0], 4 | ['Bulgaria', 0], 5 | ['Croatia', 0], 6 | ['Cyprus', 0], 7 | ['Czech Republic', 0], 8 | ['Denmark', 0], 9 | ['Estonia', 0], 10 | ['Finland', 0], 11 | ['France', 0], 12 | ['Germany', 0], 13 | ['Greece', 0], 14 | ['Hungary', 0], 15 | ['Ireland', 0], 16 | ['Italy', 0], 17 | ['Latvia', 0], 18 | ['Lithuania', 0], 19 | ['Luxembourg', 0], 20 | ['Malta', 0], 21 | ['Netherlands', 0], 22 | ['Poland', 0], 23 | ['Portugal', 0], 24 | ['Romania', 0], 25 | ['Slovakia', 0], 26 | ['Slovenia', 0], 27 | ['Spain', 0], 28 | ['Sweden', 0], 29 | ]); 30 | -------------------------------------------------------------------------------- /src/components/calendar/MonthViewCalendar/DroppableArea.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | 3 | interface DroppableAreaProps { 4 | date: Date; 5 | hour?: number; 6 | minute?: number; 7 | children: ReactNode; 8 | className?: string; 9 | } 10 | 11 | export function DroppableArea({ children, className }: DroppableAreaProps) { 12 | return ( 13 |
{ 16 | // Prevent default to allow drop 17 | e.preventDefault(); 18 | e.currentTarget.classList.add('bg-primary/10'); 19 | }} 20 | onDragLeave={(e) => { 21 | e.currentTarget.classList.remove('bg-primary/10'); 22 | }} 23 | > 24 | {children} 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/domain/entities/calendar/generated-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Auto-generated calendar types extracted from OpenAPI specification 3 | * Run `npm run api:update` to regenerate 4 | * 5 | * These are pure API types without frontend extensions 6 | */ 7 | import type { components, operations } from '@/lib/api-types'; 8 | 9 | export type Topic = components['schemas']['Topic']; 10 | export type MeetingSuggestion = components['schemas']['MeetingSuggestion']; 11 | export type MeetingSuggestionResponse = 12 | components['schemas']['MeetingSuggestionResponse']; 13 | export type GetMeetingsParams = 14 | operations['get_meetings_meetings_get']['parameters']['query']; 15 | export type MeetingData = components['schemas']['Meeting']; 16 | export type Person = components['schemas']['Person']; 17 | -------------------------------------------------------------------------------- /src/repositories/countryRepository.ts: -------------------------------------------------------------------------------- 1 | import { ToastOperations } from '@/operations/toast/toastOperations'; 2 | 3 | const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/countries`; 4 | 5 | export const countryRepository = { 6 | async getCountries(): Promise { 7 | try { 8 | const res = await fetch(API_URL); 9 | if (!res.ok) { 10 | ToastOperations.showError({ 11 | title: 'Error fetching countries', 12 | message: 'Failed to fetch countries. Please try again later.', 13 | }); 14 | throw new Error('Failed to fetch countries'); 15 | } 16 | 17 | const response = await res.json(); 18 | return Array.isArray(response.data) ? response.data : []; 19 | } catch { 20 | return []; 21 | } 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/app/monitor/[legisId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Section } from '@/components/section'; 2 | import { Skeleton } from '@/components/ui/skeleton'; 3 | 4 | export default function LegislationSkeleton() { 5 | return ( 6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Chat/ChatMessage.tsx: -------------------------------------------------------------------------------- 1 | import { Message } from '@/domain/entities/chat/generated-types'; 2 | 3 | import { StreamingMarkdown } from './StreamingMarkdown'; 4 | 5 | interface ChatMessageProps { 6 | message: Message; 7 | } 8 | 9 | export function ChatMessage({ message }: ChatMessageProps) { 10 | if (message.author === 'user') { 11 | return ( 12 |
13 |

14 | {message.content} 15 |

16 |
17 | ); 18 | } 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/domain/entities/notifications/generated-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Auto-generated notification types extracted from OpenAPI specification 3 | * Run `yarn run api:update` to regenerate 4 | * 5 | * Backend API: https://openeu-backend-1.onrender.com/docs#/notifications/get_notifications_for_user_notifications__user_id__get 6 | */ 7 | 8 | // Import the auto-generated types 9 | import type { components, operations } from '@/lib/api-types'; 10 | 11 | // === API TYPES (truly generated from backend) === 12 | export type Notification = components['schemas']['Notification']; 13 | 14 | // Extract the response type for the notifications endpoint 15 | export type GetNotificationsResponse = 16 | operations['get_notifications_for_user_notifications__user_id__get']['responses']['200']['content']['application/json']; 17 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import FeaturesSection from '@/components/home/FeaturesSection'; 2 | import Footer from '@/components/home/Footer'; 3 | import HeroSection from '@/components/home/HeroSection'; 4 | import LoggedInLanding from '@/components/home/LoggedInLanding'; 5 | import MissionSection from '@/components/home/MissionSection'; 6 | import { verifySession } from '@/lib/dal'; 7 | 8 | export default async function HomePage() { 9 | const session = await verifySession(); 10 | 11 | if (session) { 12 | return ; 13 | } 14 | 15 | return ( 16 |
17 | 18 | 19 | 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/domain/hooks/useNewsletterDialog.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | 3 | import { InboxItem } from '@/domain/entities/inbox-item/inbox-item'; 4 | 5 | export function useNewsletterDialog() { 6 | const [selectedItem, setSelectedItem] = useState(null); 7 | const [isOpen, setIsOpen] = useState(false); 8 | 9 | const openDialog = useCallback((item: InboxItem) => { 10 | setSelectedItem(item); 11 | setIsOpen(true); 12 | }, []); 13 | 14 | const closeDialog = useCallback(() => { 15 | setIsOpen(false); 16 | // Delay clearing selectedItem to allow for closing animation 17 | setTimeout(() => setSelectedItem(null), 150); 18 | }, []); 19 | 20 | return { 21 | selectedItem, 22 | isOpen, 23 | openDialog, 24 | closeDialog, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/map/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@/components/ui/skeleton'; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | {/* Toolbar skeleton - top right */} 7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 | {/* Zoom controls skeleton - bottom right */} 15 |
16 |
17 | 18 | 19 |
20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/domain/entities/calendar/CalendarTypes.ts: -------------------------------------------------------------------------------- 1 | // Import the auto-generated types 2 | import { ColorSchemeKey } from '@/lib/utils'; 3 | 4 | import { MeetingData } from './generated-types'; 5 | 6 | // === EXTENDED API TYPES === 7 | export type Meeting = MeetingData & { 8 | color: ColorSchemeKey; 9 | meeting_end_datetime: string; 10 | }; 11 | 12 | // === FRONTEND-ONLY TYPES === 13 | export type TCalendarView = 'day' | 'week' | 'month' | 'year' | 'agenda'; 14 | 15 | export type TMeetingColor = ColorSchemeKey; 16 | 17 | export interface CalendarCell { 18 | day: number; 19 | currentMonth: boolean; 20 | date: Date; 21 | } 22 | 23 | export type Member = { 24 | id: string; 25 | type: string; 26 | label: string; 27 | family_name: string; 28 | given_name: string; 29 | sort_label: string; 30 | country: string; 31 | }; 32 | -------------------------------------------------------------------------------- /src/domain/entities/alerts/alert.ts: -------------------------------------------------------------------------------- 1 | export type Alert = { 2 | id: string; 3 | title?: string; 4 | description: string; 5 | created_at: string | null; 6 | relevancy_threshold: number; 7 | is_active: boolean; 8 | }; 9 | 10 | export type AlertTableItem = { 11 | id: string; 12 | title: string; 13 | date: string | null; 14 | is_active: boolean; 15 | description: string; 16 | }; 17 | 18 | export interface AlertActions { 19 | onView?: (alert: AlertTableItem) => void; 20 | onToggleActive: (alertId: string, active: boolean) => void; 21 | } 22 | 23 | export const mapAlertToTableItem = (alert: Alert): AlertTableItem => ({ 24 | id: alert.id, 25 | title: alert.title ? alert.title.replace(/^"|"$/g, '') : '', 26 | date: alert.created_at, 27 | is_active: alert.is_active, 28 | description: alert.description, 29 | }); 30 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Separator({ 9 | className, 10 | orientation = "horizontal", 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ) 26 | } 27 | 28 | export { Separator } 29 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |