mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +02:00
feat(zitare): add vertical scroll-snap quote feed with infinite scroll
- Replace single quote view with TikTok-style vertical scrolling feed - Add infinite scroll to load more quotes as user scrolls - Remove discover page (consolidated into homepage) - Fix theme store to use appId instead of storagePrefix - Fix Sidebar theme toggle to use toggleMode() and isDark - Add zitare and picture branding to shared-branding config - Add missing manacore, mana, moodlit URLs to APP_URLS 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4e4db4612c
commit
6cfab65b96
8 changed files with 165 additions and 40 deletions
|
|
@ -11,6 +11,8 @@ pnpm dev:picture:app
|
|||
|
||||
pnpm dev:manacore:app
|
||||
|
||||
pnpm dev:zitare:app
|
||||
|
||||
|
||||
|
||||
Übersicht aller wichtigen Befehle zum Starten, Stoppen und Verwalten der Apps.
|
||||
|
|
|
|||
|
|
@ -109,9 +109,9 @@
|
|||
<div class="divider"></div>
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
<button onclick={() => theme.toggle()} class="nav-item">
|
||||
<button onclick={() => theme.toggleMode()} class="nav-item">
|
||||
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
{#if $theme === 'dark'}
|
||||
{#if theme.isDark}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
|
@ -127,7 +127,7 @@
|
|||
/>
|
||||
{/if}
|
||||
</svg>
|
||||
<span>{$theme === 'dark' ? 'Light Mode' : 'Dark Mode'}</span>
|
||||
<span>{theme.isDark ? 'Light Mode' : 'Dark Mode'}</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
|
|
@ -209,13 +209,13 @@
|
|||
<!-- Theme Toggle Mobile -->
|
||||
<button
|
||||
onclick={() => {
|
||||
theme.toggle();
|
||||
theme.toggleMode();
|
||||
showUserMenu = false;
|
||||
}}
|
||||
class="mobile-nav-item"
|
||||
>
|
||||
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
{#if $theme === 'dark'}
|
||||
{#if theme.isDark}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
|
@ -231,7 +231,7 @@
|
|||
/>
|
||||
{/if}
|
||||
</svg>
|
||||
{$theme === 'dark' ? 'Light Mode' : 'Dark Mode'}
|
||||
{theme.isDark ? 'Light Mode' : 'Dark Mode'}
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@ import { createThemeStore } from '@manacore/shared-theme';
|
|||
|
||||
// Create theme store with Zitare's primary color (amber)
|
||||
export const theme = createThemeStore({
|
||||
appId: 'zitare',
|
||||
defaultVariant: 'lume',
|
||||
storagePrefix: 'zitare',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,26 +1,37 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { quotesDE, authorsDE, getRandomQuote } from '@zitare/shared';
|
||||
import QuoteCard from '$lib/components/QuoteCard.svelte';
|
||||
|
||||
// Get a random quote for the homepage
|
||||
const randomQuote = getRandomQuote(quotesDE);
|
||||
const quote = randomQuote
|
||||
? {
|
||||
...randomQuote,
|
||||
author: authorsDE.find((a) => a.id === randomQuote.authorId),
|
||||
isFavorite: false,
|
||||
}
|
||||
: null;
|
||||
|
||||
// Load multiple quotes for scrolling
|
||||
let quotes = $state<any[]>([]);
|
||||
let favorites = $state<Set<string>>(new Set());
|
||||
let containerRef = $state<HTMLElement | null>(null);
|
||||
|
||||
function loadMoreQuotes(count = 5) {
|
||||
const newQuotes = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const randomQuote = getRandomQuote(quotesDE);
|
||||
if (randomQuote) {
|
||||
newQuotes.push({
|
||||
...randomQuote,
|
||||
author: authorsDE.find((a) => a.id === randomQuote.authorId),
|
||||
isFavorite: favorites.has(randomQuote.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
quotes = [...quotes, ...newQuotes];
|
||||
}
|
||||
|
||||
// Load favorites from localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
onMount(() => {
|
||||
const savedFavorites = localStorage.getItem('favorites');
|
||||
if (savedFavorites) {
|
||||
favorites = new Set(JSON.parse(savedFavorites));
|
||||
}
|
||||
}
|
||||
// Initial load
|
||||
loadMoreQuotes(10);
|
||||
});
|
||||
|
||||
function handleToggleFavorite(event: CustomEvent) {
|
||||
const { quoteId } = event.detail;
|
||||
|
|
@ -31,10 +42,11 @@
|
|||
}
|
||||
favorites = new Set(favorites);
|
||||
|
||||
// Update quote's favorite status
|
||||
quotes = quotes.map((q) => (q.id === quoteId ? { ...q, isFavorite: favorites.has(quoteId) } : q));
|
||||
|
||||
// Save to localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('favorites', JSON.stringify([...favorites]));
|
||||
}
|
||||
localStorage.setItem('favorites', JSON.stringify([...favorites]));
|
||||
}
|
||||
|
||||
function handleAuthorClick(event: CustomEvent) {
|
||||
|
|
@ -43,6 +55,15 @@
|
|||
window.location.href = `/authors/${authorId}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Infinite scroll - load more when near bottom
|
||||
function handleScroll(event: Event) {
|
||||
const target = event.target as HTMLElement;
|
||||
const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
|
||||
if (scrollBottom < 500) {
|
||||
loadMoreQuotes(5);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -50,27 +71,105 @@
|
|||
<meta name="description" content="Entdecke inspirierende Zitate von großen Denkern" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="home">
|
||||
{#if quote}
|
||||
<QuoteCard
|
||||
{quote}
|
||||
variant="daily"
|
||||
on:toggleFavorite={handleToggleFavorite}
|
||||
on:authorClick={handleAuthorClick}
|
||||
/>
|
||||
<div class="scroll-container" bind:this={containerRef} onscroll={handleScroll}>
|
||||
{#each quotes as quote, index (quote.id + '-' + index)}
|
||||
<div class="quote-slide">
|
||||
<QuoteCard
|
||||
{quote}
|
||||
variant="daily"
|
||||
on:toggleFavorite={handleToggleFavorite}
|
||||
on:authorClick={handleAuthorClick}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if quotes.length > 0}
|
||||
<div class="scroll-hint">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
<span>Scrollen für mehr</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.home {
|
||||
.scroll-container {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
scroll-snap-type: y mandatory;
|
||||
scroll-behavior: smooth;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-top: 100px; /* Space for floating nav */
|
||||
}
|
||||
|
||||
.quote-slide {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
scroll-snap-align: start;
|
||||
scroll-snap-stop: always;
|
||||
}
|
||||
|
||||
.quote-slide:first-child {
|
||||
padding-top: 0;
|
||||
min-height: calc(100vh - 100px);
|
||||
}
|
||||
|
||||
.quote-slide :global(.quote-card) {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.scroll-hint {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: 0.8125rem;
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.scroll-hint svg {
|
||||
animation: bounce-arrow 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce-arrow {
|
||||
0%, 20%, 50%, 80%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(6px);
|
||||
}
|
||||
60% {
|
||||
transform: translateY(3px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.home {
|
||||
max-width: 100%;
|
||||
.scroll-container {
|
||||
padding-top: 80px;
|
||||
}
|
||||
|
||||
.quote-slide {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.quote-slide:first-child {
|
||||
min-height: calc(100vh - 80px);
|
||||
}
|
||||
|
||||
.scroll-hint {
|
||||
bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { DiscoverAppsPage } from '@zitare/web-ui';
|
||||
</script>
|
||||
|
||||
<DiscoverAppsPage currentAppName="quotes" />
|
||||
|
|
@ -105,6 +105,32 @@ export const APP_BRANDING: Record<AppId, AppBranding> = {
|
|||
logoStroke: true,
|
||||
logoStrokeWidth: 1.5,
|
||||
},
|
||||
zitare: {
|
||||
id: 'zitare',
|
||||
name: 'Zitare',
|
||||
tagline: 'Daily Inspiration',
|
||||
primaryColor: '#f59e0b',
|
||||
secondaryColor: '#fbbf24',
|
||||
// Quote/chat bubble icon
|
||||
logoPath:
|
||||
'M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z',
|
||||
logoViewBox: '0 0 24 24',
|
||||
logoStroke: true,
|
||||
logoStrokeWidth: 1.5,
|
||||
},
|
||||
picture: {
|
||||
id: 'picture',
|
||||
name: 'Picture',
|
||||
tagline: 'AI Image Generation',
|
||||
primaryColor: '#3b82f6',
|
||||
secondaryColor: '#60a5fa',
|
||||
// Image/picture icon
|
||||
logoPath:
|
||||
'M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z',
|
||||
logoViewBox: '0 0 24 24',
|
||||
logoStroke: true,
|
||||
logoStrokeWidth: 1.5,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -257,6 +257,9 @@ export const APP_URLS: Record<AppIconId, { dev: string; prod: string }> = {
|
|||
zitare: { dev: 'http://localhost:5180', prod: 'https://zitare.manacore.app' },
|
||||
wisekeep: { dev: 'http://localhost:5181', prod: 'https://wisekeep.manacore.app' },
|
||||
nutriphi: { dev: 'http://localhost:5182', prod: 'https://nutriphi.manacore.app' },
|
||||
manacore: { dev: 'http://localhost:5173', prod: 'https://manacore.app' },
|
||||
mana: { dev: 'http://localhost:5173', prod: 'https://manacore.app' },
|
||||
moodlit: { dev: 'http://localhost:5183', prod: 'https://moodlit.manacore.app' },
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* App identifiers for branding
|
||||
*/
|
||||
export type AppId = 'memoro' | 'manacore' | 'manadeck' | 'maerchenzauber' | 'uload' | 'chat' | 'presi' | 'nutriphi';
|
||||
export type AppId = 'memoro' | 'manacore' | 'manadeck' | 'maerchenzauber' | 'uload' | 'chat' | 'presi' | 'nutriphi' | 'zitare' | 'picture';
|
||||
|
||||
/**
|
||||
* App branding configuration
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue