├── .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 |
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 |
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 |
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 |
13 |
14 | {/* Zoom controls skeleton - bottom right */}
15 |
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 |
15 | )
16 | }
17 |
18 | export { Textarea }
19 |
--------------------------------------------------------------------------------
/src/operations/inbox/ToolbarOperations.ts:
--------------------------------------------------------------------------------
1 | import { Column } from '@tanstack/react-table';
2 |
3 | import { DateRangeFilterProps } from '@/components/DateRangeFilter';
4 |
5 | export default class ToolbarOperations {
6 | static handleDateRangeChange(column?: Column) {
7 | return (range: DateRangeFilterProps) => {
8 | if (range.from || range.to) {
9 | column?.setFilterValue(range);
10 | } else {
11 | column?.setFilterValue(undefined);
12 | }
13 | };
14 | }
15 |
16 | static formatColumnDisplayName(columnId: string): string {
17 | return columnId
18 | .replace(/^is_([a-z])/, (_, firstLetter) => firstLetter.toUpperCase())
19 | .replace(/_([a-z])/g, (_, letter) => ` ${letter.toUpperCase()}`)
20 | .replace(/^[a-z]/, (letter) => letter.toUpperCase());
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/repositories/topicRepository.ts:
--------------------------------------------------------------------------------
1 | import { Topic } from '@/domain/entities/calendar/generated-types';
2 | import { ToastOperations } from '@/operations/toast/toastOperations';
3 |
4 | const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/topics`;
5 |
6 | export const topicRepository = {
7 | async getTopics(): Promise {
8 | try {
9 | const res = await fetch(API_URL);
10 | if (!res.ok) {
11 | ToastOperations.showError({
12 | title: 'Error fetching topic',
13 | message: 'Failed to fetch topic. Please try again later.',
14 | });
15 | throw new Error('Failed to fetch topics');
16 | }
17 |
18 | const response = await res.json();
19 | const data = Array.isArray(response.data) ? response.data : [];
20 | return data;
21 | } catch {
22 | return [];
23 | }
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/src/components/TooltipMotionButton.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion';
2 | import React from 'react';
3 |
4 | import { Button } from '@/components/ui/button';
5 | import {
6 | Tooltip,
7 | TooltipContent,
8 | TooltipTrigger,
9 | } from '@/components/ui/tooltip';
10 |
11 | export const MotionButton = motion.create(Button);
12 |
13 | export const TooltipButton = React.forwardRef<
14 | HTMLButtonElement,
15 | React.ComponentProps & { tooltipContent: string }
16 | >(({ tooltipContent, children, ...props }, ref) => (
17 |
18 |
19 |
20 | {children}
21 |
22 |
23 |
24 | {tooltipContent}
25 |
26 |
27 | ));
28 |
29 | TooltipButton.displayName = 'TooltipButton';
30 |
--------------------------------------------------------------------------------
/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Progress({
9 | className,
10 | value,
11 | ...props
12 | }: React.ComponentProps) {
13 | return (
14 |
22 |
27 |
28 | )
29 | }
30 |
31 | export { Progress }
32 |
--------------------------------------------------------------------------------
/src/domain/animations.ts:
--------------------------------------------------------------------------------
1 | import { Variants } from 'framer-motion';
2 |
3 | export const slideFromLeft: Variants = {
4 | initial: { x: -20, opacity: 0 },
5 | animate: { x: 0, opacity: 1 },
6 | exit: { x: 20, opacity: 0 },
7 | };
8 | export const transition = {
9 | type: 'spring',
10 | stiffness: 200,
11 | damping: 20,
12 | };
13 | export const slideFromRight: Variants = {
14 | initial: { x: 20, opacity: 0 },
15 | animate: { x: 0, opacity: 1 },
16 | exit: { x: -20, opacity: 0 },
17 | };
18 | export const buttonHover: Variants = {
19 | hover: { scale: 1.05 },
20 | tap: { scale: 0.95 },
21 | };
22 | export const staggerContainer: Variants = {
23 | animate: {
24 | transition: {
25 | staggerChildren: 0.1,
26 | },
27 | },
28 | };
29 | export const fadeIn: Variants = {
30 | initial: { opacity: 0 },
31 | animate: { opacity: 1 },
32 | exit: { opacity: 0 },
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/Chat/ContextBadge.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { X } from 'lucide-react';
4 |
5 | import { useChatContext } from '@/app/chat/ChatContext';
6 | import { Badge } from '@/components/ui/badge';
7 | import { Button } from '@/components/ui/button';
8 |
9 | interface ContextBadgeProps {
10 | id: string;
11 | title?: string;
12 | }
13 |
14 | export function ContextBadge({ id, title }: ContextBadgeProps) {
15 | const { clearContext } = useChatContext();
16 |
17 | const handleRemove = () => {
18 | clearContext();
19 | };
20 |
21 | return (
22 |
23 | {title || id}
24 |
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/Chat/ChatToolbar.tsx:
--------------------------------------------------------------------------------
1 | import { Send } from 'lucide-react';
2 |
3 | import { ManageChatContextDialog } from '@/components/Chat/ManageChatContextDialog';
4 | import { Button } from '@/components/ui/button';
5 |
6 | interface ChatToolbarProps {
7 | onSubmit: () => void;
8 | disabled: boolean;
9 | }
10 |
11 | export function ChatToolbar({ onSubmit, disabled }: ChatToolbarProps) {
12 | return (
13 |
14 | {/* Left side - Action buttons */}
15 |
16 |
17 | {/* Right side - Send button */}
18 |
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/map/constants.ts:
--------------------------------------------------------------------------------
1 | import { LatLngBoundsExpression } from 'leaflet';
2 |
3 | export const oceanBounds: LatLngBoundsExpression = [
4 | [-90, -180],
5 | [90, 180],
6 | ];
7 |
8 | export const countryBaseStyle = {
9 | weight: 1,
10 | opacity: 1,
11 | fillOpacity: 1,
12 | };
13 |
14 | export const countryBorderStyle = {
15 | weight: 1,
16 | opacity: 1,
17 | fillOpacity: 0,
18 | };
19 |
20 | export const europeanCountries = [
21 | 'Austria',
22 | 'Belgium',
23 | 'Bulgaria',
24 | 'Croatia',
25 | 'Cyprus',
26 | 'Czechia',
27 | 'Denmark',
28 | 'Estonia',
29 | 'Finland',
30 | 'France',
31 | 'Germany',
32 | 'Greece',
33 | 'Hungary',
34 | 'Ireland',
35 | 'Italy',
36 | 'Latvia',
37 | 'Lithuania',
38 | 'Luxembourg',
39 | 'Malta',
40 | 'Netherlands',
41 | 'Poland',
42 | 'Portugal',
43 | 'Romania',
44 | 'Slovakia',
45 | 'Slovenia',
46 | 'Spain',
47 | 'Sweden',
48 | ];
49 |
--------------------------------------------------------------------------------
/src/app/onboarding/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from 'next';
2 | import { redirect } from 'next/navigation';
3 | import React from 'react';
4 |
5 | import OnboardingLayout from '@/components/onboarding/OnboardingLayout';
6 | import { Section } from '@/components/section';
7 | import { getProfile } from '@/domain/actions/profile';
8 | import { requireAuth } from '@/lib/dal';
9 |
10 | export const metadata: Metadata = {
11 | title: 'Onboarding',
12 | description: 'Complete your OpenEU profile setup',
13 | keywords: ['onboarding', 'openeu', 'profile', 'setup'],
14 | };
15 |
16 | export default async function OnboardingStepPage() {
17 | const { user } = await requireAuth();
18 |
19 | const existingProfile = await getProfile(user.id);
20 |
21 | if (existingProfile) {
22 | redirect('/profile');
23 | }
24 |
25 | return (
26 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/calendar/CalendarSkeleton/CalendarSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { DayViewSkeleton } from '@/components/calendar/CalendarSkeleton/DayViewSkeleton';
4 | import { MonthViewSkeleton } from '@/components/calendar/CalendarSkeleton/MonthViewSkeleton';
5 | import { WeekViewSkeleton } from '@/components/calendar/CalendarSkeleton/WeekViewSkeleton';
6 | import { TCalendarView } from '@/domain/entities/calendar/CalendarTypes';
7 | interface CalendarSkeletonProps {
8 | view: TCalendarView;
9 | }
10 |
11 | export function CalendarSkeleton({ view }: CalendarSkeletonProps) {
12 | const calendarView = (view: TCalendarView) => {
13 | switch (view) {
14 | case 'month':
15 | return ;
16 | case 'day':
17 | return ;
18 | case 'week':
19 | return ;
20 | }
21 | };
22 |
23 | return calendarView(view);
24 | }
25 |
--------------------------------------------------------------------------------
/src/lib/supabase/server.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient } from '@supabase/ssr';
2 | import { cookies } from 'next/headers';
3 |
4 | export async function createClient() {
5 | const cookieStore = await cookies();
6 |
7 | return createServerClient(
8 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
9 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
10 | {
11 | cookies: {
12 | getAll() {
13 | return cookieStore.getAll();
14 | },
15 | setAll(cookiesToSet) {
16 | try {
17 | cookiesToSet.forEach(({ name, value, options }) => {
18 | cookieStore.set(name, value, options);
19 | });
20 | } catch {
21 | // The `setAll` method was called from a Server Component.
22 | // This can be ignored if you have middleware refreshing
23 | // user sessions.
24 | }
25 | },
26 | },
27 | },
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/navigation/AuthAwareNavItems.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import NavItem from '@/components/navigation/NavItem';
4 | import { NavigationMenuList } from '@/components/ui/navigation-menu';
5 | import { NavItemType } from '@/domain/entities/navbar/NavItemType';
6 | import { useAuth } from '@/domain/hooks/useAuth';
7 |
8 | interface AuthAwareNavItemsProps {
9 | navItems: NavItemType[];
10 | initialIsAuthenticated: boolean;
11 | }
12 |
13 | export function AuthAwareNavItems({
14 | navItems,
15 | initialIsAuthenticated,
16 | }: AuthAwareNavItemsProps) {
17 | const { user, loading } = useAuth();
18 |
19 | const isAuthenticated = loading ? initialIsAuthenticated : !!user;
20 |
21 | if (!isAuthenticated) {
22 | return null;
23 | }
24 |
25 | return (
26 |
27 | {navItems.map((item) => (
28 |
29 | ))}
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/calendar/TagBadge.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import { Badge } from '@/components/ui/badge';
4 | import { cn, COLOR_SCHEMES, getColorKeyByHash } from '@/lib/utils';
5 |
6 | interface TagBadgeProps {
7 | children: ReactNode;
8 | className?: string;
9 | colorHash?: string | null;
10 | }
11 |
12 | export function TagBadge({
13 | children,
14 | className = '',
15 | colorHash,
16 | }: TagBadgeProps) {
17 | const tagString = typeof children === 'string' ? children : '';
18 | const colorKey = getColorKeyByHash(colorHash || tagString);
19 |
20 | return (
21 |
31 | {children}
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/home/features/MapFeature.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Globe } from 'lucide-react';
4 |
5 | import FeatureCard from '@/components/home/features/FeatureCard';
6 | import MapComponent from '@/components/map/MapComponent';
7 | import MOCK_COUNTRY_MEETING_MAP from '@/components/map/mockCountryMeetingMap';
8 |
9 | import MapData from '../../../../public/map.geo.json';
10 |
11 | export default function MapFeature() {
12 | return (
13 |
18 |
19 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4 |
5 | function Collapsible({
6 | ...props
7 | }: React.ComponentProps) {
8 | return
9 | }
10 |
11 | function CollapsibleTrigger({
12 | ...props
13 | }: React.ComponentProps) {
14 | return (
15 |
19 | )
20 | }
21 |
22 | function CollapsibleContent({
23 | ...props
24 | }: React.ComponentProps) {
25 | return (
26 |
30 | )
31 | }
32 |
33 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
34 |
--------------------------------------------------------------------------------
/src/app/inbox/page.tsx:
--------------------------------------------------------------------------------
1 | import { Section } from '@/components/section';
2 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
3 | import { getUser } from '@/lib/dal';
4 |
5 | import { AlertsSection } from './AlertsSection';
6 | import { InboxSection } from './InboxSection';
7 |
8 | export default async function InboxPage() {
9 | const user = await getUser();
10 |
11 | return (
12 |
13 |
14 |
15 | Inbox
16 | Alerts
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | import { AlertCircle } from 'lucide-react';
2 | import Link from 'next/link';
3 |
4 | import { Section } from '@/components/section';
5 | import { Button } from '@/components/ui/button';
6 | import NotFoundOperations from '@/operations/not-found/NotFoundOperations';
7 |
8 | export default function NotFound() {
9 | const quote = NotFoundOperations.getRandomQuote();
10 |
11 | return (
12 |
13 |
14 | Page Not Found
15 |
16 |
17 | “{quote}”
18 |
19 |
20 |
21 | Home
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/calendar/MonthViewCalendar/DraggableEvent.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion';
2 | import React, { ReactNode } from 'react';
3 |
4 | import { EventDetailsDialog } from '@/components/calendar/MonthViewCalendar/EventDetailsDialog';
5 | import { Meeting } from '@/domain/entities/calendar/CalendarTypes';
6 |
7 | interface DraggableEventProps {
8 | event: Meeting;
9 | children: ReactNode;
10 | className?: string;
11 | }
12 |
13 | export function DraggableEvent({
14 | event,
15 | children,
16 | className,
17 | }: DraggableEventProps) {
18 | const handleClick = (e: React.MouseEvent) => {
19 | e.stopPropagation();
20 | };
21 |
22 | return (
23 |
24 | ) => handleClick(e)}
27 | >
28 | {children}
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/section.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { tv, type VariantProps } from 'tailwind-variants';
3 |
4 | const sectionVariants = tv({
5 | base: 'container p-4 m-auto md:px-0',
6 | variants: {
7 | variant: {
8 | default: '',
9 | noPadding: 'p-0',
10 | centered: 'flex items-center justify-center',
11 | screenCentered:
12 | 'flex items-center justify-center min-h-[calc(100vh_-_theme(spacing.16))]',
13 | },
14 | },
15 | defaultVariants: {
16 | variant: 'default',
17 | },
18 | });
19 |
20 | export interface SectionProps
21 | extends React.HTMLAttributes,
22 | VariantProps {}
23 |
24 | const Section = React.forwardRef(
25 | ({ children, variant, className }, ref) => (
26 |
29 | ),
30 | );
31 | Section.displayName = 'Section';
32 |
33 | export { Section };
34 |
--------------------------------------------------------------------------------
/src/app/auth/confirm/route.ts:
--------------------------------------------------------------------------------
1 | import { type EmailOtpType } from '@supabase/supabase-js';
2 | import { redirect } from 'next/navigation';
3 | import { type NextRequest } from 'next/server';
4 |
5 | import { createClient } from '@/lib/supabase/server';
6 |
7 | export async function GET(request: NextRequest) {
8 | const { searchParams } = new URL(request.url);
9 | const tokenHash = searchParams.get('token_hash');
10 | const type = searchParams.get('type') as EmailOtpType | null;
11 | const next = searchParams.get('next') ?? '/';
12 |
13 | if (tokenHash && type) {
14 | const supabase = await createClient();
15 |
16 | const { error } = await supabase.auth.verifyOtp({
17 | type,
18 |
19 | token_hash: tokenHash,
20 | });
21 | if (!error) {
22 | // redirect user to specified redirect URL or root of app
23 | redirect(next);
24 | } else {
25 | redirect('/login?error=' + error.message);
26 | }
27 | } else {
28 | redirect('/login?confirm=2');
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/domain/hooks/meetingHooks.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useQuery } from '@tanstack/react-query';
4 | import { useContext } from 'react';
5 |
6 | import {
7 | IMeetingContext,
8 | MeetingContext,
9 | } from '@/components/calendar/MeetingContext';
10 | import { Meeting } from '@/domain/entities/calendar/CalendarTypes';
11 | import { GetMeetingsParams } from '@/domain/entities/calendar/generated-types';
12 | import { meetingRepository } from '@/repositories/meetingRepository';
13 |
14 | export type GetMeetingsQueryParams = GetMeetingsParams;
15 |
16 | export const useMeetings = (props: GetMeetingsQueryParams, enabled = true) =>
17 | useQuery({
18 | queryKey: ['meetings', props],
19 | queryFn: () => meetingRepository.getMeetings(props),
20 | enabled,
21 | });
22 |
23 | export function useMeetingContext(): IMeetingContext {
24 | const context = useContext(MeetingContext);
25 | if (context === undefined)
26 | throw new Error('useCalendar must be used within a MeetingProvider.');
27 | return context;
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/map/Map.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import dynamic from 'next/dynamic';
4 | import React from 'react';
5 |
6 | import { useMeetingContext } from '@/domain/hooks/meetingHooks';
7 | import { useCountryMeetingMap } from '@/operations/map/MapOperations';
8 |
9 | import MapData from '../../../public/map.geo.json';
10 |
11 | const MapComponent = dynamic(() => import('@/components/map/MapComponent'), {
12 | ssr: false,
13 | });
14 |
15 | function MapInner({
16 | countryClickDisabled = false,
17 | }: {
18 | countryClickDisabled?: boolean;
19 | }) {
20 | const { meetings } = useMeetingContext();
21 |
22 | const countryMeetingMap = useCountryMeetingMap(meetings);
23 |
24 | return (
25 |
33 | );
34 | }
35 |
36 | export default React.memo(MapInner);
37 |
--------------------------------------------------------------------------------
/src/domain/hooks/legislative-hooks.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query';
2 |
3 | import {
4 | LegislativeFilesParams,
5 | LegislativeSuggestionsParams,
6 | } from '@/domain/entities/monitor/generated-types';
7 | import { legislationRepository } from '@/repositories/legislationRepository';
8 |
9 | export const useLegislativeFiles = (params?: LegislativeFilesParams) =>
10 | useQuery({
11 | queryKey: ['legislative-files', params],
12 | queryFn: () => legislationRepository.getLegislativeFiles(params),
13 | });
14 |
15 | export const useLegislativeSuggestions = (
16 | params: LegislativeSuggestionsParams,
17 | ) =>
18 | useQuery({
19 | queryKey: ['legislative-suggestions', params],
20 | queryFn: () => legislationRepository.getLegislationSuggestions(params),
21 | enabled: params.query.length >= 2,
22 | });
23 |
24 | export const useLegislativeUniqueValues = () =>
25 | useQuery({
26 | queryKey: ['legislative-unique-values'],
27 | queryFn: () => legislationRepository.getLegislativeUniqueValues(),
28 | });
29 |
--------------------------------------------------------------------------------
/src/operations/home/HomeOperations.tsx:
--------------------------------------------------------------------------------
1 | export default class HomeOperations {
2 | static getInboxItems(): {
3 | id: number;
4 | title: string;
5 | country: string;
6 | urgent: boolean;
7 | }[] {
8 | return [
9 | { id: 1, title: 'New GDPR Amendment', country: 'EU-Wide', urgent: true },
10 | {
11 | id: 2,
12 | title: 'Digital Services Act Update',
13 | country: 'Germany',
14 | urgent: false,
15 | },
16 | { id: 3, title: 'AI Regulation Draft', country: 'France', urgent: true },
17 | ];
18 | }
19 |
20 | static getFeatures(): {
21 | title: string;
22 | href: string;
23 | }[] {
24 | return [
25 | {
26 | title: 'EU Calendar',
27 | href: '/calendar',
28 | },
29 | {
30 | title: 'Smart Inbox',
31 | href: '/inbox',
32 | },
33 | {
34 | title: 'EU Chat',
35 | href: '/chat',
36 | },
37 | {
38 | title: 'EU Map',
39 | href: '/map',
40 | },
41 | ];
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/chat/page.tsx:
--------------------------------------------------------------------------------
1 | import ChatFooter from '@/components/Chat/ChatFooter';
2 | import { getUser } from '@/lib/dal';
3 | import { getFirstName } from '@/lib/utils';
4 |
5 | export default async function Chat() {
6 | const user = await getUser();
7 |
8 | return (
9 |
10 |
11 |
12 |
13 | OpenEU
14 |
15 |
16 | Welcome {user ? getFirstName(user) : ''}! OpenEU helps companies
17 | track and understand upcoming EU laws and national implementations.
18 | Ask anything about regulations, directives, or compliance, from
19 | sustainability reporting to AI governance.
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/domain/hooks/use-debounce.ts:
--------------------------------------------------------------------------------
1 | import { debounce } from 'lodash';
2 | import { useCallback, useEffect, useRef } from 'react';
3 |
4 | interface DebounceOptions {
5 | leading?: boolean;
6 | maxWait?: number;
7 | trailing?: boolean;
8 | }
9 |
10 | export function useDebounce<
11 | T extends (...args: Parameters) => ReturnType,
12 | >(
13 | callback: T,
14 | delay: number,
15 | options?: DebounceOptions,
16 | ): T & { cancel: () => void; flush: () => void } {
17 | const callbackRef = useRef(callback);
18 |
19 | useEffect(() => {
20 | callbackRef.current = callback;
21 | }, [callback]);
22 |
23 | const debouncedFunction = useCallback(
24 | debounce(
25 | (...args: Parameters) => callbackRef.current(...args),
26 | delay,
27 | options,
28 | ),
29 | [delay, options],
30 | );
31 |
32 | useEffect(() => {
33 | return () => {
34 | debouncedFunction.cancel();
35 | };
36 | }, [debouncedFunction]);
37 |
38 | return debouncedFunction as T & { cancel: () => void; flush: () => void };
39 | }
40 |
--------------------------------------------------------------------------------
/src/domain/schemas/auth.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | export const registerSchema = z
4 | .object({
5 | name: z
6 | .string()
7 | .min(1, 'Name is required')
8 | .min(2, 'Name must be at least 2 characters'),
9 | surname: z
10 | .string()
11 | .min(1, 'Surname is required')
12 | .min(2, 'Surname must be at least 2 characters'),
13 | email: z.email('Please enter a valid email address'),
14 | password: z.string().min(8, 'Password must be at least 8 characters'),
15 | confirmPassword: z.string().min(1, 'Please confirm your password'),
16 | })
17 | .refine((data) => data.password === data.confirmPassword, {
18 | message: "Passwords don't match",
19 | path: ['confirmPassword'],
20 | });
21 |
22 | export const loginSchema = z.object({
23 | email: z.email('Please enter a valid email address'),
24 | password: z.string().min(1, 'Password is required'),
25 | });
26 |
27 | export type RegisterFormData = z.infer;
28 | export type LoginFormData = z.infer;
29 |
--------------------------------------------------------------------------------
/src/lib/supabase/client.ts:
--------------------------------------------------------------------------------
1 | import { createBrowserClient } from '@supabase/ssr';
2 |
3 | export function createClient() {
4 | return createBrowserClient(
5 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
6 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
7 | {
8 | cookies: {
9 | getAll() {
10 | if (typeof document !== 'undefined') {
11 | return document.cookie.split(';').map((cookie) => {
12 | const [name, value] = cookie.trim().split('=');
13 | return { name, value };
14 | });
15 | }
16 | return [];
17 | },
18 | setAll(cookiesToSet) {
19 | if (typeof document !== 'undefined') {
20 | cookiesToSet.forEach(({ name, value, options }) => {
21 | document.cookie = `${name}=${value}; path=/; ${
22 | options?.secure ? 'secure;' : ''
23 | } ${options?.sameSite ? `samesite=${options.sameSite};` : ''}`;
24 | });
25 | }
26 | },
27 | },
28 | },
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/onboarding/Step1PathDecision.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { UseFormReturn } from 'react-hook-form';
3 | import z from 'zod';
4 |
5 | import { PathDecisionForm } from '@/components/profile/forms/PathDecisionForm';
6 | import {
7 | Card,
8 | CardContent,
9 | CardDescription,
10 | CardHeader,
11 | CardTitle,
12 | } from '@/components/ui/card';
13 | import { onboardingSchema } from '@/domain/schemas/profile';
14 |
15 | interface Step1PathDecisionProps {
16 | form: UseFormReturn>;
17 | }
18 |
19 | export default function Step1PathDecision({ form }: Step1PathDecisionProps) {
20 | return (
21 |
22 |
23 | Tell us about yourself
24 |
25 | Help us personalize your OpenEU experience
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/home/Footer.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | import { Button } from '@/components/ui/button';
4 |
5 | const navigation = [
6 | {
7 | name: 'Privacy Policy',
8 | href: '/privacy',
9 | },
10 | ];
11 |
12 | export default function Footer() {
13 | return (
14 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | type InputProps = React.InputHTMLAttributes;
6 | function Input({ className, type = 'text', ...props }: InputProps) {
7 | return (
8 |
19 | );
20 | }
21 |
22 | export { Input };
23 |
--------------------------------------------------------------------------------
/src/lib/scripts/fetch-api.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { execSync } from 'child_process';
4 | import { config } from 'dotenv';
5 | import { existsSync, mkdirSync } from 'fs';
6 | import { dirname } from 'path';
7 |
8 | config();
9 |
10 | const apiUrl = process.env.NEXT_PUBLIC_API_URL;
11 |
12 | if (!apiUrl) {
13 | console.error('❌ NEXT_PUBLIC_API_URL not found in environment variables');
14 | console.error('Please set NEXT_PUBLIC_API_URL in your .env file');
15 | process.exit(1);
16 | }
17 |
18 | const baseUrl = apiUrl.replace(/\/$/, '');
19 | const openApiUrl = `${baseUrl}/openapi.json`;
20 |
21 | console.log(`Fetching OpenAPI spec from: ${openApiUrl}`);
22 |
23 | try {
24 | const outputDir = dirname('src/lib/openapi.json');
25 | if (!existsSync(outputDir)) {
26 | mkdirSync(outputDir, { recursive: true });
27 | }
28 |
29 | execSync(`curl -s "${openApiUrl}" -o src/lib/openapi.json`, {
30 | stdio: 'inherit',
31 | });
32 |
33 | console.log('✅ OpenAPI spec successfully fetched');
34 | } catch (error) {
35 | console.error('❌ Failed to fetch OpenAPI spec:', error.message);
36 | process.exit(1);
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/onboarding/Step3FocusArea.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { UseFormReturn } from 'react-hook-form';
3 | import z from 'zod';
4 |
5 | import { FocusAreaForm } from '@/components/profile/forms/FocusAreaForm';
6 | import {
7 | Card,
8 | CardContent,
9 | CardDescription,
10 | CardHeader,
11 | CardTitle,
12 | } from '@/components/ui/card';
13 | import { focusAreaSchema, onboardingSchema } from '@/domain/schemas/profile';
14 |
15 | interface Step3FocusAreaProps {
16 | form: UseFormReturn>;
17 | }
18 |
19 | export default function Step3FocusArea({ form }: Step3FocusAreaProps) {
20 | return (
21 |
22 |
23 | Focus Areas
24 |
25 | Tell us what topics and regions you're most interested in
26 |
27 |
28 |
29 | >
32 | }
33 | />
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/onboarding/FeaturePreviewCard.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowRight } from 'lucide-react';
2 | import Link from 'next/link';
3 |
4 | import { Button } from '@/components/ui/button';
5 |
6 | interface FeaturePreviewCardProps {
7 | emoji: string;
8 | title: string;
9 | href: string;
10 | description: string;
11 | benefit: string;
12 | }
13 |
14 | export default function FeaturePreviewCard({
15 | emoji,
16 | title,
17 | href,
18 | description,
19 | benefit,
20 | }: FeaturePreviewCardProps) {
21 | return (
22 |
23 |
24 | {emoji}
25 | {title}
26 |
27 |
{description}
28 |
{benefit}
29 |
30 |
31 |
32 | Explore now
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/operations/home/ChatOperations.ts:
--------------------------------------------------------------------------------
1 | interface ChatMessage {
2 | id: number;
3 | text: string;
4 | isUser: boolean;
5 | timestamp: string;
6 | }
7 |
8 | export default class ChatOperations {
9 | static getChatMessages(): ChatMessage[] {
10 | return [
11 | {
12 | id: 1,
13 | text: 'What are the key changes in the Digital Services Act?',
14 | isUser: true,
15 | timestamp: '10:31 AM',
16 | },
17 | {
18 | id: 2,
19 | text: 'DSA introduces content moderation, algorithm transparency, and new platform rules.',
20 | isUser: false,
21 | timestamp: '10:31 AM',
22 | },
23 | ];
24 | }
25 |
26 | static generateMockAIResponse(): string {
27 | return 'I can help with that regulation. Let me provide details for your situation.';
28 | }
29 |
30 | static createChatMessage(
31 | id: number,
32 | text: string,
33 | isUser: boolean,
34 | ): ChatMessage {
35 | return {
36 | id,
37 | text,
38 | isUser,
39 | timestamp: new Date().toLocaleTimeString([], {
40 | hour: '2-digit',
41 | minute: '2-digit',
42 | }),
43 | };
44 | }
45 | }
46 |
47 | export type { ChatMessage };
48 |
--------------------------------------------------------------------------------
/src/components/Chat/ChatSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | export default function ChatSkeleton() {
4 | return (
5 |
6 |
7 | {/* Message skeletons */}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {/* Input skeleton */}
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/ui/spinner.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import { VariantProps, cva } from 'class-variance-authority';
4 | import { Loader2 } from 'lucide-react';
5 |
6 | const spinnerVariants = cva('flex-col items-center justify-center', {
7 | variants: {
8 | show: {
9 | true: 'flex',
10 | false: 'hidden',
11 | },
12 | },
13 | defaultVariants: {
14 | show: true,
15 | },
16 | });
17 |
18 | const loaderVariants = cva('animate-spin text-primary', {
19 | variants: {
20 | size: {
21 | xsmall: 'size-4',
22 | small: 'size-6',
23 | medium: 'size-8',
24 | large: 'size-12',
25 | },
26 | },
27 | defaultVariants: {
28 | size: 'medium',
29 | },
30 | });
31 |
32 | interface SpinnerContentProps
33 | extends VariantProps,
34 | VariantProps {
35 | className?: string;
36 | children?: React.ReactNode;
37 | }
38 |
39 | export function Spinner({ size, show, children, className }: SpinnerContentProps) {
40 | return (
41 |
42 |
43 | {children}
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/Chat/ChatFooter.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ArrowDown } from 'lucide-react';
4 |
5 | import ChatInputCard from '@/components/Chat/ChatInputCard';
6 |
7 | import { Button } from '../ui/button';
8 | import { useScrollToBottomButton } from './ChatScrollContainer';
9 |
10 | export default function ChatFooter() {
11 | const { showScrollButton, scrollToBottom } = useScrollToBottomButton();
12 |
13 | return (
14 |
15 | {showScrollButton && (
16 |
27 | )}
28 |
29 |
30 |
31 | OpenEU may occasionally provide incomplete or outdated information.
32 | Always verify critical details with official EU or national sources.
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/domain/hooks/use-incomplete-profile.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { User } from '@supabase/supabase-js';
4 | import Link from 'next/link';
5 | import { useEffect, useRef } from 'react';
6 |
7 | import { useProfile } from '@/domain/hooks/profileHooks';
8 | import { ToastOperations } from '@/operations/toast/toastOperations';
9 |
10 | export function useIncompleteProfile(user: User | null) {
11 | const hasShownToast = useRef(false);
12 | const { data: profile, isLoading } = useProfile(user?.id || '', !!user?.id);
13 |
14 | useEffect(() => {
15 | if (user && !isLoading && profile === null && !hasShownToast.current) {
16 | const timer = setTimeout(() => {
17 | ToastOperations.showWarning({
18 | title: 'Finish your profile to unlock all features',
19 | message: (
20 |
21 | Click here to complete your profile.
22 |
23 | ),
24 | });
25 |
26 | hasShownToast.current = true;
27 | }, 600);
28 |
29 | return () => clearTimeout(timer);
30 | }
31 | }, [user, profile, isLoading]);
32 |
33 | useEffect(() => {
34 | if (!user) {
35 | hasShownToast.current = false;
36 | }
37 | }, [user?.id]);
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/calendar/CalendarSkeleton/MonthViewSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | // Simple deterministic random number generator
4 | function seededRandom(seed: number) {
5 | const x = Math.sin(seed) * 10000;
6 | return x - Math.floor(x);
7 | }
8 |
9 | export function MonthViewSkeleton() {
10 | return (
11 |
12 |
13 | {Array.from({ length: 7 }).map((_, i) => (
14 |
15 |
16 |
17 | ))}
18 |
19 |
20 |
21 | {Array.from({ length: 42 }).map((_, i) => (
22 |
23 |
24 |
25 | {Array.from({ length: Math.floor(seededRandom(i) * 3) }).map(
26 | (_, j) => (
27 |
28 | ),
29 | )}
30 |
31 |
32 | ))}
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/inbox/AlertBulkActions.tsx:
--------------------------------------------------------------------------------
1 | import { Archive, ArchiveRestore, Trash2 } from 'lucide-react';
2 |
3 | import { Button } from '@/components/ui/button';
4 | import { Separator } from '@/components/ui/separator';
5 |
6 | interface DataTableBulkActionsProps {
7 | selectedCount: number;
8 | onActivate: () => void;
9 | onDelete: () => void;
10 | activationLabel?: string;
11 | }
12 |
13 | export function DataTableBulkActions({
14 | selectedCount,
15 | onActivate,
16 | onDelete,
17 | activationLabel = 'Deactivate',
18 | }: DataTableBulkActionsProps) {
19 | return (
20 |
21 |
22 | {selectedCount} row{selectedCount === 1 ? '' : 's'} selected
23 |
24 |
25 |
26 | {activationLabel == 'Deactivate' ? (
27 |
28 | ) : (
29 |
30 | )}
31 | {activationLabel}
32 |
33 |
34 |
35 | Delete
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitive from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Switch({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 |
27 |
28 | )
29 | }
30 |
31 | export { Switch }
32 |
--------------------------------------------------------------------------------
/src/repositories/notificationRepository.ts:
--------------------------------------------------------------------------------
1 | import { getCookie } from 'cookies-next';
2 |
3 | import { Notification } from '@/domain/entities/notifications/generated-types';
4 | import { ToastOperations } from '@/operations/toast/toastOperations';
5 |
6 | const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/notifications`;
7 |
8 | export async function fetchBackendNotifications(
9 | userId: string,
10 | ): Promise {
11 | const token = getCookie('token');
12 |
13 | try {
14 | const response = await fetch(`${API_URL}/${userId}`, {
15 | method: 'GET',
16 | mode: 'cors',
17 | headers: {
18 | 'Content-Type': 'application/json',
19 | Authorization: `Bearer ${token}`,
20 | },
21 | });
22 | if (!response.ok) {
23 | ToastOperations.showError({
24 | title: 'Error fetching notifications',
25 | message: 'Failed to fetch notifications. Please try again later.',
26 | });
27 | throw new Error(`HTTP error! status: ${response.status}`);
28 | }
29 | const res: { data: Notification[] } = await response.json();
30 | return res.data;
31 | } catch (error) {
32 | throw new Error(
33 | 'Error fetching notifications: ' +
34 | (error instanceof Error ? error.message : 'Unknown error'),
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Avatar({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | function AvatarImage({
25 | className,
26 | ...props
27 | }: React.ComponentProps) {
28 | return (
29 |
34 | )
35 | }
36 |
37 | function AvatarFallback({
38 | className,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
50 | )
51 | }
52 |
53 | export { Avatar, AvatarImage, AvatarFallback }
54 |
--------------------------------------------------------------------------------
/src/operations/chat/ChatSidebarOperations.ts:
--------------------------------------------------------------------------------
1 | import { SquarePen } from 'lucide-react';
2 |
3 | interface SidebarItem {
4 | title: string;
5 | onClick: () => void;
6 | icon?: React.ComponentType;
7 | }
8 |
9 | interface SidebarGroup {
10 | label: string;
11 | items: SidebarItem[];
12 | }
13 |
14 | interface ChatSidebarOperationsConfig {
15 | handleNewChat: () => void;
16 | handleTemplateClick: (message: string) => void;
17 | }
18 |
19 | const TEMPLATES = [
20 | {
21 | title: 'Last 3 Meetings',
22 | message: 'Show me the last 3 meetings',
23 | },
24 | ];
25 |
26 | class ChatSidebarOperations {
27 | static getTemplates() {
28 | return TEMPLATES;
29 | }
30 |
31 | static getSidebarGroups(config: ChatSidebarOperationsConfig): SidebarGroup[] {
32 | return [
33 | {
34 | label: 'Actions',
35 | items: [
36 | {
37 | title: 'New Chat',
38 | onClick: config.handleNewChat,
39 | icon: SquarePen,
40 | },
41 | ],
42 | },
43 | {
44 | label: 'Quick Questions',
45 | items: TEMPLATES.map((template) => ({
46 | title: template.title,
47 | onClick: () => config.handleTemplateClick(template.message),
48 | })),
49 | },
50 | ];
51 | }
52 | }
53 |
54 | export default ChatSidebarOperations;
55 |
--------------------------------------------------------------------------------
/src/components/onboarding/Step2PoliticalRoleDetails.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { UseFormReturn } from 'react-hook-form';
5 | import z from 'zod';
6 |
7 | import { PoliticalRoleForm } from '@/components/profile/forms/PoliticalRoleForm';
8 | import {
9 | Card,
10 | CardContent,
11 | CardDescription,
12 | CardHeader,
13 | CardTitle,
14 | } from '@/components/ui/card';
15 | import {
16 | onboardingSchema,
17 | politicianRoleSchema,
18 | } from '@/domain/schemas/profile';
19 |
20 | interface Step2PoliticalRoleDetailsProps {
21 | form: UseFormReturn>;
22 | }
23 |
24 | export default function Step2PoliticalRoleDetails({
25 | form,
26 | }: Step2PoliticalRoleDetailsProps) {
27 | return (
28 |
29 |
30 | Tell us about your role
31 |
32 | Help us understand your political work and expertise
33 |
34 |
35 |
36 |
40 | >
41 | }
42 | />
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { CheckIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function Checkbox({
10 | className,
11 | ...props
12 | }: React.ComponentProps) {
13 | return (
14 |
22 |
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | export { Checkbox }
33 |
--------------------------------------------------------------------------------
/src/components/calendar/WeekViewCalendar/CalendarTimeline.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | import { useMeetingContext } from '@/domain/hooks/meetingHooks';
4 | import { formatTime } from '@/operations/meeting/CalendarHelpers';
5 |
6 | export function CalendarTimeline() {
7 | const { use24HourFormat } = useMeetingContext();
8 | const [currentTime, setCurrentTime] = useState(new Date());
9 |
10 | useEffect(() => {
11 | const timer = setInterval(() => setCurrentTime(new Date()), 60 * 1000);
12 | return () => clearInterval(timer);
13 | }, []);
14 |
15 | const getCurrentTimePosition = () => {
16 | const minutes = currentTime.getHours() * 60 + currentTime.getMinutes();
17 | return (minutes / 1440) * 100;
18 | };
19 |
20 | const formatCurrentTime = () => {
21 | return formatTime(currentTime, use24HourFormat);
22 | };
23 |
24 | return (
25 |
29 |
30 |
31 |
32 | {formatCurrentTime()}
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/inbox/ColHeader.tsx:
--------------------------------------------------------------------------------
1 | import { Column } from '@tanstack/react-table';
2 | import { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react';
3 | import { useCallback } from 'react';
4 |
5 | import { Button } from '@/components/ui/button';
6 | import { cn } from '@/lib/utils';
7 |
8 | interface DataTableColumnHeaderProps
9 | extends React.HTMLAttributes {
10 | column: Column;
11 | title: string;
12 | }
13 |
14 | export function DataTableColumnHeader({
15 | column,
16 | title,
17 | className,
18 | }: DataTableColumnHeaderProps) {
19 | const handleSort = useCallback(() => {
20 | column.toggleSorting(column.getIsSorted() === 'asc');
21 | }, [column]);
22 |
23 | if (!column.getCanSort()) {
24 | return {title}
;
25 | }
26 |
27 | return (
28 |
33 | {title}
34 | {column.getIsSorted() === 'asc' ? (
35 |
36 | ) : column.getIsSorted() === 'desc' ? (
37 |
38 | ) : (
39 |
40 | )}
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/RelevanceScore.tsx:
--------------------------------------------------------------------------------
1 | import CircularProgress from '@/components/ui/circular-progress';
2 | import { Progress } from '@/components/ui/progress';
3 | import { Meeting } from '@/domain/entities/calendar/CalendarTypes';
4 | import { COLOR_SCHEMES } from '@/lib/utils';
5 |
6 | interface RelevanceScoreProps {
7 | meeting: Meeting;
8 | type: 'bar' | 'circle';
9 | }
10 |
11 | export function RelevanceScore({ meeting, type }: RelevanceScoreProps) {
12 | const isBar = type === 'bar';
13 | const relevanceScore = isBar
14 | ? Math.round(meeting.similarity! * 10000) / 100
15 | : Math.round(meeting.similarity! * 100);
16 |
17 | const strokeColors = COLOR_SCHEMES[meeting.color].stroke;
18 |
19 | if (isBar) {
20 | return (
21 |
22 |
23 |
{relevanceScore.toFixed(2)}%
24 |
25 | );
26 | } else {
27 | return (
28 | progress}
37 | labelClassName="text-xs font-medium"
38 | />
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/home/features/FeatureCard.tsx:
--------------------------------------------------------------------------------
1 | import { LucideIcon } from 'lucide-react';
2 | import { ReactNode } from 'react';
3 |
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardHeader,
9 | CardTitle,
10 | } from '@/components/ui/card';
11 |
12 | interface FeatureCardProps {
13 | icon: LucideIcon;
14 | title: string;
15 | description: string;
16 | children: ReactNode;
17 | }
18 |
19 | export default function FeatureCard({
20 | icon: Icon,
21 | title,
22 | description,
23 | children,
24 | }: FeatureCardProps) {
25 | return (
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {title}
34 |
35 |
36 | {description}
37 |
38 |
39 |
40 | {children}
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/calendar/CalendarHeader/TodayButton.tsx:
--------------------------------------------------------------------------------
1 | import { format } from 'date-fns';
2 | import { motion } from 'framer-motion';
3 | import React from 'react';
4 |
5 | import { Button } from '@/components/ui/button';
6 | import { buttonHover, transition } from '@/domain/animations';
7 | import { getCurrentMonthRange } from '@/operations/meeting/CalendarHelpers';
8 |
9 | const MotionButton = motion.create(Button);
10 |
11 | export function TodayButton() {
12 | const { now } = getCurrentMonthRange();
13 |
14 | return (
15 |
23 |
29 | {format(now, 'MMM').toUpperCase()}
30 |
31 |
37 | {now.getDate()}
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/app/chat/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 |
3 | import { ChatProvider } from '@/app/chat/ChatContext';
4 | import ChatScrollContainer from '@/components/Chat/ChatScrollContainer';
5 | import ChatSidebar from '@/components/Chat/ChatSidebar';
6 | import {
7 | SidebarInset,
8 | SidebarProvider,
9 | SidebarTrigger,
10 | } from '@/components/ui/sidebar';
11 | import {
12 | Tooltip,
13 | TooltipContent,
14 | TooltipTrigger,
15 | } from '@/components/ui/tooltip';
16 |
17 | export const metadata: Metadata = {
18 | title: 'Chat - OpenEU',
19 | description: 'AI-powered chat interface',
20 | };
21 |
22 | export default function ChatLayout({
23 | children,
24 | }: {
25 | children: React.ReactNode;
26 | }) {
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | Toggle (⌘B)
39 |
40 |
41 |
42 | {children}
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/calendar/CalendarSkeleton/WeekViewSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton';
2 |
3 | export function WeekViewSkeleton() {
4 | return (
5 |
6 |
7 |
8 | {Array.from({ length: 7 }).map((_, i) => (
9 |
13 |
14 |
15 |
16 | ))}
17 |
18 |
19 |
20 |
21 | {Array.from({ length: 12 }).map((_, i) => (
22 |
23 |
24 |
25 | ))}
26 |
27 |
28 |
29 | {Array.from({ length: 7 }).map((_, dayIndex) => (
30 |
31 | {Array.from({ length: 12 }).map((_, hourIndex) => (
32 |
33 | ))}
34 |
35 | ))}
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/navigation/MobileNav.tsx:
--------------------------------------------------------------------------------
1 | import { ChevronsUpDown } from 'lucide-react';
2 | import Link from 'next/link';
3 |
4 | import { Button, buttonVariants } from '@/components/ui/button';
5 | import {
6 | Collapsible,
7 | CollapsibleContent,
8 | CollapsibleTrigger,
9 | } from '@/components/ui/collapsible';
10 | import { NavItemType } from '@/domain/entities/navbar/NavItemType';
11 | import { cn } from '@/lib/utils';
12 |
13 | function MobileNavLink({ item }: { item: NavItemType }) {
14 | return (
15 |
22 | {item.title}
23 |
24 | );
25 | }
26 |
27 | export function MobileNavItem({ item }: { item: NavItemType }) {
28 | if ('items' in item && item.items) {
29 | return (
30 |
31 |
32 |
33 | {item.title}
34 |
35 |
36 |
37 |
38 | {item.items.map((subItem) => (
39 |
40 | ))}
41 |
42 |
43 | );
44 | }
45 |
46 | return ;
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/inbox/NewsletterDialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { format, parseISO } from 'date-fns';
4 |
5 | import {
6 | Dialog,
7 | DialogContent,
8 | DialogDescription,
9 | DialogHeader,
10 | DialogTitle,
11 | } from '@/components/ui/dialog';
12 | import { InboxItem } from '@/domain/entities/inbox-item/inbox-item';
13 |
14 | interface NewsletterDialogProps {
15 | item: InboxItem | null;
16 | open: boolean;
17 | onOpenChange: (open: boolean) => void;
18 | }
19 |
20 | export function NewsletterDialog({
21 | item,
22 | open,
23 | onOpenChange,
24 | }: NewsletterDialogProps) {
25 | if (!item) return null;
26 |
27 | const formattedDate = format(parseISO(item.date), 'PPP');
28 |
29 | return (
30 |
31 |
32 |
33 | {item.title}
34 |
35 | {formattedDate}
36 | •
37 | {item.country}
38 |
39 |
40 | {item.message ? (
41 |
45 | ) : (
46 | No content available
47 | )}
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/onboarding/Step2EntrepreneurRoleDetails.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { motion } from 'framer-motion';
4 | import React from 'react';
5 | import { UseFormReturn } from 'react-hook-form';
6 | import z from 'zod';
7 |
8 | import { EntrepreneurRoleForm } from '@/components/profile/forms/EntrepreneurRoleForm';
9 | import {
10 | Card,
11 | CardContent,
12 | CardDescription,
13 | CardHeader,
14 | CardTitle,
15 | } from '@/components/ui/card';
16 | import {
17 | entrepreneurRoleSchema,
18 | onboardingSchema,
19 | } from '@/domain/schemas/profile';
20 |
21 | interface Step2EntrepreneurRoleDetailsProps {
22 | form: UseFormReturn>;
23 | }
24 |
25 | export default function Step2EntrepreneurRoleDetails({
26 | form,
27 | }: Step2EntrepreneurRoleDetailsProps) {
28 | return (
29 |
30 |
31 | Tell us about your business
32 |
33 | Help us understand your company and industry
34 |
35 |
36 |
37 |
42 |
46 | >
47 | }
48 | />
49 |
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/domain/hooks/use-debounced-search.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useQuery } from '@tanstack/react-query';
4 | import { useState } from 'react';
5 |
6 | import { useDebounce } from './use-debounce';
7 |
8 | export interface SearchConfig {
9 | minQueryLength?: number;
10 | debounceDelay?: number;
11 | staleTime?: number;
12 | gcTime?: number;
13 | }
14 |
15 | export const useDebouncedSearch = (
16 | fetchFn: (query: string) => Promise,
17 | config: SearchConfig = {},
18 | ) => {
19 | const { minQueryLength = 2, debounceDelay = 300 } = config;
20 |
21 | const [searchTerm, setSearchTerm] = useState('');
22 | const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
23 |
24 | const debouncedSetSearch = useDebounce((term: string) => {
25 | setDebouncedSearchTerm(term);
26 | }, debounceDelay);
27 |
28 | const {
29 | data: results = [],
30 | isLoading,
31 | error,
32 | isFetching,
33 | } = useQuery({
34 | queryKey: ['search', fetchFn.name, debouncedSearchTerm],
35 | queryFn: () => fetchFn(debouncedSearchTerm),
36 | enabled: debouncedSearchTerm.length >= minQueryLength,
37 | });
38 |
39 | const search = (query: string) => {
40 | setSearchTerm(query);
41 | if (query.length >= minQueryLength) {
42 | debouncedSetSearch(query);
43 | } else {
44 | setDebouncedSearchTerm('');
45 | }
46 | };
47 |
48 | const clearResults = () => {
49 | setSearchTerm('');
50 | setDebouncedSearchTerm('');
51 | };
52 |
53 | return {
54 | results,
55 | isLoading: isLoading || isFetching,
56 | error: error?.message || null,
57 | search,
58 | clearResults,
59 | searchTerm,
60 | debouncedSearchTerm,
61 | };
62 | };
63 |
--------------------------------------------------------------------------------
/src/components/navigation/AuthAwareNavContent.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useRouter } from 'next/navigation';
4 |
5 | import { NotificationsPopover } from '@/components/navigation/NotificationsPopover';
6 | import { ProfilePopover } from '@/components/navigation/ProfilePopover';
7 | import { SettingsPopover } from '@/components/navigation/SettingsPopover';
8 | import { Button } from '@/components/ui/button';
9 | import { Skeleton } from '@/components/ui/skeleton';
10 | import { useAuth } from '@/domain/hooks/useAuth';
11 |
12 | interface AuthAwareNavContentProps {
13 | initialIsAuthenticated: boolean;
14 | }
15 |
16 | export function AuthAwareNavContent({
17 | initialIsAuthenticated,
18 | }: AuthAwareNavContentProps) {
19 | const { user, loading } = useAuth();
20 | const router = useRouter();
21 |
22 | const isAuthenticated = loading ? initialIsAuthenticated : !!user;
23 |
24 | const handleSignIn = () => {
25 | router.push('/login');
26 | };
27 |
28 | if (loading) {
29 | return (
30 |
31 |
32 |
33 |
34 |
35 | );
36 | }
37 |
38 | if (!isAuthenticated) {
39 | return (
40 |
41 |
42 |
43 | Sign In
44 |
45 |
46 | );
47 | }
48 |
49 | return (
50 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TogglePrimitive from "@radix-ui/react-toggle"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const toggleVariants = cva(
10 | "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
17 | },
18 | size: {
19 | default: "h-9 px-2 min-w-9",
20 | sm: "h-8 px-1.5 min-w-8",
21 | lg: "h-10 px-2.5 min-w-10",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | }
29 | )
30 |
31 | function Toggle({
32 | className,
33 | variant,
34 | size,
35 | ...props
36 | }: React.ComponentProps &
37 | VariantProps) {
38 | return (
39 |
44 | )
45 | }
46 |
47 | export { Toggle, toggleVariants }
--------------------------------------------------------------------------------
/src/components/profile/forms/CompletionForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { UseFormReturn } from 'react-hook-form';
5 | import { z } from 'zod';
6 |
7 | import {
8 | Form,
9 | FormControl,
10 | FormField,
11 | FormItem,
12 | FormLabel,
13 | FormMessage,
14 | } from '@/components/ui/form';
15 | import {
16 | Select,
17 | SelectContent,
18 | SelectItem,
19 | SelectTrigger,
20 | SelectValue,
21 | } from '@/components/ui/select';
22 | import { completionSchema } from '@/domain/schemas/profile';
23 |
24 | interface CompletionFormProps {
25 | form: UseFormReturn>;
26 | }
27 |
28 | export function CompletionForm({ form }: CompletionFormProps) {
29 | return (
30 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/Chat/ChatInputCard.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { KeyboardEvent, useState } from 'react';
4 |
5 | import { useChatContext } from '@/app/chat/ChatContext';
6 | import { ChatToolbar } from '@/components/Chat/ChatToolbar';
7 | import { ContextBadge } from '@/components/Chat/ContextBadge';
8 | import { Card, CardContent } from '@/components/ui/card';
9 | import { Textarea } from '@/components/ui/textarea';
10 |
11 | export default function ChatInputCard() {
12 | const [input, setInput] = useState('');
13 |
14 | const { sendMessage, context } = useChatContext();
15 |
16 | const handleSubmit = () => {
17 | const trimmedValue = input.trim();
18 | if (!trimmedValue) return;
19 |
20 | sendMessage(trimmedValue);
21 | setInput('');
22 | };
23 |
24 | const handleKeyDown = (e: KeyboardEvent) => {
25 | if (e.key === 'Enter' && !e.shiftKey) {
26 | e.preventDefault();
27 | handleSubmit();
28 | }
29 | };
30 |
31 | return (
32 |
33 |
34 | {context && (
35 |
36 |
37 |
38 | )}
39 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/lib/dal.ts:
--------------------------------------------------------------------------------
1 | import 'server-only';
2 |
3 | import { cache } from 'react';
4 |
5 | import { createClient } from '@/lib/supabase/server';
6 |
7 | /**
8 | * Server-side authentication Data Access Layer (DAL).
9 | * Single source of truth for auth operations in server components/actions.
10 | * @module auth/dal
11 | */
12 |
13 | /**
14 | * Gets current user without session validation.
15 | * @returns {Promise} Current user or null
16 | * @example
17 | * const user = await getUser();
18 | * if (user) await fetchUserData(user.id);
19 | */
20 | export const getUser = cache(async () => {
21 | const supabase = await createClient();
22 |
23 | const {
24 | data: { user },
25 | error,
26 | } = await supabase.auth.getUser();
27 |
28 | if (error || !user) {
29 | return null;
30 | }
31 |
32 | return user;
33 | });
34 |
35 | /**
36 | * Validates session and returns auth status with user.
37 | * Use for conditional auth checks without throwing.
38 | * @returns {Promise<{ isAuth: boolean; user: User | null }>}
39 | * @example
40 | * const { isAuth, user } = await verifySession();
41 | * return isAuth ? : ;
42 | */
43 | export const verifySession = cache(async () => {
44 | const user = await getUser();
45 | return user ? { isAuth: true, user } : null;
46 | });
47 |
48 | /**
49 | * Protected route/action guard.
50 | * @throws {AuthError} If not authenticated
51 | * @example
52 | * async function createPost() {
53 | * await requireAuth();
54 | * const user = await getUser(); // Guaranteed to exist
55 | * }
56 | */
57 | export async function requireAuth() {
58 | const session = await verifySession();
59 | if (!session) {
60 | throw new Error('Unauthorized');
61 | }
62 | return session;
63 | }
64 |
--------------------------------------------------------------------------------
/src/operations/filter-modal/FilterModalOperations.ts:
--------------------------------------------------------------------------------
1 | import { FilterModalState } from '@/domain/entities/FilterModalState';
2 | import { getCurrentWeekRange } from '@/lib/formatters';
3 | import {
4 | getCurrentMonthRange,
5 | MEETING_TYPE_MAPPING,
6 | } from '@/operations/meeting/CalendarHelpers';
7 | const { now } = getCurrentMonthRange();
8 |
9 | export default class FilterModalOperations {
10 | static getCountries(): string[] {
11 | return [
12 | 'Austria',
13 | 'Belgium',
14 | 'Bulgaria',
15 | 'Croatia',
16 | 'Cyprus',
17 | 'Czech Republic',
18 | 'Denmark',
19 | 'Estonia',
20 | 'European Union',
21 | 'Finland',
22 | 'France',
23 | 'Germany',
24 | 'Greece',
25 | 'Hungary',
26 | 'Ireland',
27 | 'Italy',
28 | 'Latvia',
29 | 'Lithuania',
30 | 'Luxembourg',
31 | 'Malta',
32 | 'Netherlands',
33 | 'Poland',
34 | 'Portugal',
35 | 'Romania',
36 | 'Slovakia',
37 | 'Slovenia',
38 | 'Spain',
39 | 'Sweden',
40 | ];
41 | }
42 |
43 | static getDefaultState(useWeekDefault = false): FilterModalState {
44 | const { startDate, endDate } = getCurrentWeekRange();
45 | return {
46 | startDate: useWeekDefault ? startDate : now,
47 | endDate: useWeekDefault ? endDate : now,
48 | countries: [],
49 | topics: [],
50 | institutions: [],
51 | };
52 | }
53 |
54 | static getInstitutions(): { label: string; value: string }[] {
55 | return Object.values(MEETING_TYPE_MAPPING).map((institution) => ({
56 | label: institution,
57 | value: institution,
58 | }));
59 | }
60 |
61 | static validateDateRange(startDate: Date, endDate: Date): boolean {
62 | return endDate > startDate;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/SampleCard/SampleComponent.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { User } from '@supabase/supabase-js';
4 | import Link from 'next/link';
5 | import React from 'react';
6 |
7 | import { Button } from '@/components/ui/button';
8 | import {
9 | Card,
10 | CardContent,
11 | CardDescription,
12 | CardFooter,
13 | CardHeader,
14 | CardTitle,
15 | } from '@/components/ui/card';
16 | import { createClient } from '@/lib/supabase/client';
17 |
18 | interface SampleComponentProps {
19 | email: string;
20 | data: { user: User } | { user: null };
21 | }
22 |
23 | function SampleComponent({ email, data }: SampleComponentProps) {
24 | const supabase = createClient();
25 |
26 | const signOut = async () => {
27 | await supabase.auth.signOut();
28 | window.location.reload();
29 | };
30 |
31 | return (
32 |
33 |
34 |
35 | You are logged in as {email}
36 |
37 |
38 | Use the link or button below to login/logout
39 |
40 |
41 |
42 |
43 | This is additional content inside the card. You can customize it
44 | further.
45 |
46 |
47 |
48 | {!data?.user && Login}
49 | {data?.user && signOut()}>Logout }
50 |
51 |
52 | );
53 | }
54 |
55 | export default SampleComponent;
56 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Popover({
9 | ...props
10 | }: React.ComponentProps) {
11 | return
12 | }
13 |
14 | function PopoverTrigger({
15 | ...props
16 | }: React.ComponentProps) {
17 | return
18 | }
19 |
20 | function PopoverContent({
21 | className,
22 | align = "center",
23 | sideOffset = 4,
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
28 |
38 |
39 | )
40 | }
41 |
42 | function PopoverAnchor({
43 | ...props
44 | }: React.ComponentProps) {
45 | return
46 | }
47 |
48 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
49 |
--------------------------------------------------------------------------------
/src/app/chat/[sessionId]/error.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { AlertCircle, Home, RotateCcw } from 'lucide-react';
4 | import Link from 'next/link';
5 |
6 | import { Button } from '@/components/ui/button';
7 | import {
8 | Card,
9 | CardContent,
10 | CardDescription,
11 | CardHeader,
12 | CardTitle,
13 | } from '@/components/ui/card';
14 | import { Separator } from '@/components/ui/separator';
15 |
16 | export default function Error({
17 | reset,
18 | }: {
19 | error: Error & { digest?: string };
20 | reset: () => void;
21 | }) {
22 | return (
23 |
24 |
25 |
26 |
29 | Chat Session Not Found
30 |
31 | The chat session you're looking for doesn't exist or may
32 | have been deleted.
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | Try Again
41 |
42 |
43 |
44 |
45 | Start New Chat
46 |
47 |
48 |
49 |
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/operations/toast/toastOperations.tsx:
--------------------------------------------------------------------------------
1 | import { toast } from 'sonner';
2 |
3 | enum ToastType {
4 | INFO = 'info',
5 | WARNING = 'warning',
6 | ERROR = 'error',
7 | SUCCESS = 'success',
8 | }
9 |
10 | interface ToastConfig {
11 | title: React.ReactNode | string;
12 | message: React.ReactNode | string;
13 | }
14 |
15 | const TOAST_STYLES: Record = {
16 | [ToastType.INFO]:
17 | '!bg-sky-50 !text-sky-800 border !border-sky-200 dark:!bg-sky-950 dark:!text-sky-50 dark:!border-sky-800',
18 | [ToastType.WARNING]:
19 | '!bg-yellow-50 !text-yellow-700 border !border-yellow-400 dark:!bg-yellow-950 dark:!text-yellow-100 dark:!border-yellow-800',
20 | [ToastType.ERROR]:
21 | '!bg-red-50 !text-red-800 border !border-red-200 dark:!bg-red-950 dark:!text-red-50 dark:!border-red-800',
22 | [ToastType.SUCCESS]:
23 | '!bg-green-50 !text-green-800 border !border-green-200 dark:!bg-green-950 dark:!text-green-50 dark:!border-green-800',
24 | };
25 |
26 | export class ToastOperations {
27 | private static show(type: ToastType, { title, message }: ToastConfig): void {
28 | toast[type](title, {
29 | description: message,
30 | className: `!items-start [&>div:first-child]:!mt-[2px] !font-bold ${TOAST_STYLES[type]}`,
31 | descriptionClassName: '!text-inherit',
32 | });
33 | }
34 |
35 | static showInfo({ title, message }: ToastConfig): void {
36 | this.show(ToastType.INFO, { title, message });
37 | }
38 |
39 | static showWarning({ title, message }: ToastConfig): void {
40 | this.show(ToastType.WARNING, { title, message });
41 | }
42 |
43 | static showError({ title, message }: ToastConfig): void {
44 | this.show(ToastType.ERROR, { title, message });
45 | }
46 |
47 | static showSuccess({ title, message }: ToastConfig): void {
48 | this.show(ToastType.SUCCESS, { title, message });
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function ScrollArea({
9 | className,
10 | children,
11 | ...props
12 | }: React.ComponentProps) {
13 | return (
14 |
19 |
23 | {children}
24 |
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | function ScrollBar({
32 | className,
33 | orientation = "vertical",
34 | ...props
35 | }: React.ComponentProps) {
36 | return (
37 |
50 |
54 |
55 | )
56 | }
57 |
58 | export { ScrollArea, ScrollBar }
59 |
--------------------------------------------------------------------------------
/src/app/register/page.tsx:
--------------------------------------------------------------------------------
1 | import { AlertCircleIcon } from 'lucide-react';
2 |
3 | import { RegisterForm } from '@/components/auth/RegisterForm';
4 | import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
5 |
6 | export default async function Page(props: {
7 | searchParams?: Promise<{ error?: string }>;
8 | }) {
9 | const searchParams = await props.searchParams;
10 | const error = decodeURIComponent(searchParams?.error || '');
11 | return (
12 |
13 |
14 |
15 |
16 | OpenEU
17 |
18 |
19 | OpenEU transforms how citizens, businesses, and organizations engage
20 | with and participate in the EU, ensuring transparency and
21 | accessibility across all member states.
22 |
23 |
24 |
25 |
26 |
27 |
28 | {error !== '' && (
29 |
30 |
31 |
32 | Sign up failed, please try again.
33 |
34 | {error}
35 |
36 |
37 |
38 | )}
39 |
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const badgeVariants = cva(
8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14 | secondary:
15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16 | destructive:
17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18 | outline:
19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20 | subtle:
21 | "text-foreground border-gray-300 [a&]:hover:bg-accent [a&]:hover:text-accent-foreground"
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | },
27 | }
28 | )
29 |
30 | function Badge({
31 | className,
32 | variant,
33 | asChild = false,
34 | ...props
35 | }: React.ComponentProps<"span"> &
36 | VariantProps & { asChild?: boolean }) {
37 | const Comp = asChild ? Slot : "span"
38 |
39 | return (
40 |
45 | )
46 | }
47 |
48 | export { Badge, badgeVariants }
49 |
--------------------------------------------------------------------------------
/src/app/monitor/columns.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ColumnDef } from '@tanstack/react-table';
4 |
5 | import { LegislativeFile } from '@/domain/entities/monitor/generated-types';
6 | import MonitorOperations from '@/operations/monitor/MonitorOperations';
7 |
8 | export const kanbanColumns: ColumnDef[] = [
9 | {
10 | accessorKey: 'title',
11 | id: 'title',
12 | header: 'Title',
13 | enableSorting: true,
14 | },
15 | {
16 | accessorKey: 'id',
17 | id: 'year',
18 | header: 'Year',
19 | enableSorting: true,
20 | accessorFn: (row) => {
21 | return MonitorOperations.extractYearFromId(row.id);
22 | },
23 | },
24 | {
25 | accessorKey: 'lastpubdate',
26 | id: 'lastpubdate',
27 | header: 'Last Publication Date',
28 | enableSorting: true,
29 | },
30 | {
31 | accessorKey: 'committee',
32 | id: 'committee',
33 | header: 'Committee',
34 | enableSorting: true,
35 | },
36 | {
37 | accessorKey: 'rapporteur',
38 | id: 'rapporteur',
39 | header: 'Rapporteur',
40 | enableSorting: false,
41 | },
42 | {
43 | accessorKey: 'status',
44 | id: 'status',
45 | header: 'Status',
46 | enableSorting: false,
47 | },
48 | {
49 | accessorKey: 'subjects',
50 | id: 'subjects',
51 | header: 'Subjects',
52 | enableSorting: false,
53 | },
54 | {
55 | accessorKey: 'key_players',
56 | id: 'key_players',
57 | header: 'Key Players',
58 | enableSorting: false,
59 | },
60 | {
61 | accessorKey: 'key_events',
62 | id: 'key_events',
63 | header: 'Key Events',
64 | enableSorting: false,
65 | },
66 | {
67 | accessorKey: 'documentation_gateway',
68 | id: 'documentation_gateway',
69 | header: 'Documentation',
70 | enableSorting: false,
71 | },
72 | {
73 | accessorKey: 'similarity',
74 | id: 'similarity',
75 | header: 'Similarity',
76 | enableSorting: false,
77 | },
78 | ];
79 |
--------------------------------------------------------------------------------
/src/domain/actions/auth.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { setCookie } from 'cookies-next';
4 | import { revalidatePath } from 'next/cache';
5 | import { cookies } from 'next/headers';
6 | import { headers } from 'next/headers';
7 | import { redirect } from 'next/navigation';
8 |
9 | import { LoginFormData, RegisterFormData } from '@/domain/schemas/auth';
10 | import { createClient } from '@/lib/supabase/server';
11 |
12 | async function getCurrentURL() {
13 | const headersList = await headers();
14 | const host = headersList.get('host');
15 | const protocol = headersList.get('x-forwarded-proto') ?? 'http';
16 | return `${protocol}://${host}/`;
17 | }
18 |
19 | export async function signup(formData: RegisterFormData) {
20 | const supabase = await createClient();
21 | const url = await getCurrentURL();
22 |
23 | const { error, data } = await supabase.auth.signUp({
24 | email: formData.email,
25 | password: formData.password,
26 | options: {
27 | data: {
28 | first_name: formData.name.trim(),
29 | last_name: formData.surname.trim(),
30 | },
31 | emailRedirectTo: `${url}auth/confirm`,
32 | },
33 | });
34 |
35 | if (error) {
36 | redirect('/register?error=' + error.message);
37 | }
38 |
39 | if (data.session) {
40 | setCookie('token', data.session.access_token, { cookies });
41 | }
42 |
43 | revalidatePath('/', 'layout');
44 | redirect('/login?confirm=1');
45 | }
46 |
47 | export async function login(formData: LoginFormData) {
48 | const supabase = await createClient();
49 |
50 | const { error, data } = await supabase.auth.signInWithPassword({
51 | email: formData.email,
52 | password: formData.password,
53 | });
54 |
55 | if (error) {
56 | redirect('/login?error=' + error.message);
57 | }
58 |
59 | if (data.session) {
60 | setCookie('token', data.session.access_token, { cookies });
61 | }
62 |
63 | revalidatePath('/', 'layout');
64 | redirect('/');
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/Chat/ChatInterface.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useLayoutEffect, useRef } from 'react';
4 |
5 | import { useChatContext } from '@/app/chat/ChatContext';
6 |
7 | import { AIResponseSkeleton } from './AIResponseSkeleton';
8 | import { ChatMessage } from './ChatMessage';
9 | import { useScrollToBottomButton } from './ChatScrollContainer';
10 | import { StreamingMarkdown } from './StreamingMarkdown';
11 |
12 | export default function ChatInterface() {
13 | const { messages, streamingMessage, isLoading = false } = useChatContext();
14 | const { scrollToBottomAfterRender } = useScrollToBottomButton();
15 | const prevMessageCountRef = useRef(messages.length);
16 | const isInitialLoadRef = useRef(true);
17 |
18 | useLayoutEffect(() => {
19 | const currentMessageCount = messages.length;
20 | const hasNewMessages = currentMessageCount > prevMessageCountRef.current;
21 |
22 | if (isInitialLoadRef.current || hasNewMessages || isLoading) {
23 | scrollToBottomAfterRender();
24 | isInitialLoadRef.current = false;
25 | }
26 |
27 | prevMessageCountRef.current = currentMessageCount;
28 | }, [messages, scrollToBottomAfterRender, isLoading]);
29 |
30 | return (
31 |
32 | {messages.map((message) => (
33 |
34 | ))}
35 |
36 | {/* Show skeleton when loading but no streaming message yet */}
37 | {isLoading && !streamingMessage && (
38 |
41 | )}
42 |
43 | {/* Show streaming message when it exists */}
44 | {streamingMessage && (
45 |
46 |
47 |
48 | )}
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/inbox/ViewOptions.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Table } from '@tanstack/react-table';
4 | import { Settings2 } from 'lucide-react';
5 |
6 | import { Button } from '@/components/ui/button';
7 | import {
8 | DropdownMenu,
9 | DropdownMenuCheckboxItem,
10 | DropdownMenuContent,
11 | DropdownMenuLabel,
12 | DropdownMenuSeparator,
13 | DropdownMenuTrigger,
14 | } from '@/components/ui/dropdown-menu';
15 | import ToolbarOperations from '@/operations/inbox/ToolbarOperations';
16 |
17 | interface DataTableViewOptionsProps {
18 | table: Table;
19 | }
20 |
21 | export function DataTableViewOptions({
22 | table,
23 | }: DataTableViewOptionsProps) {
24 | return (
25 |
26 |
27 |
32 |
33 | View
34 |
35 |
36 |
37 | Toggle columns
38 |
39 | {table
40 | .getAllColumns()
41 | .filter(
42 | (column) =>
43 | typeof column.accessorFn !== 'undefined' && column.getCanHide(),
44 | )
45 | .map((column) => {
46 | return (
47 | column.toggleVisibility(!!value)}
52 | >
53 | {ToolbarOperations.formatColumnDisplayName(column.id)}
54 |
55 | );
56 | })}
57 |
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/src/domain/actions/chat-actions.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { cookies } from 'next/headers';
4 |
5 | import { requireAuth } from '@/lib/dal';
6 | import { ToastOperations } from '@/operations/toast/toastOperations';
7 |
8 | import {
9 | CreateSessionRequest,
10 | CreateSessionResponse,
11 | } from '../entities/chat/generated-types';
12 |
13 | export async function createChatSession(
14 | data: Omit,
15 | ): Promise {
16 | try {
17 | // Require authentication and get user
18 | const { user } = await requireAuth();
19 | const token = (await cookies()).get('token')?.value;
20 |
21 | const requestData: CreateSessionRequest = {
22 | ...data,
23 | user_id: user.id, // Use real authenticated user ID
24 | };
25 |
26 | const response = await fetch(
27 | `${process.env.NEXT_PUBLIC_API_URL}/chat/start`,
28 | {
29 | method: 'POST',
30 | mode: 'cors',
31 | headers: {
32 | 'Content-Type': 'application/json',
33 | Authorization: `Bearer ${token}`,
34 | },
35 | body: JSON.stringify(requestData),
36 | },
37 | );
38 |
39 | if (!response.ok) {
40 | // Get the response text for better error debugging
41 | const errorText = await response.text();
42 | ToastOperations.showError({
43 | title: 'API Error',
44 | message: `Failed to create session: ${response.status} ${response.statusText}`,
45 | });
46 |
47 | throw new Error(
48 | `Failed to create session: ${response.status} ${response.statusText} - ${errorText}`,
49 | );
50 | }
51 |
52 | const session = await response.json();
53 |
54 | return session;
55 | } catch (error) {
56 | ToastOperations.showError({
57 | title: 'Session Creation Failed',
58 | message:
59 | error instanceof Error ? error.message : 'An unexpected error occurred',
60 | });
61 | throw error;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/operations/home/CalendarOperations.ts:
--------------------------------------------------------------------------------
1 | import { getDay } from 'date-fns';
2 |
3 | interface CalendarEvent {
4 | weekdayIndex: number;
5 | title: string;
6 | type: 'urgent' | 'normal';
7 | }
8 |
9 | export default class CalendarOperations {
10 | static getCalendarEvents(): CalendarEvent[] {
11 | const currentWeekday = getDay(new Date());
12 |
13 | const baseEvents = [
14 | { weekdayIndex: 1, title: 'Digital Markets Act Update', type: 'urgent' },
15 | { weekdayIndex: 3, title: 'AI Ethics Guidelines', type: 'normal' },
16 | { weekdayIndex: 5, title: 'Data Protection Summit', type: 'normal' },
17 | ] as CalendarEvent[];
18 |
19 | const filteredBaseEvents = baseEvents.filter(
20 | (event) => event.weekdayIndex !== currentWeekday,
21 | );
22 |
23 | return [
24 | {
25 | weekdayIndex: currentWeekday,
26 | title: 'Today: EU Policy Brief',
27 | type: 'urgent',
28 | },
29 | ...filteredBaseEvents,
30 | ];
31 | }
32 |
33 | private static getWeekdayIndex(date: number): number {
34 | const targetDate = new Date();
35 | targetDate.setDate(date);
36 | return getDay(targetDate);
37 | }
38 |
39 | static getEventsForDate(date: number): CalendarEvent[] {
40 | const weekdayIndex = this.getWeekdayIndex(date);
41 | return this.getCalendarEvents().filter(
42 | (event) => event.weekdayIndex === weekdayIndex,
43 | );
44 | }
45 |
46 | static hasEventOnDate(date: number): boolean {
47 | const weekdayIndex = this.getWeekdayIndex(date);
48 | return this.getCalendarEvents().some(
49 | (event) => event.weekdayIndex === weekdayIndex,
50 | );
51 | }
52 |
53 | static getEventTypeForDate(date: number): 'urgent' | 'normal' | undefined {
54 | const weekdayIndex = this.getWeekdayIndex(date);
55 | return this.getCalendarEvents().find(
56 | (event) => event.weekdayIndex === weekdayIndex,
57 | )?.type;
58 | }
59 | }
60 |
61 | export type { CalendarEvent };
62 |
--------------------------------------------------------------------------------
/src/domain/actions/profile.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { cookies } from 'next/headers';
4 | import { redirect } from 'next/navigation';
5 |
6 | import { Profile } from '@/domain/entities/profile/generated-types';
7 | import { createClient } from '@/lib/supabase/server';
8 |
9 | const API_BASE_URL =
10 | process.env.NEXT_PUBLIC_API_URL || 'https://openeu-backend-1.onrender.com';
11 |
12 | export async function updatePassword(password: string) {
13 | const client = await createClient();
14 | return client.auth.updateUser({ password });
15 | }
16 |
17 | export async function linkGoogleAccount() {
18 | const client = await createClient();
19 | const { data, error } = await client.auth.linkIdentity({
20 | provider: 'google',
21 | });
22 |
23 | if (!data && error) {
24 | throw error;
25 | }
26 | redirect(data.url!);
27 | }
28 |
29 | export async function unlinkGoogleAccount() {
30 | const client = await createClient();
31 | // retrieve all identities linked to a user
32 | const { data: identities, error: identitiesError } =
33 | await client.auth.getUserIdentities();
34 | if (!identitiesError) {
35 | // find the google identity linked to the user
36 | const googleIdentity = identities.identities.find(
37 | (identity) => identity.provider === 'google',
38 | );
39 | if (googleIdentity) {
40 | // unlink the google identity from the user
41 | return await client.auth.unlinkIdentity(googleIdentity);
42 | }
43 | }
44 | }
45 |
46 | export async function getProfile(profileId: string): Promise {
47 | const cookieStore = await cookies();
48 | const token = cookieStore.get('token')?.value;
49 |
50 | const res = await fetch(`${API_BASE_URL}/profile/${profileId}`, {
51 | method: 'GET',
52 | headers: {
53 | 'Content-Type': 'application/json',
54 | Authorization: `Bearer ${token}`,
55 | },
56 | });
57 |
58 | if (!res.ok) {
59 | return null;
60 | }
61 | return await res.json();
62 | }
63 |
--------------------------------------------------------------------------------
/src/app/login/page.tsx:
--------------------------------------------------------------------------------
1 | import { AlertCircleIcon, CheckCircleIcon } from 'lucide-react';
2 |
3 | import { LoginForm } from '@/components/auth/LoginForm';
4 | import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
5 |
6 | export default async function Page(props: {
7 | searchParams?: Promise<{ error?: string; confirm?: string }>;
8 | }) {
9 | const searchParams = await props.searchParams;
10 | const error = decodeURIComponent(searchParams?.error || '');
11 | const confirm = searchParams?.confirm || '';
12 | return (
13 |
14 |
15 | {error !== '' && (
16 |
17 |
18 |
19 | Sign in failed, please try again.
20 |
21 | {error}
22 |
23 |
24 |
25 | )}
26 | {confirm !== '' && (
27 |
28 |
29 |
30 |
31 | {confirm === '1' ? 'Sign up successful.' : 'Account activated.'}
32 |
33 |
34 |
35 | {confirm === '1'
36 | ? 'Please confirm your address by clicking on the link in the E-Mail we just sent you. Afterwards you can login into your account.'
37 | : 'Your account is now active. Please login with the credentials you provided during registration.'}
38 |
39 |
40 |
41 |
42 | )}
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/src/domain/entities/monitor/generated-types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Auto-generated monitor 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, operations } from '@/lib/api-types';
8 |
9 | // === API TYPES (truly generated) ===
10 | export type LegislativeFilesParams =
11 | operations['get_legislative_files_legislative_files_get']['parameters']['query'];
12 | export type LegislativeSuggestionsParams =
13 | operations['get_legislation_suggestions_legislative_files_suggestions_get']['parameters']['query'];
14 | export type LegislativeFileParams =
15 | operations['get_legislative_file_legislative_file_get']['parameters']['query'];
16 | export type LegislativeMeetingsParams =
17 | operations['get_meetings_by_legislative_id_legislative_files_meetings_get']['parameters']['query'];
18 |
19 | export type LegislativeFilesResponse =
20 | components['schemas']['LegislativeFilesResponse'];
21 | export type LegislativeFileResponse =
22 | components['schemas']['LegislativeFileResponse'];
23 | export type LegislativeFileSuggestionResponse =
24 | components['schemas']['LegislativeFileSuggestionResponse'];
25 | export type LegislativeMeetingsResponse =
26 | components['schemas']['LegislativeMeetingsResponse'];
27 | export type SubscriptionResponse =
28 | components['schemas']['SubscriptionResponse'];
29 |
30 | export type LegislativeFile = components['schemas']['LegislativeFile'];
31 | export type LegislativeFileSuggestion =
32 | components['schemas']['LegislativeFileSuggestion'];
33 | export type KeyEvent = components['schemas']['KeyEvent'];
34 | export type KeyPlayer = components['schemas']['KeyPlayer'];
35 | export type Rapporteur = components['schemas']['Rapporteur'];
36 | export type LegislativeMeeting = components['schemas']['LegislativeMeeting'];
37 | export type LegislativeUniqueValues =
38 | components['schemas']['LegislativeFileUniqueValuesResponse'];
39 | export type DocumentData = components['schemas']['DocumentationGateway'];
40 |
--------------------------------------------------------------------------------
/src/components/navigation/NavItem.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | import {
4 | NavigationMenuContent,
5 | NavigationMenuItem,
6 | NavigationMenuLink,
7 | NavigationMenuTrigger,
8 | navigationMenuTriggerStyle,
9 | } from '@/components/ui/navigation-menu';
10 | import type { NavItemType } from '@/domain/entities/navbar/NavItemType';
11 |
12 | export default function NavItem({ item }: { item: NavItemType }) {
13 | if ('items' in item && item.items) {
14 | return (
15 |
16 | {item.title}
17 |
18 |
19 | {item.items.map((subItem) => (
20 |
21 |
22 |
26 |
27 | {subItem.title}
28 |
29 | {subItem.description && (
30 |
31 | {subItem.description}
32 |
33 | )}
34 |
35 |
36 |
37 | ))}
38 |
39 |
40 |
41 | );
42 | }
43 |
44 | return (
45 |
46 |
47 | {item.title}
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-card text-card-foreground",
12 | destructive:
13 | "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | function Alert({
23 | className,
24 | variant,
25 | ...props
26 | }: React.ComponentProps<"div"> & VariantProps) {
27 | return (
28 |
34 | )
35 | }
36 |
37 | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38 | return (
39 |
47 | )
48 | }
49 |
50 | function AlertDescription({
51 | className,
52 | ...props
53 | }: React.ComponentProps<"div">) {
54 | return (
55 |
63 | )
64 | }
65 |
66 | export { Alert, AlertTitle, AlertDescription }
67 |
--------------------------------------------------------------------------------
/src/components/calendar/MonthViewCalendar/MonthViewCalendar.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion';
2 |
3 | import { DayCell } from '@/components/calendar/MonthViewCalendar/DayCell';
4 | import { staggerContainer, transition } from '@/domain/animations';
5 | import { Meeting } from '@/domain/entities/calendar/CalendarTypes';
6 | import { useMeetingContext } from '@/domain/hooks/meetingHooks';
7 | import {
8 | calculateMonthEventPositions,
9 | getCalendarCells,
10 | } from '@/operations/meeting/CalendarHelpers';
11 |
12 | interface IProps {
13 | singleDayEvents: Meeting[];
14 | multiDayEvents: Meeting[];
15 | }
16 |
17 | const WEEK_DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
18 |
19 | export function CalendarMonthView({ singleDayEvents, multiDayEvents }: IProps) {
20 | const { selectedDate } = useMeetingContext();
21 | const allEvents = [...multiDayEvents, ...singleDayEvents];
22 | const cells = getCalendarCells(selectedDate);
23 |
24 | const eventPositions = calculateMonthEventPositions(
25 | multiDayEvents,
26 | singleDayEvents,
27 | selectedDate,
28 | );
29 |
30 | return (
31 |
32 |
33 | {WEEK_DAYS.map((day, index) => (
34 |
41 | {day}
42 |
43 | ))}
44 |
45 |
46 |
47 | {cells.map((cell, index) => (
48 |
54 | ))}
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/repositories/alertRepository.ts:
--------------------------------------------------------------------------------
1 | import { getCookie } from 'cookies-next';
2 |
3 | import {
4 | createNewAlert,
5 | deleteAlert,
6 | toggleAlertActive,
7 | } from '@/domain/actions/alert-actions';
8 | import { Alert } from '@/domain/entities/alerts/generated-types';
9 | import { Meeting } from '@/domain/entities/calendar/CalendarTypes';
10 | import { ToastOperations } from '@/operations/toast/toastOperations';
11 |
12 | const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/alerts`;
13 | export async function fetchBackendAlerts(userId: string): Promise {
14 | const token = getCookie('token');
15 |
16 | try {
17 | const response = await fetch(
18 | `${API_URL}?user_id=${userId}&include_inactive=true`,
19 | {
20 | method: 'GET',
21 | mode: 'cors',
22 | headers: {
23 | 'Content-Type': 'application/json',
24 | Authorization: `Bearer ${token}`,
25 | },
26 | },
27 | );
28 | if (!response.ok) {
29 | ToastOperations.showError({
30 | title: 'Error fetching alerts',
31 | message: 'Failed to fetch alerts. Please try again later.',
32 | });
33 | throw new Error(`HTTP error! status: ${response.status}`);
34 | }
35 | const res: Alert[] = await response.json();
36 | return res;
37 | } catch (error) {
38 | throw new Error(
39 | 'Error fetching alerts: ' +
40 | (error instanceof Error ? error.message : 'Unknown error'),
41 | );
42 | }
43 | }
44 |
45 | export { createNewAlert, deleteAlert, toggleAlertActive };
46 |
47 | export async function getMeetingsForAlert(alertId: string): Promise {
48 | const token = getCookie('token');
49 | const response = await fetch(`${API_URL}/${alertId}/meetings`, {
50 | method: 'GET',
51 | mode: 'cors',
52 | headers: {
53 | 'Content-Type': 'application/json',
54 | Authorization: `Bearer ${token}`,
55 | },
56 | });
57 | if (!response.ok) {
58 | throw new Error(`HTTP error! status: ${response.status}`);
59 | }
60 | const res = await response.json();
61 | return res.data || [];
62 | }
63 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import '@/styles/globals.css';
2 |
3 | import type { Metadata } from 'next';
4 | import { Inter } from 'next/font/google';
5 | import React from 'react';
6 |
7 | import NavBar from '@/components/navigation/NavBar';
8 | import { Toaster } from '@/components/ui/sonner';
9 | import { TooltipProvider } from '@/components/ui/tooltip';
10 | import { AuthProvider } from '@/domain/hooks/useAuth';
11 | import { getUser } from '@/lib/dal';
12 | import ReactQueryProvider from '@/lib/provider/ReactQueryProvider';
13 | import { ThemeProvider } from '@/lib/provider/ThemeProvider';
14 |
15 | const inter = Inter({ subsets: ['latin'] });
16 |
17 | export const metadata: Metadata = {
18 | title: 'OpenEU',
19 | description:
20 | 'The Transparency Backbone for the European Union using Agentic AI',
21 | icons: {
22 | icon: '/favicon.png',
23 | apple: '/favicon.png',
24 | shortcut: '/favicon.png',
25 | },
26 | };
27 |
28 | export default async function RootLayout({
29 | children,
30 | }: {
31 | children: React.ReactNode;
32 | }) {
33 | const user = await getUser();
34 |
35 | return (
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
50 |
51 |
52 |
53 |
54 |
55 | {children}
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function TooltipProvider({
9 | delayDuration = 0,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
18 | )
19 | }
20 |
21 | function Tooltip({
22 | ...props
23 | }: React.ComponentProps) {
24 | return (
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | function TooltipTrigger({
32 | ...props
33 | }: React.ComponentProps) {
34 | return
35 | }
36 |
37 | function TooltipContent({
38 | className,
39 | sideOffset = 0,
40 | children,
41 | ...props
42 | }: React.ComponentProps) {
43 | return (
44 |
45 |
54 | {children}
55 |
56 |
57 |
58 | )
59 | }
60 |
61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
62 |
--------------------------------------------------------------------------------
/src/app/elements/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Section } from '@/components/section';
4 | import { Button } from '@/components/ui/button';
5 | import { ToastOperations } from '@/operations/toast/toastOperations';
6 |
7 | export default function Elements() {
8 | return (
9 |
13 | Paste and test you components here. ✌️
14 |
15 | {
18 | ToastOperations.showInfo({
19 | title: 'Info',
20 | message:
21 | 'This is a long info toast! Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.',
22 | });
23 | }}
24 | >
25 | Show Info Toast
26 |
27 | {
30 | ToastOperations.showWarning({
31 | title: 'Warning',
32 | message: 'This is an warning toast!',
33 | });
34 | }}
35 | >
36 | Show Warning Toast
37 |
38 | {
41 | ToastOperations.showError({
42 | title: 'Error',
43 | message: 'This is an error toast!',
44 | });
45 | }}
46 | >
47 | Show Error Toast
48 |
49 | {
52 | ToastOperations.showSuccess({
53 | title: 'Success',
54 | message: 'This is a success toast!',
55 | });
56 | }}
57 | >
58 | Show Success Toast
59 |
60 |
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/DateRangeFilter.tsx:
--------------------------------------------------------------------------------
1 | import { format } from 'date-fns';
2 | import { CalendarIcon } from 'lucide-react';
3 |
4 | import { Button } from '@/components/ui/button';
5 | import { Calendar } from '@/components/ui/calendar';
6 | import {
7 | Popover,
8 | PopoverContent,
9 | PopoverTrigger,
10 | } from '@/components/ui/popover';
11 |
12 | export interface DateRangeFilterProps {
13 | from?: Date;
14 | to?: Date;
15 | onSelect?: (range: { from?: Date; to?: Date }) => void;
16 | }
17 |
18 | export function DateRangeFilter({ from, to, onSelect }: DateRangeFilterProps) {
19 | const hasDateRange = from || to;
20 |
21 | const handleDateRangeChange = (range: { from?: Date; to?: Date }) => {
22 | onSelect?.(range);
23 | };
24 |
25 | const clearFilter = () => {
26 | onSelect?.({});
27 | };
28 |
29 | return (
30 |
31 |
32 |
33 |
34 |
35 | {hasDateRange ? (
36 | <>
37 | {from && format(from, 'MMM dd')}
38 | {from && to && ' - '}
39 | {to && format(to, 'MMM dd')}
40 | >
41 | ) : (
42 | Select dates
43 | )}
44 |
45 |
46 |
47 |
48 | {
55 | handleDateRangeChange({
56 | from: range?.from,
57 | to: range?.to,
58 | });
59 | }}
60 | />
61 | {hasDateRange && (
62 |
67 | Clear dates
68 |
69 | )}
70 |
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/src/app/inbox/data-table.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ColumnDef,
3 | flexRender,
4 | Table as ReactTable,
5 | } from '@tanstack/react-table';
6 |
7 | import {
8 | Table,
9 | TableBody,
10 | TableCell,
11 | TableHead,
12 | TableHeader,
13 | TableRow,
14 | } from '@/components/ui/table';
15 |
16 | interface DataTableProps {
17 | table: ReactTable;
18 | columns: ColumnDef[];
19 | }
20 |
21 | export function DataTable({
22 | table,
23 | columns,
24 | }: DataTableProps) {
25 | return (
26 |
27 |
28 |
29 | {table.getHeaderGroups().map((headerGroup) => (
30 |
31 | {headerGroup.headers.map((header) => {
32 | return (
33 |
34 | {header.isPlaceholder
35 | ? null
36 | : flexRender(
37 | header.column.columnDef.header,
38 | header.getContext(),
39 | )}
40 |
41 | );
42 | })}
43 |
44 | ))}
45 |
46 |
47 | {table.getRowModel().rows?.length ? (
48 | table.getRowModel().rows.map((row) => (
49 |
53 | {row.getVisibleCells().map((cell) => (
54 |
55 | {flexRender(cell.column.columnDef.cell, cell.getContext())}
56 |
57 | ))}
58 |
59 | ))
60 | ) : (
61 |
62 |
63 | No results.
64 |
65 |
66 | )}
67 |
68 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/src/components/ui/button-group.tsx:
--------------------------------------------------------------------------------
1 | import {Children, ReactElement, cloneElement} from 'react';
2 |
3 | import {cn} from '@/lib/utils';
4 | import * as React from "react";
5 | import type {VariantProps} from "class-variance-authority";
6 | import {buttonVariants} from "@/components/ui/button";
7 |
8 | interface ButtonGroupProps {
9 | className?: string;
10 | orientation?: 'horizontal' | 'vertical';
11 | children: ReactElement &
12 | VariantProps & {
13 | asChild?: boolean
14 | }>[];
15 | }
16 |
17 | export const ButtonGroup = ({
18 | className,
19 | orientation = 'horizontal',
20 | children
21 | }: ButtonGroupProps) => {
22 | const totalButtons = Children.count(children);
23 | const isHorizontal = orientation === 'horizontal';
24 | const isVertical = orientation === 'vertical';
25 |
26 | return (
27 |
37 | {Children.map(children, (child, index) => {
38 | const isFirst = index === 0;
39 | const isLast = index === totalButtons - 1;
40 |
41 | return cloneElement(child, {
42 | className: cn(
43 | {
44 | 'rounded-s-none': isHorizontal && !isFirst,
45 | 'rounded-e-none': isHorizontal && !isLast,
46 | 'border-s-0': isHorizontal && !isFirst,
47 |
48 | 'rounded-t-none': isVertical && !isFirst,
49 | 'rounded-b-none': isVertical && !isLast,
50 | 'border-t-0': isVertical && !isFirst
51 | },
52 | child.props.className
53 | )
54 | });
55 | })}
56 |
57 | );
58 | };
--------------------------------------------------------------------------------
/src/components/calendar/WeekViewCalendar/RenderGroupedEvents.tsx:
--------------------------------------------------------------------------------
1 | import { areIntervalsOverlapping, parseISO } from 'date-fns';
2 |
3 | import { EventBlock } from '@/components/calendar/WeekViewCalendar/EventBlock';
4 | import { EventListBlock } from '@/components/calendar/WeekViewCalendar/EventListBlock';
5 | import { Meeting } from '@/domain/entities/calendar/CalendarTypes';
6 | import { getEventBlockStyle } from '@/operations/meeting/CalendarHelpers';
7 |
8 | interface RenderGroupedEventsProps {
9 | groupedEvents: Meeting[][][];
10 | day: Date;
11 | }
12 |
13 | export function RenderGroupedEvents({
14 | groupedEvents,
15 | day,
16 | }: RenderGroupedEventsProps) {
17 | return groupedEvents.map((group, groupIndex) =>
18 | group.map((events, eventIndex) => {
19 | let style = getEventBlockStyle(
20 | events[0],
21 | day,
22 | groupIndex,
23 | groupedEvents.length,
24 | );
25 | const hasOverlap = groupedEvents.some(
26 | (otherGroup, otherIndex) =>
27 | otherIndex !== groupIndex &&
28 | otherGroup.some((otherEvent) =>
29 | areIntervalsOverlapping(
30 | {
31 | start: parseISO(events[0].meeting_start_datetime),
32 | end: parseISO(events[0].meeting_end_datetime),
33 | },
34 | {
35 | start: parseISO(otherEvent[0].meeting_start_datetime),
36 | end: parseISO(otherEvent[0].meeting_end_datetime),
37 | },
38 | ),
39 | ),
40 | );
41 |
42 | if (!hasOverlap) style = { ...style, width: '100%', left: '0%' };
43 |
44 | if (events.length == 1) {
45 | return (
46 |
51 |
52 |
53 | );
54 | }
55 |
56 | return (
57 |
62 |
63 |
64 | );
65 | }),
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/auth/UpdatePasswordForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { redirect } from 'next/navigation';
4 | import React, { ChangeEvent, FormEvent, useState } from 'react';
5 |
6 | import { Button } from '@/components/ui/button';
7 | import {
8 | Card,
9 | CardContent,
10 | CardDescription,
11 | CardHeader,
12 | CardTitle,
13 | } from '@/components/ui/card';
14 | import { Input } from '@/components/ui/input';
15 | import { Label } from '@/components/ui/label';
16 | import { createClient } from '@/lib/supabase/client';
17 | import { cn } from '@/lib/utils';
18 |
19 | export function UpdatePasswordForm({
20 | className,
21 | ...props
22 | }: React.ComponentProps<'div'>) {
23 | const supabase = createClient();
24 |
25 | const [newPassword, setNewPassword] = useState('');
26 |
27 | const resetPassword = async (e: FormEvent) => {
28 | e.preventDefault();
29 |
30 | await supabase.auth.updateUser({ password: newPassword });
31 | redirect('/login');
32 | };
33 |
34 | return (
35 |
36 |
37 |
38 | Update your password
39 | Enter your new password
40 |
41 |
42 |
63 |
64 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/ExportModal/ExportModal.tsx:
--------------------------------------------------------------------------------
1 | import { Download } from 'lucide-react';
2 | import { useState } from 'react';
3 |
4 | import { MotionButton } from '@/components/TooltipMotionButton';
5 | import { Button } from '@/components/ui/button';
6 | import {
7 | Dialog,
8 | DialogContent,
9 | DialogDescription,
10 | DialogHeader,
11 | DialogTitle,
12 | } from '@/components/ui/dialog';
13 | import {
14 | Tooltip,
15 | TooltipContent,
16 | TooltipTrigger,
17 | } from '@/components/ui/tooltip';
18 | import { buttonHover } from '@/domain/animations';
19 | import { useMeetingContext } from '@/domain/hooks/meetingHooks';
20 | import {
21 | handleExportPDF,
22 | handleExportXLSX,
23 | } from '@/operations/meeting/ExportHelpers';
24 |
25 | export default function ExportModal() {
26 | const [dialogOpen, setDialogOpen] = useState(false);
27 | const { meetings } = useMeetingContext();
28 | return (
29 |
30 |
31 |
32 | setDialogOpen(true)}
35 | variants={buttonHover}
36 | whileHover="hover"
37 | whileTap="tap"
38 | >
39 |
40 |
41 |
42 |
43 | Export Data
44 |
45 |
46 |
47 |
48 |
49 |
50 | Export Data
51 |
52 | Choose the format to export your data.
53 |
54 |
55 |
56 | handleExportXLSX(meetings, setDialogOpen)}>
57 | Export as Spreadsheet
58 |
59 | handleExportPDF(meetings, setDialogOpen)}>
60 | Export as PDF
61 |
62 | setDialogOpen(false)}>
63 | Cancel
64 |
65 |
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Tabs({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
18 | )
19 | }
20 |
21 | function TabsList({
22 | className,
23 | ...props
24 | }: React.ComponentProps) {
25 | return (
26 |
34 | )
35 | }
36 |
37 | function TabsTrigger({
38 | className,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
50 | )
51 | }
52 |
53 | function TabsContent({
54 | className,
55 | ...props
56 | }: React.ComponentProps) {
57 | return (
58 |
63 | )
64 | }
65 |
66 | export { Tabs, TabsList, TabsTrigger, TabsContent }
67 |
--------------------------------------------------------------------------------
/src/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
5 | import { type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { toggleVariants } from "@/components/ui/toggle"
9 |
10 | const ToggleGroupContext = React.createContext<
11 | VariantProps
12 | >({
13 | size: "default",
14 | variant: "default",
15 | })
16 |
17 | function ToggleGroup({
18 | className,
19 | variant,
20 | size,
21 | children,
22 | ...props
23 | }: React.ComponentProps &
24 | VariantProps) {
25 | return (
26 |
36 |
37 | {children}
38 |
39 |
40 | )
41 | }
42 |
43 | function ToggleGroupItem({
44 | className,
45 | children,
46 | variant,
47 | size,
48 | ...props
49 | }: React.ComponentProps &
50 | VariantProps) {
51 | const context = React.useContext(ToggleGroupContext)
52 |
53 | return (
54 |
68 | {children}
69 |
70 | )
71 | }
72 |
73 | export { ToggleGroup, ToggleGroupItem }
74 |
--------------------------------------------------------------------------------
/src/components/profile/NotificationsForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { zodResolver } from '@hookform/resolvers/zod';
4 | import { Bell } from 'lucide-react';
5 | import { useForm } from 'react-hook-form';
6 | import { z } from 'zod';
7 |
8 | import LoadingSpinner from '@/components/LoadingSpinner';
9 | import { CompletionForm } from '@/components/profile/forms/CompletionForm';
10 | import { Button } from '@/components/ui/button';
11 | import { Card, CardContent, CardHeader } from '@/components/ui/card';
12 | import { Form } from '@/components/ui/form';
13 | import {
14 | Profile,
15 | ProfileUpdate,
16 | } from '@/domain/entities/profile/generated-types';
17 | import { completionSchema } from '@/domain/schemas/profile';
18 |
19 | interface NotificationsFormProps {
20 | profile: Profile;
21 | updateProfile: (userId: string, data: ProfileUpdate) => void;
22 | loading: boolean;
23 | }
24 |
25 | export default function NotificationsForm({
26 | profile,
27 | updateProfile,
28 | loading,
29 | }: NotificationsFormProps) {
30 | function onSubmit(values: z.infer) {
31 | updateProfile(profile.id, { ...values });
32 | }
33 |
34 | const form = useForm>({
35 | resolver: zodResolver(completionSchema),
36 | defaultValues: {
37 | newsletter_frequency: profile.newsletter_frequency,
38 | },
39 | });
40 |
41 | return (
42 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/src/app/auth/callback/route.ts:
--------------------------------------------------------------------------------
1 | import { setCookie } from 'cookies-next';
2 | import { cookies } from 'next/headers';
3 | import { NextResponse } from 'next/server';
4 |
5 | // The client you created from the Server-Side Auth instructions
6 | import { createClient } from '@/lib/supabase/server';
7 |
8 | export async function GET(request: Request) {
9 | const { searchParams, origin } = new URL(request.url);
10 | const code = searchParams.get('code');
11 | // if "next" is in param, use it as the redirect URL
12 | let next = searchParams.get('next') ?? '/';
13 | if (!next.startsWith('/')) {
14 | // if "next" is not a relative URL, use the default
15 | next = '/';
16 | }
17 |
18 | if (code) {
19 | const supabase = await createClient();
20 | const {
21 | data: { user, session },
22 | error,
23 | } = await supabase.auth.exchangeCodeForSession(code);
24 | if (!error) {
25 | const { count } = await supabase
26 | .from('profiles')
27 | .select('*', { count: 'exact' })
28 | .eq('id', user?.id);
29 |
30 | if (!count) {
31 | await supabase.auth.updateUser({ data: { incompleteProfile: true } });
32 | }
33 |
34 | if (session) {
35 | await supabase.auth.updateUser({
36 | data: {
37 | oauthRefreshToken: session.provider_refresh_token,
38 | oauthAccessToken: session.provider_token,
39 | },
40 | });
41 | setCookie('token', session.access_token, { cookies });
42 | }
43 |
44 | const forwardedHost = request.headers.get('x-forwarded-host'); // original origin before load balancer
45 | const isLocalEnv = process.env.NODE_ENV === 'development';
46 | if (isLocalEnv) {
47 | // we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
48 | return NextResponse.redirect(`${origin}${next}`);
49 | } else if (forwardedHost) {
50 | return NextResponse.redirect(`https://${forwardedHost}${next}`);
51 | } else {
52 | return NextResponse.redirect(`${origin}${next}`);
53 | }
54 | }
55 | }
56 |
57 | // return the user to an error page with instructions
58 | return NextResponse.redirect(`${origin}/auth/auth-code-error`);
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SliderPrimitive from "@radix-ui/react-slider"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Slider({
9 | className,
10 | defaultValue,
11 | value,
12 | min = 0,
13 | max = 100,
14 | ...props
15 | }: React.ComponentProps) {
16 | const _values = React.useMemo(
17 | () =>
18 | Array.isArray(value)
19 | ? value
20 | : Array.isArray(defaultValue)
21 | ? defaultValue
22 | : [min, max],
23 | [value, defaultValue, min, max]
24 | )
25 |
26 | return (
27 |
39 |
45 |
51 |
52 | {Array.from({ length: _values.length }, (_, index) => (
53 |
58 | ))}
59 |
60 | )
61 | }
62 |
63 | export { Slider }
64 |
--------------------------------------------------------------------------------
/src/lib/formatters.ts:
--------------------------------------------------------------------------------
1 | import {
2 | endOfWeek,
3 | format,
4 | formatISO,
5 | parseISO,
6 | startOfDay,
7 | startOfWeek,
8 | } from 'date-fns';
9 |
10 | /**
11 | * Converts a Date to ISO string format
12 | * Uses date-fns for reliable formatting
13 | */
14 | export function dateToISOString(date?: Date): string {
15 | if (!date) return '';
16 | return formatISO(date);
17 | }
18 |
19 | /**
20 | * Parses ISO string to Date with fallback to current date
21 | * Uses date-fns for reliable parsing
22 | */
23 | export function isoStringToDate(iso: string): Date {
24 | try {
25 | return parseISO(iso);
26 | } catch {
27 | return startOfDay(new Date());
28 | }
29 | }
30 |
31 | /**
32 | * Gets current week range (Monday to Sunday)
33 | * Uses date-fns for accurate week calculations
34 | */
35 | export function getCurrentWeekRange(): {
36 | startDate: Date;
37 | endDate: Date;
38 | } {
39 | const now = new Date();
40 |
41 | return {
42 | startDate: startOfWeek(now, { weekStartsOn: 1 }), // Monday
43 | endDate: endOfWeek(now, { weekStartsOn: 1 }), // Sunday
44 | };
45 | }
46 |
47 | /**
48 | * Formats a date range into a human-readable string
49 | */
50 | export function dateRangeToString(from?: Date, to?: Date): string {
51 | let result = '';
52 |
53 | if (from) {
54 | result += format(from, 'MMM dd');
55 | }
56 |
57 | if (from && to) {
58 | result += ' - ';
59 | }
60 |
61 | if (to) {
62 | result += format(to, 'MMM dd');
63 | }
64 |
65 | return result;
66 | }
67 |
68 | /**
69 | * Formats topic or countries filters for display, showing first selected value + count if multiple
70 | * @param selection Array of selection strings
71 | * @returns Object with display text and whether there are multiple selections
72 | */
73 | export function formatSelectionForDisplay(selection?: string[] | null): {
74 | displayText: string;
75 | hasMultiple: boolean;
76 | } | null {
77 | if (!selection || selection.length === 0) {
78 | return null;
79 | }
80 |
81 | if (selection.length === 1) {
82 | return {
83 | displayText: selection[0],
84 | hasMultiple: false,
85 | };
86 | }
87 |
88 | return {
89 | displayText: `${selection[0]} + ${selection.length - 1}`,
90 | hasMultiple: true,
91 | };
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/profile/InterestsForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { zodResolver } from '@hookform/resolvers/zod';
4 | import { Compass } from 'lucide-react';
5 | import React from 'react';
6 | import { useForm } from 'react-hook-form';
7 | import { z } from 'zod';
8 |
9 | import LoadingSpinner from '@/components/LoadingSpinner';
10 | import { FocusAreaForm } from '@/components/profile/forms/FocusAreaForm';
11 | import { Button } from '@/components/ui/button';
12 | import { Card, CardContent, CardHeader } from '@/components/ui/card';
13 | import { Form } from '@/components/ui/form';
14 | import {
15 | Profile,
16 | ProfileUpdate,
17 | } from '@/domain/entities/profile/generated-types';
18 | import { focusAreaSchema } from '@/domain/schemas/profile';
19 |
20 | export interface InterestsFormProps {
21 | profile: Profile;
22 | updateProfile: (userId: string, data: ProfileUpdate) => void;
23 | loading: boolean;
24 | }
25 |
26 | export default function InterestsForm({
27 | profile,
28 | updateProfile,
29 | loading,
30 | }: InterestsFormProps) {
31 | const form = useForm>({
32 | resolver: zodResolver(focusAreaSchema),
33 | defaultValues: {
34 | countries: profile.countries,
35 | topic_ids: profile.topic_ids,
36 | },
37 | });
38 |
39 | function onSubmit(values: z.infer) {
40 | updateProfile(profile.id, { ...values });
41 | }
42 |
43 | return (
44 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/src/lib/supabase/middleware.ts:
--------------------------------------------------------------------------------
1 | import { createServerClient } from '@supabase/ssr';
2 | import { type NextRequest, NextResponse } from 'next/server';
3 |
4 | export async function updateSession(request: NextRequest) {
5 | let supabaseResponse = NextResponse.next({
6 | request,
7 | });
8 |
9 | const supabase = createServerClient(
10 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
11 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
12 | {
13 | cookies: {
14 | getAll() {
15 | return request.cookies.getAll();
16 | },
17 | setAll(cookiesToSet) {
18 | cookiesToSet.forEach(({ name, value }) => {
19 | request.cookies.set(name, value);
20 | });
21 | supabaseResponse = NextResponse.next({
22 | request,
23 | });
24 | cookiesToSet.forEach(({ name, value, options }) => {
25 | supabaseResponse.cookies.set(name, value, options);
26 | });
27 | },
28 | },
29 | },
30 | );
31 |
32 | // IMPORTANT: DO NOT REMOVE auth.getUser()
33 | const {
34 | data: { user },
35 | } = await supabase.auth.getUser();
36 |
37 | const { pathname } = request.nextUrl;
38 |
39 | // Define allowed routes for unauthenticated users
40 | const publicRoutes = [
41 | '/',
42 | '/login',
43 | '/register',
44 | '/forgot-password',
45 | '/auth/confirm',
46 | '/auth/callback',
47 | '/auth/error',
48 | '/privacy',
49 | '/project-europe.png',
50 | '/project-europe-no-bg.png',
51 | ];
52 |
53 | const isPublicRoute = publicRoutes.some((route) => {
54 | if (route === '/') {
55 | return pathname === '/';
56 | }
57 | return pathname.startsWith(route);
58 | });
59 |
60 | // If user is not authenticated and trying to access a protected route
61 | if (!user && !isPublicRoute) {
62 | const url = request.nextUrl.clone();
63 | url.pathname = '/login';
64 | return NextResponse.redirect(url);
65 | }
66 |
67 | // If user is authenticated and trying to access login/register, redirect to home
68 | if (user && (pathname === '/login' || pathname === '/register')) {
69 | const url = request.nextUrl.clone();
70 | url.pathname = '/';
71 | return NextResponse.redirect(url);
72 | }
73 |
74 | return supabaseResponse;
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/navigation/SettingsPopover.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Monitor, Moon, Settings, Shield, Sun } from 'lucide-react';
4 | import Link from 'next/link';
5 | import { useTheme } from 'next-themes';
6 |
7 | import { Button } from '@/components/ui/button';
8 | import {
9 | Popover,
10 | PopoverContent,
11 | PopoverTrigger,
12 | } from '@/components/ui/popover';
13 | import { Separator } from '@/components/ui/separator';
14 | import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
15 |
16 | export function SettingsPopover() {
17 | const { theme, setTheme } = useTheme();
18 |
19 | return (
20 |
21 |
22 |
23 |
24 | Toggle theme
25 |
26 |
27 |
28 | Settings
29 |
30 |
31 |
32 |
33 | Privacy Policy
34 |
35 |
36 |
37 |
38 |
Theme
39 |
{
45 | if (value && value !== theme) {
46 | setTheme(value);
47 | }
48 | }}
49 | className="w-full"
50 | >
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/calendar/WeekViewCalendar/EventListBlock.tsx:
--------------------------------------------------------------------------------
1 | import { differenceInMinutes, parseISO } from 'date-fns';
2 | import type { HTMLAttributes } from 'react';
3 |
4 | import { EventListDialog } from '@/components/calendar/MonthViewCalendar/EventListDialog';
5 | import { Meeting } from '@/domain/entities/calendar/CalendarTypes';
6 | import { useMeetingContext } from '@/domain/hooks/meetingHooks';
7 | import { cn, COLOR_SCHEMES } from '@/lib/utils';
8 | import { formatTime } from '@/operations/meeting/CalendarHelpers';
9 |
10 | interface IProps extends HTMLAttributes {
11 | events: Meeting[];
12 | }
13 |
14 | export function EventListBlock({ events, className }: IProps) {
15 | const { badgeVariant, use24HourFormat } = useMeetingContext();
16 |
17 | const start = parseISO(events[0].meeting_start_datetime);
18 | const end = parseISO(events[0].meeting_end_datetime);
19 | const durationInMinutes = differenceInMinutes(end, start);
20 | const heightInPixels = (durationInMinutes / 60) * 96 - 8;
21 |
22 | const calendarWeekEventCardClasses = cn(
23 | 'flex select-none flex-col gap-0.5 truncate whitespace-nowrap rounded-md border px-2 py-1.5 text-xs focus-visible:outline-offset-2 cursor-pointer',
24 | COLOR_SCHEMES[events[0].color].bg,
25 | COLOR_SCHEMES[events[0].color].text,
26 | COLOR_SCHEMES[events[0].color].outline,
27 | durationInMinutes < 35 && 'py-0 justify-center',
28 | className,
29 | );
30 |
31 | return (
32 |
33 |
39 |
40 | {badgeVariant === 'dot' && (
41 |
42 |
43 |
44 | )}
45 |
46 |
{events.length} Events
47 |
48 |
49 | {durationInMinutes > 25 && (
50 |
51 | {formatTime(start, use24HourFormat)} -{' '}
52 | {formatTime(end, use24HourFormat)}
53 |
54 | )}
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from '@radix-ui/react-slot';
2 | import { cva, type VariantProps } from 'class-variance-authority';
3 | import * as React from 'react';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:cursor-pointer",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
14 | destructive:
15 | 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
16 | outline:
17 | 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
18 | secondary:
19 | 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
20 | ghost:
21 | 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
22 | link: 'text-primary underline-offset-4 hover:underline',
23 | },
24 | size: {
25 | default: 'h-9 px-4 py-2 has-[>svg]:px-3',
26 | sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
27 | lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
28 | icon: 'size-9',
29 | },
30 | },
31 | defaultVariants: {
32 | variant: 'default',
33 | size: 'default',
34 | },
35 | },
36 | );
37 |
38 | function Button({
39 | className,
40 | variant,
41 | size,
42 | asChild = false,
43 | ...props
44 | }: React.ComponentProps<'button'> &
45 | VariantProps & {
46 | asChild?: boolean;
47 | }) {
48 | const Comp = asChild ? Slot : 'button';
49 |
50 | return (
51 |
56 | );
57 | }
58 |
59 | export { Button, buttonVariants };
60 |
--------------------------------------------------------------------------------
/src/domain/actions/alert-actions.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { cookies } from 'next/headers';
4 |
5 | import { Alert } from '@/domain/entities/alerts/generated-types';
6 | import { ToastOperations } from '@/operations/toast/toastOperations';
7 |
8 | const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/alerts`;
9 |
10 | export async function createNewAlert(params: {
11 | user_id: string;
12 | description: string;
13 | }): Promise {
14 | const token = (await cookies()).get('token')?.value;
15 | const response = await fetch(`${API_URL}`, {
16 | method: 'POST',
17 | mode: 'cors',
18 | headers: {
19 | 'Content-Type': 'application/json',
20 | Authorization: `Bearer ${token}`,
21 | },
22 | body: JSON.stringify(params),
23 | });
24 | if (!response.ok) {
25 | ToastOperations.showError({
26 | title: 'Error creating alert',
27 | message: 'Failed to create alert. Please try again later.',
28 | });
29 | throw new Error(`HTTP error! status: ${response.status}`);
30 | }
31 | return response.json();
32 | }
33 |
34 | export async function deleteAlert(alertId: string): Promise {
35 | const token = (await cookies()).get('token')?.value;
36 | const response = await fetch(`${API_URL}/${alertId}`, {
37 | method: 'DELETE',
38 | mode: 'cors',
39 | headers: {
40 | 'Content-Type': 'application/json',
41 | Authorization: `Bearer ${token}`,
42 | },
43 | });
44 | if (!response.ok) {
45 | ToastOperations.showError({
46 | title: 'Error deleting alert',
47 | message: 'Failed to delete alert. Please try again later.',
48 | });
49 | throw new Error(`HTTP error! status: ${response.status}`);
50 | }
51 | }
52 |
53 | export async function toggleAlertActive(
54 | alertId: string,
55 | active: boolean,
56 | ): Promise {
57 | const token = (await cookies()).get('token')?.value;
58 | const response = await fetch(`${API_URL}/${alertId}?active=${active}`, {
59 | method: 'PATCH',
60 | mode: 'cors',
61 | headers: {
62 | 'Content-Type': 'application/json',
63 | Authorization: `Bearer ${token}`,
64 | },
65 | });
66 | if (!response.ok) {
67 | ToastOperations.showError({
68 | title: 'Error updating alert',
69 | message: 'Failed to update alert. Please try again later.',
70 | });
71 | throw new Error(`HTTP error! status: ${response.status}`);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | function Card({ className, ...props }: React.ComponentProps<'div'>) {
6 | return (
7 |
15 | );
16 | }
17 |
18 | function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
19 | return (
20 |
28 | );
29 | }
30 |
31 | function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
32 | return (
33 |
38 | );
39 | }
40 |
41 | function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
42 | return (
43 |
48 | );
49 | }
50 |
51 | function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
52 | return (
53 |
61 | );
62 | }
63 |
64 | function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
65 | return (
66 |
71 | );
72 | }
73 |
74 | function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
75 | return (
76 |
81 | );
82 | }
83 |
84 | export {
85 | Card,
86 | CardAction,
87 | CardContent,
88 | CardDescription,
89 | CardFooter,
90 | CardHeader,
91 | CardTitle,
92 | };
93 |
--------------------------------------------------------------------------------