From 1cca51e518cad8d8f270df41857fcc03acfe4937 Mon Sep 17 00:00:00 2001 From: bunker-admin Date: Sat, 7 Mar 2026 13:10:08 -0700 Subject: [PATCH] Tonne of updates to things like social systems, calendars, and the documentation system (making it mobile friendly and fixing up navigation) --- SOCIAL_CALENDAR_PLAN.md | 29 +- admin/public/auth-check.html | 28 + admin/src/App.tsx | 35 +- admin/src/components/AppLayout.tsx | 164 +- admin/src/components/PublicLayout.tsx | 46 +- admin/src/components/PublicNavBar.tsx | 429 ++- admin/src/components/VolunteerFooterNav.tsx | 20 +- admin/src/components/VolunteerLayout.tsx | 246 +- .../calendar/AvailabilityFinder.tsx | 252 ++ .../components/calendar/CalendarComments.tsx | 156 + .../calendar/CalendarExportPanel.tsx | 172 + .../calendar/CalendarFeedsPanel.tsx | 233 ++ .../components/calendar/CalendarReactions.tsx | 97 + .../calendar/SharedViewMembersPanel.tsx | 204 ++ .../src/components/docs/MobileDocsEditor.tsx | 799 ++++ .../docs/MobileFormattingToolbar.tsx | 184 + admin/src/hooks/useDocsEditor.ts | 526 +++ admin/src/lib/nav-defaults.ts | 289 ++ admin/src/pages/AdminCalendarPage.tsx | 239 ++ admin/src/pages/AdminCalendarViewPage.tsx | 308 ++ admin/src/pages/DocsPage.tsx | 12 +- admin/src/pages/JitsiMeetPage.tsx | 88 +- admin/src/pages/LandingPagesPage.tsx | 4 +- admin/src/pages/NavigationSettingsPage.tsx | 448 ++- admin/src/pages/events/CheckInScannerPage.tsx | 10 +- admin/src/pages/events/EventDetailPage.tsx | 14 +- admin/src/pages/events/TicketedEventsPage.tsx | 6 +- admin/src/pages/public/HomePage.tsx | 8 +- .../pages/volunteer/FriendCalendarPage.tsx | 196 + admin/src/pages/volunteer/MyCalendarPage.tsx | 64 +- admin/src/pages/volunteer/MyTicketsPage.tsx | 2 +- .../volunteer/SharedCalendarViewPage.tsx | 347 ++ .../pages/volunteer/SharedCalendarsPage.tsx | 255 ++ .../src/pages/volunteer/SocialProfilePage.tsx | 9 +- .../src/pages/volunteer/VolunteerChatPage.tsx | 163 +- .../src/pages/volunteer/VolunteerMapPage.tsx | 10 +- admin/src/types/api.ts | 76 +- admin/src/utils/textareaSnippets.ts | 99 + api/package-lock.json | 98 + api/package.json | 2 + .../modules/calendar/admin-calendar.routes.ts | 74 + .../calendar/admin-calendar.schemas.ts | 21 + .../calendar/admin-calendar.service.ts | 173 + api/src/modules/calendar/feed.routes.ts | 127 + api/src/modules/calendar/feed.schemas.ts | 22 + api/src/modules/calendar/feed.service.ts | 514 +++ .../calendar/shared-calendar.routes.ts | 291 ++ .../calendar/shared-calendar.service.ts | 1084 ++++++ .../modules/docs/header-builder.service.ts | 645 +++- api/src/modules/pages/pages.schemas.ts | 2 + api/src/modules/pages/pages.service.ts | 1 + api/src/modules/qr/qr.routes.ts | 1 + api/src/modules/settings/settings.routes.ts | 17 +- api/src/modules/settings/settings.schemas.ts | 15 +- .../ticketed-events-public.routes.ts | 4 +- api/src/server.ts | 9 + .../services/calendar-feed-queue.service.ts | 78 + .../services/gancio-settings-sync.service.ts | 33 +- .../docs/getting-started/control-panel.png | Bin 0 -> 72355 bytes .../docs/getting-started/installation.png | Bin 64260 -> 67832 bytes .../social/docs/getting-started/services.png | Bin 0 -> 71011 bytes .../social/docs/getting-started/upgrades.png | Bin 0 -> 71301 bytes .../social/assets/images/social/test-page.png | Bin 0 -> 66077 bytes mkdocs/.cache/plugin/social/manifest.json | 6 +- .../repo-data/admin-changemaker.lite.json | 4 +- .../repo-data/anthropics-claude-code.json | 10 +- .../assets/repo-data/coder-code-server.json | 10 +- .../repo-data/gethomepage-homepage.json | 8 +- .../docs/assets/repo-data/go-gitea-gitea.json | 10 +- .../docs/assets/repo-data/knadh-listmonk.json | 10 +- .../docs/assets/repo-data/lyqht-mini-qr.json | 8 +- mkdocs/docs/assets/repo-data/n8n-io-n8n.json | 10 +- .../docs/assets/repo-data/nocodb-nocodb.json | 10 +- .../docs/assets/repo-data/ollama-ollama.json | 10 +- .../repo-data/squidfunk-mkdocs-material.json | 8 +- .../docs/getting-started/control-panel.md | 270 ++ .../docs/docs/getting-started/first-steps.md | 3 + mkdocs/docs/docs/getting-started/index.md | 143 +- .../docs/docs/getting-started/installation.md | 297 +- mkdocs/docs/docs/getting-started/services.md | 280 ++ mkdocs/docs/docs/getting-started/upgrades.md | 285 ++ mkdocs/docs/overrides/main.html | 246 +- mkdocs/docs/overrides/test-page.html | 7 + mkdocs/docs/test-page.md | 7 + mkdocs/mkdocs.yml | 3 + mkdocs/site/404.html | 244 +- .../docs/getting-started/control-panel.png | Bin 0 -> 72355 bytes .../docs/getting-started/installation.png | Bin 64260 -> 67832 bytes .../social/docs/getting-started/services.png | Bin 0 -> 71011 bytes .../social/docs/getting-started/upgrades.png | Bin 0 -> 71301 bytes .../site/assets/images/social/test-page.png | Bin 0 -> 66077 bytes .../repo-data/admin-changemaker.lite.json | 4 +- .../repo-data/anthropics-claude-code.json | 10 +- .../assets/repo-data/coder-code-server.json | 10 +- .../repo-data/gethomepage-homepage.json | 8 +- .../site/assets/repo-data/go-gitea-gitea.json | 10 +- .../site/assets/repo-data/knadh-listmonk.json | 10 +- .../site/assets/repo-data/lyqht-mini-qr.json | 8 +- mkdocs/site/assets/repo-data/n8n-io-n8n.json | 10 +- .../site/assets/repo-data/nocodb-nocodb.json | 10 +- .../site/assets/repo-data/ollama-ollama.json | 10 +- .../repo-data/squidfunk-mkdocs-material.json | 8 +- mkdocs/site/blog/index.html | 244 +- mkdocs/site/comments/callback/index.html | 244 +- .../docs/admin/advocacy/campaigns/index.html | 250 +- .../admin/advocacy/email-queue/index.html | 250 +- mkdocs/site/docs/admin/advocacy/index.html | 250 +- .../admin/advocacy/representatives/index.html | 250 +- .../docs/admin/advocacy/responses/index.html | 250 +- .../broadcast/email-templates/index.html | 250 +- mkdocs/site/docs/admin/broadcast/index.html | 250 +- .../admin/broadcast/newsletter/index.html | 250 +- .../site/docs/admin/broadcast/sms/index.html | 250 +- mkdocs/site/docs/admin/dashboard/index.html | 250 +- mkdocs/site/docs/admin/index.html | 250 +- mkdocs/site/docs/admin/map/areas/index.html | 250 +- .../site/docs/admin/map/canvassing/index.html | 250 +- .../docs/admin/map/data-quality/index.html | 250 +- mkdocs/site/docs/admin/map/index.html | 250 +- .../site/docs/admin/map/locations/index.html | 250 +- .../site/docs/admin/map/settings/index.html | 250 +- mkdocs/site/docs/admin/map/shifts/index.html | 250 +- mkdocs/site/docs/admin/media/ads/index.html | 250 +- .../docs/admin/media/analytics/index.html | 250 +- .../site/docs/admin/media/curated/index.html | 250 +- mkdocs/site/docs/admin/media/index.html | 250 +- .../site/docs/admin/media/library/index.html | 250 +- .../docs/admin/media/moderation/index.html | 250 +- .../docs/admin/payments/donations/index.html | 250 +- mkdocs/site/docs/admin/payments/index.html | 250 +- .../site/docs/admin/payments/plans/index.html | 250 +- .../docs/admin/payments/products/index.html | 250 +- .../docs/admin/payments/settings/index.html | 250 +- .../site/docs/admin/people-access/index.html | 250 +- .../docs/admin/services/crowdsec/index.html | 250 +- mkdocs/site/docs/admin/services/index.html | 250 +- .../admin/services/integrations/index.html | 250 +- .../docs/admin/services/monitoring/index.html | 250 +- .../docs/admin/services/tunnel/index.html | 250 +- .../services/user-provisioning/index.html | 250 +- mkdocs/site/docs/admin/settings/index.html | 250 +- .../docs/admin/web/documentation/index.html | 250 +- .../site/docs/admin/web/homepage/index.html | 250 +- mkdocs/site/docs/admin/web/index.html | 250 +- .../docs/admin/web/landing-pages/index.html | 250 +- .../site/docs/admin/web/navigation/index.html | 250 +- mkdocs/site/docs/api/index.html | 250 +- mkdocs/site/docs/architecture/index.html | 250 +- mkdocs/site/docs/deployment/index.html | 250 +- .../getting-started/control-panel/index.html | 3242 +++++++++++++++++ .../environment-variables/index.html | 346 +- .../docs/getting-started/features/index.html | 346 +- .../getting-started/first-steps/index.html | 349 +- mkdocs/site/docs/getting-started/index.html | 680 ++-- .../getting-started/installation/index.html | 1255 ++++++- .../docs/getting-started/services/index.html | 3173 ++++++++++++++++ .../docs/getting-started/upgrades/index.html | 3075 ++++++++++++++++ mkdocs/site/docs/index.html | 250 +- mkdocs/site/docs/phil/index.html | 250 +- mkdocs/site/docs/services/index.html | 250 +- mkdocs/site/docs/troubleshooting/index.html | 250 +- .../site/docs/user-guide/campaigns/index.html | 250 +- .../site/docs/user-guide/donations/index.html | 250 +- mkdocs/site/docs/user-guide/events/index.html | 250 +- .../site/docs/user-guide/gallery/index.html | 250 +- mkdocs/site/docs/user-guide/index.html | 250 +- mkdocs/site/docs/user-guide/map/index.html | 250 +- .../site/docs/user-guide/profile/index.html | 250 +- mkdocs/site/docs/user-guide/shifts/index.html | 250 +- mkdocs/site/docs/user-guide/shop/index.html | 250 +- .../docs/volunteer/achievements/index.html | 250 +- .../site/docs/volunteer/canvassing/index.html | 250 +- mkdocs/site/docs/volunteer/index.html | 250 +- mkdocs/site/docs/volunteer/shifts/index.html | 250 +- mkdocs/site/docs/volunteer/social/index.html | 250 +- mkdocs/site/index.html | 4 + mkdocs/site/lander/index.html | 4 + mkdocs/site/main/index.html | 244 +- mkdocs/site/overrides/lander.html | 4 + mkdocs/site/overrides/main.html | 246 +- mkdocs/site/overrides/test-page.html | 7 + mkdocs/site/search/search_index.json | 2 +- mkdocs/site/sitemap.xml | 166 +- mkdocs/site/sitemap.xml.gz | Bin 725 -> 755 bytes mkdocs/site/test-page/index.html | 1194 ++++++ mkdocs/site/test/index.html | 244 +- nginx/conf.d/default.conf.template | 9 + nginx/conf.d/services.conf.template | 11 + 188 files changed, 39689 insertions(+), 2615 deletions(-) create mode 100644 admin/public/auth-check.html create mode 100644 admin/src/components/calendar/AvailabilityFinder.tsx create mode 100644 admin/src/components/calendar/CalendarComments.tsx create mode 100644 admin/src/components/calendar/CalendarExportPanel.tsx create mode 100644 admin/src/components/calendar/CalendarFeedsPanel.tsx create mode 100644 admin/src/components/calendar/CalendarReactions.tsx create mode 100644 admin/src/components/calendar/SharedViewMembersPanel.tsx create mode 100644 admin/src/components/docs/MobileDocsEditor.tsx create mode 100644 admin/src/components/docs/MobileFormattingToolbar.tsx create mode 100644 admin/src/hooks/useDocsEditor.ts create mode 100644 admin/src/lib/nav-defaults.ts create mode 100644 admin/src/pages/AdminCalendarPage.tsx create mode 100644 admin/src/pages/AdminCalendarViewPage.tsx create mode 100644 admin/src/pages/volunteer/FriendCalendarPage.tsx create mode 100644 admin/src/pages/volunteer/SharedCalendarViewPage.tsx create mode 100644 admin/src/pages/volunteer/SharedCalendarsPage.tsx create mode 100644 admin/src/utils/textareaSnippets.ts create mode 100644 api/src/modules/calendar/admin-calendar.routes.ts create mode 100644 api/src/modules/calendar/admin-calendar.schemas.ts create mode 100644 api/src/modules/calendar/admin-calendar.service.ts create mode 100644 api/src/modules/calendar/feed.routes.ts create mode 100644 api/src/modules/calendar/feed.schemas.ts create mode 100644 api/src/modules/calendar/feed.service.ts create mode 100644 api/src/modules/calendar/shared-calendar.routes.ts create mode 100644 api/src/modules/calendar/shared-calendar.service.ts create mode 100644 api/src/services/calendar-feed-queue.service.ts create mode 100644 mkdocs/.cache/plugin/social/assets/images/social/docs/getting-started/control-panel.png create mode 100644 mkdocs/.cache/plugin/social/assets/images/social/docs/getting-started/services.png create mode 100644 mkdocs/.cache/plugin/social/assets/images/social/docs/getting-started/upgrades.png create mode 100644 mkdocs/.cache/plugin/social/assets/images/social/test-page.png create mode 100644 mkdocs/docs/docs/getting-started/control-panel.md create mode 100644 mkdocs/docs/docs/getting-started/services.md create mode 100644 mkdocs/docs/docs/getting-started/upgrades.md create mode 100644 mkdocs/docs/overrides/test-page.html create mode 100644 mkdocs/docs/test-page.md create mode 100644 mkdocs/site/assets/images/social/docs/getting-started/control-panel.png create mode 100644 mkdocs/site/assets/images/social/docs/getting-started/services.png create mode 100644 mkdocs/site/assets/images/social/docs/getting-started/upgrades.png create mode 100644 mkdocs/site/assets/images/social/test-page.png create mode 100644 mkdocs/site/docs/getting-started/control-panel/index.html create mode 100644 mkdocs/site/docs/getting-started/services/index.html create mode 100644 mkdocs/site/docs/getting-started/upgrades/index.html create mode 100644 mkdocs/site/overrides/test-page.html create mode 100644 mkdocs/site/test-page/index.html diff --git a/SOCIAL_CALENDAR_PLAN.md b/SOCIAL_CALENDAR_PLAN.md index 11e7e91b..a5f07d9a 100644 --- a/SOCIAL_CALENDAR_PLAN.md +++ b/SOCIAL_CALENDAR_PLAN.md @@ -459,14 +459,15 @@ GET /api/admin/calendar/shared/:id/items — merged system-layer data for mat ### Phase C: .ics Integration **Scope:** -- [ ] Prisma models: CalendarFeed, CalendarExportToken -- [ ] .ics feed parser (node-ical or similar) -- [ ] BullMQ job: refresh feeds on configured intervals -- [ ] Feed CRUD: subscribe, update, delete, force refresh -- [ ] Auto-create layer per feed, cache items as CalendarItem rows -- [ ] .ics export: generate feed from user's calendar, token-authenticated URL -- [ ] Export token management (create, revoke) -- [ ] CalendarFeedsPanel, CalendarExportPanel components +- [x] Prisma models: CalendarFeed, CalendarExportToken (already existed from Phase A migration) +- [x] .ics feed parser (node-ical v0.25.5) +- [x] BullMQ job: refresh feeds every 15 minutes (calendar-feed-refresh queue) +- [x] Feed CRUD: subscribe, update, delete, force refresh +- [x] Auto-create EXTERNAL layer per feed, cache items as CalendarItem rows (sourceType: ICS_FEED) +- [x] .ics export: generate feed from user's calendar via ical-generator v10, token-authenticated URL +- [x] Export token management (create, list, revoke) +- [x] CalendarFeedsPanel, CalendarExportPanel components +- [x] MyCalendarPage settings Drawer integration (gear icon) ### Phase D: Admin Shared Views **Scope:** @@ -554,3 +555,15 @@ The existing `UnifiedCalendar` component and `unified-calendar.service.ts` remai - Smoke tested: layers auto-create, item CRUD works, recurring events materialize correctly (Weekly Mon/Wed/Fri generated 11 instances through June) - Both API and Admin compile with zero TypeScript errors - Remaining Phase A item: BullMQ job for extending recurring series (not critical for launch, series materializes 3 months on creation) + +### 2026-03-07 — Phase C Implementation Complete +- Backend: feed.schemas.ts (3 Zod schemas), feed.service.ts (feed CRUD, ICS parsing, RRULE materialization, export generation), feed.routes.ts (1 public + 8 auth routes), calendar-feed-queue.service.ts (BullMQ 15min repeatable job) +- Dependencies: node-ical v0.25.5 (ICS parsing), ical-generator v10.0.0 (ICS output) +- Feed import: streaming body read with 5MB limit, 1000 event cap, RRULE materialization via rrule.between(), stale event cleanup, status tracking (OK/ERROR/PENDING) +- Feed export: 32-byte random token, configurable layer/personal inclusion, past 1 month + future 3 months, standard iCalendar output with Content-Type: text/calendar +- Frontend: CalendarFeedsPanel (add/edit/delete/refresh with status badges), CalendarExportPanel (create/copy/revoke tokens), settings Drawer in MyCalendarPage (gear icon) +- Types: CalendarFeed, CalendarExportToken, CalendarFeedStatus, CalendarFeedInterval added to admin/src/types/api.ts +- server.ts: feedRoutes mounted before calendarRoutes (public .ics route needs no auth), queue worker started on bootstrap, graceful shutdown +- Smoke tested: Google US Holidays feed → 317 events imported with status OK; export token → valid .ics with VEVENT entries; revoke → 404 +- Docker gotcha: anonymous volume `/app/node_modules` caches old dependencies — must `docker compose rm -sf api` to clear when adding new npm packages +- Both API and Admin compile with zero TypeScript errors diff --git a/admin/public/auth-check.html b/admin/public/auth-check.html new file mode 100644 index 00000000..917e4931 --- /dev/null +++ b/admin/public/auth-check.html @@ -0,0 +1,28 @@ + +Auth Check + + + + diff --git a/admin/src/App.tsx b/admin/src/App.tsx index ab6430d3..26ef8ced 100644 --- a/admin/src/App.tsx +++ b/admin/src/App.tsx @@ -128,6 +128,8 @@ import SchedulingPollPage from '@/pages/public/SchedulingPollPage'; import PollsListPage from '@/pages/public/PollsListPage'; import JitsiAuthPage from '@/pages/JitsiAuthPage'; import SchedulingCalendarPage from '@/pages/SchedulingCalendarPage'; +import AdminCalendarPage from '@/pages/AdminCalendarPage'; +import AdminCalendarViewPage from '@/pages/AdminCalendarViewPage'; import TicketedEventsPage from '@/pages/events/TicketedEventsPage'; import EventDetailPage from '@/pages/events/EventDetailPage'; import CheckInScannerPage from '@/pages/events/CheckInScannerPage'; @@ -135,6 +137,9 @@ import TicketedEventDetailPage from '@/pages/public/TicketedEventDetailPage'; import TicketConfirmationPage from '@/pages/public/TicketConfirmationPage'; import MyTicketsPage from '@/pages/volunteer/MyTicketsPage'; import MyCalendarPage from '@/pages/volunteer/MyCalendarPage'; +import SharedCalendarsPage from '@/pages/volunteer/SharedCalendarsPage'; +import SharedCalendarViewPage from '@/pages/volunteer/SharedCalendarViewPage'; +import FriendCalendarPage from '@/pages/volunteer/FriendCalendarPage'; import NotFoundPage from '@/pages/NotFoundPage'; import CommandPalette from '@/components/command-palette/CommandPalette'; @@ -326,6 +331,16 @@ export default function App() { } /> + {/* Full-screen volunteer chat — outside VolunteerLayout for max screen space */} + + + + } + /> + {/* Volunteer pages with VolunteerLayout */} } /> } /> } /> + } /> + } /> + } /> } /> - } /> } /> @@ -775,6 +792,22 @@ export default function App() { } /> + + + + } + /> + + + + } + /> = { - HomeOutlined: , +/** Admin icon overrides: some icons differ in the admin header context */ +const ADMIN_ICON_OVERRIDES: Record = { SendOutlined: , - EnvironmentOutlined: , - CalendarOutlined: , - ScheduleOutlined: , PlayCircleOutlined: , - HeartOutlined: , - DollarOutlined: , - ShoppingOutlined: , - LinkOutlined: , - GlobalOutlined: , - BookOutlined: , }; -/** Merge missing builtin defaults into stored navConfig items and sync icons */ -function mergeAdminNavDefaults(stored: NavItem[]): NavItem[] { - const defaultMap = new Map(DEFAULT_ADMIN_NAV_ITEMS.filter(d => d.type === 'builtin').map(d => [d.id, d])); - // Sync icon for existing builtin items so code-level icon changes propagate - const synced = stored.map(item => { - const def = defaultMap.get(item.id); - return (def && item.type === 'builtin') ? { ...item, icon: def.icon } : item; - }); - const ids = new Set(synced.map(i => i.id)); - const missing = DEFAULT_ADMIN_NAV_ITEMS.filter(d => d.type === 'builtin' && !ids.has(d.id)); - return missing.length > 0 ? [...synced, ...missing] : synced; -} - function buildMenuItems(settings: import('@/types/api').SiteSettings | null, isSuperAdmin: boolean, badges?: { pendingResponses?: number; pendingEmails?: number; pendingCampaignReview?: number; pendingComments?: number }): MenuProps['items'] { const items: MenuProps['items'] = [ { @@ -209,6 +178,9 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, isS { key: '/app/listmonk', icon: , label: 'Newsletter' }, { key: '/app/email-templates', icon: , label: 'Email Templates' }, ]; + if (settings?.enableChat) { + broadcastChildren.push({ key: '/app/services/rocketchat', icon: , label: 'Team Chat' }); + } if (settings?.enableSms || isSuperAdmin) { broadcastChildren.push( { key: '/app/sms/setup', icon: , label: 'SMS Setup' }, @@ -265,17 +237,24 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, isS } // Scheduling submenu — visible if Shifts, Meeting Planner, or Ticketed Events is enabled - if (settings?.enableMap !== false || settings?.enableMeetingPlanner || settings?.enableTicketedEvents) { + if (settings?.enableMap !== false || settings?.enableMeetingPlanner || settings?.enableTicketedEvents || settings?.enableMeet || settings?.enableEvents) { const schedulingChildren: any[] = []; if (settings?.enableMap !== false) { schedulingChildren.push({ key: '/app/map/shifts', icon: , label: 'Shifts' }); } if (settings?.enableMeetingPlanner) { - schedulingChildren.push({ key: '/app/meeting-planner', icon: , label: 'Meeting Planner' }); + schedulingChildren.push({ key: '/app/meeting-planner', icon: , label: 'Meeting Planner' }); } if (settings?.enableTicketedEvents) { schedulingChildren.push({ key: '/app/events', icon: , label: 'Events' }); } + if (settings?.enableMeet) { + schedulingChildren.push({ key: '/app/services/jitsi', icon: , label: 'Video Meet' }); + } + if (settings?.enableEvents) { + schedulingChildren.push({ key: '/app/services/gancio', icon: , label: 'Gancio' }); + } + schedulingChildren.push({ key: '/app/scheduling/calendar-views', icon: , label: 'Calendar Views' }); // Always add Calendar as the last item in scheduling schedulingChildren.push({ key: '/app/scheduling/calendar', icon: , label: 'Calendar' }); if (schedulingChildren.length > 0) { @@ -338,9 +317,6 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, isS { key: '/app/services/n8n', icon: , label: 'Workflows' }, { key: '/app/services/gitea', icon: , label: 'Git' }, { key: '/app/services/excalidraw', icon: , label: 'Whiteboard' }, - ...(settings?.enableChat ? [{ key: '/app/services/rocketchat', icon: , label: 'Team Chat' }] : []), - ...(settings?.enableMeet ? [{ key: '/app/services/jitsi', icon: , label: 'Video Meet' }] : []), - { key: '/app/services/gancio', icon: , label: 'Events' }, { key: '/app/services/miniqr', icon: , label: 'QR Codes' }, ]}, ], @@ -626,58 +602,70 @@ export default function AppLayout() { {pageHeader?.actions} {(() => { - const items = mergeAdminNavDefaults(settings?.navConfig?.items ?? DEFAULT_ADMIN_NAV_ITEMS); - const featureFlags: Record = { - enableInfluence: settings?.enableInfluence, - enableMap: settings?.enableMap, - enableMediaFeatures: settings?.enableMediaFeatures, - enablePayments: settings?.enablePayments, - enableEvents: settings?.enableEvents, + const merged = mergeNavDefaults(settings?.navConfig?.items ?? DEFAULT_NAV_ITEMS); + const withOverrides = applyAdminOverrides(merged); + const flags = buildFeatureFlags(settings); + const filtered = filterNavItems(withOverrides, flags); + + const getIcon = (iconName: string) => ADMIN_ICON_OVERRIDES[iconName] ?? ICON_MAP[iconName] ?? ; + const handleItemClick = (item: NavItem) => { + if (item.path.startsWith('$')) { + window.open(resolveNavUrl(item.path), '_blank'); + } else if (item.external && item.id === 'home') { + window.open(buildHomeUrl(), '_blank'); + } else if (item.external) { + window.open(item.path, '_blank'); + } else { + navigate(item.path); + } }; - return items - .filter(item => item.enabled) - .filter(item => { - if (!item.featureFlag) return true; - if (item.featureFlag === 'enablePayments') return featureFlags[item.featureFlag] === true; - return featureFlags[item.featureFlag] !== false; - }) - .sort((a, b) => a.order - b.order) - .map(item => ( + + return filtered.map(item => { + if (item.type === 'group' && item.children) { + return ( + ({ + key: child.id, + icon: getIcon(child.icon), + label: child.label, + onClick: () => handleItemClick(child), + })), + }} + placement="bottomRight" + > + + + ); + } + return ( - )); + ); + }); })()} - {/* Canvass button — always tied to enableMap, not in navConfig */} - {settings?.enableMap !== false && ( - - - - )} + {/* Volunteer Portal button — always visible for quick switching */} + + + - - + + + {!welcomeDismissed && ( + setWelcomeDismissed(true)} + style={{ marginBottom: 16 }} + /> + )} @@ -93,9 +125,145 @@ export default function VolunteerLayout() { zIndex: 100, }} > - + setMenuOpen(true)} + menuActive={menuOpen} + /> + + {/* Navigation Menu Drawer */} + setMenuOpen(false)} + open={menuOpen} + width={280} + styles={{ + header: { display: 'none' }, + body: { background: colorBgBase, padding: 0 }, + }} + > + {/* User profile section */} +
+
+
+ +
+
+ + {user?.name || 'Volunteer'} + + + {user?.email} + +
+
+ + {user?.role === 'USER' ? 'Volunteer' : user?.role?.replace('_', ' ') ?? 'Volunteer'} + +
+ + {/* Navigation items */} +
+ {navItems.map(({ key, icon, label }) => { + const isActive = activeKey === key; + return ( +
{ navigate(key); setMenuOpen(false); }} + style={{ + display: 'flex', + alignItems: 'center', + gap: 12, + padding: '12px 20px', + cursor: 'pointer', + color: isActive ? '#fff' : 'rgba(255,255,255,0.7)', + fontWeight: isActive ? 600 : 400, + fontSize: 14, + background: isActive ? 'rgba(52,152,219,0.15)' : 'transparent', + borderRight: isActive ? '3px solid #3498db' : '3px solid transparent', + transition: 'all 0.2s', + }} + > + {icon} + {label} +
+ ); + })} +
+ + + + {/* Cross-navigation links */} +
+
{ navigate('/home'); setMenuOpen(false); }} + style={{ + display: 'flex', + alignItems: 'center', + gap: 12, + padding: '12px 20px', + cursor: 'pointer', + color: 'rgba(255,255,255,0.7)', + fontSize: 14, + }} + > + + Public Website +
+ {isAdmin && ( +
{ navigate('/app'); setMenuOpen(false); }} + style={{ + display: 'flex', + alignItems: 'center', + gap: 12, + padding: '12px 20px', + cursor: 'pointer', + color: 'rgba(255,255,255,0.7)', + fontSize: 14, + }} + > + + Admin Panel +
+ )} +
+ + + + {/* Logout */} +
+
{ handleLogout(); setMenuOpen(false); }} + style={{ + display: 'flex', + alignItems: 'center', + gap: 12, + padding: '12px 20px', + cursor: 'pointer', + color: 'rgba(255,255,255,0.7)', + fontSize: 14, + }} + > + + Logout +
+
+
); } diff --git a/admin/src/components/calendar/AvailabilityFinder.tsx b/admin/src/components/calendar/AvailabilityFinder.tsx new file mode 100644 index 00000000..54779778 --- /dev/null +++ b/admin/src/components/calendar/AvailabilityFinder.tsx @@ -0,0 +1,252 @@ +import { useState, useEffect, useCallback } from 'react'; +import { + DatePicker, + Select, + Space, + Typography, + Skeleton, + Empty, + Tooltip, + theme, +} from 'antd'; +import dayjs, { Dayjs } from 'dayjs'; +import { api } from '@/lib/api'; +import type { AvailabilityResponse } from '@/types/api'; + +const { Text } = Typography; +const { RangePicker } = DatePicker; + +interface AvailabilityFinderProps { + viewId: string; + onSlotClick?: (date: string, time: string) => void; +} + +const DURATION_OPTIONS = [ + { value: 15, label: '15 min' }, + { value: 30, label: '30 min' }, + { value: 60, label: '1 hour' }, +]; + +export default function AvailabilityFinder({ viewId, onSlotClick }: AvailabilityFinderProps) { + const { token } = theme.useToken(); + + const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>([ + dayjs(), + dayjs().add(6, 'day'), + ]); + const [dayStart, setDayStart] = useState(9); + const [dayEnd, setDayEnd] = useState(18); + const [slotDuration, setSlotDuration] = useState(30); + const [availability, setAvailability] = useState(null); + const [loading, setLoading] = useState(false); + + const hourOptions = Array.from({ length: 24 }, (_, i) => ({ + value: i, + label: `${String(i).padStart(2, '0')}:00`, + })); + + const fetchAvailability = useCallback(async () => { + setLoading(true); + try { + const { data } = await api.get( + `/calendar/shared/${viewId}/availability`, + { + params: { + startDate: dateRange[0].format('YYYY-MM-DD'), + endDate: dateRange[1].format('YYYY-MM-DD'), + dayStart, + dayEnd, + slotMinutes: slotDuration, + }, + }, + ); + setAvailability(data); + } catch { + setAvailability(null); + } finally { + setLoading(false); + } + }, [viewId, dateRange, dayStart, dayEnd, slotDuration]); + + useEffect(() => { + fetchAvailability(); + }, [fetchAvailability]); + + // Build time slots + const timeSlots: string[] = []; + for (let h = dayStart; h < dayEnd; h++) { + for (let m = 0; m < 60; m += slotDuration) { + if (h === dayEnd - 1 && m + slotDuration > 60) break; + timeSlots.push(`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`); + } + } + + // Build date columns + const dates: string[] = []; + let d = dateRange[0]; + while (d.isBefore(dateRange[1]) || d.isSame(dateRange[1], 'day')) { + dates.push(d.format('YYYY-MM-DD')); + d = d.add(1, 'day'); + } + + const statusColors: Record = { + free: '#52c41a', + busy: '#ff4d4f', + tentative: '#faad14', + }; + + return ( +
+ + Find Availability + + + + { + if (vals && vals[0] && vals[1]) { + setDateRange([vals[0], vals[1]]); + } + }} + /> + + setNewComment(e.target.value)} + onPressEnter={handleSubmit} + disabled={submitting} + /> +
+ + ); +} diff --git a/admin/src/components/calendar/CalendarExportPanel.tsx b/admin/src/components/calendar/CalendarExportPanel.tsx new file mode 100644 index 00000000..cd321179 --- /dev/null +++ b/admin/src/components/calendar/CalendarExportPanel.tsx @@ -0,0 +1,172 @@ +import { useState, useEffect, useCallback } from 'react'; +import { + List, + Button, + Modal, + Form, + Checkbox, + Select, + Typography, + message, +} from 'antd'; +import { + PlusOutlined, + CopyOutlined, + DeleteOutlined, +} from '@ant-design/icons'; +import dayjs from 'dayjs'; +import { api } from '@/lib/api'; +import type { CalendarExportToken, CalendarLayer } from '@/types/api'; + +const { Text } = Typography; + +interface Props { + layers: CalendarLayer[]; +} + +export default function CalendarExportPanel({ layers }: Props) { + const [tokens, setTokens] = useState([]); + const [loading, setLoading] = useState(true); + const [modalOpen, setModalOpen] = useState(false); + const [form] = Form.useForm(); + + const fetchTokens = useCallback(async () => { + try { + const { data } = await api.get<{ tokens: CalendarExportToken[] }>('/calendar/export/tokens'); + setTokens(data.tokens); + } catch { + // ignore + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchTokens(); + }, [fetchTokens]); + + const handleCreate = async () => { + try { + const values = await form.validateFields(); + await api.post('/calendar/export/tokens', { + includePersonal: values.includePersonal ?? true, + includeLayers: values.includeLayers?.length ? values.includeLayers : null, + }); + message.success('Export link created'); + setModalOpen(false); + form.resetFields(); + await fetchTokens(); + } catch { + message.error('Failed to create export link'); + } + }; + + const handleRevoke = (token: CalendarExportToken) => { + Modal.confirm({ + title: 'Revoke export link?', + content: 'Anyone using this link will no longer be able to access your calendar.', + okText: 'Revoke', + okType: 'danger', + onOk: async () => { + try { + await api.delete(`/calendar/export/tokens/${token.id}`); + message.success('Export link revoked'); + await fetchTokens(); + } catch { + message.error('Failed to revoke link'); + } + }, + }); + }; + + const copyUrl = (token: string) => { + const url = `${window.location.origin}/api/calendar/feed/${token}.ics`; + navigator.clipboard.writeText(url).then( + () => message.success('URL copied'), + () => message.error('Failed to copy'), + ); + }; + + const describeScope = (t: CalendarExportToken) => { + const parts: string[] = []; + if (t.includePersonal) parts.push('Personal events'); + if (t.includeLayers?.length) { + const names = t.includeLayers + .map((id) => layers.find((l) => l.id === id)?.name) + .filter(Boolean); + if (names.length) parts.push(names.join(', ')); + } + return parts.length ? parts.join(' + ') : 'All layers'; + }; + + return ( +
+
+ Export Calendar + +
+ + ( + } + onClick={() => copyUrl(t.token)} + />, +