chore: global ToastContainer, migrate inline toasts, delete SETUP.md

Add ToastContainer.svelte to (app) layout — renders toasts from the
central toast.svelte store (stacked, auto-dismiss, color-coded by
type). Previously the store existed but had no renderer.

Migrate inline toast implementations to the central store:
- SyncSection: showToast() → toast.success/error(), strip DOM + CSS
- Credits ListView: same migration, remove showToast + inline toast
- Profile ListView: same migration, remove showToast + inline toast

Delete apps/mana/apps/web/SETUP.md — completely outdated (references
Supabase, teams, organizations — all removed long ago). Real docs
live in CLAUDE.md and docs/LOCAL_DEVELOPMENT.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-16 15:18:50 +02:00
parent 0ddaab53e4
commit 8def989ed9
6 changed files with 107 additions and 480 deletions

View file

@ -1,364 +0,0 @@
# Mana Web - Setup Guide
## 🎉 Project Created Successfully!
A brand new SvelteKit application has been created in the `mana-web` folder, separate from the existing React Native `mana_app`.
## ✅ What's Been Implemented
### Core Infrastructure
- ✅ SvelteKit 2.x with Svelte 5 (Runes)
- ✅ TypeScript configuration
- ✅ Tailwind CSS styling
- ✅ Supabase authentication integration
- ✅ Server-side hooks for auth management
- ✅ Environment configuration
### Authentication System
- ✅ Login page (`/login`)
- ✅ Registration page (`/register`)
- ✅ Protected routes with automatic redirects
- ✅ Server-side session management
- ✅ SSR-safe Supabase client setup
### Dashboard
- ✅ Main dashboard with stats display
- ✅ Available mana/credits tracking
- ✅ Organization and team counts
- ✅ Quick action links
- ✅ Responsive design
### UI Components
- ✅ Button component (primary, secondary, danger, ghost variants)
- ✅ Card component
- ✅ Input component with validation
- ✅ Responsive navigation with mobile menu
## 📁 Project Structure
```
mana-web/
├── src/
│ ├── routes/
│ │ ├── (auth)/ # Public auth routes
│ │ │ ├── login/ # Login page
│ │ │ └── register/ # Registration page
│ │ ├── (app)/ # Protected app routes
│ │ │ └── dashboard/ # Main dashboard
│ │ ├── +layout.svelte # Root layout
│ │ ├── +layout.ts # Client-side layout load
│ │ ├── +layout.server.ts # Server-side layout load
│ │ └── +page.svelte # Home page (redirects)
│ ├── lib/
│ │ ├── components/
│ │ │ └── ui/ # Reusable UI components
│ │ ├── server/ # Server-only utilities
│ │ ├── stores/ # Svelte stores
│ │ ├── types/ # TypeScript types
│ │ └── utils/ # Utility functions
│ ├── hooks.server.ts # Server hooks (auth middleware)
│ ├── app.css # Global Tailwind styles
│ ├── app.d.ts # TypeScript declarations
│ └── app.html # HTML template
├── static/ # Static assets
├── tests/ # Test files (future)
├── .env # Environment variables
├── .env.example # Environment template
├── svelte.config.js # SvelteKit configuration
├── vite.config.ts # Vite configuration
├── tsconfig.json # TypeScript configuration
└── package.json # Dependencies
```
## 🚀 Getting Started
### 1. Configure Environment Variables
Update `.env` with your actual Supabase credentials:
```bash
# Get these from your Supabase project settings
PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
```
### 2. Install Dependencies (if not already done)
```bash
cd mana-web
pnpm install
```
### 3. Start Development Server
```bash
pnpm dev
```
Visit `http://localhost:5173`
### 4. Database Setup
The app expects the same Supabase database schema as the React Native app. Ensure you have these tables:
- `profiles` - User profile data
- `organizations` - Organization entities
- `teams` - Team entities
- `team_members` - Team membership
- `user_roles` - Role assignments
- `roles` - Role definitions
- `credit_transactions` - Credit history
## 📝 Next Steps
### Immediate (Must Do)
1. **Update .env file** with real Supabase credentials
2. **Test login/registration** flow
3. **Verify database schema** matches expected structure
### Short-Term Features to Add
1. **Organizations Management**
- List organizations page
- Organization detail page
- Create organization form
2. **Teams Management**
- List teams page
- Team detail page with members
- Create team form
- Add/remove team members
3. **Credit/Mana Transfer**
- Send mana page
- Credit transaction history
- Balance tracking
4. **Settings Page**
- Profile management
- Theme toggle (light/dark)
- Account settings
### Medium-Term Enhancements
1. **Real-time Updates**
- Supabase realtime subscriptions
- Live credit balance updates
- Team activity notifications
2. **Enhanced UI/UX**
- Loading skeletons
- Error boundaries
- Toast notifications
- Optimistic UI updates
3. **Testing**
- Vitest unit tests
- Playwright E2E tests
- Component testing
4. **Performance**
- Image optimization
- Code splitting
- Caching strategies
- Performance monitoring
## 🔧 Available Scripts
```bash
# Development
pnpm dev # Start dev server
# Building
pnpm build # Build for production
pnpm preview # Preview production build
# Type Checking
pnpm check # Run TypeScript checks
pnpm check:watch # Watch mode
# Code Quality
pnpm format # Format code with Prettier
pnpm lint # Lint code
# Testing
pnpm test # Run unit tests
pnpm test:ui # Run tests with UI
pnpm test:e2e # Run E2E tests
```
## 🎨 Design System
### Colors
- Primary: `#0055FF` (blue-600)
- Background Light: `#FFFFFF`
- Background Dark: `#121212`
- Text Light: `#1F2937`
- Text Dark: `#F9FAFB`
### Components
All components support:
- Dark mode (automatic via system preference)
- TypeScript props
- Tailwind CSS classes
- Accessibility features
### Responsive Breakpoints
- `sm`: 640px
- `md`: 768px
- `lg`: 1024px
- `xl`: 1280px
- `2xl`: 1536px
## 🔐 Authentication Flow
```
┌─────────────────────────────────────┐
│ User visits site │
│ ↓ │
│ hooks.server.ts checks session │
│ ↓ │
│ Has session? → Go to /dashboard │
│ ↓ │
│ No session? → Go to /login │
│ ↓ │
│ User logs in │
│ ↓ │
│ Supabase creates session │
│ ↓ │
│ Redirect to /dashboard │
└─────────────────────────────────────┘
```
## 📚 Key Technologies
| Technology | Version | Purpose |
| ------------ | ------- | ------------- |
| SvelteKit | 2.48.4 | Web framework |
| Svelte | 5.43.3 | UI components |
| TypeScript | 5.9.3 | Type safety |
| Tailwind CSS | 3.4.18 | Styling |
| Supabase | 2.79.0 | Backend/Auth |
| Vite | 6.4.1 | Build tool |
| Playwright | 1.56.1 | E2E testing |
| Vitest | 3.2.4 | Unit testing |
## 🆚 Comparison: React Native vs SvelteKit
| Feature | React Native (mana_app) | SvelteKit (mana-web) |
| ---------- | ----------------------- | -------------------- |
| Platform | Mobile (iOS/Android) | Web (Browser) |
| Routing | Expo Router | File-based routing |
| State | useState/Context | Svelte Runes/$state |
| Styling | NativeWind | Tailwind CSS |
| Auth | Supabase | Supabase (same) |
| Database | Supabase | Supabase (same) |
| Build | Expo | Vite |
| Deployment | App Stores | Vercel/Netlify |
## 🚨 Common Issues & Solutions
### Issue: Type errors on first run
**Solution**: Run `pnpm run check` to sync types
### Issue: Port already in use
**Solution**: Change port in `vite.config.ts` or kill process on port 5173
### Issue: Supabase auth not working
**Solution**:
1. Check `.env` variables are correct
2. Verify Supabase project is active
3. Check browser console for errors
### Issue: Dark mode not switching
**Solution**: Check system dark mode preference, or implement manual toggle
## 📖 Documentation & Resources
### SvelteKit
- [SvelteKit Docs](https://svelte.dev/docs/kit)
- [Svelte 5 Tutorial](https://svelte.dev/tutorial/svelte/welcome)
### Supabase
- [Supabase Docs](https://supabase.com/docs)
- [Supabase Auth](https://supabase.com/docs/guides/auth)
### Tailwind CSS
- [Tailwind Docs](https://tailwindcss.com/docs)
- [Tailwind UI](https://tailwindui.com)
## 💡 Development Tips
1. **Use Runes ($state, $derived, $effect)** - Modern Svelte 5 reactivity
2. **Server-side data loading** - Use `+page.server.ts` for secure data fetching
3. **Progressive enhancement** - Forms work without JavaScript
4. **Type safety** - Leverage generated types from SvelteKit
5. **Component composition** - Use snippets for flexible component APIs
## ✨ What Makes This Different
Unlike a migration, this is a **clean, modern implementation** that:
- ✅ Follows 2025 SvelteKit best practices
- ✅ Uses latest Svelte 5 features (Runes)
- ✅ Implements SSR-safe authentication
- ✅ Provides better developer experience
- ✅ Enables easy deployment to web platforms
- ✅ Maintains compatibility with same backend
## 🎯 Success Criteria
The project is ready for development when:
- [x] Project structure created
- [x] Dependencies installed
- [x] TypeScript configured
- [x] Tailwind CSS working
- [x] Authentication pages created
- [x] Dashboard implemented
- [ ] Environment variables configured with real credentials
- [ ] Database schema verified
- [ ] First successful login completed
## 🤝 Contributing
When adding new features:
1. Create components in `src/lib/components/`
2. Add pages in appropriate route groups
3. Use server-side data loading for security
4. Follow existing code patterns
5. Test in both light and dark modes
6. Add TypeScript types
## 📞 Support
For issues or questions:
- Check SvelteKit docs
- Review Supabase documentation
- Check browser console for errors
- Verify environment variables
---
**Status**: ✅ Core application structure complete and ready for feature development!
**Next**: Configure Supabase credentials and start building organization/team management features.

View file

@ -0,0 +1,85 @@
<script lang="ts">
import { toast } from '$lib/stores/toast.svelte';
</script>
{#if toast.toasts.length > 0}
<div class="toast-stack">
{#each toast.toasts as t (t.id)}
<div
class="toast-item"
class:success={t.type === 'success'}
class:error={t.type === 'error'}
class:warning={t.type === 'warning'}
>
<span>{t.message}</span>
<button class="dismiss" onclick={() => toast.dismiss(t.id)}>✕</button>
</div>
{/each}
</div>
{/if}
<style>
.toast-stack {
position: fixed;
bottom: 1rem;
right: 1rem;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 0.5rem;
max-width: 24rem;
}
.toast-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
font-size: 0.8125rem;
color: white;
background: hsl(var(--color-foreground));
box-shadow: 0 4px 12px hsl(0 0% 0% / 0.15);
animation: slide-in 0.2s ease-out;
}
.toast-item.success {
background: hsl(142 71% 45%);
}
.toast-item.error {
background: hsl(var(--color-error));
}
.toast-item.warning {
background: hsl(45 93% 47%);
color: hsl(0 0% 10%);
}
.dismiss {
background: none;
border: none;
color: inherit;
opacity: 0.7;
cursor: pointer;
font-size: 0.75rem;
padding: 0;
line-height: 1;
}
.dismiss:hover {
opacity: 1;
}
@keyframes slide-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View file

@ -3,6 +3,7 @@
import { syncBilling } from '$lib/stores/sync-billing.svelte';
import { creditsService, type CreditBalance } from '$lib/api/credits';
import type { BillingInterval } from '$lib/api/sync';
import { toast } from '$lib/stores/toast.svelte';
import { onMount } from 'svelte';
const SYNC_PRICES: Record<BillingInterval, number> = {
@ -22,10 +23,6 @@
let error = $state<string | null>(null);
let selectedInterval = $state<BillingInterval>('monthly');
// Toast
let toastMessage = $state<string | null>(null);
let toastType = $state<'success' | 'error'>('success');
onMount(async () => {
await Promise.all([syncBilling.load(), loadBalance()]);
selectedInterval = syncBilling.interval;
@ -45,10 +42,10 @@
try {
await syncBilling.activate(selectedInterval);
await loadBalance();
showToast('Cloud Sync aktiviert!', 'success');
toast.success('Cloud Sync aktiviert!');
} catch (e) {
error = e instanceof Error ? e.message : 'Aktivierung fehlgeschlagen';
showToast(error, 'error');
toast.error(error);
} finally {
loading = false;
}
@ -63,7 +60,7 @@
showToast('Cloud Sync deaktiviert', 'success');
} catch (e) {
error = e instanceof Error ? e.message : 'Deaktivierung fehlgeschlagen';
showToast(error, 'error');
toast.error(error);
} finally {
loading = false;
}
@ -78,7 +75,7 @@
showToast(`Intervall auf ${INTERVAL_LABELS[selectedInterval]} geändert`, 'success');
} catch (e) {
error = e instanceof Error ? e.message : 'Änderung fehlgeschlagen';
showToast(error, 'error');
toast.error(error);
} finally {
loading = false;
}
@ -95,14 +92,6 @@
year: 'numeric',
});
}
function showToast(message: string, type: 'success' | 'error') {
toastMessage = message;
toastType = type;
setTimeout(() => {
toastMessage = null;
}, 4000);
}
</script>
<div>
@ -267,14 +256,3 @@
</div>
{/if}
</div>
<!-- Toast Notification -->
{#if toastMessage}
<div
class="fixed bottom-4 right-4 z-50 px-4 py-3 rounded-lg shadow-lg {toastType === 'success'
? 'bg-green-600 text-white'
: 'bg-red-600 text-white'}"
>
{toastMessage}
</div>
{/if}

View file

@ -34,6 +34,7 @@
} from '@mana/credits';
import { ManaEvents } from '@mana/shared-utils/analytics';
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from '$lib/stores/toast.svelte';
// ── Credits state ──────────────────────────────────────
let balance = $state<CreditBalance | null>(null);
@ -58,8 +59,6 @@
);
let costFilter = $state<'all' | 'ai' | 'premium'>('all');
let processingPackageId = $state<string | null>(null);
let toastMessage = $state<string | null>(null);
let toastType = $state<'success' | 'error'>('success');
// ── Derived pricing data ───────────────────────────────
const allOperations = $derived(
@ -139,10 +138,10 @@
const canceled = params.get('canceled');
if (success === 'true') {
showToast('Zahlung erfolgreich!', 'success');
toast.success('\1');
history.replaceState({}, '', '/');
} else if (canceled === 'true') {
showToast('Kauf wurde abgebrochen', 'error');
toast.error('Kauf wurde abgebrochen');
history.replaceState({}, '', '/');
}
@ -251,10 +250,7 @@
const result = await creditsService.initiatePurchase(pkg.id);
window.location.href = result.checkoutUrl;
} catch (e) {
showToast(
e instanceof Error ? e.message : 'Fehler beim Erstellen der Checkout-Session',
'error'
);
toast.error(e instanceof Error ? e.message : 'Fehler beim Erstellen der Checkout-Session');
} finally {
processingPackageId = null;
}
@ -285,7 +281,7 @@
const { url } = await subscriptionsService.createCheckout(plan.id, billingInterval);
window.location.href = url;
} catch (e) {
showToast(e instanceof Error ? e.message : 'Fehler beim Checkout', 'error');
toast.error(e instanceof Error ? e.message : 'Fehler beim Checkout');
} finally {
processingPlanId = null;
}
@ -297,7 +293,7 @@
const { url } = await subscriptionsService.openPortal();
window.location.href = url;
} catch (e) {
showToast(e instanceof Error ? e.message : 'Fehler beim Billing-Portal', 'error');
toast.error(e instanceof Error ? e.message : 'Fehler beim Billing-Portal');
} finally {
openingPortal = false;
}
@ -308,10 +304,10 @@
cancelingSub = true;
try {
await subscriptionsService.cancelSubscription();
showToast('Abonnement wird zum Ende der Laufzeit gekündigt', 'success');
toast.success('\1');
await loadData();
} catch (e) {
showToast(e instanceof Error ? e.message : 'Fehler beim Kündigen', 'error');
toast.error(e instanceof Error ? e.message : 'Fehler beim Kündigen');
} finally {
cancelingSub = false;
}
@ -321,20 +317,14 @@
reactivatingSub = true;
try {
await subscriptionsService.reactivateSubscription();
showToast('Abonnement wurde reaktiviert', 'success');
toast.success('\1');
await loadData();
} catch (e) {
showToast(e instanceof Error ? e.message : 'Fehler beim Reaktivieren', 'error');
toast.error(e instanceof Error ? e.message : 'Fehler beim Reaktivieren');
} finally {
reactivatingSub = false;
}
}
function showToast(message: string, type: 'success' | 'error') {
toastMessage = message;
toastType = type;
setTimeout(() => (toastMessage = null), 4000);
}
</script>
<div class="credits-page">
@ -714,10 +704,6 @@
{/if}
</div>
{#if toastMessage}
<div class="toast" class:toast-error={toastType === 'error'}>{toastMessage}</div>
{/if}
<style>
.credits-page {
padding: 0.75rem;
@ -1518,33 +1504,4 @@
opacity: 0.5;
cursor: not-allowed;
}
.toast {
position: fixed;
bottom: 1rem;
right: 1rem;
z-index: 50;
padding: 0.75rem 1rem;
background: hsl(142 71% 45%);
color: white;
border-radius: 0.5rem;
box-shadow: 0 4px 12px hsl(0 0% 0% / 0.15);
font-size: 0.8125rem;
animation: fadeIn 0.2s ease-out;
}
.toast-error {
background: hsl(var(--color-error));
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View file

@ -5,6 +5,7 @@
import { onMount } from 'svelte';
import { profileService, type UserProfile as ApiUserProfile } from '$lib/api/profile';
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from '$lib/stores/toast.svelte';
import { goto } from '$app/navigation';
import {
EditProfileModal,
@ -24,7 +25,6 @@
let showEditModal = $state(false);
let showPasswordModal = $state(false);
let showDeleteModal = $state(false);
let toastMessage = $state<string | null>(null);
onMount(async () => {
try {
@ -45,23 +45,18 @@
function handleProfileUpdate(user: ApiUserProfile) {
apiProfile = user;
showToast('Profil erfolgreich aktualisiert');
toast.success('Profil erfolgreich aktualisiert');
}
function handlePasswordChange() {
showToast('Passwort erfolgreich geändert');
toast.success('Passwort erfolgreich geändert');
}
async function handleAccountDeleted() {
showToast('Konto wird gelöscht...');
toast.info('Konto wird gelöscht...');
await authStore.signOut();
goto('/login');
}
function showToast(message: string) {
toastMessage = message;
setTimeout(() => (toastMessage = null), 3000);
}
</script>
<div class="profile-page">
@ -155,10 +150,6 @@
onSuccess={handleAccountDeleted}
/>
{#if toastMessage}
<div class="toast">{toastMessage}</div>
{/if}
<style>
.profile-page {
display: flex;
@ -289,27 +280,4 @@
.account-btn.danger:hover {
background: hsl(var(--color-destructive, 0 84% 60%) / 0.08);
}
.toast {
position: fixed;
bottom: 1rem;
right: 1rem;
z-index: 50;
padding: 0.75rem 1rem;
background: hsl(142 71% 45%);
color: white;
border-radius: 0.5rem;
box-shadow: 0 4px 12px hsl(0 0% 0% / 0.15);
font-size: 0.875rem;
animation: fade-in 0.2s ease-out;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View file

@ -2,6 +2,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import type { Component, Snippet } from 'svelte';
import ToastContainer from '$lib/components/ToastContainer.svelte';
import { onDestroy, setContext, tick } from 'svelte';
import { createReminderScheduler } from '@mana/shared-stores';
import { todoReminderSource } from '$lib/modules/todo/reminder-source';
@ -1063,6 +1064,8 @@
{/if}
</AuthGate>
<ToastContainer />
<style>
.bottom-stack {
position: fixed;