mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
💄 style: apply prettier formatting across codebase
Run prettier --write to fix formatting inconsistencies in 80 files across calendar, contacts, picture, presi, storage, zitare apps and shared packages/documentation.
This commit is contained in:
parent
6c9e8972a7
commit
ea3582d487
79 changed files with 3122 additions and 2387 deletions
|
|
@ -1,13 +1,4 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
|
||||
import { CalendarService } from './calendar.service';
|
||||
import { CreateCalendarDto, UpdateCalendarDto } from './dto';
|
||||
|
|
|
|||
|
|
@ -81,9 +81,7 @@ export class CalendarService {
|
|||
}
|
||||
}
|
||||
|
||||
await this.db
|
||||
.delete(calendars)
|
||||
.where(and(eq(calendars.id, id), eq(calendars.userId, userId)));
|
||||
await this.db.delete(calendars).where(and(eq(calendars.id, id), eq(calendars.userId, userId)));
|
||||
}
|
||||
|
||||
async getOrCreateDefaultCalendar(userId: string): Promise<Calendar> {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,13 @@
|
|||
import { pgTable, uuid, timestamp, varchar, text, boolean, jsonb, index } from 'drizzle-orm/pg-core';
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
varchar,
|
||||
text,
|
||||
boolean,
|
||||
jsonb,
|
||||
index,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { calendars } from './calendars.schema';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,4 +1,13 @@
|
|||
import { pgTable, uuid, timestamp, varchar, text, boolean, jsonb, integer } from 'drizzle-orm/pg-core';
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
varchar,
|
||||
text,
|
||||
boolean,
|
||||
jsonb,
|
||||
integer,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
|
||||
/**
|
||||
* Provider-specific metadata
|
||||
|
|
|
|||
|
|
@ -1,14 +1,4 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
|
||||
import { EventService } from './event.service';
|
||||
import { CreateEventDto, UpdateEventDto, QueryEventsDto } from './dto';
|
||||
|
|
|
|||
|
|
@ -32,9 +32,7 @@ export class EventService {
|
|||
|
||||
// Exclude cancelled unless requested
|
||||
if (!query.includeCancelled) {
|
||||
conditions.push(
|
||||
or(eq(events.status, 'confirmed'), eq(events.status, 'tentative')) as any
|
||||
);
|
||||
conditions.push(or(eq(events.status, 'confirmed'), eq(events.status, 'tentative')) as any);
|
||||
}
|
||||
|
||||
// Search filter
|
||||
|
|
|
|||
|
|
@ -1,12 +1,4 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { Controller, Get, Post, Delete, Body, Param, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
|
||||
import { ReminderService } from './reminder.service';
|
||||
import { CreateReminderDto } from './dto';
|
||||
|
|
@ -17,10 +9,7 @@ export class ReminderController {
|
|||
constructor(private readonly reminderService: ReminderService) {}
|
||||
|
||||
@Get('events/:eventId/reminders')
|
||||
async findByEvent(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('eventId') eventId: string
|
||||
) {
|
||||
async findByEvent(@CurrentUser() user: CurrentUserData, @Param('eventId') eventId: string) {
|
||||
const reminders = await this.reminderService.findByEvent(eventId, user.userId);
|
||||
return { reminders };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,9 +61,7 @@ export class ReminderService {
|
|||
throw new NotFoundException(`Reminder with id ${id} not found`);
|
||||
}
|
||||
|
||||
await this.db
|
||||
.delete(reminders)
|
||||
.where(and(eq(reminders.id, id), eq(reminders.userId, userId)));
|
||||
await this.db.delete(reminders).where(and(eq(reminders.id, id), eq(reminders.userId, userId)));
|
||||
}
|
||||
|
||||
async getPendingReminders(): Promise<Reminder[]> {
|
||||
|
|
@ -74,9 +72,7 @@ export class ReminderService {
|
|||
return this.db
|
||||
.select()
|
||||
.from(reminders)
|
||||
.where(
|
||||
and(eq(reminders.status, 'pending'), lte(reminders.reminderTime, oneMinuteFromNow))
|
||||
);
|
||||
.where(and(eq(reminders.status, 'pending'), lte(reminders.reminderTime, oneMinuteFromNow)));
|
||||
}
|
||||
|
||||
async markAsSent(id: string): Promise<void> {
|
||||
|
|
@ -116,7 +112,9 @@ export class ReminderService {
|
|||
|
||||
// TODO: Implement actual notification sending
|
||||
// For now, just log and mark as sent
|
||||
console.log(`[Reminder] Event "${event.title}" starting in ${reminder.minutesBefore} minutes`);
|
||||
console.log(
|
||||
`[Reminder] Event "${event.title}" starting in ${reminder.minutesBefore} minutes`
|
||||
);
|
||||
|
||||
if (reminder.notifyPush) {
|
||||
// TODO: Send push notification via Expo Push API
|
||||
|
|
@ -145,9 +143,7 @@ export class ReminderService {
|
|||
.where(and(eq(reminders.eventId, eventId), eq(reminders.status, 'pending')));
|
||||
|
||||
for (const reminder of eventReminders) {
|
||||
const newReminderTime = new Date(
|
||||
newStartTime.getTime() - reminder.minutesBefore * 60 * 1000
|
||||
);
|
||||
const newReminderTime = new Date(newStartTime.getTime() - reminder.minutesBefore * 60 * 1000);
|
||||
|
||||
await this.db
|
||||
.update(reminders)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
import { IsString, IsOptional, IsBoolean, IsIn, IsEmail, IsDateString, IsUUID } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
IsIn,
|
||||
IsEmail,
|
||||
IsDateString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateShareDto {
|
||||
@IsUUID()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,4 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
|
||||
import { ShareService } from './share.service';
|
||||
import { CreateShareDto, UpdateShareDto } from './dto';
|
||||
|
|
@ -50,10 +41,7 @@ export class ShareController {
|
|||
}
|
||||
|
||||
@Delete('calendars/:calendarId/shares/:shareId')
|
||||
async delete(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('shareId') shareId: string
|
||||
) {
|
||||
async delete(@CurrentUser() user: CurrentUserData, @Param('shareId') shareId: string) {
|
||||
await this.shareService.delete(shareId, user.userId);
|
||||
return { success: true };
|
||||
}
|
||||
|
|
@ -69,19 +57,13 @@ export class ShareController {
|
|||
}
|
||||
|
||||
@Post('shares/:shareId/accept')
|
||||
async acceptInvitation(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('shareId') shareId: string
|
||||
) {
|
||||
async acceptInvitation(@CurrentUser() user: CurrentUserData, @Param('shareId') shareId: string) {
|
||||
const share = await this.shareService.acceptInvitation(shareId, user.userId);
|
||||
return { share };
|
||||
}
|
||||
|
||||
@Post('shares/:shareId/decline')
|
||||
async declineInvitation(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('shareId') shareId: string
|
||||
) {
|
||||
async declineInvitation(@CurrentUser() user: CurrentUserData, @Param('shareId') shareId: string) {
|
||||
const share = await this.shareService.declineInvitation(shareId, user.userId);
|
||||
return { share };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,17 +22,11 @@ export class ShareService {
|
|||
// Verify user owns the calendar
|
||||
await this.calendarService.findByIdOrThrow(calendarId, userId);
|
||||
|
||||
return this.db
|
||||
.select()
|
||||
.from(calendarShares)
|
||||
.where(eq(calendarShares.calendarId, calendarId));
|
||||
return this.db.select().from(calendarShares).where(eq(calendarShares.calendarId, calendarId));
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<CalendarShare | null> {
|
||||
const result = await this.db
|
||||
.select()
|
||||
.from(calendarShares)
|
||||
.where(eq(calendarShares.id, id));
|
||||
const result = await this.db.select().from(calendarShares).where(eq(calendarShares.id, id));
|
||||
return result[0] || null;
|
||||
}
|
||||
|
||||
|
|
@ -43,10 +37,7 @@ export class ShareService {
|
|||
.where(
|
||||
and(
|
||||
eq(calendarShares.status, 'pending'),
|
||||
or(
|
||||
eq(calendarShares.sharedWithUserId, userId),
|
||||
eq(calendarShares.sharedWithEmail, email)
|
||||
)
|
||||
or(eq(calendarShares.sharedWithUserId, userId), eq(calendarShares.sharedWithEmail, email))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -174,10 +165,7 @@ export class ShareService {
|
|||
.select()
|
||||
.from(calendarShares)
|
||||
.where(
|
||||
and(
|
||||
eq(calendarShares.sharedWithUserId, userId),
|
||||
eq(calendarShares.status, 'accepted')
|
||||
)
|
||||
and(eq(calendarShares.sharedWithUserId, userId), eq(calendarShares.status, 'accepted'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
|
||||
<section class="relative overflow-hidden bg-dark-bg">
|
||||
<!-- Background gradient -->
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-primary-950/30 via-dark-bg to-primary-950/30"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-primary-950/30 via-dark-bg to-primary-950/30">
|
||||
</div>
|
||||
|
||||
<div class="container relative">
|
||||
<div class="mx-auto max-w-3xl text-center">
|
||||
|
|
@ -12,39 +13,44 @@
|
|||
Bereit, deine Zeit zu organisieren?
|
||||
</h2>
|
||||
<p class="mb-10 text-lg text-gray-400">
|
||||
Starte kostenlos und erlebe, wie einfach Zeitmanagement sein kann.
|
||||
Keine Kreditkarte erforderlich.
|
||||
Starte kostenlos und erlebe, wie einfach Zeitmanagement sein kann. Keine Kreditkarte
|
||||
erforderlich.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col items-center justify-center gap-4 sm:flex-row">
|
||||
<a href="#" class="btn btn-primary text-lg">
|
||||
Jetzt kostenlos starten
|
||||
<svg class="ml-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#features" class="btn btn-secondary">
|
||||
Mehr erfahren
|
||||
</a>
|
||||
<a href="#features" class="btn btn-secondary"> Mehr erfahren </a>
|
||||
</div>
|
||||
|
||||
<!-- Benefits list -->
|
||||
<div class="mt-12 flex flex-wrap items-center justify-center gap-6 text-sm text-gray-500">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="h-5 w-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Kostenlos starten</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="h-5 w-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Keine Kreditkarte</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="h-5 w-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Jederzeit kündbar</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,43 +7,49 @@ const features = [
|
|||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>`,
|
||||
title: 'Mehrere Kalender',
|
||||
description: 'Verwalte verschiedene Kalender für Arbeit, Privates, Familie und mehr - alles übersichtlich farbcodiert.'
|
||||
description:
|
||||
'Verwalte verschiedene Kalender für Arbeit, Privates, Familie und mehr - alles übersichtlich farbcodiert.',
|
||||
},
|
||||
{
|
||||
icon: `<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||
</svg>`,
|
||||
title: 'Kalender teilen',
|
||||
description: 'Teile Kalender mit Familie, Freunden oder Kollegen. Vergib Lese- oder Bearbeitungsrechte.'
|
||||
description:
|
||||
'Teile Kalender mit Familie, Freunden oder Kollegen. Vergib Lese- oder Bearbeitungsrechte.',
|
||||
},
|
||||
{
|
||||
icon: `<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>`,
|
||||
title: 'CalDAV & iCal Sync',
|
||||
description: 'Synchronisiere mit Google Calendar, Apple Calendar, Outlook und jedem CalDAV-kompatiblen Dienst.'
|
||||
description:
|
||||
'Synchronisiere mit Google Calendar, Apple Calendar, Outlook und jedem CalDAV-kompatiblen Dienst.',
|
||||
},
|
||||
{
|
||||
icon: `<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path>
|
||||
</svg>`,
|
||||
title: 'Smarte Erinnerungen',
|
||||
description: 'Nie wieder einen Termin verpassen. Push-Benachrichtigungen und E-Mail-Erinnerungen zur rechten Zeit.'
|
||||
description:
|
||||
'Nie wieder einen Termin verpassen. Push-Benachrichtigungen und E-Mail-Erinnerungen zur rechten Zeit.',
|
||||
},
|
||||
{
|
||||
icon: `<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>`,
|
||||
title: 'Wiederkehrende Termine',
|
||||
description: 'Erstelle einmalige oder wiederkehrende Termine mit flexiblen Wiederholungsregeln nach RFC 5545.'
|
||||
description:
|
||||
'Erstelle einmalige oder wiederkehrende Termine mit flexiblen Wiederholungsregeln nach RFC 5545.',
|
||||
},
|
||||
{
|
||||
icon: `<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
|
||||
</svg>`,
|
||||
title: 'Mobile & Desktop',
|
||||
description: 'Greife von überall auf deine Termine zu - Web-App, iOS und Android mit Offline-Support.'
|
||||
}
|
||||
description:
|
||||
'Greife von überall auf deine Termine zu - Web-App, iOS und Android mit Offline-Support.',
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
|
|
@ -54,9 +60,7 @@ const features = [
|
|||
<span class="mb-4 inline-block text-sm font-medium uppercase tracking-wider text-primary-400">
|
||||
Funktionen
|
||||
</span>
|
||||
<h2 class="mb-6 text-3xl font-bold md:text-4xl lg:text-5xl">
|
||||
Alles was du brauchst
|
||||
</h2>
|
||||
<h2 class="mb-6 text-3xl font-bold md:text-4xl lg:text-5xl">Alles was du brauchst</h2>
|
||||
<p class="text-lg text-gray-400">
|
||||
Kalender bietet alle Funktionen, die du für effektives Zeitmanagement benötigst.
|
||||
</p>
|
||||
|
|
@ -64,15 +68,17 @@ const features = [
|
|||
|
||||
<!-- Features grid -->
|
||||
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{features.map((feature) => (
|
||||
<div class="group rounded-xl border border-dark-border bg-dark-card p-6 transition-all duration-300 hover:border-primary-500/50 hover:bg-dark-card/80">
|
||||
<div class="mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-primary-500/10 text-primary-400 transition-colors group-hover:bg-primary-500/20">
|
||||
<Fragment set:html={feature.icon} />
|
||||
{
|
||||
features.map((feature) => (
|
||||
<div class="group rounded-xl border border-dark-border bg-dark-card p-6 transition-all duration-300 hover:border-primary-500/50 hover:bg-dark-card/80">
|
||||
<div class="mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-primary-500/10 text-primary-400 transition-colors group-hover:bg-primary-500/20">
|
||||
<Fragment set:html={feature.icon} />
|
||||
</div>
|
||||
<h3 class="mb-3 text-xl font-semibold">{feature.title}</h3>
|
||||
<p class="text-gray-400">{feature.description}</p>
|
||||
</div>
|
||||
<h3 class="mb-3 text-xl font-semibold">{feature.title}</h3>
|
||||
<p class="text-gray-400">{feature.description}</p>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -8,18 +8,18 @@ const links = {
|
|||
{ name: 'Funktionen', href: '#features' },
|
||||
{ name: 'Preise', href: '#pricing' },
|
||||
{ name: 'Changelog', href: '/changelog' },
|
||||
{ name: 'Roadmap', href: '/roadmap' }
|
||||
{ name: 'Roadmap', href: '/roadmap' },
|
||||
],
|
||||
legal: [
|
||||
{ name: 'Impressum', href: '/impressum' },
|
||||
{ name: 'Datenschutz', href: '/datenschutz' },
|
||||
{ name: 'AGB', href: '/agb' }
|
||||
{ name: 'AGB', href: '/agb' },
|
||||
],
|
||||
support: [
|
||||
{ name: 'FAQ', href: '/faq' },
|
||||
{ name: 'Kontakt', href: '/kontakt' },
|
||||
{ name: 'Status', href: '/status' }
|
||||
]
|
||||
{ name: 'Status', href: '/status' },
|
||||
],
|
||||
};
|
||||
---
|
||||
|
||||
|
|
@ -29,53 +29,75 @@ const links = {
|
|||
<!-- Brand -->
|
||||
<div class="md:col-span-1">
|
||||
<div class="mb-4 flex items-center gap-2">
|
||||
<svg class="h-8 w-8 text-primary-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
<svg
|
||||
class="h-8 w-8 text-primary-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="text-xl font-bold">Kalender</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500">
|
||||
Smart Calendar Management für besseres Zeitmanagement.
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">Smart Calendar Management für besseres Zeitmanagement.</p>
|
||||
</div>
|
||||
|
||||
<!-- Links -->
|
||||
<div>
|
||||
<h4 class="mb-4 font-semibold">Produkt</h4>
|
||||
<ul class="space-y-2 text-sm text-gray-400">
|
||||
{links.product.map((link) => (
|
||||
<li>
|
||||
<a href={link.href} class="transition-colors hover:text-white">{link.name}</a>
|
||||
</li>
|
||||
))}
|
||||
{
|
||||
links.product.map((link) => (
|
||||
<li>
|
||||
<a href={link.href} class="transition-colors hover:text-white">
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="mb-4 font-semibold">Rechtliches</h4>
|
||||
<ul class="space-y-2 text-sm text-gray-400">
|
||||
{links.legal.map((link) => (
|
||||
<li>
|
||||
<a href={link.href} class="transition-colors hover:text-white">{link.name}</a>
|
||||
</li>
|
||||
))}
|
||||
{
|
||||
links.legal.map((link) => (
|
||||
<li>
|
||||
<a href={link.href} class="transition-colors hover:text-white">
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="mb-4 font-semibold">Support</h4>
|
||||
<ul class="space-y-2 text-sm text-gray-400">
|
||||
{links.support.map((link) => (
|
||||
<li>
|
||||
<a href={link.href} class="transition-colors hover:text-white">{link.name}</a>
|
||||
</li>
|
||||
))}
|
||||
{
|
||||
links.support.map((link) => (
|
||||
<li>
|
||||
<a href={link.href} class="transition-colors hover:text-white">
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom bar -->
|
||||
<div class="mt-12 flex flex-col items-center justify-between gap-4 border-t border-dark-border pt-8 md:flex-row">
|
||||
<div
|
||||
class="mt-12 flex flex-col items-center justify-between gap-4 border-t border-dark-border pt-8 md:flex-row"
|
||||
>
|
||||
<p class="text-sm text-gray-500">
|
||||
© {currentYear} Kalender. Alle Rechte vorbehalten.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -4,23 +4,24 @@
|
|||
|
||||
<section class="relative overflow-hidden py-20 md:py-32">
|
||||
<!-- Background gradient -->
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-b from-primary-950/30 via-dark-bg to-dark-bg"
|
||||
>
|
||||
</div>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-primary-950/30 via-dark-bg to-dark-bg"></div>
|
||||
|
||||
<!-- Grid pattern -->
|
||||
<div
|
||||
class="absolute inset-0 bg-[url('/grid.svg')] bg-center opacity-10"
|
||||
>
|
||||
</div>
|
||||
<div class="absolute inset-0 bg-[url('/grid.svg')] bg-center opacity-10"></div>
|
||||
|
||||
<div class="container relative">
|
||||
<div class="mx-auto max-w-4xl text-center">
|
||||
<!-- Badge -->
|
||||
<div class="mb-8 inline-flex items-center gap-2 rounded-full border border-primary-500/30 bg-primary-500/10 px-4 py-2 text-sm text-primary-400">
|
||||
<div
|
||||
class="mb-8 inline-flex items-center gap-2 rounded-full border border-primary-500/30 bg-primary-500/10 px-4 py-2 text-sm text-primary-400"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Smart Kalender-Management</span>
|
||||
</div>
|
||||
|
|
@ -33,34 +34,38 @@
|
|||
|
||||
<!-- Subheadline -->
|
||||
<p class="mx-auto mb-10 max-w-2xl text-lg text-gray-400 md:text-xl">
|
||||
Persönliche Kalender, geteilte Termine, CalDAV-Synchronisation und smarte Erinnerungen - alles an einem Ort. Behalte den Überblick über dein Leben.
|
||||
Persönliche Kalender, geteilte Termine, CalDAV-Synchronisation und smarte Erinnerungen -
|
||||
alles an einem Ort. Behalte den Überblick über dein Leben.
|
||||
</p>
|
||||
|
||||
<!-- CTA Buttons -->
|
||||
<div class="flex flex-col items-center justify-center gap-4 sm:flex-row">
|
||||
<a
|
||||
href="#"
|
||||
class="btn btn-primary group text-lg"
|
||||
>
|
||||
<a href="#" class="btn btn-primary group text-lg">
|
||||
Kostenlos starten
|
||||
<svg class="ml-2 h-5 w-5 transition-transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
|
||||
<svg
|
||||
class="ml-2 h-5 w-5 transition-transform group-hover:translate-x-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="#features"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
Funktionen entdecken
|
||||
</a>
|
||||
<a href="#features" class="btn btn-secondary"> Funktionen entdecken </a>
|
||||
</div>
|
||||
|
||||
<!-- Social proof -->
|
||||
<div class="mt-16 flex flex-col items-center gap-4">
|
||||
<div class="flex -space-x-2">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<div class="h-10 w-10 rounded-full border-2 border-dark-bg bg-gradient-to-br from-primary-400 to-primary-600"></div>
|
||||
))}
|
||||
{
|
||||
[1, 2, 3, 4, 5].map((i) => (
|
||||
<div class="h-10 w-10 rounded-full border-2 border-dark-bg bg-gradient-to-br from-primary-400 to-primary-600" />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<p class="text-sm text-gray-500">
|
||||
<span class="font-semibold text-white">500+</span> Nutzer vertrauen Kalender
|
||||
|
|
@ -70,7 +75,10 @@
|
|||
|
||||
<!-- Preview mockup -->
|
||||
<div class="relative mx-auto mt-16 max-w-5xl">
|
||||
<div class="absolute -inset-4 rounded-2xl bg-gradient-to-r from-primary-500/20 via-transparent to-primary-500/20 blur-3xl"></div>
|
||||
<div
|
||||
class="absolute -inset-4 rounded-2xl bg-gradient-to-r from-primary-500/20 via-transparent to-primary-500/20 blur-3xl"
|
||||
>
|
||||
</div>
|
||||
<div class="relative rounded-xl border border-dark-border bg-dark-card p-2 shadow-2xl">
|
||||
<div class="flex gap-2 px-4 py-3">
|
||||
<div class="h-3 w-3 rounded-full bg-red-500"></div>
|
||||
|
|
@ -89,14 +97,20 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-7 gap-2">
|
||||
{['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'].map((day) => (
|
||||
<div class="text-center text-sm text-gray-500">{day}</div>
|
||||
))}
|
||||
{Array.from({ length: 35 }, (_, i) => (
|
||||
<div class={`rounded-lg p-2 text-center text-sm ${i === 14 ? 'bg-primary-500 text-white' : 'bg-dark-card'}`}>
|
||||
{((i % 31) + 1).toString()}
|
||||
</div>
|
||||
))}
|
||||
{
|
||||
['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'].map((day) => (
|
||||
<div class="text-center text-sm text-gray-500">{day}</div>
|
||||
))
|
||||
}
|
||||
{
|
||||
Array.from({ length: 35 }, (_, i) => (
|
||||
<div
|
||||
class={`rounded-lg p-2 text-center text-sm ${i === 14 ? 'bg-primary-500 text-white' : 'bg-dark-card'}`}
|
||||
>
|
||||
{((i % 31) + 1).toString()}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -114,104 +114,139 @@ const pricingPlans = [
|
|||
<Hero />
|
||||
<Features />
|
||||
|
||||
{StepsSection && (
|
||||
<StepsSection
|
||||
id="how-it-works"
|
||||
title="So einfach geht's"
|
||||
subtitle="In drei Schritten zum organisierten Leben"
|
||||
steps={steps}
|
||||
showImages={false}
|
||||
alternateLayout={true}
|
||||
class="bg-dark-surface"
|
||||
/>
|
||||
)}
|
||||
{
|
||||
StepsSection && (
|
||||
<StepsSection
|
||||
id="how-it-works"
|
||||
title="So einfach geht's"
|
||||
subtitle="In drei Schritten zum organisierten Leben"
|
||||
steps={steps}
|
||||
showImages={false}
|
||||
alternateLayout={true}
|
||||
class="bg-dark-surface"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{!StepsSection && (
|
||||
<section id="how-it-works" class="bg-dark-surface">
|
||||
<div class="container">
|
||||
<div class="mx-auto mb-16 max-w-3xl text-center">
|
||||
<span class="mb-4 inline-block text-sm font-medium uppercase tracking-wider text-primary-400">
|
||||
So funktioniert's
|
||||
</span>
|
||||
<h2 class="mb-6 text-3xl font-bold md:text-4xl">So einfach geht's</h2>
|
||||
<p class="text-lg text-gray-400">In drei Schritten zum organisierten Leben</p>
|
||||
</div>
|
||||
{
|
||||
!StepsSection && (
|
||||
<section id="how-it-works" class="bg-dark-surface">
|
||||
<div class="container">
|
||||
<div class="mx-auto mb-16 max-w-3xl text-center">
|
||||
<span class="mb-4 inline-block text-sm font-medium uppercase tracking-wider text-primary-400">
|
||||
So funktioniert's
|
||||
</span>
|
||||
<h2 class="mb-6 text-3xl font-bold md:text-4xl">So einfach geht's</h2>
|
||||
<p class="text-lg text-gray-400">In drei Schritten zum organisierten Leben</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-8 md:grid-cols-3">
|
||||
{steps.map((step) => (
|
||||
<div class="text-center">
|
||||
<div class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary-500/20 text-2xl font-bold text-primary-400">
|
||||
{step.number}
|
||||
</div>
|
||||
<h3 class="mb-3 text-xl font-semibold">{step.title}</h3>
|
||||
<p class="text-gray-400">{step.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{PricingSection && (
|
||||
<PricingSection
|
||||
id="pricing"
|
||||
title="Einfache, transparente Preise"
|
||||
subtitle="Starte kostenlos, upgrade wenn du mehr brauchst"
|
||||
plans={pricingPlans}
|
||||
class="bg-dark-bg"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!PricingSection && (
|
||||
<section id="pricing" class="bg-dark-bg">
|
||||
<div class="container">
|
||||
<div class="mx-auto mb-16 max-w-3xl text-center">
|
||||
<span class="mb-4 inline-block text-sm font-medium uppercase tracking-wider text-primary-400">
|
||||
Preise
|
||||
</span>
|
||||
<h2 class="mb-6 text-3xl font-bold md:text-4xl">Einfache, transparente Preise</h2>
|
||||
<p class="text-lg text-gray-400">Starte kostenlos, upgrade wenn du mehr brauchst</p>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto grid max-w-5xl gap-8 md:grid-cols-3">
|
||||
{pricingPlans.map((plan) => (
|
||||
<div class={`relative rounded-xl border p-6 ${plan.highlighted ? 'border-primary-500 bg-primary-500/10' : 'border-dark-border bg-dark-card'}`}>
|
||||
{plan.badge && (
|
||||
<div class="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-primary-500 px-3 py-1 text-xs font-medium text-white">
|
||||
{plan.badge}
|
||||
<div class="grid gap-8 md:grid-cols-3">
|
||||
{steps.map((step) => (
|
||||
<div class="text-center">
|
||||
<div class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary-500/20 text-2xl font-bold text-primary-400">
|
||||
{step.number}
|
||||
</div>
|
||||
)}
|
||||
<h3 class="mb-2 text-xl font-semibold">{plan.name}</h3>
|
||||
<p class="mb-4 text-sm text-gray-400">{plan.description}</p>
|
||||
<div class="mb-6">
|
||||
<span class="text-4xl font-bold">{plan.price}€</span>
|
||||
<span class="text-gray-500">{plan.period}</span>
|
||||
<h3 class="mb-3 text-xl font-semibold">{step.title}</h3>
|
||||
<p class="text-gray-400">{step.description}</p>
|
||||
</div>
|
||||
<ul class="mb-8 space-y-3">
|
||||
{plan.features.map((feature) => (
|
||||
<li class={`flex items-center gap-2 text-sm ${feature.included ? 'text-white' : 'text-gray-600'}`}>
|
||||
{feature.included ? (
|
||||
<svg class="h-5 w-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg class="h-5 w-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
)}
|
||||
{feature.text}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<a href={plan.cta.href} class={`btn w-full ${plan.highlighted ? 'btn-primary' : 'btn-secondary'}`}>
|
||||
{plan.cta.text}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
PricingSection && (
|
||||
<PricingSection
|
||||
id="pricing"
|
||||
title="Einfache, transparente Preise"
|
||||
subtitle="Starte kostenlos, upgrade wenn du mehr brauchst"
|
||||
plans={pricingPlans}
|
||||
class="bg-dark-bg"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
!PricingSection && (
|
||||
<section id="pricing" class="bg-dark-bg">
|
||||
<div class="container">
|
||||
<div class="mx-auto mb-16 max-w-3xl text-center">
|
||||
<span class="mb-4 inline-block text-sm font-medium uppercase tracking-wider text-primary-400">
|
||||
Preise
|
||||
</span>
|
||||
<h2 class="mb-6 text-3xl font-bold md:text-4xl">Einfache, transparente Preise</h2>
|
||||
<p class="text-lg text-gray-400">Starte kostenlos, upgrade wenn du mehr brauchst</p>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto grid max-w-5xl gap-8 md:grid-cols-3">
|
||||
{pricingPlans.map((plan) => (
|
||||
<div
|
||||
class={`relative rounded-xl border p-6 ${plan.highlighted ? 'border-primary-500 bg-primary-500/10' : 'border-dark-border bg-dark-card'}`}
|
||||
>
|
||||
{plan.badge && (
|
||||
<div class="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-primary-500 px-3 py-1 text-xs font-medium text-white">
|
||||
{plan.badge}
|
||||
</div>
|
||||
)}
|
||||
<h3 class="mb-2 text-xl font-semibold">{plan.name}</h3>
|
||||
<p class="mb-4 text-sm text-gray-400">{plan.description}</p>
|
||||
<div class="mb-6">
|
||||
<span class="text-4xl font-bold">{plan.price}€</span>
|
||||
<span class="text-gray-500">{plan.period}</span>
|
||||
</div>
|
||||
<ul class="mb-8 space-y-3">
|
||||
{plan.features.map((feature) => (
|
||||
<li
|
||||
class={`flex items-center gap-2 text-sm ${feature.included ? 'text-white' : 'text-gray-600'}`}
|
||||
>
|
||||
{feature.included ? (
|
||||
<svg
|
||||
class="h-5 w-5 text-green-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
class="h-5 w-5 text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{feature.text}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<a
|
||||
href={plan.cta.href}
|
||||
class={`btn w-full ${plan.highlighted ? 'btn-primary' : 'btn-secondary'}`}
|
||||
>
|
||||
{plan.cta.text}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
<CTA />
|
||||
<Footer />
|
||||
|
|
|
|||
|
|
@ -23,9 +23,17 @@
|
|||
const weekStart = viewStore.viewRange.start;
|
||||
const weekEnd = viewStore.viewRange.end;
|
||||
if (weekStart.getMonth() === weekEnd.getMonth()) {
|
||||
return format(weekStart, 'd.', { locale: de }) + ' - ' + format(weekEnd, 'd. MMMM yyyy', { locale: de });
|
||||
return (
|
||||
format(weekStart, 'd.', { locale: de }) +
|
||||
' - ' +
|
||||
format(weekEnd, 'd. MMMM yyyy', { locale: de })
|
||||
);
|
||||
}
|
||||
return format(weekStart, 'd. MMM', { locale: de }) + ' - ' + format(weekEnd, 'd. MMM yyyy', { locale: de });
|
||||
return (
|
||||
format(weekStart, 'd. MMM', { locale: de }) +
|
||||
' - ' +
|
||||
format(weekEnd, 'd. MMM yyyy', { locale: de })
|
||||
);
|
||||
case 'month':
|
||||
return format(date, 'MMMM yyyy', { locale: de });
|
||||
case 'year':
|
||||
|
|
@ -44,17 +52,28 @@
|
|||
|
||||
<header class="calendar-header">
|
||||
<div class="header-left">
|
||||
<button class="btn btn-ghost" onclick={() => viewStore.goToToday()}>
|
||||
Heute
|
||||
</button>
|
||||
<button class="btn btn-ghost" onclick={() => viewStore.goToToday()}> Heute </button>
|
||||
|
||||
<div class="nav-buttons">
|
||||
<button class="btn btn-ghost btn-icon" onclick={() => viewStore.goToPrevious()} aria-label="Zurück">
|
||||
<button
|
||||
class="btn btn-ghost btn-icon"
|
||||
onclick={() => viewStore.goToPrevious()}
|
||||
aria-label="Zurück"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-icon" onclick={() => viewStore.goToNext()} aria-label="Weiter">
|
||||
<button
|
||||
class="btn btn-ghost btn-icon"
|
||||
onclick={() => viewStore.goToNext()}
|
||||
aria-label="Weiter"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
|
|
@ -66,7 +85,7 @@
|
|||
|
||||
<div class="header-right">
|
||||
<div class="view-selector">
|
||||
{#each (['day', 'week', 'month'] as const) as type}
|
||||
{#each ['day', 'week', 'month'] as const as type}
|
||||
<button
|
||||
class="view-btn"
|
||||
class:active={viewStore.viewType === type}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,7 @@
|
|||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
format,
|
||||
isToday,
|
||||
parseISO,
|
||||
differenceInMinutes,
|
||||
} from 'date-fns';
|
||||
import { format, isToday, parseISO, differenceInMinutes } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
|
||||
let hours = Array.from({ length: 24 }, (_, i) => i);
|
||||
|
|
@ -110,8 +105,14 @@
|
|||
onclick={() => handleEventClick(event)}
|
||||
>
|
||||
<span class="event-time">
|
||||
{format(typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime, 'HH:mm')} -
|
||||
{format(typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime, 'HH:mm')}
|
||||
{format(
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime,
|
||||
'HH:mm'
|
||||
)} -
|
||||
{format(
|
||||
typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime,
|
||||
'HH:mm'
|
||||
)}
|
||||
</span>
|
||||
<span class="event-title">{event.title}</span>
|
||||
{#if event.location}
|
||||
|
|
|
|||
|
|
@ -94,17 +94,21 @@
|
|||
onclick={(e) => handleEventClick(event, e)}
|
||||
>
|
||||
{#if !event.isAllDay}
|
||||
<span class="event-time">{format(typeof event.startTime === 'string' ? new Date(event.startTime) : event.startTime, 'HH:mm')}</span>
|
||||
<span class="event-time"
|
||||
>{format(
|
||||
typeof event.startTime === 'string'
|
||||
? new Date(event.startTime)
|
||||
: event.startTime,
|
||||
'HH:mm'
|
||||
)}</span
|
||||
>
|
||||
{/if}
|
||||
<span class="event-title">{event.title}</span>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
{#if eventsStore.getEventsForDay(day).length > 3}
|
||||
<button
|
||||
class="more-events"
|
||||
onclick={(e) => handleMoreClick(day, e)}
|
||||
>
|
||||
<button class="more-events" onclick={(e) => handleMoreClick(day, e)}>
|
||||
+{eventsStore.getEventsForDay(day).length - 3} mehr
|
||||
</button>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -137,7 +137,10 @@
|
|||
onclick={() => handleEventClick(event)}
|
||||
>
|
||||
<span class="event-time">
|
||||
{format(typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime, 'HH:mm')}
|
||||
{format(
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime,
|
||||
'HH:mm'
|
||||
)}
|
||||
</span>
|
||||
<span class="event-title">{event.title}</span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@
|
|||
// Initialize date/time fields
|
||||
$effect(() => {
|
||||
if (event) {
|
||||
const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const start =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
startDate = format(start, 'yyyy-MM-dd');
|
||||
startTime = format(start, 'HH:mm');
|
||||
|
|
@ -92,7 +93,11 @@
|
|||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="calendar" class="text-sm font-medium text-foreground">Kalender</label>
|
||||
<select id="calendar" class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors" bind:value={calendarId}>
|
||||
<select
|
||||
id="calendar"
|
||||
class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors"
|
||||
bind:value={calendarId}
|
||||
>
|
||||
{#each calendarsStore.calendars as cal}
|
||||
<option value={cal.id}>{cal.name}</option>
|
||||
{/each}
|
||||
|
|
@ -109,12 +114,24 @@
|
|||
<div class="flex gap-4">
|
||||
<div class="flex-1 flex flex-col gap-2">
|
||||
<label for="startDate" class="text-sm font-medium text-foreground">Beginn</label>
|
||||
<input type="date" id="startDate" class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors" bind:value={startDate} required />
|
||||
<input
|
||||
type="date"
|
||||
id="startDate"
|
||||
class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors"
|
||||
bind:value={startDate}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{#if !isAllDay}
|
||||
<div class="flex-1 flex flex-col gap-2">
|
||||
<label for="startTime" class="text-sm font-medium text-foreground">Uhrzeit</label>
|
||||
<input type="time" id="startTime" class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors" bind:value={startTime} required />
|
||||
<input
|
||||
type="time"
|
||||
id="startTime"
|
||||
class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors"
|
||||
bind:value={startTime}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -122,12 +139,24 @@
|
|||
<div class="flex gap-4">
|
||||
<div class="flex-1 flex flex-col gap-2">
|
||||
<label for="endDate" class="text-sm font-medium text-foreground">Ende</label>
|
||||
<input type="date" id="endDate" class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors" bind:value={endDate} required />
|
||||
<input
|
||||
type="date"
|
||||
id="endDate"
|
||||
class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors"
|
||||
bind:value={endDate}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{#if !isAllDay}
|
||||
<div class="flex-1 flex flex-col gap-2">
|
||||
<label for="endTime" class="text-sm font-medium text-foreground">Uhrzeit</label>
|
||||
<input type="time" id="endTime" class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors" bind:value={endTime} required />
|
||||
<input
|
||||
type="time"
|
||||
id="endTime"
|
||||
class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors"
|
||||
bind:value={endTime}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -155,12 +184,19 @@
|
|||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4 border-t border-border">
|
||||
<button type="button" class="px-4 py-2 rounded-lg font-medium text-foreground bg-transparent hover:bg-muted transition-colors" onclick={onCancel}>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 rounded-lg font-medium text-foreground bg-transparent hover:bg-muted transition-colors"
|
||||
onclick={onCancel}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button type="submit" class="px-4 py-2 rounded-lg font-medium text-primary-foreground bg-primary hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" disabled={submitting || !title.trim()}>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 rounded-lg font-medium text-primary-foreground bg-primary hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
disabled={submitting || !title.trim()}
|
||||
>
|
||||
{mode === 'create' ? 'Erstellen' : 'Speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
|
|
|||
|
|
@ -57,12 +57,16 @@ export const eventsStore = {
|
|||
if (!Array.isArray(currentEvents)) return [];
|
||||
|
||||
return currentEvents.filter((event) => {
|
||||
const eventStart = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const eventStart =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const eventEnd = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
|
||||
// For all-day events, check if day falls within event range
|
||||
if (event.isAllDay) {
|
||||
return isWithinInterval(date, { start: eventStart, end: eventEnd }) || isSameDay(date, eventStart);
|
||||
return (
|
||||
isWithinInterval(date, { start: eventStart, end: eventEnd }) ||
|
||||
isSameDay(date, eventStart)
|
||||
);
|
||||
}
|
||||
|
||||
// For timed events, check if event starts on this day
|
||||
|
|
@ -79,7 +83,8 @@ export const eventsStore = {
|
|||
if (!Array.isArray(currentEvents)) return [];
|
||||
|
||||
return currentEvents.filter((event) => {
|
||||
const eventStart = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const eventStart =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const eventEnd = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
|
||||
// Check if event overlaps with the range
|
||||
|
|
|
|||
|
|
@ -60,10 +60,7 @@
|
|||
Neuer Termin
|
||||
</button>
|
||||
|
||||
<MiniCalendar
|
||||
selectedDate={viewStore.currentDate}
|
||||
onDateSelect={handleDateSelect}
|
||||
/>
|
||||
<MiniCalendar selectedDate={viewStore.currentDate} onDateSelect={handleDateSelect} />
|
||||
|
||||
<CalendarSidebar />
|
||||
</aside>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@
|
|||
const groups: Map<string, typeof eventsStore.events> = new Map();
|
||||
|
||||
for (const event of eventsStore.events) {
|
||||
const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const start =
|
||||
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const dateKey = format(start, 'yyyy-MM-dd');
|
||||
|
||||
if (!groups.has(dateKey)) {
|
||||
|
|
@ -79,9 +80,7 @@
|
|||
{:else if groupedEvents.length === 0}
|
||||
<div class="empty-state card">
|
||||
<p>Keine Termine in den nächsten 30 Tagen</p>
|
||||
<button class="btn btn-primary" onclick={() => goto('/event/new')}>
|
||||
Termin erstellen
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick={() => goto('/event/new')}> Termin erstellen </button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="event-list">
|
||||
|
|
@ -102,8 +101,16 @@
|
|||
{#if event.isAllDay}
|
||||
Ganztägig
|
||||
{:else}
|
||||
{format(typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime, 'HH:mm')} -
|
||||
{format(typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime, 'HH:mm')}
|
||||
{format(
|
||||
typeof event.startTime === 'string'
|
||||
? parseISO(event.startTime)
|
||||
: event.startTime,
|
||||
'HH:mm'
|
||||
)} -
|
||||
{format(
|
||||
typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime,
|
||||
'HH:mm'
|
||||
)}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="event-title">{event.title}</div>
|
||||
|
|
|
|||
|
|
@ -71,15 +71,18 @@
|
|||
<div class="calendars-page">
|
||||
<header class="page-header">
|
||||
<h1>Meine Kalender</h1>
|
||||
<button class="btn btn-primary" onclick={() => (showNewForm = true)}>
|
||||
Neuer Kalender
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick={() => (showNewForm = true)}> Neuer Kalender </button>
|
||||
</header>
|
||||
|
||||
{#if showNewForm}
|
||||
<div class="card new-calendar-form">
|
||||
<h2>Neuer Kalender</h2>
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleCreateCalendar(); }}>
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleCreateCalendar();
|
||||
}}
|
||||
>
|
||||
<div class="form-row">
|
||||
<input
|
||||
type="text"
|
||||
|
|
@ -87,11 +90,7 @@
|
|||
placeholder="Kalender Name"
|
||||
bind:value={newCalendarName}
|
||||
/>
|
||||
<input
|
||||
type="color"
|
||||
class="color-input"
|
||||
bind:value={newCalendarColor}
|
||||
/>
|
||||
<input type="color" class="color-input" bind:value={newCalendarColor} />
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-ghost" onclick={() => (showNewForm = false)}>
|
||||
|
|
@ -119,26 +118,14 @@
|
|||
}}
|
||||
>
|
||||
<div class="form-row">
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
class="input"
|
||||
value={calendar.name}
|
||||
/>
|
||||
<input
|
||||
type="color"
|
||||
name="color"
|
||||
class="color-input"
|
||||
value={calendar.color}
|
||||
/>
|
||||
<input type="text" name="name" class="input" value={calendar.name} />
|
||||
<input type="color" name="color" class="color-input" value={calendar.color} />
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-ghost" onclick={() => (editingCalendar = null)}>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Speichern
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary"> Speichern </button>
|
||||
</div>
|
||||
</form>
|
||||
{:else}
|
||||
|
|
@ -150,10 +137,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
<div class="calendar-actions">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
onclick={() => (editingCalendar = calendar)}
|
||||
>
|
||||
<button class="btn btn-ghost btn-sm" onclick={() => (editingCalendar = calendar)}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
{#if !calendar.isDefault}
|
||||
|
|
|
|||
|
|
@ -87,23 +87,14 @@
|
|||
<h1 class="page-title">{isEditing ? 'Termin bearbeiten' : event.title}</h1>
|
||||
{#if !isEditing}
|
||||
<div class="actions">
|
||||
<button class="btn btn-ghost" onclick={() => (isEditing = true)}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button class="btn btn-ghost text-destructive" onclick={handleDelete}>
|
||||
Löschen
|
||||
</button>
|
||||
<button class="btn btn-ghost" onclick={() => (isEditing = true)}> Bearbeiten </button>
|
||||
<button class="btn btn-ghost text-destructive" onclick={handleDelete}> Löschen </button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isEditing}
|
||||
<EventForm
|
||||
mode="edit"
|
||||
{event}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
<EventForm mode="edit" {event} onSave={handleSave} onCancel={handleCancel} />
|
||||
{:else}
|
||||
<div class="event-details">
|
||||
<div class="detail-row">
|
||||
|
|
@ -133,9 +124,7 @@
|
|||
{/if}
|
||||
|
||||
<div class="detail-row">
|
||||
<button class="btn btn-ghost" onclick={() => goto('/')}>
|
||||
Zurück zum Kalender
|
||||
</button>
|
||||
<button class="btn btn-ghost" onclick={() => goto('/')}> Zurück zum Kalender </button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,9 @@
|
|||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="5"></circle>
|
||||
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"></path>
|
||||
<path
|
||||
d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"
|
||||
></path>
|
||||
</svg>
|
||||
Hell
|
||||
</button>
|
||||
|
|
@ -102,7 +104,10 @@
|
|||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<button class="btn btn-ghost text-destructive" onclick={() => authStore.signOut().then(() => goto('/login'))}>
|
||||
<button
|
||||
class="btn btn-ghost text-destructive"
|
||||
onclick={() => authStore.signOut().then(() => goto('/login'))}
|
||||
>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -232,11 +232,6 @@ export function getEventDurationMinutes(start: Date, end: Date): number {
|
|||
/**
|
||||
* Check if two time ranges overlap
|
||||
*/
|
||||
export function doTimeRangesOverlap(
|
||||
start1: Date,
|
||||
end1: Date,
|
||||
start2: Date,
|
||||
end2: Date
|
||||
): boolean {
|
||||
export function doTimeRangesOverlap(start1: Date, end1: Date, start2: Date, end2: Date): boolean {
|
||||
return start1 < end2 && end1 > start2;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,11 @@ export function describeRecurrence(pattern: RecurrencePattern | null): string {
|
|||
|
||||
case 'WEEKLY':
|
||||
if (pattern.byDay && pattern.byDay.length > 0) {
|
||||
if (pattern.byDay.length === 5 && !pattern.byDay.includes('SA') && !pattern.byDay.includes('SU')) {
|
||||
if (
|
||||
pattern.byDay.length === 5 &&
|
||||
!pattern.byDay.includes('SA') &&
|
||||
!pattern.byDay.includes('SU')
|
||||
) {
|
||||
return interval === 1 ? 'Every weekday' : `Every ${interval} weeks on weekdays`;
|
||||
}
|
||||
const days = pattern.byDay.map(dayToLabel).join(', ');
|
||||
|
|
@ -122,7 +126,9 @@ export function describeRecurrence(pattern: RecurrencePattern | null): string {
|
|||
case 'MONTHLY':
|
||||
if (pattern.byMonthDay && pattern.byMonthDay.length > 0) {
|
||||
const days = pattern.byMonthDay.join(', ');
|
||||
return interval === 1 ? `Monthly on day ${days}` : `Every ${interval} months on day ${days}`;
|
||||
return interval === 1
|
||||
? `Monthly on day ${days}`
|
||||
: `Every ${interval} months on day ${days}`;
|
||||
}
|
||||
return interval === 1 ? 'Monthly' : `Every ${interval} months`;
|
||||
|
||||
|
|
@ -205,7 +211,10 @@ export function generateOccurrences(
|
|||
// Check if this date matches the pattern
|
||||
if (matchesPattern(currentDate, pattern)) {
|
||||
// Check if date is in range and not in exceptions
|
||||
if (currentDate >= rangeStart && !exceptionsSet.has(currentDate.toISOString().split('T')[0])) {
|
||||
if (
|
||||
currentDate >= rangeStart &&
|
||||
!exceptionsSet.has(currentDate.toISOString().split('T')[0])
|
||||
) {
|
||||
occurrences.push(new Date(currentDate));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ export default defineConfig({
|
|||
schema: './src/db/schema/index.ts',
|
||||
out: './src/db/migrations',
|
||||
dbCredentials: {
|
||||
url: process.env.CONTACTS_DATABASE_URL || process.env.DATABASE_URL || 'postgresql://manacore:devpassword@localhost:5432/contacts',
|
||||
url:
|
||||
process.env.CONTACTS_DATABASE_URL ||
|
||||
process.env.DATABASE_URL ||
|
||||
'postgresql://manacore:devpassword@localhost:5432/contacts',
|
||||
},
|
||||
verbose: true,
|
||||
strict: true,
|
||||
|
|
|
|||
|
|
@ -10,11 +10,7 @@ export type ActivityType = 'created' | 'updated' | 'called' | 'emailed' | 'met'
|
|||
export class ActivityService {
|
||||
constructor(@Inject(DATABASE_CONNECTION) private db: Database) {}
|
||||
|
||||
async findByContactId(
|
||||
contactId: string,
|
||||
userId: string,
|
||||
limit = 50
|
||||
): Promise<ContactActivity[]> {
|
||||
async findByContactId(contactId: string, userId: string, limit = 50): Promise<ContactActivity[]> {
|
||||
return this.db
|
||||
.select()
|
||||
.from(contactActivities)
|
||||
|
|
|
|||
|
|
@ -173,10 +173,7 @@ export class ContactController {
|
|||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('id', ParseUUIDPipe) id: string
|
||||
) {
|
||||
async findOne(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) {
|
||||
const contact = await this.contactService.findById(id, user.userId);
|
||||
if (!contact) {
|
||||
return { contact: null };
|
||||
|
|
@ -212,10 +209,7 @@ export class ContactController {
|
|||
}
|
||||
|
||||
@Delete(':id')
|
||||
async delete(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('id', ParseUUIDPipe) id: string
|
||||
) {
|
||||
async delete(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) {
|
||||
await this.contactService.delete(id, user.userId);
|
||||
return { success: true };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,10 +62,7 @@ export class GroupController {
|
|||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('id', ParseUUIDPipe) id: string
|
||||
) {
|
||||
async findOne(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) {
|
||||
const group = await this.groupService.findById(id, user.userId);
|
||||
const contactIds = group ? await this.groupService.getContactsInGroup(id) : [];
|
||||
return { group, contactIds };
|
||||
|
|
@ -91,10 +88,7 @@ export class GroupController {
|
|||
}
|
||||
|
||||
@Delete(':id')
|
||||
async delete(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('id', ParseUUIDPipe) id: string
|
||||
) {
|
||||
async delete(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) {
|
||||
await this.groupService.delete(id, user.userId);
|
||||
return { success: true };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,10 +51,7 @@ export class GroupService {
|
|||
}
|
||||
|
||||
async addContactToGroup(contactId: string, groupId: string): Promise<void> {
|
||||
await this.db
|
||||
.insert(contactToGroups)
|
||||
.values({ contactId, groupId })
|
||||
.onConflictDoNothing();
|
||||
await this.db.insert(contactToGroups).values({ contactId, groupId }).onConflictDoNothing();
|
||||
}
|
||||
|
||||
async removeContactFromGroup(contactId: string, groupId: string): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -77,19 +77,13 @@ export class NoteController {
|
|||
}
|
||||
|
||||
@Delete(':id')
|
||||
async delete(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('id', ParseUUIDPipe) id: string
|
||||
) {
|
||||
async delete(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) {
|
||||
await this.noteService.delete(id, user.userId);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@Post(':id/pin')
|
||||
async togglePin(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('id', ParseUUIDPipe) id: string
|
||||
) {
|
||||
async togglePin(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) {
|
||||
const note = await this.noteService.togglePin(id, user.userId);
|
||||
return { note };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,10 +67,7 @@ export class TagController {
|
|||
}
|
||||
|
||||
@Delete(':id')
|
||||
async delete(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('id', ParseUUIDPipe) id: string
|
||||
) {
|
||||
async delete(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) {
|
||||
await this.tagService.delete(id, user.userId);
|
||||
return { success: true };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,9 +30,7 @@
|
|||
</button>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
class="absolute right-0 mt-2 w-40 rounded-md border border-border bg-card shadow-lg z-50"
|
||||
>
|
||||
<div class="absolute right-0 mt-2 w-40 rounded-md border border-border bg-card shadow-lg z-50">
|
||||
{#each supportedLocales as lang}
|
||||
<button
|
||||
onclick={() => handleSelect(lang)}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@
|
|||
class="flex items-center gap-3 rounded-lg bg-card px-4 py-3 shadow-lg border border-border animate-in slide-in-from-right duration-200"
|
||||
>
|
||||
<span
|
||||
class="{getColorClass(toast.type)} flex h-6 w-6 items-center justify-center rounded-full text-white text-sm"
|
||||
class="{getColorClass(
|
||||
toast.type
|
||||
)} flex h-6 w-6 items-center justify-center rounded-full text-white text-sm"
|
||||
>
|
||||
{getIcon(toast.type)}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -52,10 +52,7 @@
|
|||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold text-foreground">{$_('contacts.title')}</h1>
|
||||
<a
|
||||
href="/contacts/new"
|
||||
class="btn btn-primary flex items-center gap-2"
|
||||
>
|
||||
<a href="/contacts/new" class="btn btn-primary flex items-center gap-2">
|
||||
<span>+</span>
|
||||
<span>{$_('contacts.new')}</span>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -93,7 +93,12 @@
|
|||
<h1 class="title">Archiv</h1>
|
||||
<div class="title-icon">
|
||||
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -102,7 +107,12 @@
|
|||
{#if contacts.length > 0}
|
||||
<div class="search-wrapper">
|
||||
<svg class="search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
|
|
@ -116,10 +126,15 @@
|
|||
{#if error}
|
||||
<div class="error-banner" role="alert">
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{error}</span>
|
||||
<button onclick={() => error = null} class="dismiss-btn">×</button>
|
||||
<button onclick={() => (error = null)} class="dismiss-btn">×</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
@ -131,14 +146,27 @@
|
|||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="empty-title">Archiv ist leer</h2>
|
||||
<p class="empty-description">Archivierte Kontakte erscheinen hier. Du kannst sie später wiederherstellen oder endgültig löschen.</p>
|
||||
<p class="empty-description">
|
||||
Archivierte Kontakte erscheinen hier. Du kannst sie später wiederherstellen oder endgültig
|
||||
löschen.
|
||||
</p>
|
||||
<a href="/" class="btn btn-primary">
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
Zu Kontakten
|
||||
</a>
|
||||
|
|
@ -147,7 +175,12 @@
|
|||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="empty-title">Keine Ergebnisse</h2>
|
||||
|
|
@ -156,7 +189,12 @@
|
|||
{:else}
|
||||
<div class="info-banner">
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Archivierte Kontakte können wiederhergestellt oder endgültig gelöscht werden.</span>
|
||||
</div>
|
||||
|
|
@ -201,7 +239,12 @@
|
|||
title="Wiederherstellen"
|
||||
>
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -211,7 +254,12 @@
|
|||
title="Endgültig löschen"
|
||||
>
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -219,7 +267,9 @@
|
|||
{/each}
|
||||
</div>
|
||||
|
||||
<p class="contacts-count">{contacts.length} archiviert{contacts.length !== 1 ? 'e Kontakte' : 'er Kontakt'}</p>
|
||||
<p class="contacts-count">
|
||||
{contacts.length} archiviert{contacts.length !== 1 ? 'e Kontakte' : 'er Kontakt'}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
@ -443,7 +493,11 @@
|
|||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, hsl(var(--color-primary)) 0%, hsl(var(--color-primary) / 0.7) 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--color-primary)) 0%,
|
||||
hsl(var(--color-primary) / 0.7) 100%
|
||||
);
|
||||
color: hsl(var(--color-primary-foreground));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -153,14 +153,36 @@
|
|||
<h1 class="title">{editing ? 'Bearbeiten' : 'Kontakt'}</h1>
|
||||
{#if contact && !editing && !loading}
|
||||
<div class="header-actions">
|
||||
<button onclick={() => { editing = true; populateForm(); }} class="action-btn" aria-label="Bearbeiten">
|
||||
<button
|
||||
onclick={() => {
|
||||
editing = true;
|
||||
populateForm();
|
||||
}}
|
||||
class="action-btn"
|
||||
aria-label="Bearbeiten"
|
||||
>
|
||||
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick={handleDelete} disabled={deleting} class="action-btn action-btn-danger" aria-label="Löschen">
|
||||
<button
|
||||
onclick={handleDelete}
|
||||
disabled={deleting}
|
||||
class="action-btn action-btn-danger"
|
||||
aria-label="Löschen"
|
||||
>
|
||||
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -172,8 +194,20 @@
|
|||
{#if loading}
|
||||
<div class="loading-container">
|
||||
<svg class="spinner-lg" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" stroke-opacity="0.25" />
|
||||
<path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" stroke-width="3" stroke-linecap="round" />
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-opacity="0.25"
|
||||
/>
|
||||
<path
|
||||
d="M12 2a10 10 0 0 1 10 10"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<p class="loading-text">Lade Kontakt...</p>
|
||||
</div>
|
||||
|
|
@ -181,7 +215,12 @@
|
|||
<div class="error-container">
|
||||
<div class="error-icon-wrapper">
|
||||
<svg class="error-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="error-text">{error}</p>
|
||||
|
|
@ -191,7 +230,12 @@
|
|||
{#if error}
|
||||
<div class="error-banner" role="alert">
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
|
|
@ -207,8 +251,18 @@
|
|||
</div>
|
||||
<button type="button" class="avatar-edit-btn" aria-label="Foto ändern">
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -218,13 +272,24 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSave(); }} class="form">
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
}}
|
||||
class="form"
|
||||
>
|
||||
<!-- Name Section -->
|
||||
<section class="form-section">
|
||||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Name</h2>
|
||||
|
|
@ -246,7 +311,12 @@
|
|||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Kontakt</h2>
|
||||
|
|
@ -255,7 +325,12 @@
|
|||
<label for="email" class="label">E-Mail</label>
|
||||
<div class="input-with-icon">
|
||||
<svg class="input-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"
|
||||
/>
|
||||
</svg>
|
||||
<input id="email" type="email" bind:value={email} class="input input-padded" />
|
||||
</div>
|
||||
|
|
@ -265,7 +340,12 @@
|
|||
<label for="phone" class="label">Telefon</label>
|
||||
<div class="input-with-icon">
|
||||
<svg class="input-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
|
||||
/>
|
||||
</svg>
|
||||
<input id="phone" type="tel" bind:value={phone} class="input input-padded" />
|
||||
</div>
|
||||
|
|
@ -274,7 +354,12 @@
|
|||
<label for="mobile" class="label">Mobil</label>
|
||||
<div class="input-with-icon">
|
||||
<svg class="input-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<input id="mobile" type="tel" bind:value={mobile} class="input input-padded" />
|
||||
</div>
|
||||
|
|
@ -287,7 +372,12 @@
|
|||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Arbeit</h2>
|
||||
|
|
@ -307,8 +397,18 @@
|
|||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Adresse</h2>
|
||||
|
|
@ -338,7 +438,12 @@
|
|||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Notizen</h2>
|
||||
|
|
@ -348,19 +453,42 @@
|
|||
|
||||
<!-- Action Buttons -->
|
||||
<div class="actions">
|
||||
<button type="button" onclick={() => { editing = false; }} class="btn btn-secondary">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
editing = false;
|
||||
}}
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button type="submit" disabled={saving} class="btn btn-primary">
|
||||
{#if saving}
|
||||
<svg class="spinner" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" stroke-opacity="0.25" />
|
||||
<path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" stroke-width="3" stroke-linecap="round" />
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-opacity="0.25"
|
||||
/>
|
||||
<path
|
||||
d="M12 2a10 10 0 0 1 10 10"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<span>Speichern...</span>
|
||||
{:else}
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<span>Speichern</span>
|
||||
{/if}
|
||||
|
|
@ -375,14 +503,25 @@
|
|||
<div class="avatar-circle avatar-large">
|
||||
{initials()}
|
||||
</div>
|
||||
<button onclick={handleToggleFavorite} class="favorite-btn" aria-label={contact.isFavorite ? 'Von Favoriten entfernen' : 'Zu Favoriten hinzufügen'}>
|
||||
<button
|
||||
onclick={handleToggleFavorite}
|
||||
class="favorite-btn"
|
||||
aria-label={contact.isFavorite ? 'Von Favoriten entfernen' : 'Zu Favoriten hinzufügen'}
|
||||
>
|
||||
{#if contact.isFavorite}
|
||||
<svg class="favorite-icon favorite-active" viewBox="0 0 24 24">
|
||||
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
|
||||
<path
|
||||
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg class="favorite-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
|
|
@ -401,7 +540,12 @@
|
|||
<a href="tel:{contact.phone}" class="quick-action">
|
||||
<div class="quick-action-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>Anrufen</span>
|
||||
|
|
@ -411,7 +555,12 @@
|
|||
<a href="mailto:{contact.email}" class="quick-action">
|
||||
<div class="quick-action-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>E-Mail</span>
|
||||
|
|
@ -421,7 +570,12 @@
|
|||
<a href="sms:{contact.mobile}" class="quick-action">
|
||||
<div class="quick-action-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>Nachricht</span>
|
||||
|
|
@ -437,7 +591,12 @@
|
|||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="section-title">Kontakt</h3>
|
||||
|
|
@ -446,7 +605,12 @@
|
|||
{#if contact.email}
|
||||
<a href="mailto:{contact.email}" class="detail-item detail-link">
|
||||
<svg class="detail-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"
|
||||
/>
|
||||
</svg>
|
||||
<div class="detail-content">
|
||||
<span class="detail-label">E-Mail</span>
|
||||
|
|
@ -457,7 +621,12 @@
|
|||
{#if contact.phone}
|
||||
<a href="tel:{contact.phone}" class="detail-item detail-link">
|
||||
<svg class="detail-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="detail-content">
|
||||
<span class="detail-label">Telefon</span>
|
||||
|
|
@ -468,7 +637,12 @@
|
|||
{#if contact.mobile}
|
||||
<a href="tel:{contact.mobile}" class="detail-item detail-link">
|
||||
<svg class="detail-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="detail-content">
|
||||
<span class="detail-label">Mobil</span>
|
||||
|
|
@ -486,7 +660,12 @@
|
|||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="section-title">Arbeit</h3>
|
||||
|
|
@ -495,7 +674,12 @@
|
|||
{#if contact.company}
|
||||
<div class="detail-item">
|
||||
<svg class="detail-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||
/>
|
||||
</svg>
|
||||
<div class="detail-content">
|
||||
<span class="detail-label">Firma</span>
|
||||
|
|
@ -506,7 +690,12 @@
|
|||
{#if contact.jobTitle}
|
||||
<div class="detail-item">
|
||||
<svg class="detail-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="detail-content">
|
||||
<span class="detail-label">Position</span>
|
||||
|
|
@ -524,8 +713,18 @@
|
|||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="section-title">Adresse</h3>
|
||||
|
|
@ -533,7 +732,9 @@
|
|||
<div class="address-card">
|
||||
{#if contact.street}<div class="address-line">{contact.street}</div>{/if}
|
||||
{#if contact.postalCode || contact.city}
|
||||
<div class="address-line">{[contact.postalCode, contact.city].filter(Boolean).join(' ')}</div>
|
||||
<div class="address-line">
|
||||
{[contact.postalCode, contact.city].filter(Boolean).join(' ')}
|
||||
</div>
|
||||
{/if}
|
||||
{#if contact.country}<div class="address-line">{contact.country}</div>{/if}
|
||||
</div>
|
||||
|
|
@ -546,7 +747,12 @@
|
|||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="section-title">Notizen</h3>
|
||||
|
|
@ -719,7 +925,11 @@
|
|||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, hsl(var(--color-primary)) 0%, hsl(var(--color-primary) / 0.7) 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--color-primary)) 0%,
|
||||
hsl(var(--color-primary) / 0.7) 100%
|
||||
);
|
||||
color: hsl(var(--color-primary-foreground));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -89,8 +89,18 @@
|
|||
</div>
|
||||
<button type="button" class="avatar-edit-btn" aria-label="Foto hinzufügen">
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -103,19 +113,35 @@
|
|||
{#if error}
|
||||
<div class="error-banner" role="alert">
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="form">
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
class="form"
|
||||
>
|
||||
<!-- Name Section -->
|
||||
<section class="form-section">
|
||||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Name</h2>
|
||||
|
|
@ -149,7 +175,12 @@
|
|||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Kontakt</h2>
|
||||
|
|
@ -158,7 +189,12 @@
|
|||
<label for="email" class="label">E-Mail</label>
|
||||
<div class="input-with-icon">
|
||||
<svg class="input-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
id="email"
|
||||
|
|
@ -174,7 +210,12 @@
|
|||
<label for="phone" class="label">Telefon</label>
|
||||
<div class="input-with-icon">
|
||||
<svg class="input-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
id="phone"
|
||||
|
|
@ -189,7 +230,12 @@
|
|||
<label for="mobile" class="label">Mobil</label>
|
||||
<div class="input-with-icon">
|
||||
<svg class="input-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
id="mobile"
|
||||
|
|
@ -208,7 +254,12 @@
|
|||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Arbeit</h2>
|
||||
|
|
@ -240,8 +291,18 @@
|
|||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Adresse</h2>
|
||||
|
|
@ -269,13 +330,7 @@
|
|||
</div>
|
||||
<div class="form-field col-span-2">
|
||||
<label for="city" class="label">Stadt</label>
|
||||
<input
|
||||
id="city"
|
||||
type="text"
|
||||
bind:value={city}
|
||||
class="input"
|
||||
placeholder="Berlin"
|
||||
/>
|
||||
<input id="city" type="text" bind:value={city} class="input" placeholder="Berlin" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
|
|
@ -295,7 +350,12 @@
|
|||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Notizen</h2>
|
||||
|
|
@ -310,19 +370,34 @@
|
|||
|
||||
<!-- Action Buttons -->
|
||||
<div class="actions">
|
||||
<a href="/" class="btn btn-secondary">
|
||||
Abbrechen
|
||||
</a>
|
||||
<a href="/" class="btn btn-secondary"> Abbrechen </a>
|
||||
<button type="submit" disabled={loading} class="btn btn-primary">
|
||||
{#if loading}
|
||||
<svg class="spinner" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" stroke-opacity="0.25" />
|
||||
<path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" stroke-width="3" stroke-linecap="round" />
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-opacity="0.25"
|
||||
/>
|
||||
<path
|
||||
d="M12 2a10 10 0 0 1 10 10"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<span>Speichern...</span>
|
||||
{:else}
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<span>Kontakt speichern</span>
|
||||
{/if}
|
||||
|
|
@ -396,7 +471,11 @@
|
|||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, hsl(var(--color-primary)) 0%, hsl(var(--color-primary) / 0.7) 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--color-primary)) 0%,
|
||||
hsl(var(--color-primary) / 0.7) 100%
|
||||
);
|
||||
color: hsl(var(--color-primary-foreground));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -82,7 +82,9 @@
|
|||
<h1 class="title">Favoriten</h1>
|
||||
<div class="title-icon">
|
||||
<svg class="icon" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
|
||||
<path
|
||||
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -90,7 +92,12 @@
|
|||
<!-- Search -->
|
||||
<div class="search-wrapper">
|
||||
<svg class="search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
|
|
@ -103,10 +110,15 @@
|
|||
{#if error}
|
||||
<div class="error-banner" role="alert">
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{error}</span>
|
||||
<button onclick={() => error = null} class="dismiss-btn">×</button>
|
||||
<button onclick={() => (error = null)} class="dismiss-btn">×</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
@ -118,14 +130,26 @@
|
|||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="empty-title">Keine Favoriten</h2>
|
||||
<p class="empty-description">Markiere Kontakte als Favoriten, um sie hier schnell zu finden.</p>
|
||||
<p class="empty-description">
|
||||
Markiere Kontakte als Favoriten, um sie hier schnell zu finden.
|
||||
</p>
|
||||
<a href="/" class="btn btn-primary">
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
Zu Kontakten
|
||||
</a>
|
||||
|
|
@ -134,7 +158,12 @@
|
|||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="empty-title">Keine Ergebnisse</h2>
|
||||
|
|
@ -179,7 +208,9 @@
|
|||
aria-label="Aus Favoriten entfernen"
|
||||
>
|
||||
<svg class="heart-icon" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
|
||||
<path
|
||||
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -399,7 +430,11 @@
|
|||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, hsl(var(--color-primary)) 0%, hsl(var(--color-primary) / 0.7) 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--color-primary)) 0%,
|
||||
hsl(var(--color-primary) / 0.7) 100%
|
||||
);
|
||||
color: hsl(var(--color-primary-foreground));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -13,9 +13,7 @@
|
|||
if (!searchQuery.trim()) return groups;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return groups.filter(
|
||||
(g) =>
|
||||
g.name.toLowerCase().includes(query) ||
|
||||
g.description?.toLowerCase().includes(query)
|
||||
(g) => g.name.toLowerCase().includes(query) || g.description?.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -77,7 +75,12 @@
|
|||
<!-- Search -->
|
||||
<div class="search-wrapper">
|
||||
<svg class="search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
|
|
@ -90,7 +93,12 @@
|
|||
{#if error}
|
||||
<div class="error-banner" role="alert">
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
|
|
@ -104,14 +112,24 @@
|
|||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="empty-title">Keine Gruppen</h2>
|
||||
<p class="empty-description">Erstelle deine erste Gruppe um Kontakte zu organisieren.</p>
|
||||
<a href="/groups/new" class="btn btn-primary">
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Neue Gruppe
|
||||
</a>
|
||||
|
|
@ -120,7 +138,12 @@
|
|||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="empty-title">Keine Ergebnisse</h2>
|
||||
|
|
@ -150,11 +173,21 @@
|
|||
aria-label="Gruppe löschen"
|
||||
>
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<svg class="chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,8 +22,18 @@
|
|||
let color = $state('#6366f1');
|
||||
|
||||
const presetColors = [
|
||||
'#ef4444', '#f97316', '#f59e0b', '#84cc16', '#22c55e', '#14b8a6',
|
||||
'#06b6d4', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7', '#ec4899',
|
||||
'#ef4444',
|
||||
'#f97316',
|
||||
'#f59e0b',
|
||||
'#84cc16',
|
||||
'#22c55e',
|
||||
'#14b8a6',
|
||||
'#06b6d4',
|
||||
'#3b82f6',
|
||||
'#6366f1',
|
||||
'#8b5cf6',
|
||||
'#a855f7',
|
||||
'#ec4899',
|
||||
];
|
||||
|
||||
const groupContacts = $derived(() => {
|
||||
|
|
@ -162,11 +172,16 @@
|
|||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</a>
|
||||
<h1 class="title">{isEditing ? 'Gruppe bearbeiten' : (group?.name || 'Gruppe')}</h1>
|
||||
<h1 class="title">{isEditing ? 'Gruppe bearbeiten' : group?.name || 'Gruppe'}</h1>
|
||||
{#if !loading && group && !isEditing}
|
||||
<button onclick={() => isEditing = true} class="edit-button" aria-label="Bearbeiten">
|
||||
<button onclick={() => (isEditing = true)} class="edit-button" aria-label="Bearbeiten">
|
||||
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{:else}
|
||||
|
|
@ -182,7 +197,12 @@
|
|||
<div class="error-state">
|
||||
<div class="error-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="error-title">Fehler</h2>
|
||||
|
|
@ -193,10 +213,15 @@
|
|||
{#if error}
|
||||
<div class="error-banner" role="alert">
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{error}</span>
|
||||
<button onclick={() => error = null} class="dismiss-btn">×</button>
|
||||
<button onclick={() => (error = null)} class="dismiss-btn">×</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
@ -205,18 +230,34 @@
|
|||
<div class="preview-section">
|
||||
<div class="preview-color" style="background-color: {color}">
|
||||
<svg class="preview-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="preview-name">{name || 'Gruppenname'}</p>
|
||||
</div>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSave(); }} class="form">
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
}}
|
||||
class="form"
|
||||
>
|
||||
<section class="form-section">
|
||||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Details</h2>
|
||||
|
|
@ -227,7 +268,8 @@
|
|||
</div>
|
||||
<div class="form-field">
|
||||
<label for="description" class="label">Beschreibung</label>
|
||||
<textarea id="description" bind:value={description} rows="3" class="input textarea"></textarea>
|
||||
<textarea id="description" bind:value={description} rows="3" class="input textarea"
|
||||
></textarea>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -235,7 +277,12 @@
|
|||
<div class="section-header">
|
||||
<div class="section-icon" style="background-color: {color}20; color: {color}">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Farbe</h2>
|
||||
|
|
@ -247,11 +294,16 @@
|
|||
class="color-option"
|
||||
class:selected={color === presetColor}
|
||||
style="background-color: {presetColor}"
|
||||
onclick={() => color = presetColor}
|
||||
onclick={() => (color = presetColor)}
|
||||
>
|
||||
{#if color === presetColor}
|
||||
<svg class="check-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="3"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
|
|
@ -264,8 +316,22 @@
|
|||
<button type="submit" disabled={saving} class="btn btn-primary">
|
||||
{#if saving}
|
||||
<svg class="spinner-sm" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" stroke-opacity="0.25" fill="none" />
|
||||
<path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" stroke-width="3" stroke-linecap="round" fill="none" />
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-opacity="0.25"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M12 2a10 10 0 0 1 10 10"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
Speichern...
|
||||
{:else}
|
||||
|
|
@ -278,7 +344,12 @@
|
|||
<!-- Delete Button -->
|
||||
<button onclick={handleDelete} class="delete-group-btn">
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
Gruppe löschen
|
||||
</button>
|
||||
|
|
@ -287,7 +358,12 @@
|
|||
<div class="preview-section">
|
||||
<div class="preview-color" style="background-color: {group.color || '#6366f1'}">
|
||||
<svg class="preview-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="preview-name">{group.name}</p>
|
||||
|
|
@ -301,13 +377,23 @@
|
|||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Kontakte ({groupContacts().length})</h2>
|
||||
<button onclick={() => showAddContacts = true} class="add-contact-btn">
|
||||
<button onclick={() => (showAddContacts = true)} class="add-contact-btn">
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Hinzufügen
|
||||
</button>
|
||||
|
|
@ -332,9 +418,18 @@
|
|||
<span class="contact-email">{contact.email}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<button onclick={() => handleRemoveContact(contact.id)} class="remove-btn" aria-label="Entfernen">
|
||||
<button
|
||||
onclick={() => handleRemoveContact(contact.id)}
|
||||
class="remove-btn"
|
||||
aria-label="Entfernen"
|
||||
>
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -348,15 +443,20 @@
|
|||
|
||||
<!-- Add Contacts Modal -->
|
||||
{#if showAddContacts}
|
||||
<div class="modal-backdrop" onclick={() => showAddContacts = false} role="presentation">
|
||||
<div class="modal-backdrop" onclick={() => (showAddContacts = false)} role="presentation">
|
||||
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Kontakte hinzufügen</h2>
|
||||
<button onclick={() => showAddContacts = false} class="modal-close">×</button>
|
||||
<button onclick={() => (showAddContacts = false)} class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-search">
|
||||
<svg class="search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
|
|
@ -368,7 +468,9 @@
|
|||
<div class="modal-content">
|
||||
{#if availableContacts().length === 0}
|
||||
<p class="no-results">
|
||||
{searchQuery ? 'Keine Kontakte gefunden' : 'Alle Kontakte sind bereits in dieser Gruppe'}
|
||||
{searchQuery
|
||||
? 'Keine Kontakte gefunden'
|
||||
: 'Alle Kontakte sind bereits in dieser Gruppe'}
|
||||
</p>
|
||||
{:else}
|
||||
{#each availableContacts() as contact (contact.id)}
|
||||
|
|
@ -387,7 +489,12 @@
|
|||
{/if}
|
||||
</div>
|
||||
<svg class="add-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/each}
|
||||
|
|
@ -417,7 +524,8 @@
|
|||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.back-button, .edit-button {
|
||||
.back-button,
|
||||
.edit-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -475,7 +583,9 @@
|
|||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.error-state {
|
||||
|
|
@ -724,7 +834,8 @@
|
|||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.contact-item, .add-contact-item {
|
||||
.contact-item,
|
||||
.add-contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
|
|
@ -753,7 +864,11 @@
|
|||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, hsl(var(--color-primary)) 0%, hsl(var(--color-primary) / 0.7) 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--color-primary)) 0%,
|
||||
hsl(var(--color-primary) / 0.7) 100%
|
||||
);
|
||||
color: hsl(var(--color-primary-foreground));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -816,7 +931,8 @@
|
|||
color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.no-contacts, .no-results {
|
||||
.no-contacts,
|
||||
.no-results {
|
||||
text-align: center;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
padding: 1rem;
|
||||
|
|
|
|||
|
|
@ -69,7 +69,12 @@
|
|||
<div class="preview-section">
|
||||
<div class="preview-color" style="background-color: {color}">
|
||||
<svg class="preview-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="preview-name">{name || 'Neue Gruppe'}</p>
|
||||
|
|
@ -81,19 +86,35 @@
|
|||
{#if error}
|
||||
<div class="error-banner" role="alert">
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="form">
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
class="form"
|
||||
>
|
||||
<!-- Name Section -->
|
||||
<section class="form-section">
|
||||
<div class="section-header">
|
||||
<div class="section-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Gruppenname</h2>
|
||||
|
|
@ -126,7 +147,12 @@
|
|||
<div class="section-header">
|
||||
<div class="section-icon" style="background-color: {color}20; color: {color}">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="section-title">Farbe</h2>
|
||||
|
|
@ -138,12 +164,17 @@
|
|||
class="color-option"
|
||||
class:selected={color === presetColor}
|
||||
style="background-color: {presetColor}"
|
||||
onclick={() => color = presetColor}
|
||||
onclick={() => (color = presetColor)}
|
||||
aria-label="Farbe {presetColor}"
|
||||
>
|
||||
{#if color === presetColor}
|
||||
<svg class="check-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="3"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
|
|
@ -152,12 +183,7 @@
|
|||
<div class="custom-color">
|
||||
<label for="customColor" class="label">Oder eigene Farbe wählen:</label>
|
||||
<div class="color-input-wrapper">
|
||||
<input
|
||||
id="customColor"
|
||||
type="color"
|
||||
bind:value={color}
|
||||
class="color-input"
|
||||
/>
|
||||
<input id="customColor" type="color" bind:value={color} class="color-input" />
|
||||
<input
|
||||
type="text"
|
||||
bind:value={color}
|
||||
|
|
@ -171,19 +197,34 @@
|
|||
|
||||
<!-- Action Buttons -->
|
||||
<div class="actions">
|
||||
<a href="/groups" class="btn btn-secondary">
|
||||
Abbrechen
|
||||
</a>
|
||||
<a href="/groups" class="btn btn-secondary"> Abbrechen </a>
|
||||
<button type="submit" disabled={loading} class="btn btn-primary">
|
||||
{#if loading}
|
||||
<svg class="spinner" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" stroke-opacity="0.25" />
|
||||
<path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" stroke-width="3" stroke-linecap="round" />
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-opacity="0.25"
|
||||
/>
|
||||
<path
|
||||
d="M12 2a10 10 0 0 1 10 10"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<span>Erstellen...</span>
|
||||
{:else}
|
||||
<svg class="icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<span>Gruppe erstellen</span>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,10 @@ export function useImageGeneration() {
|
|||
setSteps(selectedModel.defaultSteps ?? 4);
|
||||
setGuidanceScale(selectedModel.defaultGuidanceScale ?? 3.5);
|
||||
|
||||
const maxDimension = Math.min(selectedModel.maxWidth ?? 1024, selectedModel.maxHeight ?? 1024);
|
||||
const maxDimension = Math.min(
|
||||
selectedModel.maxWidth ?? 1024,
|
||||
selectedModel.maxHeight ?? 1024
|
||||
);
|
||||
const minDimension = Math.max(selectedModel.minWidth ?? 256, selectedModel.minHeight ?? 256);
|
||||
|
||||
let newWidth = selectedAspectRatio.width;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import Container from '@manacore/shared-landing-ui/atoms/Container.astro';
|
|||
<span>Presi</span>
|
||||
</a>
|
||||
<p class="text-text-secondary text-sm max-w-md">
|
||||
Erstelle beeindruckende Präsentationen in Minuten. Mit KI-Unterstützung, schönen Themes und einfacher Bedienung.
|
||||
Erstelle beeindruckende Präsentationen in Minuten. Mit KI-Unterstützung, schönen Themes
|
||||
und einfacher Bedienung.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -21,17 +22,26 @@ import Container from '@manacore/shared-landing-ui/atoms/Container.astro';
|
|||
<h4 class="text-text-muted text-xs uppercase tracking-wider mb-4">Produkt</h4>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<a href="#features" class="text-text-secondary hover:text-text-primary text-sm transition-colors">
|
||||
<a
|
||||
href="#features"
|
||||
class="text-text-secondary hover:text-text-primary text-sm transition-colors"
|
||||
>
|
||||
Features
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://presi.manacore.app" class="text-text-secondary hover:text-text-primary text-sm transition-colors">
|
||||
<a
|
||||
href="https://presi.manacore.app"
|
||||
class="text-text-secondary hover:text-text-primary text-sm transition-colors"
|
||||
>
|
||||
Web App
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#download" class="text-text-secondary hover:text-text-primary text-sm transition-colors">
|
||||
<a
|
||||
href="#download"
|
||||
class="text-text-secondary hover:text-text-primary text-sm transition-colors"
|
||||
>
|
||||
Mobile App
|
||||
</a>
|
||||
</li>
|
||||
|
|
@ -43,17 +53,26 @@ import Container from '@manacore/shared-landing-ui/atoms/Container.astro';
|
|||
<h4 class="text-text-muted text-xs uppercase tracking-wider mb-4">Rechtliches</h4>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<a href="/privacy" class="text-text-secondary hover:text-text-primary text-sm transition-colors">
|
||||
<a
|
||||
href="/privacy"
|
||||
class="text-text-secondary hover:text-text-primary text-sm transition-colors"
|
||||
>
|
||||
Datenschutz
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/terms" class="text-text-secondary hover:text-text-primary text-sm transition-colors">
|
||||
<a
|
||||
href="/terms"
|
||||
class="text-text-secondary hover:text-text-primary text-sm transition-colors"
|
||||
>
|
||||
AGB
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/imprint" class="text-text-secondary hover:text-text-primary text-sm transition-colors">
|
||||
<a
|
||||
href="/imprint"
|
||||
class="text-text-secondary hover:text-text-primary text-sm transition-colors"
|
||||
>
|
||||
Impressum
|
||||
</a>
|
||||
</li>
|
||||
|
|
@ -63,11 +82,11 @@ import Container from '@manacore/shared-landing-ui/atoms/Container.astro';
|
|||
|
||||
<!-- Copyright -->
|
||||
<div class="pt-8 border-t border-border text-center">
|
||||
<p class="text-text-muted text-sm">
|
||||
© 2025 Presi. Alle Rechte vorbehalten.
|
||||
</p>
|
||||
<p class="text-text-muted text-sm">© 2025 Presi. Alle Rechte vorbehalten.</p>
|
||||
<p class="text-text-muted text-xs mt-1">
|
||||
Ein Produkt von <a href="https://manacore.ai" class="hover:text-primary transition-colors">ManaCore</a>
|
||||
Ein Produkt von <a href="https://manacore.ai" class="hover:text-primary transition-colors"
|
||||
>ManaCore</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</Container>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import Container from '@manacore/shared-landing-ui/atoms/Container.astro';
|
|||
import Button from '@manacore/shared-landing-ui/atoms/Button.astro';
|
||||
---
|
||||
|
||||
<nav class="fixed top-0 left-0 right-0 z-50 bg-background-page/80 backdrop-blur-lg border-b border-border">
|
||||
<nav
|
||||
class="fixed top-0 left-0 right-0 z-50 bg-background-page/80 backdrop-blur-lg border-b border-border"
|
||||
>
|
||||
<Container>
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- Logo -->
|
||||
|
|
@ -17,7 +19,10 @@ import Button from '@manacore/shared-landing-ui/atoms/Button.astro';
|
|||
<a href="#features" class="text-text-secondary hover:text-text-primary transition-colors">
|
||||
Features
|
||||
</a>
|
||||
<a href="#how-it-works" class="text-text-secondary hover:text-text-primary transition-colors">
|
||||
<a
|
||||
href="#how-it-works"
|
||||
class="text-text-secondary hover:text-text-primary transition-colors"
|
||||
>
|
||||
So funktioniert's
|
||||
</a>
|
||||
<a href="#faq" class="text-text-secondary hover:text-text-primary transition-colors">
|
||||
|
|
@ -27,9 +32,7 @@ import Button from '@manacore/shared-landing-ui/atoms/Button.astro';
|
|||
|
||||
<!-- CTA -->
|
||||
<div class="flex items-center gap-4">
|
||||
<Button href="https://presi.manacore.app" variant="primary" size="sm">
|
||||
App öffnen
|
||||
</Button>
|
||||
<Button href="https://presi.manacore.app" variant="primary" size="sm"> App öffnen </Button>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
|
|
|||
|
|
@ -72,23 +72,28 @@ const steps = [
|
|||
const faqs = [
|
||||
{
|
||||
question: 'Ist Presi kostenlos?',
|
||||
answer: 'Ja, Presi ist kostenlos nutzbar. Du kannst unbegrenzt Präsentationen erstellen, teilen und präsentieren.',
|
||||
answer:
|
||||
'Ja, Presi ist kostenlos nutzbar. Du kannst unbegrenzt Präsentationen erstellen, teilen und präsentieren.',
|
||||
},
|
||||
{
|
||||
question: 'Kann ich Präsentationen offline bearbeiten?',
|
||||
answer: 'Mit der mobilen App kannst du deine Präsentationen auch offline bearbeiten. Änderungen werden synchronisiert, sobald du wieder online bist.',
|
||||
answer:
|
||||
'Mit der mobilen App kannst du deine Präsentationen auch offline bearbeiten. Änderungen werden synchronisiert, sobald du wieder online bist.',
|
||||
},
|
||||
{
|
||||
question: 'Wie teile ich eine Präsentation?',
|
||||
answer: 'Klicke auf "Teilen" und erstelle einen Link. Jeder mit dem Link kann die Präsentation ansehen - ohne Account oder Download.',
|
||||
answer:
|
||||
'Klicke auf "Teilen" und erstelle einen Link. Jeder mit dem Link kann die Präsentation ansehen - ohne Account oder Download.',
|
||||
},
|
||||
{
|
||||
question: 'Welche Slide-Typen gibt es?',
|
||||
answer: 'Presi unterstützt Titel-Slides, Content-Slides mit Text und Bullet Points, Bild-Slides und Split-Views mit Text und Bild nebeneinander.',
|
||||
answer:
|
||||
'Presi unterstützt Titel-Slides, Content-Slides mit Text und Bullet Points, Bild-Slides und Split-Views mit Text und Bild nebeneinander.',
|
||||
},
|
||||
{
|
||||
question: 'Kann ich eigene Themes erstellen?',
|
||||
answer: 'Aktuell bieten wir vorgefertigte Themes. Custom Themes sind für zukünftige Versionen geplant.',
|
||||
answer:
|
||||
'Aktuell bieten wir vorgefertigte Themes. Custom Themes sind für zukünftige Versionen geplant.',
|
||||
},
|
||||
];
|
||||
---
|
||||
|
|
@ -122,31 +127,31 @@ const faqs = [
|
|||
<section id="how-it-works" class="py-20 bg-background-card">
|
||||
<Container>
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-text-primary mb-4">
|
||||
So einfach geht's
|
||||
</h2>
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-text-primary mb-4">So einfach geht's</h2>
|
||||
<p class="text-text-secondary text-lg max-w-2xl mx-auto">
|
||||
In vier Schritten zur perfekten Präsentation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-4 gap-6">
|
||||
{steps.map((step, index) => (
|
||||
<div class="relative">
|
||||
<div class="bg-background-page rounded-2xl p-6 border border-border hover:border-primary/30 transition-all duration-300 h-full">
|
||||
<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center mb-4">
|
||||
<span class="text-primary font-bold text-xl">{step.number}</span>
|
||||
{
|
||||
steps.map((step, index) => (
|
||||
<div class="relative">
|
||||
<div class="bg-background-page rounded-2xl p-6 border border-border hover:border-primary/30 transition-all duration-300 h-full">
|
||||
<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center mb-4">
|
||||
<span class="text-primary font-bold text-xl">{step.number}</span>
|
||||
</div>
|
||||
<h3 class="text-text-primary font-semibold text-lg mb-2">{step.title}</h3>
|
||||
<p class="text-text-secondary text-sm">{step.description}</p>
|
||||
</div>
|
||||
<h3 class="text-text-primary font-semibold text-lg mb-2">{step.title}</h3>
|
||||
<p class="text-text-secondary text-sm">{step.description}</p>
|
||||
{index < steps.length - 1 && (
|
||||
<div class="hidden md:block absolute top-1/2 -right-3 transform -translate-y-1/2 text-text-muted">
|
||||
→
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div class="hidden md:block absolute top-1/2 -right-3 transform -translate-y-1/2 text-text-muted">
|
||||
→
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
|
@ -170,8 +175,8 @@ const faqs = [
|
|||
</h2>
|
||||
<p class="text-text-secondary text-lg mb-8 leading-relaxed">
|
||||
Der Präsentationsmodus bietet alles was du brauchst: Vollbild-Ansicht,
|
||||
Tastaturnavigation mit Pfeiltasten, Timer für perfektes Timing und
|
||||
Speaker Notes für deine Notizen.
|
||||
Tastaturnavigation mit Pfeiltasten, Timer für perfektes Timing und Speaker Notes für
|
||||
deine Notizen.
|
||||
</p>
|
||||
<div class="bg-background-page rounded-2xl p-8 border border-border">
|
||||
<div class="flex flex-wrap justify-center gap-4 text-sm text-text-secondary">
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ export default defineConfig({
|
|||
schema: './src/db/schema/index.ts',
|
||||
out: './src/db/migrations',
|
||||
dbCredentials: {
|
||||
url: process.env.STORAGE_DATABASE_URL || process.env.DATABASE_URL || 'postgresql://manacore:devpassword@localhost:5432/storage',
|
||||
url:
|
||||
process.env.STORAGE_DATABASE_URL ||
|
||||
process.env.DATABASE_URL ||
|
||||
'postgresql://manacore:devpassword@localhost:5432/storage',
|
||||
},
|
||||
verbose: true,
|
||||
strict: true,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,13 @@
|
|||
import { pgTable, uuid, varchar, text, timestamp, bigint, boolean, integer } from 'drizzle-orm/pg-core';
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
varchar,
|
||||
text,
|
||||
timestamp,
|
||||
bigint,
|
||||
boolean,
|
||||
integer,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { relations } from 'drizzle-orm';
|
||||
import { folders } from './folders.schema';
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,10 @@ export class FileController {
|
|||
constructor(private readonly fileService: FileService) {}
|
||||
|
||||
@Get()
|
||||
async findAll(@CurrentUser() user: CurrentUserData, @Query('parentFolderId') parentFolderId?: string) {
|
||||
async findAll(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Query('parentFolderId') parentFolderId?: string
|
||||
) {
|
||||
return this.fileService.findAll(user.userId, parentFolderId);
|
||||
}
|
||||
|
||||
|
|
@ -101,12 +104,20 @@ export class FileController {
|
|||
}
|
||||
|
||||
@Patch(':id')
|
||||
async update(@CurrentUser() user: CurrentUserData, @Param('id') id: string, @Body() dto: UpdateFileDto) {
|
||||
async update(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateFileDto
|
||||
) {
|
||||
return this.fileService.update(user.userId, id, dto);
|
||||
}
|
||||
|
||||
@Patch(':id/move')
|
||||
async move(@CurrentUser() user: CurrentUserData, @Param('id') id: string, @Body() dto: MoveFileDto) {
|
||||
async move(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: MoveFileDto
|
||||
) {
|
||||
return this.fileService.move(user.userId, id, dto);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,11 @@ export class FileService {
|
|||
.select()
|
||||
.from(files)
|
||||
.where(
|
||||
and(eq(files.userId, userId), eq(files.parentFolderId, parentFolderId), eq(files.isDeleted, false))
|
||||
and(
|
||||
eq(files.userId, userId),
|
||||
eq(files.parentFolderId, parentFolderId),
|
||||
eq(files.isDeleted, false)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -27,7 +31,9 @@ export class FileService {
|
|||
return this.db
|
||||
.select()
|
||||
.from(files)
|
||||
.where(and(eq(files.userId, userId), isNull(files.parentFolderId), eq(files.isDeleted, false)));
|
||||
.where(
|
||||
and(eq(files.userId, userId), isNull(files.parentFolderId), eq(files.isDeleted, false))
|
||||
);
|
||||
}
|
||||
|
||||
async findOne(userId: string, id: string): Promise<File> {
|
||||
|
|
@ -43,11 +49,7 @@ export class FileService {
|
|||
return result[0];
|
||||
}
|
||||
|
||||
async upload(
|
||||
userId: string,
|
||||
file: Express.Multer.File,
|
||||
dto: CreateFileDto
|
||||
): Promise<File> {
|
||||
async upload(userId: string, file: Express.Multer.File, dto: CreateFileDto): Promise<File> {
|
||||
if (!file) {
|
||||
throw new BadRequestException('No file provided');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
import { Controller, Get, Post, Patch, Delete, Body, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { JwtAuthGuard, CurrentUser, type CurrentUserData } from '@manacore/shared-nestjs-auth';
|
||||
import { FolderService } from './folder.service';
|
||||
import { CreateFolderDto } from './dto/create-folder.dto';
|
||||
|
|
@ -10,7 +20,10 @@ export class FolderController {
|
|||
constructor(private readonly folderService: FolderService) {}
|
||||
|
||||
@Get()
|
||||
async findAll(@CurrentUser() user: CurrentUserData, @Query('parentFolderId') parentFolderId?: string) {
|
||||
async findAll(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Query('parentFolderId') parentFolderId?: string
|
||||
) {
|
||||
return this.folderService.findAll(user.userId, parentFolderId);
|
||||
}
|
||||
|
||||
|
|
@ -25,12 +38,20 @@ export class FolderController {
|
|||
}
|
||||
|
||||
@Patch(':id')
|
||||
async update(@CurrentUser() user: CurrentUserData, @Param('id') id: string, @Body() dto: UpdateFolderDto) {
|
||||
async update(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateFolderDto
|
||||
) {
|
||||
return this.folderService.update(user.userId, id, dto);
|
||||
}
|
||||
|
||||
@Patch(':id/move')
|
||||
async move(@CurrentUser() user: CurrentUserData, @Param('id') id: string, @Body() dto: MoveFolderDto) {
|
||||
async move(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: MoveFolderDto
|
||||
) {
|
||||
return this.folderService.move(user.userId, id, dto);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,13 @@ export class FolderService {
|
|||
return this.db
|
||||
.select()
|
||||
.from(folders)
|
||||
.where(and(eq(folders.userId, userId), isNull(folders.parentFolderId), eq(folders.isDeleted, false)));
|
||||
.where(
|
||||
and(
|
||||
eq(folders.userId, userId),
|
||||
isNull(folders.parentFolderId),
|
||||
eq(folders.isDeleted, false)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async findOne(userId: string, id: string): Promise<Folder> {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,9 @@ export class SearchService {
|
|||
const favoriteFolders = await this.db
|
||||
.select()
|
||||
.from(folders)
|
||||
.where(and(eq(folders.userId, userId), eq(folders.isDeleted, false), eq(folders.isFavorite, true)));
|
||||
.where(
|
||||
and(eq(folders.userId, userId), eq(folders.isDeleted, false), eq(folders.isFavorite, true))
|
||||
);
|
||||
|
||||
return { files: favoriteFiles, folders: favoriteFolders };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ export class StorageService {
|
|||
subfolder?: string
|
||||
) {
|
||||
if (!validateFileSize(buffer.length, this.maxFileSize / (1024 * 1024))) {
|
||||
throw new Error(`File size exceeds maximum allowed size of ${this.maxFileSize / (1024 * 1024)}MB`);
|
||||
throw new Error(
|
||||
`File size exceeds maximum allowed size of ${this.maxFileSize / (1024 * 1024)}MB`
|
||||
);
|
||||
}
|
||||
|
||||
const storageKey = generateUserFileKey(userId, originalName, subfolder);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ export class TagController {
|
|||
}
|
||||
|
||||
@Post()
|
||||
async create(@CurrentUser() user: CurrentUserData, @Body() dto: { name: string; color?: string }) {
|
||||
async create(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Body() dto: { name: string; color?: string }
|
||||
) {
|
||||
return this.tagService.create(user.userId, dto.name, dto.color);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,9 @@ export class TagService {
|
|||
}
|
||||
|
||||
async removeTagFromFile(fileId: string, tagId: string): Promise<void> {
|
||||
await this.db.delete(fileTags).where(and(eq(fileTags.fileId, fileId), eq(fileTags.tagId, tagId)));
|
||||
await this.db
|
||||
.delete(fileTags)
|
||||
.where(and(eq(fileTags.fileId, fileId), eq(fileTags.tagId, tagId)));
|
||||
}
|
||||
|
||||
async getFileTags(fileId: string): Promise<Tag[]> {
|
||||
|
|
|
|||
|
|
@ -99,6 +99,8 @@ export class TrashService {
|
|||
|
||||
// Delete from database
|
||||
await this.db.delete(files).where(and(eq(files.userId, userId), eq(files.isDeleted, true)));
|
||||
await this.db.delete(folders).where(and(eq(folders.userId, userId), eq(folders.isDeleted, true)));
|
||||
await this.db
|
||||
.delete(folders)
|
||||
.where(and(eq(folders.userId, userId), eq(folders.isDeleted, true)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,7 @@ async function getHeaders(): Promise<HeadersInit> {
|
|||
return headers;
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<ApiResponse<T>> {
|
||||
async function request<T>(endpoint: string, options: RequestInit = {}): Promise<ApiResponse<T>> {
|
||||
try {
|
||||
const headers = await getHeaders();
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
|
|
@ -170,11 +167,9 @@ export const filesApi = {
|
|||
body: JSON.stringify({ parentFolderId }),
|
||||
}),
|
||||
|
||||
delete: (id: string) =>
|
||||
request<{ success: boolean }>(`/files/${id}`, { method: 'DELETE' }),
|
||||
delete: (id: string) => request<{ success: boolean }>(`/files/${id}`, { method: 'DELETE' }),
|
||||
|
||||
toggleFavorite: (id: string) =>
|
||||
request<StorageFile>(`/files/${id}/favorite`, { method: 'POST' }),
|
||||
toggleFavorite: (id: string) => request<StorageFile>(`/files/${id}/favorite`, { method: 'POST' }),
|
||||
};
|
||||
|
||||
// Folders API
|
||||
|
|
@ -205,8 +200,7 @@ export const foldersApi = {
|
|||
body: JSON.stringify({ parentFolderId }),
|
||||
}),
|
||||
|
||||
delete: (id: string) =>
|
||||
request<{ success: boolean }>(`/folders/${id}`, { method: 'DELETE' }),
|
||||
delete: (id: string) => request<{ success: boolean }>(`/folders/${id}`, { method: 'DELETE' }),
|
||||
|
||||
toggleFavorite: (id: string) =>
|
||||
request<StorageFolder>(`/folders/${id}/favorite`, { method: 'POST' }),
|
||||
|
|
@ -232,8 +226,7 @@ export const sharesApi = {
|
|||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
delete: (id: string) =>
|
||||
request<{ success: boolean }>(`/shares/${id}`, { method: 'DELETE' }),
|
||||
delete: (id: string) => request<{ success: boolean }>(`/shares/${id}`, { method: 'DELETE' }),
|
||||
};
|
||||
|
||||
// Tags API
|
||||
|
|
@ -252,8 +245,7 @@ export const tagsApi = {
|
|||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
delete: (id: string) =>
|
||||
request<{ success: boolean }>(`/tags/${id}`, { method: 'DELETE' }),
|
||||
delete: (id: string) => request<{ success: boolean }>(`/tags/${id}`, { method: 'DELETE' }),
|
||||
};
|
||||
|
||||
// Trash API
|
||||
|
|
@ -274,8 +266,9 @@ export const trashApi = {
|
|||
// Search API
|
||||
export const searchApi = {
|
||||
search: (query: string) =>
|
||||
request<{ files: StorageFile[]; folders: StorageFolder[] }>(`/search?q=${encodeURIComponent(query)}`),
|
||||
request<{ files: StorageFile[]; folders: StorageFolder[] }>(
|
||||
`/search?q=${encodeURIComponent(query)}`
|
||||
),
|
||||
|
||||
favorites: () =>
|
||||
request<{ files: StorageFile[]; folders: StorageFolder[] }>('/favorites'),
|
||||
favorites: () => request<{ files: StorageFile[]; folders: StorageFolder[] }>('/favorites'),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -63,7 +63,12 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
>
|
||||
<div class="form-group">
|
||||
<label for="folder-name">Ordnername</label>
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -39,7 +39,12 @@
|
|||
Funktionen du dir wünschst.
|
||||
</p>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
>
|
||||
<div class="form-group">
|
||||
<label>Art des Feedbacks</label>
|
||||
<div class="type-selector">
|
||||
|
|
|
|||
|
|
@ -25,7 +25,10 @@
|
|||
<span class="setting-description">Wähle zwischen Hell, Dunkel oder System</span>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<select value={theme.mode} onchange={(e) => theme.setMode((e.target as HTMLSelectElement).value as any)}>
|
||||
<select
|
||||
value={theme.mode}
|
||||
onchange={(e) => theme.setMode((e.target as HTMLSelectElement).value as any)}
|
||||
>
|
||||
<option value="light">Hell</option>
|
||||
<option value="dark">Dunkel</option>
|
||||
<option value="system">System</option>
|
||||
|
|
@ -39,7 +42,10 @@
|
|||
<span class="setting-description">Wähle eine Farbpalette</span>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<select value={theme.variant} onchange={(e) => theme.setVariant((e.target as HTMLSelectElement).value as any)}>
|
||||
<select
|
||||
value={theme.variant}
|
||||
onchange={(e) => theme.setVariant((e.target as HTMLSelectElement).value as any)}
|
||||
>
|
||||
{#each theme.variants as variant}
|
||||
<option value={variant}>{THEME_DEFINITIONS[variant].label}</option>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@
|
|||
class:active={theme.variant === variant}
|
||||
onclick={() => theme.setVariant(variant)}
|
||||
>
|
||||
<div class="theme-preview" style="background: linear-gradient(135deg, {def.colors.primary}, {def.colors.accent})">
|
||||
<div
|
||||
class="theme-preview"
|
||||
style="background: linear-gradient(135deg, {def.colors.primary}, {def.colors.accent})"
|
||||
>
|
||||
{#if theme.variant === variant}
|
||||
<div class="check-badge">
|
||||
<Check size={16} />
|
||||
|
|
|
|||
|
|
@ -132,10 +132,7 @@
|
|||
<RotateCcw size={16} />
|
||||
Wiederherstellen
|
||||
</button>
|
||||
<button
|
||||
class="delete-btn"
|
||||
onclick={() => handlePermanentDelete(folder.id, 'folder')}
|
||||
>
|
||||
<button class="delete-btn" onclick={() => handlePermanentDelete(folder.id, 'folder')}>
|
||||
Endgültig löschen
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import Container from '@manacore/shared-landing-ui/atoms/Container.astro';
|
|||
<span>Zitare</span>
|
||||
</a>
|
||||
<p class="text-text-secondary text-sm max-w-md">
|
||||
Deine tägliche Quelle für Inspiration und Weisheit. Entdecke über 1000 Zitate von den größten Denkern der Geschichte.
|
||||
Deine tägliche Quelle für Inspiration und Weisheit. Entdecke über 1000 Zitate von den
|
||||
größten Denkern der Geschichte.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -21,17 +22,26 @@ import Container from '@manacore/shared-landing-ui/atoms/Container.astro';
|
|||
<h4 class="text-text-muted text-xs uppercase tracking-wider mb-4">Produkt</h4>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<a href="#features" class="text-text-secondary hover:text-text-primary text-sm transition-colors">
|
||||
<a
|
||||
href="#features"
|
||||
class="text-text-secondary hover:text-text-primary text-sm transition-colors"
|
||||
>
|
||||
Features
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://zitare.manacore.app" class="text-text-secondary hover:text-text-primary text-sm transition-colors">
|
||||
<a
|
||||
href="https://zitare.manacore.app"
|
||||
class="text-text-secondary hover:text-text-primary text-sm transition-colors"
|
||||
>
|
||||
Web App
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#download" class="text-text-secondary hover:text-text-primary text-sm transition-colors">
|
||||
<a
|
||||
href="#download"
|
||||
class="text-text-secondary hover:text-text-primary text-sm transition-colors"
|
||||
>
|
||||
Mobile App
|
||||
</a>
|
||||
</li>
|
||||
|
|
@ -43,17 +53,26 @@ import Container from '@manacore/shared-landing-ui/atoms/Container.astro';
|
|||
<h4 class="text-text-muted text-xs uppercase tracking-wider mb-4">Rechtliches</h4>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<a href="/privacy" class="text-text-secondary hover:text-text-primary text-sm transition-colors">
|
||||
<a
|
||||
href="/privacy"
|
||||
class="text-text-secondary hover:text-text-primary text-sm transition-colors"
|
||||
>
|
||||
Datenschutz
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/terms" class="text-text-secondary hover:text-text-primary text-sm transition-colors">
|
||||
<a
|
||||
href="/terms"
|
||||
class="text-text-secondary hover:text-text-primary text-sm transition-colors"
|
||||
>
|
||||
AGB
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/imprint" class="text-text-secondary hover:text-text-primary text-sm transition-colors">
|
||||
<a
|
||||
href="/imprint"
|
||||
class="text-text-secondary hover:text-text-primary text-sm transition-colors"
|
||||
>
|
||||
Impressum
|
||||
</a>
|
||||
</li>
|
||||
|
|
@ -63,11 +82,11 @@ import Container from '@manacore/shared-landing-ui/atoms/Container.astro';
|
|||
|
||||
<!-- Copyright -->
|
||||
<div class="pt-8 border-t border-border text-center">
|
||||
<p class="text-text-muted text-sm">
|
||||
© 2025 Zitare. Alle Rechte vorbehalten.
|
||||
</p>
|
||||
<p class="text-text-muted text-sm">© 2025 Zitare. Alle Rechte vorbehalten.</p>
|
||||
<p class="text-text-muted text-xs mt-1">
|
||||
Ein Produkt von <a href="https://manacore.ai" class="hover:text-primary transition-colors">ManaCore</a>
|
||||
Ein Produkt von <a href="https://manacore.ai" class="hover:text-primary transition-colors"
|
||||
>ManaCore</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</Container>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import Container from '@manacore/shared-landing-ui/atoms/Container.astro';
|
|||
import Button from '@manacore/shared-landing-ui/atoms/Button.astro';
|
||||
---
|
||||
|
||||
<nav class="fixed top-0 left-0 right-0 z-50 bg-background-page/80 backdrop-blur-lg border-b border-border">
|
||||
<nav
|
||||
class="fixed top-0 left-0 right-0 z-50 bg-background-page/80 backdrop-blur-lg border-b border-border"
|
||||
>
|
||||
<Container>
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- Logo -->
|
||||
|
|
@ -27,9 +29,7 @@ import Button from '@manacore/shared-landing-ui/atoms/Button.astro';
|
|||
|
||||
<!-- CTA -->
|
||||
<div class="flex items-center gap-4">
|
||||
<Button href="https://zitare.manacore.app" variant="primary" size="sm">
|
||||
App öffnen
|
||||
</Button>
|
||||
<Button href="https://zitare.manacore.app" variant="primary" size="sm"> App öffnen </Button>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
|
|
|||
|
|
@ -6,10 +6,7 @@ interface Props {
|
|||
description?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description = 'Zitare - Inspirierende Zitate von großen Denkern',
|
||||
} = Astro.props;
|
||||
const { title, description = 'Zitare - Inspirierende Zitate von großen Denkern' } = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
|
|
|
|||
|
|
@ -64,23 +64,28 @@ const sampleQuotes = [
|
|||
const faqs = [
|
||||
{
|
||||
question: 'Ist Zitare kostenlos?',
|
||||
answer: 'Ja, Zitare ist kostenlos nutzbar. Du hast Zugriff auf alle Zitate und Features ohne Abo oder versteckte Kosten.',
|
||||
answer:
|
||||
'Ja, Zitare ist kostenlos nutzbar. Du hast Zugriff auf alle Zitate und Features ohne Abo oder versteckte Kosten.',
|
||||
},
|
||||
{
|
||||
question: 'Welche Autoren sind in der Sammlung?',
|
||||
answer: 'Unsere Sammlung umfasst über 1000 Zitate von Philosophen wie Sokrates und Nietzsche, Wissenschaftlern wie Einstein und Curie, sowie modernen Denkern und Führungspersönlichkeiten.',
|
||||
answer:
|
||||
'Unsere Sammlung umfasst über 1000 Zitate von Philosophen wie Sokrates und Nietzsche, Wissenschaftlern wie Einstein und Curie, sowie modernen Denkern und Führungspersönlichkeiten.',
|
||||
},
|
||||
{
|
||||
question: 'Kann ich Zitate offline lesen?',
|
||||
answer: 'Ja, mit der mobilen App werden deine Lieblingszitate lokal gespeichert. So hast du auch ohne Internetverbindung Zugriff auf Inspiration.',
|
||||
answer:
|
||||
'Ja, mit der mobilen App werden deine Lieblingszitate lokal gespeichert. So hast du auch ohne Internetverbindung Zugriff auf Inspiration.',
|
||||
},
|
||||
{
|
||||
question: 'Wie kann ich Zitate teilen?',
|
||||
answer: 'Jedes Zitat kann direkt aus der App geteilt werden - per WhatsApp, Instagram, E-Mail oder als Bild für Social Media.',
|
||||
answer:
|
||||
'Jedes Zitat kann direkt aus der App geteilt werden - per WhatsApp, Instagram, E-Mail oder als Bild für Social Media.',
|
||||
},
|
||||
{
|
||||
question: 'Werden neue Zitate hinzugefügt?',
|
||||
answer: 'Ja, wir erweitern unsere Sammlung regelmäßig mit neuen, sorgfältig ausgewählten Zitaten aus verschiedenen Epochen und Kulturen.',
|
||||
answer:
|
||||
'Ja, wir erweitern unsere Sammlung regelmäßig mit neuen, sorgfältig ausgewählten Zitaten aus verschiedenen Epochen und Kulturen.',
|
||||
},
|
||||
];
|
||||
---
|
||||
|
|
@ -123,15 +128,19 @@ const faqs = [
|
|||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-6">
|
||||
{sampleQuotes.map((quote) => (
|
||||
<div class="bg-background-page rounded-2xl p-8 border border-border hover:border-primary/30 transition-all duration-300 group">
|
||||
<div class="text-4xl text-primary/30 mb-4 group-hover:text-primary/50 transition-colors">"</div>
|
||||
<p class="quote-text text-text-primary text-lg mb-6 leading-relaxed">
|
||||
{quote.text}
|
||||
</p>
|
||||
<p class="text-text-muted text-sm">— {quote.author}</p>
|
||||
</div>
|
||||
))}
|
||||
{
|
||||
sampleQuotes.map((quote) => (
|
||||
<div class="bg-background-page rounded-2xl p-8 border border-border hover:border-primary/30 transition-all duration-300 group">
|
||||
<div class="text-4xl text-primary/30 mb-4 group-hover:text-primary/50 transition-colors">
|
||||
"
|
||||
</div>
|
||||
<p class="quote-text text-text-primary text-lg mb-6 leading-relaxed">
|
||||
{quote.text}
|
||||
</p>
|
||||
<p class="text-text-muted text-sm">— {quote.author}</p>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
|
@ -150,13 +159,11 @@ const faqs = [
|
|||
<section id="about" class="py-20 bg-background-card">
|
||||
<Container size="md">
|
||||
<div class="text-center">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-text-primary mb-6">
|
||||
Über Zitare
|
||||
</h2>
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-text-primary mb-6">Über Zitare</h2>
|
||||
<p class="text-text-secondary text-lg mb-6 leading-relaxed">
|
||||
Zitare ist deine tägliche Quelle für Inspiration und Weisheit. Wir haben über 1000 Zitate
|
||||
von den einflussreichsten Denkern, Philosophen, Wissenschaftlern und Führungspersönlichkeiten
|
||||
der Geschichte sorgfältig zusammengestellt.
|
||||
Zitare ist deine tägliche Quelle für Inspiration und Weisheit. Wir haben über 1000
|
||||
Zitate von den einflussreichsten Denkern, Philosophen, Wissenschaftlern und
|
||||
Führungspersönlichkeiten der Geschichte sorgfältig zusammengestellt.
|
||||
</p>
|
||||
<p class="text-text-secondary text-lg leading-relaxed">
|
||||
Ob du Motivation suchst, nach Weisheit strebst oder einfach einen Moment der Reflexion
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue