mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 22:19:39 +02:00
feat(citycorners): add PWA, i18n (DE/EN), and migrate landing to Tailwind
PWA: @vite-pwa/sveltekit with shared-pwa config, offline fallback page, service worker with standard caching preset. i18n: svelte-i18n with DE/EN locale files, all UI strings translated, language switcher in PillNav, auth pages use shared-i18n translations. Landing: Migrated from scoped CSS to Tailwind CSS with @astrojs/tailwind. Hero section, card grid, category filter buttons, detail page with timeline. Removed unused components (Welcome, ThemeToggle, update-locations.js). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7691f66cbb
commit
a2f8c32059
30 changed files with 1090 additions and 975 deletions
|
|
@ -1,5 +1,8 @@
|
|||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({});
|
||||
export default defineConfig({
|
||||
site: 'https://citycorners.mana.how',
|
||||
integrations: [tailwind(), sitemap()],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.0",
|
||||
"@astrojs/sitemap": "^3.2.1",
|
||||
"@astrojs/tailwind": "^6.0.0",
|
||||
"astro": "^5.16.11",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
|
@ -1,25 +1,66 @@
|
|||
---
|
||||
// This is a placeholder for the filter component.
|
||||
// The actual filtering logic will be implemented on the page.
|
||||
const categories = ['Sehenswürdigkeit', 'Restaurant', 'Laden'];
|
||||
const categories = [
|
||||
{ value: '', label: 'Alle' },
|
||||
{ value: 'sehenswürdigkeit', label: 'Sehenswürdigkeiten' },
|
||||
{ value: 'restaurant', label: 'Restaurants' },
|
||||
{ value: 'laden', label: 'Läden' },
|
||||
{ value: 'museum', label: 'Museen' },
|
||||
];
|
||||
---
|
||||
|
||||
<div class="filter-container">
|
||||
<label for="category-filter">Filter by Category:</label>
|
||||
<select id="category-filter">
|
||||
<option value="">All</option>
|
||||
{categories.map((category) => <option value={category.toLowerCase()}>{category}</option>)}
|
||||
</select>
|
||||
<div class="flex flex-wrap gap-2" id="category-filter">
|
||||
{
|
||||
categories.map((cat) => (
|
||||
<button
|
||||
class="filter-btn rounded-full px-4 py-2 text-sm font-medium transition-colors"
|
||||
data-category={cat.value}
|
||||
>
|
||||
{cat.label}
|
||||
</button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.filter-container {
|
||||
margin: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
#category-filter {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const buttons = document.querySelectorAll('.filter-btn');
|
||||
const container = document.getElementById('locations-container');
|
||||
if (!container) return;
|
||||
const allCards = Array.from(container.children) as HTMLElement[];
|
||||
let activeCategory = '';
|
||||
|
||||
function updateButtons() {
|
||||
buttons.forEach((btn) => {
|
||||
const cat = (btn as HTMLElement).dataset.category || '';
|
||||
if (cat === activeCategory) {
|
||||
btn.className =
|
||||
'filter-btn rounded-full px-4 py-2 text-sm font-medium transition-colors bg-primary text-white';
|
||||
} else {
|
||||
btn.className =
|
||||
'filter-btn rounded-full px-4 py-2 text-sm font-medium transition-colors bg-white text-gray-600 hover:bg-gray-100 border border-gray-200';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function filterCards() {
|
||||
allCards.forEach((card) => {
|
||||
const cardCategory = card.dataset.category || '';
|
||||
if (!activeCategory || cardCategory === activeCategory) {
|
||||
card.style.display = '';
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
buttons.forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
activeCategory = (btn as HTMLElement).dataset.category || '';
|
||||
updateButtons();
|
||||
filterCards();
|
||||
});
|
||||
});
|
||||
|
||||
updateButtons();
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,41 @@
|
|||
---
|
||||
interface Props {
|
||||
location: {
|
||||
id: number;
|
||||
name: string;
|
||||
category: string;
|
||||
description: string;
|
||||
image: string;
|
||||
};
|
||||
}
|
||||
|
||||
const { location } = Astro.props;
|
||||
|
||||
const categoryColors: Record<string, string> = {
|
||||
Sehenswürdigkeit: 'bg-blue-100 text-blue-700',
|
||||
Restaurant: 'bg-red-100 text-red-700',
|
||||
Laden: 'bg-green-100 text-green-700',
|
||||
Museum: 'bg-purple-100 text-purple-700',
|
||||
};
|
||||
---
|
||||
|
||||
<div class="location-card" data-category="{location.category.toLowerCase()}">
|
||||
<img src={location.image} alt={location.name} />
|
||||
<h3>{location.name}</h3>
|
||||
<p>{location.category}</p>
|
||||
<a href={`/locations/${location.id}`}>Details</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.location-card {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.location-card img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
<a
|
||||
href={`/locations/${location.id}`}
|
||||
class="group block overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm transition-all hover:shadow-lg hover:-translate-y-0.5"
|
||||
data-category={location.category.toLowerCase()}
|
||||
>
|
||||
<img src={location.image} alt={location.name} class="h-48 w-full object-cover" loading="lazy" />
|
||||
<div class="p-4">
|
||||
<span
|
||||
class={`inline-block rounded-full px-2.5 py-0.5 text-xs font-medium ${categoryColors[location.category] || 'bg-gray-100 text-gray-700'}`}
|
||||
>
|
||||
{location.category}
|
||||
</span>
|
||||
<h3 class="mt-2 text-lg font-semibold text-gray-900 group-hover:text-primary">
|
||||
{location.name}
|
||||
</h3>
|
||||
<p class="mt-1 line-clamp-2 text-sm text-gray-600">
|
||||
{location.description}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
<button id="theme-toggle" type="button" title="Toggle Theme"> Toggle Theme </button>
|
||||
|
||||
<style>
|
||||
#theme-toggle {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--color-text);
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
---
|
||||
import astroLogo from '../assets/astro.svg';
|
||||
import background from '../assets/background.svg';
|
||||
---
|
||||
|
||||
<div id="container">
|
||||
<img id="background" src={background.src} alt="" fetchpriority="high" />
|
||||
<main>
|
||||
<section id="hero">
|
||||
<a href="https://astro.build"
|
||||
><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
|
||||
>
|
||||
<h1>
|
||||
To get started, open the <code><pre>src/pages</pre></code> directory in your project.
|
||||
</h1>
|
||||
<section id="links">
|
||||
<a class="button" href="https://docs.astro.build">Read our docs</a>
|
||||
<a href="https://astro.build/chat"
|
||||
>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
|
||||
></path></svg
|
||||
>
|
||||
</a>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<a href="https://astro.build/blog/astro-5/" id="news" class="box">
|
||||
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
|
||||
fill="#111827"></path></svg
|
||||
>
|
||||
<h2>What's New in Astro 5.0?</h2>
|
||||
<p>
|
||||
From content layers to server islands, click to learn more about the new features and
|
||||
improvements in Astro 5.0
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
filter: blur(100px);
|
||||
}
|
||||
|
||||
#container {
|
||||
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#hero {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
#links {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
#links a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
color: #111827;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
#links a:hover {
|
||||
color: rgb(78, 80, 86);
|
||||
}
|
||||
|
||||
#links a svg {
|
||||
height: 1em;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
#links a.button {
|
||||
color: white;
|
||||
background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
|
||||
inset 0 -2px 0 rgba(0, 0, 0, 0.24);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#links a.button:hover {
|
||||
color: rgb(230, 230, 230);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family:
|
||||
ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
|
||||
monospace;
|
||||
font-weight: normal;
|
||||
background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 1em;
|
||||
font-weight: normal;
|
||||
color: #111827;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #4b5563;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.006em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
display: inline-block;
|
||||
background:
|
||||
linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
|
||||
linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.box {
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 1);
|
||||
border-radius: 16px;
|
||||
border: 1px solid white;
|
||||
}
|
||||
|
||||
#news {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
max-width: 300px;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s;
|
||||
backdrop-filter: blur(50px);
|
||||
}
|
||||
|
||||
#news:hover {
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
@media screen and (max-height: 368px) {
|
||||
#news {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
#container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#hero {
|
||||
display: block;
|
||||
padding-top: 10%;
|
||||
}
|
||||
|
||||
#links {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#links a.button {
|
||||
padding: 14px 18px;
|
||||
}
|
||||
|
||||
#news {
|
||||
right: 16px;
|
||||
left: 16px;
|
||||
bottom: 2.5rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,22 +1,25 @@
|
|||
---
|
||||
interface Props {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const { title = 'CityCorners – Entdecke Konstanz' } = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Entdecke Sehenswürdigkeiten, Restaurants, Museen und Läden in Konstanz am Bodensee."
|
||||
/>
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Astro Basics</title>
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<body class="min-h-screen bg-gray-50 text-gray-900 antialiased">
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -6,41 +6,33 @@ import locations from '../data/locations.json';
|
|||
---
|
||||
|
||||
<Layout>
|
||||
<main>
|
||||
<h1>CityCorners Konstanz</h1>
|
||||
<Filter />
|
||||
<div id="locations-container" class="locations-grid">
|
||||
<!-- Hero -->
|
||||
<header class="bg-primary py-16 text-white">
|
||||
<div class="mx-auto max-w-6xl px-6 text-center">
|
||||
<h1 class="text-4xl font-bold sm:text-5xl">CityCorners</h1>
|
||||
<p class="mt-3 text-lg text-blue-100">Entdecke Konstanz am Bodensee</p>
|
||||
<p class="mt-1 text-sm text-blue-200">Sehenswürdigkeiten · Restaurants · Museen · Läden</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="mx-auto max-w-6xl px-6 py-10">
|
||||
<div class="mb-8">
|
||||
<Filter />
|
||||
</div>
|
||||
|
||||
<div id="locations-container" class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{locations.map((location) => <LocationCard location={location} />)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="border-t border-gray-200 bg-white py-8">
|
||||
<div class="mx-auto max-w-6xl px-6 text-center text-sm text-gray-500">
|
||||
<p>CityCorners – Ein Stadtführer für Konstanz</p>
|
||||
<p class="mt-1">
|
||||
Teil des <a href="https://mana.how" class="text-primary hover:underline">Mana</a> Ökosystems
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.locations-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const filter = document.getElementById('category-filter');
|
||||
const locationsContainer = document.getElementById('locations-container');
|
||||
const allLocations = Array.from(locationsContainer.children);
|
||||
|
||||
filter.addEventListener('change', (event) => {
|
||||
const selectedCategory = event.target.value;
|
||||
locationsContainer.innerHTML = ''; // Clear existing locations
|
||||
|
||||
const filteredLocations = allLocations.filter((locationElement) => {
|
||||
if (!selectedCategory) return true; // Show all if no category is selected
|
||||
return locationElement.dataset.category === selectedCategory;
|
||||
});
|
||||
|
||||
filteredLocations.forEach((locationElement) => {
|
||||
locationsContainer.appendChild(locationElement);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -12,102 +12,121 @@ const { id } = Astro.params;
|
|||
const location = locations.find((loc) => loc.id.toString() === id);
|
||||
|
||||
if (!location) {
|
||||
return Astro.redirect('/404');
|
||||
return Astro.redirect('/');
|
||||
}
|
||||
|
||||
const categoryColors: Record<string, string> = {
|
||||
Sehenswürdigkeit: 'bg-blue-100 text-blue-700',
|
||||
Restaurant: 'bg-red-100 text-red-700',
|
||||
Laden: 'bg-green-100 text-green-700',
|
||||
Museum: 'bg-purple-100 text-purple-700',
|
||||
};
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<main class="location-detail">
|
||||
<a href="/" class="back-link">« Zurück zur Übersicht</a>
|
||||
<article>
|
||||
<img src={location.image} alt={location.name} class="location-image" />
|
||||
<h1>{location.name}</h1>
|
||||
<p class="category">{location.category}</p>
|
||||
<p class="description">{location.description}</p>
|
||||
<Layout title={`${location.name} – CityCorners`}>
|
||||
<main class="mx-auto max-w-3xl px-6 py-8">
|
||||
<a
|
||||
href="/"
|
||||
class="mb-6 inline-flex items-center gap-1 text-sm text-gray-500 hover:text-primary"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"></path>
|
||||
</svg>
|
||||
Zurück zur Übersicht
|
||||
</a>
|
||||
|
||||
{
|
||||
location.timeline && (
|
||||
<div class="timeline">
|
||||
<h2>Historische Zeitachse</h2>
|
||||
<ul>
|
||||
{location.timeline.map((event) => (
|
||||
<li>
|
||||
<strong>{event.year}:</strong> {event.description}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<img
|
||||
src={location.image}
|
||||
alt={location.name}
|
||||
class="mb-6 h-64 w-full rounded-xl object-cover sm:h-80"
|
||||
/>
|
||||
|
||||
<span
|
||||
class={`inline-block rounded-full px-3 py-1 text-sm font-medium ${categoryColors[location.category] || 'bg-gray-100 text-gray-700'}`}
|
||||
>
|
||||
{location.category}
|
||||
</span>
|
||||
|
||||
<h1 class="mt-3 text-3xl font-bold text-gray-900">{location.name}</h1>
|
||||
|
||||
{
|
||||
location.address && (
|
||||
<p class="mt-2 flex items-center gap-1.5 text-gray-500">
|
||||
<svg
|
||||
class="h-4 w-4 flex-shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z"
|
||||
/>
|
||||
</svg>
|
||||
{location.address}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
<p class="mt-4 text-base leading-relaxed text-gray-700">{location.description}</p>
|
||||
|
||||
{
|
||||
location.timeline && location.timeline.length > 0 && (
|
||||
<div class="mt-8">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-900">Geschichte</h2>
|
||||
<div class="space-y-0">
|
||||
{location.timeline.map((event: { year: string; description: string }, i: number) => (
|
||||
<div class="relative flex gap-4 pb-6">
|
||||
{i < location.timeline!.length - 1 && (
|
||||
<div class="absolute left-[11px] top-6 h-full w-0.5 bg-gray-200" />
|
||||
)}
|
||||
<div class="relative z-10 mt-1.5 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full border-2 border-primary bg-white">
|
||||
<div class="h-2 w-2 rounded-full bg-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-mono text-sm font-bold text-primary">{event.year}</span>
|
||||
<p class="mt-0.5 text-sm text-gray-600">{event.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<p class="address"><strong>Adresse:</strong> {location.address}</p>
|
||||
<!-- In a real application, you would use a map component here -->
|
||||
<div class="map-placeholder">
|
||||
Karte für {location.coordinates.lat}, {location.coordinates.lng}
|
||||
</div>
|
||||
</article>
|
||||
{
|
||||
location.coordinates && (
|
||||
<div class="mt-8 overflow-hidden rounded-xl border border-gray-200">
|
||||
<a
|
||||
href={`https://www.openstreetmap.org/?mlat=${location.coordinates.lat}&mlon=${location.coordinates.lng}#map=17/${location.coordinates.lat}/${location.coordinates.lng}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex items-center justify-center gap-2 bg-gray-50 px-4 py-4 text-sm text-gray-500 transition-colors hover:text-primary"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
|
||||
/>
|
||||
</svg>
|
||||
In OpenStreetMap öffnen
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.location-detail {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
.location-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.category {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
}
|
||||
.description {
|
||||
line-height: 1.6;
|
||||
}
|
||||
.address {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.map-placeholder {
|
||||
margin-top: 20px;
|
||||
background-color: #f0f0f0;
|
||||
padding: 50px;
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.timeline {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.timeline h2 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.timeline ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
border-left: 2px solid #ccc;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.timeline li {
|
||||
padding: 10px 20px;
|
||||
position: relative;
|
||||
}
|
||||
.timeline li::before {
|
||||
content: '';
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: #ccc;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
left: -6px;
|
||||
top: 18px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
// This script is a placeholder for a data update agent.
|
||||
// In a real application, this script would fetch data from an API,
|
||||
// process it, and then write it to the locations.json file.
|
||||
|
||||
async function updateLocations() {
|
||||
try {
|
||||
const dataPath = path.join(process.cwd(), 'src', 'data', 'locations.json');
|
||||
const data = await fs.readFile(dataPath, 'utf-8');
|
||||
const locations = JSON.parse(data);
|
||||
|
||||
console.log('Successfully read location data.');
|
||||
console.log(`Found ${locations.length} locations.`);
|
||||
|
||||
// Here you could add logic to fetch new data and compare it
|
||||
// with the existing data to determine if an update is needed.
|
||||
|
||||
console.log('Location data is up to date.');
|
||||
} catch (error) {
|
||||
console.error('Error updating location data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
updateLocations();
|
||||
13
apps/citycorners/apps/landing/tailwind.config.mjs
Normal file
13
apps/citycorners/apps/landing/tailwind.config.mjs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#2563eb',
|
||||
'primary-dark': '#1d4ed8',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
"type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@manacore/shared-pwa": "workspace:*",
|
||||
"@manacore/shared-vite-config": "workspace:*",
|
||||
"@sveltejs/adapter-node": "^5.0.0",
|
||||
"@sveltejs/kit": "^2.47.1",
|
||||
|
|
@ -20,6 +21,7 @@
|
|||
"@tailwindcss/vite": "^4.1.7",
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@types/node": "^20.0.0",
|
||||
"@vite-pwa/sveltekit": "^1.1.0",
|
||||
"svelte": "^5.41.0",
|
||||
"svelte-check": "^4.3.3",
|
||||
"tailwindcss": "^4.1.7",
|
||||
|
|
@ -32,11 +34,13 @@
|
|||
"@manacore/shared-auth-ui": "workspace:*",
|
||||
"@manacore/shared-branding": "workspace:*",
|
||||
"@manacore/shared-error-tracking": "workspace:*",
|
||||
"@manacore/shared-i18n": "workspace:*",
|
||||
"@manacore/shared-icons": "workspace:*",
|
||||
"@manacore/shared-stores": "workspace:*",
|
||||
"@manacore/shared-tailwind": "workspace:*",
|
||||
"@manacore/shared-theme": "workspace:*",
|
||||
"@manacore/shared-ui": "workspace:*",
|
||||
"leaflet": "^1.9.4"
|
||||
"leaflet": "^1.9.4",
|
||||
"svelte-i18n": "^4.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
40
apps/citycorners/apps/web/src/lib/i18n/index.ts
Normal file
40
apps/citycorners/apps/web/src/lib/i18n/index.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { browser } from '$app/environment';
|
||||
import { init, register, locale, waitLocale } from 'svelte-i18n';
|
||||
|
||||
export const supportedLocales = ['de', 'en'] as const;
|
||||
export type SupportedLocale = (typeof supportedLocales)[number];
|
||||
|
||||
const defaultLocale = 'de';
|
||||
|
||||
register('de', () => import('./locales/de.json'));
|
||||
register('en', () => import('./locales/en.json'));
|
||||
|
||||
function getInitialLocale(): SupportedLocale {
|
||||
if (browser) {
|
||||
const stored = localStorage.getItem('citycorners_locale');
|
||||
if (stored && supportedLocales.includes(stored as SupportedLocale)) {
|
||||
return stored as SupportedLocale;
|
||||
}
|
||||
|
||||
const browserLang = navigator.language.split('-')[0];
|
||||
if (supportedLocales.includes(browserLang as SupportedLocale)) {
|
||||
return browserLang as SupportedLocale;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultLocale;
|
||||
}
|
||||
|
||||
init({
|
||||
fallbackLocale: defaultLocale,
|
||||
initialLocale: getInitialLocale(),
|
||||
});
|
||||
|
||||
export function setLocale(newLocale: SupportedLocale) {
|
||||
locale.set(newLocale);
|
||||
if (browser) {
|
||||
localStorage.setItem('citycorners_locale', newLocale);
|
||||
}
|
||||
}
|
||||
|
||||
export { waitLocale };
|
||||
82
apps/citycorners/apps/web/src/lib/i18n/locales/de.json
Normal file
82
apps/citycorners/apps/web/src/lib/i18n/locales/de.json
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"app": {
|
||||
"name": "CityCorners",
|
||||
"tagline": "Entdecke Konstanz"
|
||||
},
|
||||
"nav": {
|
||||
"explore": "Entdecken",
|
||||
"map": "Karte",
|
||||
"favorites": "Favoriten",
|
||||
"settings": "Einstellungen",
|
||||
"showNav": "Navigation einblenden",
|
||||
"hideNav": "Navigation ausblenden"
|
||||
},
|
||||
"home": {
|
||||
"title": "Entdecke Konstanz",
|
||||
"subtitle": "Sehenswürdigkeiten, Restaurants, Museen und mehr",
|
||||
"all": "Alle",
|
||||
"loading": "Laden...",
|
||||
"noResults": "Keine Locations gefunden."
|
||||
},
|
||||
"categories": {
|
||||
"sight": "Sehenswürdigkeiten",
|
||||
"restaurant": "Restaurants",
|
||||
"shop": "Läden",
|
||||
"museum": "Museen"
|
||||
},
|
||||
"category": {
|
||||
"sight": "Sehenswürdigkeit",
|
||||
"restaurant": "Restaurant",
|
||||
"shop": "Laden",
|
||||
"museum": "Museum"
|
||||
},
|
||||
"detail": {
|
||||
"history": "Geschichte",
|
||||
"openInMaps": "In OpenStreetMap öffnen",
|
||||
"back": "Zurück zur Übersicht",
|
||||
"notFound": "Location nicht gefunden."
|
||||
},
|
||||
"favorites": {
|
||||
"title": "Favoriten",
|
||||
"subtitle": "Deine gespeicherten Orte",
|
||||
"empty": "Noch keine Favoriten. Tippe auf das Herz bei einer Location, um sie zu speichern.",
|
||||
"loginRequired": "Melde dich an, um Favoriten zu speichern.",
|
||||
"add": "Zu Favoriten hinzufügen",
|
||||
"remove": "Aus Favoriten entfernen"
|
||||
},
|
||||
"map": {
|
||||
"title": "Karte",
|
||||
"subtitle": "Alle Orte in Konstanz"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Ort suchen...",
|
||||
"noResults": "Keine Ergebnisse",
|
||||
"searching": "Suche..."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"appearance": "Erscheinungsbild",
|
||||
"mode": "Modus",
|
||||
"light": "Hell",
|
||||
"dark": "Dunkel",
|
||||
"system": "System",
|
||||
"colorScheme": "Farbschema",
|
||||
"account": "Account",
|
||||
"email": "E-Mail",
|
||||
"logout": "Abmelden",
|
||||
"loginPrompt": "Melde dich an, um Favoriten zu speichern und alle Features zu nutzen.",
|
||||
"login": "Anmelden",
|
||||
"register": "Registrieren",
|
||||
"about": "Über CityCorners",
|
||||
"aboutText": "CityCorners ist ein Stadtführer für Konstanz am Bodensee. Entdecke Sehenswürdigkeiten, Restaurants, Museen und Läden."
|
||||
},
|
||||
"auth": {
|
||||
"loginTitle": "Login - CityCorners",
|
||||
"registerTitle": "Registrieren - CityCorners"
|
||||
},
|
||||
"offline": {
|
||||
"title": "Keine Verbindung",
|
||||
"message": "Du bist gerade offline. Sobald du wieder eine Internetverbindung hast, kannst du CityCorners weiter nutzen.",
|
||||
"retry": "Erneut versuchen"
|
||||
}
|
||||
}
|
||||
82
apps/citycorners/apps/web/src/lib/i18n/locales/en.json
Normal file
82
apps/citycorners/apps/web/src/lib/i18n/locales/en.json
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"app": {
|
||||
"name": "CityCorners",
|
||||
"tagline": "Discover Konstanz"
|
||||
},
|
||||
"nav": {
|
||||
"explore": "Explore",
|
||||
"map": "Map",
|
||||
"favorites": "Favorites",
|
||||
"settings": "Settings",
|
||||
"showNav": "Show navigation",
|
||||
"hideNav": "Hide navigation"
|
||||
},
|
||||
"home": {
|
||||
"title": "Discover Konstanz",
|
||||
"subtitle": "Sights, restaurants, museums and more",
|
||||
"all": "All",
|
||||
"loading": "Loading...",
|
||||
"noResults": "No locations found."
|
||||
},
|
||||
"categories": {
|
||||
"sight": "Sights",
|
||||
"restaurant": "Restaurants",
|
||||
"shop": "Shops",
|
||||
"museum": "Museums"
|
||||
},
|
||||
"category": {
|
||||
"sight": "Sight",
|
||||
"restaurant": "Restaurant",
|
||||
"shop": "Shop",
|
||||
"museum": "Museum"
|
||||
},
|
||||
"detail": {
|
||||
"history": "History",
|
||||
"openInMaps": "Open in OpenStreetMap",
|
||||
"back": "Back to overview",
|
||||
"notFound": "Location not found."
|
||||
},
|
||||
"favorites": {
|
||||
"title": "Favorites",
|
||||
"subtitle": "Your saved places",
|
||||
"empty": "No favorites yet. Tap the heart on a location to save it.",
|
||||
"loginRequired": "Sign in to save favorites.",
|
||||
"add": "Add to favorites",
|
||||
"remove": "Remove from favorites"
|
||||
},
|
||||
"map": {
|
||||
"title": "Map",
|
||||
"subtitle": "All places in Konstanz"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search places...",
|
||||
"noResults": "No results",
|
||||
"searching": "Searching..."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"appearance": "Appearance",
|
||||
"mode": "Mode",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System",
|
||||
"colorScheme": "Color scheme",
|
||||
"account": "Account",
|
||||
"email": "Email",
|
||||
"logout": "Sign out",
|
||||
"loginPrompt": "Sign in to save favorites and use all features.",
|
||||
"login": "Sign in",
|
||||
"register": "Sign up",
|
||||
"about": "About CityCorners",
|
||||
"aboutText": "CityCorners is a city guide for Konstanz at Lake Constance. Discover sights, restaurants, museums and shops."
|
||||
},
|
||||
"auth": {
|
||||
"loginTitle": "Login - CityCorners",
|
||||
"registerTitle": "Sign up - CityCorners"
|
||||
},
|
||||
"offline": {
|
||||
"title": "No connection",
|
||||
"message": "You are currently offline. You can continue using CityCorners once you have an internet connection again.",
|
||||
"retry": "Try again"
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { _, locale } from 'svelte-i18n';
|
||||
import { PillNavigation, QuickInputBar } from '@manacore/shared-ui';
|
||||
import type { PillNavItem, PillDropdownItem, QuickInputItem } from '@manacore/shared-ui';
|
||||
import { theme } from '$lib/stores/theme.svelte';
|
||||
|
|
@ -10,6 +11,8 @@
|
|||
import { THEME_DEFINITIONS, DEFAULT_THEME_VARIANTS } from '@manacore/shared-theme';
|
||||
import type { ThemeVariant } from '@manacore/shared-theme';
|
||||
import { getPillAppItems } from '@manacore/shared-branding';
|
||||
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
|
||||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
|
||||
const appItems = getPillAppItems('citycorners');
|
||||
|
||||
|
|
@ -34,13 +37,23 @@
|
|||
THEME_DEFINITIONS[theme.variant]?.label || THEME_DEFINITIONS.lume?.label || 'Lume'
|
||||
);
|
||||
|
||||
let userEmail = $derived(authStore.user?.email || 'Menü');
|
||||
// Language
|
||||
let currentLocale = $derived($locale || 'de');
|
||||
function handleLocaleChange(newLocale: string) {
|
||||
setLocale(newLocale as 'de' | 'en');
|
||||
}
|
||||
let languageItems = $derived(
|
||||
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
|
||||
);
|
||||
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
|
||||
|
||||
let userEmail = $derived(authStore.user?.email || $_('nav.settings'));
|
||||
|
||||
let navItems = $derived<PillNavItem[]>([
|
||||
{ href: '/', label: 'Entdecken', icon: 'compass' },
|
||||
{ href: '/map', label: 'Karte', icon: 'mappin' },
|
||||
{ href: '/favorites', label: 'Favoriten', icon: 'heart' },
|
||||
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
|
||||
{ href: '/', label: $_('nav.explore'), icon: 'compass' },
|
||||
{ href: '/map', label: $_('nav.map'), icon: 'mappin' },
|
||||
{ href: '/favorites', label: $_('nav.favorites'), icon: 'heart' },
|
||||
{ href: '/settings', label: $_('nav.settings'), icon: 'settings' },
|
||||
]);
|
||||
|
||||
function handleToggleTheme() {
|
||||
|
|
@ -57,13 +70,6 @@
|
|||
goto('/login');
|
||||
}
|
||||
|
||||
const categoryLabels: Record<string, string> = {
|
||||
sight: 'Sehenswürdigkeit',
|
||||
restaurant: 'Restaurant',
|
||||
shop: 'Laden',
|
||||
museum: 'Museum',
|
||||
};
|
||||
|
||||
const backendUrl =
|
||||
typeof window !== 'undefined'
|
||||
? (window as any).__PUBLIC_BACKEND_URL__ || 'http://localhost:3025'
|
||||
|
|
@ -85,7 +91,7 @@
|
|||
return data.locations.slice(0, 8).map((loc: any) => ({
|
||||
id: loc.id,
|
||||
title: loc.name,
|
||||
subtitle: categoryLabels[loc.category] || loc.category,
|
||||
subtitle: $_(`category.${loc.category}`),
|
||||
icon: 'mappin' as const,
|
||||
href: `/locations/${loc.id}`,
|
||||
}));
|
||||
|
|
@ -125,6 +131,9 @@
|
|||
{currentThemeVariantLabel}
|
||||
themeMode={theme.mode}
|
||||
onThemeModeChange={handleThemeModeChange}
|
||||
showLanguageSwitcher={true}
|
||||
{languageItems}
|
||||
{currentLanguageLabel}
|
||||
showLogout={authStore.isAuthenticated}
|
||||
onLogout={handleLogout}
|
||||
loginHref="/login"
|
||||
|
|
@ -140,9 +149,9 @@
|
|||
<QuickInputBar
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}
|
||||
placeholder="Ort suchen..."
|
||||
emptyText="Keine Ergebnisse"
|
||||
searchingText="Suche..."
|
||||
placeholder={$_('search.placeholder')}
|
||||
emptyText={$_('search.noResults')}
|
||||
searchingText={$_('search.searching')}
|
||||
appIcon="mappin"
|
||||
bottomOffset={inputBarBottomOffset}
|
||||
hasFabRight={true}
|
||||
|
|
@ -151,7 +160,7 @@
|
|||
<button
|
||||
class="pillnav-fab"
|
||||
onclick={handleNavToggle}
|
||||
title={showNav ? 'Navigation ausblenden' : 'Navigation einblenden'}
|
||||
title={showNav ? $_('nav.hideNav') : $_('nav.showNav')}
|
||||
>
|
||||
{#if !showNav}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" class="fab-icon">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { favoritesStore } from '$lib/stores/favorites.svelte';
|
||||
|
||||
|
|
@ -23,12 +24,7 @@
|
|||
? (window as any).__PUBLIC_BACKEND_URL__ || 'http://localhost:3025'
|
||||
: 'http://localhost:3025';
|
||||
|
||||
const categories = [
|
||||
{ value: 'sight', label: 'Sehenswürdigkeiten' },
|
||||
{ value: 'restaurant', label: 'Restaurants' },
|
||||
{ value: 'shop', label: 'Läden' },
|
||||
{ value: 'museum', label: 'Museen' },
|
||||
];
|
||||
const categoryKeys = ['sight', 'restaurant', 'shop', 'museum'];
|
||||
|
||||
let filtered = $derived(
|
||||
selectedCategory ? locations.filter((l) => l.category === selectedCategory) : locations
|
||||
|
|
@ -58,12 +54,12 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>CityCorners - Entdecke Konstanz</title>
|
||||
<title>{$_('app.name')} - {$_('app.tagline')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<header class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-foreground">Entdecke Konstanz</h1>
|
||||
<p class="text-foreground-secondary">Sehenswürdigkeiten, Restaurants, Museen und mehr</p>
|
||||
<h1 class="text-2xl font-bold text-foreground">{$_('home.title')}</h1>
|
||||
<p class="text-foreground-secondary">{$_('home.subtitle')}</p>
|
||||
</header>
|
||||
|
||||
<div class="mb-6 flex flex-wrap gap-2">
|
||||
|
|
@ -73,24 +69,24 @@
|
|||
: 'bg-background-card text-foreground-secondary hover:bg-background-card-hover'}"
|
||||
onclick={() => (selectedCategory = null)}
|
||||
>
|
||||
Alle
|
||||
{$_('home.all')}
|
||||
</button>
|
||||
{#each categories as cat}
|
||||
{#each categoryKeys as cat}
|
||||
<button
|
||||
class="rounded-full px-4 py-2 text-sm transition-colors {selectedCategory === cat.value
|
||||
class="rounded-full px-4 py-2 text-sm transition-colors {selectedCategory === cat
|
||||
? 'bg-primary text-white'
|
||||
: 'bg-background-card text-foreground-secondary hover:bg-background-card-hover'}"
|
||||
onclick={() => (selectedCategory = cat.value)}
|
||||
onclick={() => (selectedCategory = cat)}
|
||||
>
|
||||
{cat.label}
|
||||
{$_(`categories.${cat}`)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<p class="text-foreground-secondary">Laden...</p>
|
||||
<p class="text-foreground-secondary">{$_('home.loading')}</p>
|
||||
{:else if filtered.length === 0}
|
||||
<p class="text-foreground-secondary">Keine Locations gefunden.</p>
|
||||
<p class="text-foreground-secondary">{$_('home.noResults')}</p>
|
||||
{:else}
|
||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each filtered as location}
|
||||
|
|
@ -112,8 +108,8 @@
|
|||
class="absolute right-3 top-3 flex h-9 w-9 items-center justify-center rounded-full bg-black/30 backdrop-blur-sm transition-all hover:bg-black/50"
|
||||
onclick={(e) => handleFavoriteToggle(e, location.id)}
|
||||
title={favoritesStore.isFavorite(location.id)
|
||||
? 'Aus Favoriten entfernen'
|
||||
: 'Zu Favoriten hinzufügen'}
|
||||
? $_('favorites.remove')
|
||||
: $_('favorites.add')}
|
||||
>
|
||||
{#if favoritesStore.isFavorite(location.id)}
|
||||
<svg class="h-5 w-5 text-red-500" fill="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -143,7 +139,7 @@
|
|||
<span
|
||||
class="mb-1 inline-block rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary"
|
||||
>
|
||||
{categories.find((c) => c.value === location.category)?.label ?? location.category}
|
||||
{$_(`categories.${location.category}`)}
|
||||
</span>
|
||||
<h2 class="text-lg font-semibold text-foreground group-hover:text-primary">
|
||||
{location.name}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { favoritesStore } from '$lib/stores/favorites.svelte';
|
||||
|
||||
|
|
@ -19,13 +20,6 @@
|
|||
? (window as any).__PUBLIC_BACKEND_URL__ || 'http://localhost:3025'
|
||||
: 'http://localhost:3025';
|
||||
|
||||
const categoryLabels: Record<string, string> = {
|
||||
sight: 'Sehenswürdigkeit',
|
||||
restaurant: 'Restaurant',
|
||||
shop: 'Laden',
|
||||
museum: 'Museum',
|
||||
};
|
||||
|
||||
let favoriteLocations = $derived(allLocations.filter((l) => favoritesStore.isFavorite(l.id)));
|
||||
|
||||
onMount(async () => {
|
||||
|
|
@ -52,32 +46,30 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Favoriten - CityCorners</title>
|
||||
<title>{$_('favorites.title')} - CityCorners</title>
|
||||
</svelte:head>
|
||||
|
||||
<header class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-foreground">Favoriten</h1>
|
||||
<p class="text-foreground-secondary">Deine gespeicherten Orte</p>
|
||||
<h1 class="text-2xl font-bold text-foreground">{$_('favorites.title')}</h1>
|
||||
<p class="text-foreground-secondary">{$_('favorites.subtitle')}</p>
|
||||
</header>
|
||||
|
||||
{#if !authStore.isAuthenticated}
|
||||
<div class="rounded-xl border border-border bg-background-card p-8 text-center">
|
||||
<p class="mb-4 text-foreground-secondary">Melde dich an, um Favoriten zu speichern.</p>
|
||||
<p class="mb-4 text-foreground-secondary">{$_('favorites.loginRequired')}</p>
|
||||
<a
|
||||
href="/login"
|
||||
class="inline-block rounded-lg bg-primary px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-primary/90"
|
||||
>
|
||||
Anmelden
|
||||
{$_('settings.login')}
|
||||
</a>
|
||||
</div>
|
||||
{:else if loading}
|
||||
<p class="text-foreground-secondary">Laden...</p>
|
||||
<p class="text-foreground-secondary">{$_('home.loading')}</p>
|
||||
{:else if favoriteLocations.length === 0}
|
||||
<div class="rounded-xl border border-border bg-background-card p-8 text-center">
|
||||
<span class="mb-2 block text-4xl">💙</span>
|
||||
<p class="text-foreground-secondary">
|
||||
Noch keine Favoriten. Tippe auf das Herz bei einer Location, um sie zu speichern.
|
||||
</p>
|
||||
<p class="text-foreground-secondary">{$_('favorites.empty')}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
|
|
@ -100,9 +92,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
<div class="min-w-0 flex-1">
|
||||
<span class="text-xs text-primary"
|
||||
>{categoryLabels[location.category] || location.category}</span
|
||||
>
|
||||
<span class="text-xs text-primary">{$_(`category.${location.category}`)}</span>
|
||||
<h3 class="truncate font-semibold text-foreground group-hover:text-primary">
|
||||
{location.name}
|
||||
</h3>
|
||||
|
|
@ -110,7 +100,7 @@
|
|||
<button
|
||||
class="flex-shrink-0 p-1 text-red-500 transition-colors hover:text-red-600"
|
||||
onclick={(e) => handleRemove(e, location.id)}
|
||||
title="Aus Favoriten entfernen"
|
||||
title={$_('favorites.remove')}
|
||||
>
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { favoritesStore } from '$lib/stores/favorites.svelte';
|
||||
|
||||
|
|
@ -106,9 +107,9 @@
|
|||
{:else if !location}
|
||||
<div class="py-20 text-center">
|
||||
<span class="mb-4 block text-5xl">🔍</span>
|
||||
<p class="text-foreground-secondary">Location nicht gefunden.</p>
|
||||
<p class="text-foreground-secondary">{$_('detail.notFound')}</p>
|
||||
<a href="/" class="mt-4 inline-block text-sm text-primary hover:underline"
|
||||
>Zurück zur Übersicht</a
|
||||
>{$_('detail.back')}</a
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
|
|
@ -142,8 +143,8 @@
|
|||
class="flex h-10 w-10 items-center justify-center rounded-full bg-black/30 backdrop-blur-sm transition-all hover:bg-black/50"
|
||||
onclick={() => favoritesStore.toggle(location!.id)}
|
||||
title={favoritesStore.isFavorite(location.id)
|
||||
? 'Aus Favoriten entfernen'
|
||||
: 'Zu Favoriten hinzufügen'}
|
||||
? $_('favorites.remove')
|
||||
: $_('favorites.add')}
|
||||
>
|
||||
{#if favoritesStore.isFavorite(location.id)}
|
||||
<svg class="h-5 w-5 text-red-500" fill="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -235,7 +236,7 @@
|
|||
d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
|
||||
/>
|
||||
</svg>
|
||||
In OpenStreetMap öffnen
|
||||
{$_('detail.openInMaps')}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -243,7 +244,7 @@
|
|||
<!-- Timeline -->
|
||||
{#if location.timeline && location.timeline.length > 0}
|
||||
<div>
|
||||
<h2 class="mb-4 text-xl font-semibold text-foreground">Geschichte</h2>
|
||||
<h2 class="mb-4 text-xl font-semibold text-foreground">{$_('detail.history')}</h2>
|
||||
<div class="relative space-y-0">
|
||||
{#each location.timeline as entry, i}
|
||||
<div class="relative flex gap-4 pb-6 {i < location.timeline!.length - 1 ? '' : ''}">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { _ } from 'svelte-i18n';
|
||||
|
||||
interface Location {
|
||||
id: string;
|
||||
|
|
@ -88,14 +89,14 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Karte - CityCorners</title>
|
||||
<title>{$_('map.title')} - CityCorners</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="map-page">
|
||||
<header class="mb-4">
|
||||
<h1 class="text-2xl font-bold text-foreground">Karte</h1>
|
||||
<p class="text-foreground-secondary">Alle Orte in Konstanz</p>
|
||||
<h1 class="text-2xl font-bold text-foreground">{$_('map.title')}</h1>
|
||||
<p class="text-foreground-secondary">{$_('map.subtitle')}</p>
|
||||
</header>
|
||||
|
||||
<div class="legend mb-4 flex flex-wrap gap-3">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { theme } from '$lib/stores/theme.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
|
||||
|
|
@ -23,21 +24,22 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Einstellungen - CityCorners</title>
|
||||
<title>{$_('settings.title')} - CityCorners</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="space-y-8">
|
||||
<h1 class="text-2xl font-bold text-foreground">Einstellungen</h1>
|
||||
<h1 class="text-2xl font-bold text-foreground">{$_('settings.title')}</h1>
|
||||
|
||||
<!-- Theme Mode -->
|
||||
<section class="rounded-xl border border-border bg-background-card p-5">
|
||||
<h2 class="mb-4 text-lg font-semibold text-foreground">Erscheinungsbild</h2>
|
||||
<h2 class="mb-4 text-lg font-semibold text-foreground">{$_('settings.appearance')}</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-foreground-secondary">Modus</label>
|
||||
<label class="mb-2 block text-sm font-medium text-foreground-secondary"
|
||||
>{$_('settings.mode')}</label
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
{#each [{ value: 'light', label: '☀️ Hell' }, { value: 'dark', label: '🌙 Dunkel' }, { value: 'system', label: '💻 System' }] as opt}
|
||||
{#each [{ value: 'light', label: `☀️ ${$_('settings.light')}` }, { value: 'dark', label: `🌙 ${$_('settings.dark')}` }, { value: 'system', label: `💻 ${$_('settings.system')}` }] as opt}
|
||||
<button
|
||||
class="rounded-lg px-4 py-2 text-sm transition-colors {theme.mode === opt.value
|
||||
? 'bg-primary text-white'
|
||||
|
|
@ -51,7 +53,9 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-foreground-secondary">Farbschema</label>
|
||||
<label class="mb-2 block text-sm font-medium text-foreground-secondary"
|
||||
>{$_('settings.colorScheme')}</label
|
||||
>
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
{#each themeVariants as v}
|
||||
<button
|
||||
|
|
@ -70,14 +74,13 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Account -->
|
||||
<section class="rounded-xl border border-border bg-background-card p-5">
|
||||
<h2 class="mb-4 text-lg font-semibold text-foreground">Account</h2>
|
||||
<h2 class="mb-4 text-lg font-semibold text-foreground">{$_('settings.account')}</h2>
|
||||
|
||||
{#if authStore.isAuthenticated}
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-foreground-secondary">E-Mail</span>
|
||||
<span class="text-sm text-foreground-secondary">{$_('settings.email')}</span>
|
||||
<span class="text-sm font-medium text-foreground">{authStore.user?.email}</span>
|
||||
</div>
|
||||
<hr class="border-border" />
|
||||
|
|
@ -85,39 +88,33 @@
|
|||
class="w-full rounded-lg bg-red-500/10 px-4 py-2 text-sm font-medium text-red-500 transition-colors hover:bg-red-500/20"
|
||||
onclick={handleLogout}
|
||||
>
|
||||
Abmelden
|
||||
{$_('settings.logout')}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
<p class="text-sm text-foreground-secondary">
|
||||
Melde dich an, um Favoriten zu speichern und alle Features zu nutzen.
|
||||
</p>
|
||||
<p class="text-sm text-foreground-secondary">{$_('settings.loginPrompt')}</p>
|
||||
<div class="flex gap-2">
|
||||
<a
|
||||
href="/login"
|
||||
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary/90"
|
||||
>
|
||||
Anmelden
|
||||
{$_('settings.login')}
|
||||
</a>
|
||||
<a
|
||||
href="/register"
|
||||
class="rounded-lg bg-background px-4 py-2 text-sm font-medium text-foreground-secondary transition-colors hover:bg-background-card-hover"
|
||||
>
|
||||
Registrieren
|
||||
{$_('settings.register')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- About -->
|
||||
<section class="rounded-xl border border-border bg-background-card p-5">
|
||||
<h2 class="mb-4 text-lg font-semibold text-foreground">Über CityCorners</h2>
|
||||
<p class="text-sm text-foreground-secondary">
|
||||
CityCorners ist ein Stadtführer für Konstanz am Bodensee. Entdecke Sehenswürdigkeiten,
|
||||
Restaurants, Museen und Läden.
|
||||
</p>
|
||||
<h2 class="mb-4 text-lg font-semibold text-foreground">{$_('settings.about')}</h2>
|
||||
<p class="text-sm text-foreground-secondary">{$_('settings.aboutText')}</p>
|
||||
<p class="mt-2 text-xs text-foreground-secondary/60">Version 0.0.1</p>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import '$lib/i18n';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen flex items-center justify-center p-4 bg-background">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-3xl font-bold text-foreground">CityCorners</h1>
|
||||
<p class="text-foreground-secondary mt-2">Entdecke Konstanz</p>
|
||||
<h1 class="text-3xl font-bold text-foreground">{$_('app.name')}</h1>
|
||||
<p class="text-foreground-secondary mt-2">{$_('app.tagline')}</p>
|
||||
</div>
|
||||
{@render children()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { LoginPage } from '@manacore/shared-auth-ui';
|
||||
import { getLoginTranslations } from '@manacore/shared-i18n';
|
||||
import { CitycornersLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import '$lib/i18n';
|
||||
|
||||
const redirectTo = $derived.by(() => {
|
||||
const queryRedirect = $page.url.searchParams.get('redirectTo');
|
||||
|
|
@ -21,6 +24,7 @@
|
|||
return '/';
|
||||
});
|
||||
|
||||
const translations = $derived(getLoginTranslations($locale || 'de'));
|
||||
const verified = $derived($page.url.searchParams.get('verified') === 'true');
|
||||
const initialEmail = $derived($page.url.searchParams.get('email') || '');
|
||||
|
||||
|
|
@ -34,7 +38,7 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Login - CityCorners</title>
|
||||
<title>{translations.title} - CityCorners</title>
|
||||
</svelte:head>
|
||||
|
||||
<LoginPage
|
||||
|
|
@ -51,6 +55,7 @@
|
|||
forgotPasswordPath="/forgot-password"
|
||||
lightBackground="#eff6ff"
|
||||
darkBackground="#1e1b4b"
|
||||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { RegisterPage } from '@manacore/shared-auth-ui';
|
||||
import { getRegisterTranslations } from '@manacore/shared-i18n';
|
||||
import { CitycornersLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import '$lib/i18n';
|
||||
|
||||
const translations = $derived(getRegisterTranslations($locale || 'de'));
|
||||
|
||||
async function handleSignUp(email: string, password: string) {
|
||||
return authStore.signUp(email, password);
|
||||
|
|
@ -14,7 +19,7 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Registrieren - CityCorners</title>
|
||||
<title>{translations.title} - CityCorners</title>
|
||||
</svelte:head>
|
||||
|
||||
<RegisterPage
|
||||
|
|
@ -28,4 +33,5 @@
|
|||
loginPath="/login"
|
||||
lightBackground="#eff6ff"
|
||||
darkBackground="#1e1b4b"
|
||||
{translations}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { onMount } from 'svelte';
|
||||
import { isLoading as isLocaleLoading } from 'svelte-i18n';
|
||||
import { theme } from '$lib/stores/theme.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { waitLocale } from '$lib/i18n';
|
||||
import { ToastContainer, setupGlobalErrorHandler } from '@manacore/shared-ui';
|
||||
|
||||
let { children } = $props();
|
||||
|
|
@ -13,6 +15,7 @@
|
|||
const cleanupErrorHandler = setupGlobalErrorHandler();
|
||||
|
||||
const init = async () => {
|
||||
await waitLocale();
|
||||
theme.initialize();
|
||||
await authStore.initialize();
|
||||
loading = false;
|
||||
|
|
@ -26,7 +29,7 @@
|
|||
|
||||
<ToastContainer />
|
||||
|
||||
{#if loading}
|
||||
{#if $isLocaleLoading || loading}
|
||||
<div class="min-h-screen bg-background flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div
|
||||
|
|
|
|||
22
apps/citycorners/apps/web/src/routes/offline/+page.svelte
Normal file
22
apps/citycorners/apps/web/src/routes/offline/+page.svelte
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import '$lib/i18n';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_('offline.title')} - CityCorners</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen bg-background flex items-center justify-center p-6">
|
||||
<div class="text-center max-w-md">
|
||||
<span class="mb-4 block text-6xl">📡</span>
|
||||
<h1 class="text-2xl font-bold text-foreground mb-2">{$_('offline.title')}</h1>
|
||||
<p class="text-foreground-secondary mb-6">{$_('offline.message')}</p>
|
||||
<button
|
||||
class="rounded-lg bg-primary px-6 py-2.5 text-sm font-medium text-white transition-colors hover:bg-primary/90"
|
||||
onclick={() => window.location.reload()}
|
||||
>
|
||||
{$_('offline.retry')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,10 +1,24 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { SvelteKitPWA } from '@vite-pwa/sveltekit';
|
||||
import { createPWAConfig } from '@manacore/shared-pwa';
|
||||
import { MANACORE_SHARED_PACKAGES, getBuildDefines } from '@manacore/shared-vite-config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit(), tailwindcss()],
|
||||
plugins: [
|
||||
sveltekit(),
|
||||
tailwindcss(),
|
||||
SvelteKitPWA(
|
||||
createPWAConfig({
|
||||
name: 'CityCorners - Konstanz Guide',
|
||||
shortName: 'CityCorners',
|
||||
description: 'Entdecke Sehenswürdigkeiten, Restaurants, Museen und Läden in Konstanz',
|
||||
themeColor: '#2563eb',
|
||||
categories: ['travel', 'lifestyle'],
|
||||
})
|
||||
),
|
||||
],
|
||||
server: {
|
||||
port: 5196,
|
||||
strictPort: true,
|
||||
|
|
|
|||
915
pnpm-lock.yaml
generated
915
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue