This repository has been archived on 2026-02-06. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
serengo/src/routes/+page.svelte
2025-12-15 10:10:08 +01:00

467 lines
10 KiB
Svelte

<script lang="ts">
import { Map } from '$lib';
import {
LocationsList,
SelectLocationModal,
LocationFindsModal
} from '$lib/components/locations';
import type { PageData } from './$types';
import { coordinates } from '$lib/stores/location';
import { Button } from '$lib/components/button';
import { calculateDistance } from '$lib/utils/distance';
interface Find {
id: string;
locationId: string;
title: string;
description?: string;
category?: string;
locationName?: string;
isPublic: number;
userId: string;
username: string;
profilePictureUrl?: string | null;
likeCount?: number;
isLikedByUser?: boolean;
isFromFriend?: boolean;
createdAt: string;
media?: Array<{
id: string;
type: string;
url: string;
thumbnailUrl: string;
orderIndex?: number | null;
}>;
}
interface Location {
id: string;
latitude: string;
longitude: string;
createdAt: string;
userId: string;
username: string;
profilePictureUrl?: string | null;
findCount: number;
finds?: Find[];
distance?: number;
}
interface MapLocation {
id: string;
latitude: string;
longitude: string;
createdAt: Date;
userId: string;
user: {
id: string;
username: string;
};
finds: Array<{
id: string;
title: string;
description?: string;
isPublic: number;
media?: Array<{
type: string;
url: string;
thumbnailUrl: string;
}>;
}>;
distance?: number;
}
let { data }: { data: PageData & { locations?: Location[] } } = $props();
let showCreateFindModal = $state(false);
let showLocationFindsModal = $state(false);
let selectedLocation: Location | null = $state(null);
let isSidebarVisible = $state(true);
// Process locations with distance
let locations = $derived.by(() => {
if (!data.locations || !$coordinates) return data.locations || [];
return data.locations
.map((loc: Location) => ({
...loc,
distance: calculateDistance(
$coordinates.latitude,
$coordinates.longitude,
parseFloat(loc.latitude),
parseFloat(loc.longitude)
)
}))
.sort(
(a: Location & { distance?: number }, b: Location & { distance?: number }) =>
(a.distance || 0) - (b.distance || 0)
);
});
// Convert locations to map markers - keep the full location object
let mapLocations: MapLocation[] = $derived(
locations.map(
(loc: Location): MapLocation => ({
id: loc.id,
latitude: loc.latitude,
longitude: loc.longitude,
createdAt: new Date(loc.createdAt),
userId: loc.userId,
user: {
id: loc.userId,
username: loc.username
},
finds: (loc.finds || []).map((find) => ({
id: find.id,
title: find.title,
description: find.description,
isPublic: find.isPublic,
media: find.media || []
})),
distance: loc.distance
})
)
);
function handleLocationExplore(id: string) {
const location = locations.find((l: Location) => l.id === id);
if (location) {
selectedLocation = location;
showLocationFindsModal = true;
}
}
function handleMapLocationClick(location: MapLocation) {
handleLocationExplore(location.id);
}
function openCreateFindModal() {
showCreateFindModal = true;
}
function closeCreateFindModal() {
showCreateFindModal = false;
}
function closeLocationFindsModal() {
showLocationFindsModal = false;
selectedLocation = null;
}
function handleFindCreated() {
closeCreateFindModal();
// Reload page to show new find
window.location.reload();
}
function handleCreateFindFromLocation() {
// Close location modal and open create find modal
showLocationFindsModal = false;
showCreateFindModal = true;
}
function toggleSidebar() {
isSidebarVisible = !isSidebarVisible;
}
</script>
<svelte:head>
<title>Serengo - Meet the Unexpected</title>
<meta
name="description"
content="Discover unexpected places and experiences with Serengo's interactive map. Find hidden gems and explore your surroundings like never before."
/>
<meta property="og:title" content="Serengo - Meet the Unexpected" />
<meta
property="og:description"
content="Discover unexpected places and experiences with Serengo's interactive map. Find hidden gems and explore your surroundings like never before."
/>
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="Serengo - Meet the Unexpected" />
<meta
name="twitter:description"
content="Discover unexpected places and experiences with Serengo's interactive map. Find hidden gems and explore your surroundings like never before."
/>
</svelte:head>
<div class="home-container">
<!-- Fullscreen map -->
<div class="map-section">
<Map
autoCenter={true}
center={[$coordinates?.longitude || 0, $coordinates?.latitude || 51.505]}
locations={mapLocations}
onLocationClick={handleMapLocationClick}
sidebarVisible={isSidebarVisible}
/>
</div>
<!-- Sidebar container -->
<div class="sidebar-container">
<!-- Left sidebar with locations list -->
<div class="finds-sidebar" class:hidden={!isSidebarVisible}>
<div class="finds-header">
{#if data.user}
<h3 class="header-title">Locations</h3>
<Button onclick={openCreateFindModal} class="create-find-button">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" class="mr-2">
<line x1="12" y1="5" x2="12" y2="19" stroke="currentColor" stroke-width="2" />
<line x1="5" y1="12" x2="19" y2="12" stroke="currentColor" stroke-width="2" />
</svg>
Create Find
</Button>
{:else}
<div class="login-prompt">
<h3>Welcome to Serengo</h3>
<p>Login to create finds and view your friends' discoveries</p>
<a href="/login" class="login-button">Login</a>
</div>
{/if}
</div>
<div class="finds-list-container">
<LocationsList {locations} onLocationExplore={handleLocationExplore} hideTitle={true} />
</div>
</div>
<!-- Toggle button -->
<button
class="sidebar-toggle"
class:collapsed={!isSidebarVisible}
onclick={toggleSidebar}
aria-label="Toggle locations list"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
{#if isSidebarVisible}
<path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
{:else}
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
{/if}
</svg>
</button>
</div>
</div>
<!-- Modals -->
{#if showCreateFindModal}
<SelectLocationModal
isOpen={showCreateFindModal}
onClose={closeCreateFindModal}
onFindCreated={handleFindCreated}
/>
{/if}
{#if showLocationFindsModal && selectedLocation}
<LocationFindsModal
isOpen={showLocationFindsModal}
location={selectedLocation}
currentUserId={data.user?.id}
onClose={closeLocationFindsModal}
onCreateFind={handleCreateFindFromLocation}
/>
{/if}
<style>
.home-container {
position: relative;
}
.map-section {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 0;
overflow: hidden;
}
.map-section :global(.map-container) {
height: 100vh;
border-radius: 0;
}
.map-section :global(.maplibregl-map) {
border-radius: 0 !important;
box-shadow: none !important;
}
.sidebar-container {
display: flex;
flex-direction: row;
margin-left: 20px;
gap: 12px;
}
.sidebar-toggle {
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: none;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
z-index: 60;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
}
.sidebar-toggle:hover {
background: rgba(255, 255, 255, 1);
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.2);
}
.sidebar-toggle svg {
color: #333;
}
.finds-sidebar {
width: 40%;
max-width: 1000px;
min-width: 500px;
backdrop-filter: blur(10px);
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
z-index: 50;
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
transition:
transform 0.3s ease,
opacity 0.3s ease;
}
.finds-sidebar.hidden {
display: none;
transform: translateX(-100%);
opacity: 0;
pointer-events: none;
}
.finds-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
background: rgba(255, 255, 255, 0.5);
flex-shrink: 0;
}
.header-title {
font-family: 'Washington', serif;
font-size: 1.5rem;
font-weight: 600;
margin: 0;
color: hsl(var(--foreground));
}
.login-prompt {
width: 100%;
text-align: center;
padding: 1rem;
}
.login-prompt h3 {
font-family: 'Washington', serif;
font-size: 1.25rem;
margin: 0 0 0.5rem 0;
color: hsl(var(--foreground));
}
.login-prompt p {
font-size: 0.875rem;
color: hsl(var(--muted-foreground));
margin: 0 0 1rem 0;
}
.login-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.75rem 1.5rem;
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
transition: all 0.2s ease;
}
.login-button:hover {
background: hsl(var(--primary) / 0.9);
}
.finds-list-container {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
:global(.create-find-button) {
flex-shrink: 0;
}
:global(.mr-2) {
margin-right: 0.5rem;
}
@media (max-width: 768px) {
.sidebar-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column-reverse;
align-items: center;
margin: 0;
}
.sidebar-toggle.collapsed {
margin: 12px auto;
}
.sidebar-toggle svg {
transform: rotate(-90deg);
}
.finds-sidebar {
width: 100%;
max-width: 100vw;
min-width: 0;
border-radius: 20px 20px 0 0;
box-sizing: border-box;
}
.finds-sidebar.hidden {
display: none;
transform: translateX(-100%);
opacity: 0;
pointer-events: none;
}
.finds-header {
padding: 16px;
}
.map-section :global(.map-container) {
height: 100vh;
}
}
@media (max-width: 480px) {
.finds-sidebar {
height: 60vh;
}
.finds-header {
padding: 12px;
}
}
</style>