Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions frontend/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"html5-qrcode": "^2.3.8",
"leaflet": "^1.9.4",
"lucide-react": "^1.11.0",
"panzoom": "^9.4.4",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-leaflet": "^5.0.0",
"react-router-dom": "^7.14.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
Expand All @@ -43,6 +45,7 @@
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/leaflet": "^1.9.21",
"@types/node": "^24.12.2",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
Expand Down
114 changes: 66 additions & 48 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { lazy, Suspense, useEffect } from "react";
import {
BrowserRouter,
Navigate,
Outlet,
Route,
Routes,
useLocation,
useOutletContext,
useParams,
} from "react-router-dom";
import { MainLayout } from "./components/layout/MainLayout";
import {
type AuthOutletContext,
MainLayout,
} from "./components/layout/MainLayout";
import { RequireAuth } from "./components/RequireAuth";
import { AuthProvider } from "./lib/auth-context";
import { MARINAS } from "./lib/marinas";
Expand Down Expand Up @@ -54,6 +58,10 @@ const VerifyEmail = lazy(() =>
import("./pages/VerifyEmail").then((m) => ({ default: m.VerifyEmail })),
);

const MarinaMapPage = lazy(() =>
import("./pages/MarinaMapPage").then((m) => ({ default: m.MarinaMapPage })),
);

// scrolls window + the MainLayout inner scroller back to top on route change
// so navigating between pages doesn't dump you mid-page
function ScrollToTop() {
Expand All @@ -74,6 +82,7 @@ function ScrollToTop() {
// instead of rendering a generic dashboard for "marina admin" etc
function MarinaGuard() {
const { marinaSlug } = useParams<{ marinaSlug: string }>();
const outletContext = useOutletContext<AuthOutletContext>();

if (!marinaSlug || !(marinaSlug in MARINAS)) {
return (
Expand All @@ -83,7 +92,7 @@ function MarinaGuard() {
);
}

return <MainLayout />;
return <Outlet context={outletContext} />;
}

// cf access gates admin host at the edge so no in-app auth needed
Expand Down Expand Up @@ -126,8 +135,6 @@ export function App() {
/>
)}

<Route path="/" element={<Navigate to="/saltsjobaden" replace />} />

<Route
path="/resetpassword/:token"
element={
Expand Down Expand Up @@ -159,59 +166,70 @@ export function App() {
/>
</Route>

<Route path="/:marinaSlug" element={<MarinaGuard />}>
<Route element={<MainLayout />}>
<Route
index
element={
<Suspense fallback={<div className="h-full w-full" />}>
<Dashboard />
<MarinaMapPage />
</Suspense>
}
/>

<Route
path="settings"
element={
<RequireAuth>
<Suspense fallback={<div className="h-full w-full" />}>
<Settings />
</Suspense>
</RequireAuth>
}
/>

<Route
path="settings/invites"
element={
<RequireAuth>
<Suspense fallback={<div className="h-full w-full" />}>
<InvitesSettings />
</Suspense>
</RequireAuth>
}
/>

<Route
path="activity"
element={
<RequireAuth>
<Route path=":marinaSlug" element={<MarinaGuard />}>
<Route
index
element={
<Suspense fallback={<div className="h-full w-full" />}>
<ActivityLogPage />
<Dashboard />
</Suspense>
</RequireAuth>
}
/>

<Route
path="bookings"
element={
<RequireAuth>
<Suspense fallback={<div className="h-full w-full" />}>
<MyBookingsPage />
</Suspense>
</RequireAuth>
}
/>
}
/>

<Route
path="settings"
element={
<RequireAuth>
<Suspense fallback={<div className="h-full w-full" />}>
<Settings />
</Suspense>
</RequireAuth>
}
/>

<Route
path="settings/invites"
element={
<RequireAuth>
<Suspense fallback={<div className="h-full w-full" />}>
<InvitesSettings />
</Suspense>
</RequireAuth>
}
/>

<Route
path="activity"
element={
<RequireAuth>
<Suspense fallback={<div className="h-full w-full" />}>
<ActivityLogPage />
</Suspense>
</RequireAuth>
}
/>

<Route
path="bookings"
element={
<RequireAuth>
<Suspense fallback={<div className="h-full w-full" />}>
<MyBookingsPage />
</Suspense>
</RequireAuth>
}
/>
</Route>
</Route>

<Route
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/HarborMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ export function HarborMap() {
bounds: true,
boundsPadding: 0.15,
beforeMouseDown: (event) => {
// Leave back/forward/middle mouse buttons to the browser (e.g. mouse back → history).
if (event.button !== 0) return true;

const target = event.target as HTMLElement | null;

if (
Expand Down
125 changes: 125 additions & 0 deletions frontend/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,128 @@
0 0 10px rgba(0, 147, 233, 0.4);
}
}

.leaflet-container {
font-family: var(--font-body);
background: #e8f4fc;
}

.leaflet-interactive {
cursor: pointer;
}

/* Sweden marina picker — hover cards */
.marina-map-tooltip.leaflet-tooltip {
padding: 0;
border: 1px solid rgb(255 255 255 / 0.65);
border-radius: 1rem;
background: rgb(255 255 255 / 0.92);
box-shadow:
0 12px 40px rgb(10 37 64 / 0.14),
0 0 0 1px rgb(0 147 233 / 0.08);
backdrop-filter: blur(16px);
pointer-events: none;
}

.marina-map-tooltip.leaflet-tooltip-top::before {
border-top-color: rgb(255 255 255 / 0.92);
}

.marina-map-tip {
min-width: 10.5rem;
padding: 0.65rem 0.85rem 0.7rem;
font-family: var(--font-body);
}

.marina-map-tip--active {
padding: 0.75rem 0.95rem 0.8rem;
}

.marina-map-tip__header {
display: flex;
align-items: flex-start;
gap: 0.5rem;
}

.marina-map-tip__icon {
display: grid;
place-items: center;
width: 1.75rem;
height: 1.75rem;
flex-shrink: 0;
border-radius: 9999px;
background: rgb(0 147 233 / 0.12);
color: #0093e9;
}

.marina-map-tip--soon .marina-map-tip__icon {
background: rgb(10 37 64 / 0.06);
color: rgb(10 37 64 / 0.45);
}

.marina-map-tip__name {
font-size: 0.875rem;
font-weight: 800;
line-height: 1.2;
color: #0a2540;
}

.marina-map-tip__region {
margin-top: 0.1rem;
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgb(10 37 64 / 0.45);
}

.marina-map-tip__tagline {
margin-top: 0.45rem;
font-size: 0.7rem;
font-weight: 500;
line-height: 1.35;
color: rgb(10 37 64 / 0.55);
}

.marina-map-tip__cta {
margin-top: 0.5rem;
padding-top: 0.45rem;
border-top: 1px solid rgb(10 37 64 / 0.08);
font-size: 0.6rem;
font-weight: 800;
letter-spacing: 0.16em;
text-transform: uppercase;
color: #0093e9;
}

.marina-map-tip--soon .marina-map-tip__cta {
color: rgb(10 37 64 / 0.4);
}

.marina-map-tip--not-planned .marina-map-tip__icon {
background: rgb(239 68 68 / 0.15);
color: #dc2626;
}

.marina-map-tip--not-planned .marina-map-tip__cta {
color: #dc2626;
}

.marina-map-tip--active .marina-map-tip__cta {
color: #0093e9;
}

.marina-map-hint {
font-family: var(--font-body);
}

/* Clear the floating DockPulse header (safe-area + 1rem + h-16) */
.leaflet-top.leaflet-left {
top: calc(env(safe-area-inset-top) + 5.75rem);
}

@media (min-width: 768px) {
.leaflet-top.leaflet-left {
top: 6.25rem; /* md:top-6 + h-16 + gap */
}
}
Loading
Loading