mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:21:10 +02:00
🐛 fix(auth): skip body parser for Stripe webhooks
The JSON body parser was consuming the request body before NestJS could access the rawBody needed for Stripe webhook signature verification. Now webhooks to /api/v1/webhooks/stripe skip the body parser middleware. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
bfc2737ce5
commit
d86e9031bb
15 changed files with 238 additions and 191 deletions
|
|
@ -65,18 +65,17 @@
|
|||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Glassmorphic utilities */
|
||||
/* Elevation utilities - semantic surface styles */
|
||||
.glass {
|
||||
@apply bg-white/80 dark:bg-white/10 backdrop-blur-xl border border-black/10 dark:border-white/20;
|
||||
@apply bg-surface-elevated border border-border;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
@apply bg-white/60 dark:bg-white/5 backdrop-blur-sm border border-black/10 dark:border-white/10;
|
||||
@apply bg-surface border border-border;
|
||||
}
|
||||
|
||||
.glass-button {
|
||||
@apply bg-white/90 dark:bg-white/20 backdrop-blur-sm border border-black/10 dark:border-white/20
|
||||
hover:bg-white dark:hover:bg-white/30 hover:shadow-lg transition-all duration-200;
|
||||
@apply bg-surface border border-border hover:bg-surface-hover hover:shadow-lg transition-all duration-200;
|
||||
}
|
||||
|
||||
/* iOS Safe Area Insets for PWA */
|
||||
|
|
|
|||
|
|
@ -64,10 +64,10 @@
|
|||
let IconComponent = $derived(iconMap[bot.icon] || Robot);
|
||||
</script>
|
||||
|
||||
<div class="glass-card rounded-xl overflow-hidden border border-white/10">
|
||||
<div class="glass-card rounded-xl overflow-hidden border border-border">
|
||||
<!-- Header (always visible) -->
|
||||
<button
|
||||
class="w-full p-4 text-left hover:bg-white/5 transition-colors cursor-pointer"
|
||||
class="w-full p-4 text-left hover:bg-surface-hover transition-colors cursor-pointer"
|
||||
onclick={() => (expanded = !expanded)}
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
|
|
@ -128,7 +128,7 @@
|
|||
<h4 class="text-sm font-medium text-foreground mb-2">{$t('bots.commands')}</h4>
|
||||
<div class="space-y-1.5 max-h-48 overflow-y-auto">
|
||||
{#each bot.commands as cmd}
|
||||
<div class="text-xs bg-black/20 rounded px-2 py-1.5">
|
||||
<div class="text-xs bg-muted rounded px-2 py-1.5">
|
||||
<code class="text-primary font-mono">{cmd.command}</code>
|
||||
{#if cmd.aliases?.length}
|
||||
<span class="text-muted-foreground"> ({cmd.aliases.join(', ')})</span>
|
||||
|
|
|
|||
|
|
@ -103,20 +103,23 @@
|
|||
{#if open}
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
|
||||
onclick={handleClose}
|
||||
>
|
||||
<!-- Dialog -->
|
||||
<div
|
||||
class="w-full max-w-md rounded-xl bg-base-100 shadow-xl max-h-[90vh] overflow-y-auto"
|
||||
class="w-full max-w-md rounded-xl bg-surface-elevated shadow-xl max-h-[90vh] overflow-y-auto"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between border-b border-base-300 px-6 py-4">
|
||||
<h2 class="text-xl font-semibold">Neuer Chat</h2>
|
||||
<button class="btn btn-ghost btn-sm btn-circle" onclick={handleClose}>
|
||||
<div class="flex items-center justify-between border-b border-border px-6 py-4">
|
||||
<h2 class="text-xl font-semibold text-foreground">Neuer Chat</h2>
|
||||
<button
|
||||
class="p-2 rounded-lg hover:bg-surface-hover transition-colors"
|
||||
onclick={handleClose}
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -126,18 +129,20 @@
|
|||
<!-- Type Selection -->
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn flex-1"
|
||||
class:btn-primary={isDirect}
|
||||
class:btn-ghost={!isDirect}
|
||||
class="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium transition-colors
|
||||
{isDirect
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-surface hover:bg-surface-hover text-foreground border border-border'}"
|
||||
onclick={() => (isDirect = true)}
|
||||
>
|
||||
<ChatCircle class="h-4 w-4" />
|
||||
Direktnachricht
|
||||
</button>
|
||||
<button
|
||||
class="btn flex-1"
|
||||
class:btn-primary={!isDirect}
|
||||
class:btn-ghost={isDirect}
|
||||
class="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium transition-colors
|
||||
{!isDirect
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-surface hover:bg-surface-hover text-foreground border border-border'}"
|
||||
onclick={() => (isDirect = false)}
|
||||
>
|
||||
<Users class="h-4 w-4" />
|
||||
|
|
@ -147,60 +152,66 @@
|
|||
|
||||
<!-- Room Name (only for groups) -->
|
||||
{#if !isDirect}
|
||||
<div class="form-control">
|
||||
<label class="label" for="room-name">
|
||||
<span class="label-text">Raumname</span>
|
||||
</label>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-sm font-medium text-foreground" for="room-name">Raumname</label>
|
||||
<input
|
||||
id="room-name"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
class="input input-bordered"
|
||||
class="w-full px-4 py-2.5 rounded-lg bg-surface border border-border text-foreground
|
||||
focus:outline-none focus:ring-2 focus:ring-primary placeholder:text-muted-foreground"
|
||||
placeholder="z.B. Team Chat"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="room-topic">
|
||||
<span class="label-text">Beschreibung (optional)</span>
|
||||
</label>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-sm font-medium text-foreground" for="room-topic"
|
||||
>Beschreibung (optional)</label
|
||||
>
|
||||
<input
|
||||
id="room-topic"
|
||||
type="text"
|
||||
bind:value={topic}
|
||||
class="input input-bordered"
|
||||
class="w-full px-4 py-2.5 rounded-lg bg-surface border border-border text-foreground
|
||||
focus:outline-none focus:ring-2 focus:ring-primary placeholder:text-muted-foreground"
|
||||
placeholder="Worum geht es in diesem Raum?"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Privacy -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text flex items-center gap-2">
|
||||
{#if isPrivate}
|
||||
<Lock class="h-4 w-4" />
|
||||
Privater Raum
|
||||
{:else}
|
||||
<Globe class="h-4 w-4" />
|
||||
Öffentlicher Raum
|
||||
{/if}
|
||||
</span>
|
||||
<input type="checkbox" class="toggle" bind:checked={isPrivate} />
|
||||
</label>
|
||||
<p class="text-xs text-base-content/60 ml-1">
|
||||
{isPrivate
|
||||
? 'Nur eingeladene Benutzer können beitreten'
|
||||
: 'Jeder kann diesen Raum finden und beitreten'}
|
||||
</p>
|
||||
<div class="flex items-center justify-between p-3 rounded-lg bg-muted">
|
||||
<span class="flex items-center gap-2 text-sm text-foreground">
|
||||
{#if isPrivate}
|
||||
<Lock class="h-4 w-4" />
|
||||
Privater Raum
|
||||
{:else}
|
||||
<Globe class="h-4 w-4" />
|
||||
Öffentlicher Raum
|
||||
{/if}
|
||||
</span>
|
||||
<button
|
||||
class="relative w-11 h-6 rounded-full transition-colors {isPrivate
|
||||
? 'bg-primary'
|
||||
: 'bg-muted-foreground/30'}"
|
||||
onclick={() => (isPrivate = !isPrivate)}
|
||||
>
|
||||
<span
|
||||
class="absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white shadow transition-transform
|
||||
{isPrivate ? 'translate-x-5' : 'translate-x-0'}"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{isPrivate
|
||||
? 'Nur eingeladene Benutzer können beitreten'
|
||||
: 'Jeder kann diesen Raum finden und beitreten'}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<!-- User Search -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="user-search">
|
||||
<span class="label-text">
|
||||
{isDirect ? 'Mit wem möchtest du chatten?' : 'Benutzer einladen (optional)'}
|
||||
</span>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-sm font-medium text-foreground" for="user-search">
|
||||
{isDirect ? 'Mit wem möchtest du chatten?' : 'Benutzer einladen (optional)'}
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
|
|
@ -208,7 +219,8 @@
|
|||
type="text"
|
||||
bind:value={searchQuery}
|
||||
oninput={handleSearchInput}
|
||||
class="input input-bordered w-full"
|
||||
class="w-full px-4 py-2.5 rounded-lg bg-surface border border-border text-foreground
|
||||
focus:outline-none focus:ring-2 focus:ring-primary placeholder:text-muted-foreground"
|
||||
placeholder="@benutzer:server.de oder Name"
|
||||
/>
|
||||
{#if searching}
|
||||
|
|
@ -218,29 +230,34 @@
|
|||
|
||||
<!-- Search Results -->
|
||||
{#if searchResults.length > 0}
|
||||
<ul class="menu mt-2 max-h-40 overflow-y-auto rounded-lg bg-base-200 p-2">
|
||||
<div
|
||||
class="mt-2 rounded-lg bg-surface border border-border overflow-hidden max-h-40 overflow-y-auto"
|
||||
>
|
||||
{#each searchResults as user}
|
||||
<li>
|
||||
<button class="flex items-center gap-2" onclick={() => selectUser(user)}>
|
||||
<div class="avatar placeholder">
|
||||
<div class="w-8 rounded-full bg-neutral text-neutral-content">
|
||||
{#if user.avatarUrl}
|
||||
<img src={user.avatarUrl} alt="" />
|
||||
{:else}
|
||||
<span class="text-xs">{user.displayName?.[0] || user.userId[1]}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 text-left">
|
||||
<p class="font-medium">{user.displayName || user.userId}</p>
|
||||
{#if user.displayName}
|
||||
<p class="text-xs text-base-content/60">{user.userId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<button
|
||||
class="flex items-center gap-3 w-full px-3 py-2 hover:bg-surface-hover transition-colors"
|
||||
onclick={() => selectUser(user)}
|
||||
>
|
||||
<div
|
||||
class="w-8 h-8 rounded-full bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center text-white text-sm"
|
||||
>
|
||||
{#if user.avatarUrl}
|
||||
<img src={user.avatarUrl} alt="" class="w-8 h-8 rounded-full object-cover" />
|
||||
{:else}
|
||||
{user.displayName?.[0] || user.userId[1]}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 text-left min-w-0">
|
||||
<p class="font-medium text-foreground truncate">
|
||||
{user.displayName || user.userId}
|
||||
</p>
|
||||
{#if user.displayName}
|
||||
<p class="text-xs text-muted-foreground truncate">{user.userId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
@ -248,9 +265,14 @@
|
|||
{#if selectedUsers.length > 0}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each selectedUsers as user}
|
||||
<span class="badge badge-lg gap-1">
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-primary/10 text-primary text-sm"
|
||||
>
|
||||
{user.displayName || user.userId}
|
||||
<button onclick={() => removeUser(user.userId)}>
|
||||
<button
|
||||
class="hover:bg-primary/20 rounded-full p-0.5 transition-colors"
|
||||
onclick={() => removeUser(user.userId)}
|
||||
>
|
||||
<X class="h-3 w-3" />
|
||||
</button>
|
||||
</span>
|
||||
|
|
@ -260,16 +282,26 @@
|
|||
|
||||
<!-- Error -->
|
||||
{#if error}
|
||||
<div class="alert alert-error">
|
||||
<span>{error}</span>
|
||||
<div class="px-4 py-3 rounded-lg bg-error/10 text-error text-sm">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex justify-end gap-2 border-t border-base-300 px-6 py-4">
|
||||
<button class="btn btn-ghost" onclick={handleClose}>Abbrechen</button>
|
||||
<button class="btn btn-primary" onclick={handleCreate} disabled={loading}>
|
||||
<div class="flex justify-end gap-2 border-t border-border px-6 py-4">
|
||||
<button
|
||||
class="px-4 py-2.5 rounded-lg hover:bg-surface-hover text-foreground font-medium transition-colors"
|
||||
onclick={handleClose}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2.5 rounded-lg bg-primary hover:bg-primary/90 text-primary-foreground font-medium transition-colors
|
||||
disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
onclick={handleCreate}
|
||||
disabled={loading}
|
||||
>
|
||||
{#if loading}
|
||||
<CircleNotch class="h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
class="fixed inset-0 z-50 bg-primary/10 backdrop-blur-sm flex items-center justify-center pointer-events-none"
|
||||
>
|
||||
<div
|
||||
class="bg-white dark:bg-zinc-800 rounded-2xl p-8 shadow-2xl border-2 border-dashed border-primary flex flex-col items-center gap-4"
|
||||
class="bg-surface-elevated rounded-2xl p-8 shadow-2xl border-2 border-dashed border-primary flex flex-col items-center gap-4"
|
||||
>
|
||||
<div class="p-4 rounded-full bg-primary/10">
|
||||
<UploadSimple class="h-12 w-12 text-primary" />
|
||||
|
|
|
|||
|
|
@ -70,14 +70,14 @@
|
|||
>
|
||||
<!-- Dialog -->
|
||||
<div
|
||||
class="w-full max-w-md rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hidden"
|
||||
class="w-full max-w-md rounded-2xl bg-surface-elevated shadow-2xl overflow-hidden"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between border-b border-black/10 dark:border-white/10 px-4 py-3">
|
||||
<div class="flex items-center justify-between border-b border-border px-4 py-3">
|
||||
<h2 class="text-lg font-semibold">Nachricht weiterleiten</h2>
|
||||
<button
|
||||
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
|
||||
onclick={handleClose}
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
|
|
@ -85,20 +85,22 @@
|
|||
</div>
|
||||
|
||||
<!-- Message Preview -->
|
||||
<div class="px-4 py-3 bg-black/5 dark:bg-white/5 border-b border-black/5 dark:border-white/5">
|
||||
<div class="px-4 py-3 bg-muted border-b border-border">
|
||||
<p class="text-xs text-muted-foreground mb-1">Von {message.senderName}</p>
|
||||
<p class="text-sm line-clamp-3">{message.body}</p>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="p-4 border-b border-black/5 dark:border-white/5">
|
||||
<div class="p-4 border-b border-border">
|
||||
<div class="relative">
|
||||
<MagnifyingGlass class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<MagnifyingGlass
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={search}
|
||||
placeholder="Chat suchen..."
|
||||
class="w-full pl-10 pr-4 py-2.5 rounded-xl bg-black/5 dark:bg-white/10 border border-black/10 dark:border-white/10
|
||||
class="w-full pl-10 pr-4 py-2.5 rounded-xl bg-surface border border-border
|
||||
text-sm focus:outline-none focus:ring-2 focus:ring-violet-500"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -112,17 +114,29 @@
|
|||
{#each filteredRooms as room (room.id)}
|
||||
<button
|
||||
class="flex items-center gap-3 w-full px-4 py-3 transition-colors text-left
|
||||
{selectedRooms.has(room.id) ? 'bg-violet-500/10' : 'hover:bg-black/5 dark:hover:bg-white/5'}"
|
||||
{selectedRooms.has(room.id) ? 'bg-violet-500/10' : 'hover:bg-surface-hover'}"
|
||||
onclick={() => toggleRoom(room.id)}
|
||||
>
|
||||
<!-- Checkbox -->
|
||||
<div
|
||||
class="w-5 h-5 rounded-md border-2 flex items-center justify-center transition-colors
|
||||
{selectedRooms.has(room.id) ? 'bg-violet-500 border-violet-500' : 'border-black/20 dark:border-white/20'}"
|
||||
{selectedRooms.has(room.id)
|
||||
? 'bg-violet-500 border-violet-500'
|
||||
: 'border-black/20 dark:border-white/20'}"
|
||||
>
|
||||
{#if selectedRooms.has(room.id)}
|
||||
<svg class="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
|
||||
<svg
|
||||
class="w-3 h-3 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="3"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -133,7 +147,11 @@
|
|||
bg-gradient-to-br from-violet-500 to-purple-600 text-white"
|
||||
>
|
||||
{#if room.avatar}
|
||||
<img src={room.avatar} alt={room.name} class="w-10 h-10 rounded-full object-cover" />
|
||||
<img
|
||||
src={room.avatar}
|
||||
alt={room.name}
|
||||
class="w-10 h-10 rounded-full object-cover"
|
||||
/>
|
||||
{:else if room.isDirect}
|
||||
<User class="w-5 h-5" />
|
||||
{:else}
|
||||
|
|
@ -154,7 +172,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-between border-t border-black/10 dark:border-white/10 px-4 py-3">
|
||||
<div class="flex items-center justify-between border-t border-border px-4 py-3">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{selectedRooms.size} ausgewählt
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@
|
|||
|
||||
// Apply markdown formatting (bold, italic, code, strikethrough)
|
||||
function applyMarkdown(text: string, isOwn: boolean): string {
|
||||
const codeColor = isOwn ? 'bg-white/20 text-white' : 'bg-black/5 dark:bg-white/10';
|
||||
const codeColor = isOwn ? 'bg-white/20 text-white' : 'bg-muted';
|
||||
|
||||
// Inline code (backticks) - process first to avoid conflicts
|
||||
text = text.replace(
|
||||
|
|
@ -331,7 +331,7 @@
|
|||
class="relative px-4 py-3 shadow-md
|
||||
{message.isOwn
|
||||
? 'bg-gradient-to-br from-blue-500 to-indigo-600 text-white rounded-2xl rounded-tr-md'
|
||||
: 'bg-white dark:bg-white/10 text-foreground border border-black/5 dark:border-white/10 rounded-2xl rounded-tl-md'}"
|
||||
: 'bg-surface text-foreground border border-border rounded-2xl rounded-tl-md'}"
|
||||
>
|
||||
{#if message.redacted}
|
||||
<p class="italic text-white/70">Nachricht wurde gelöscht</p>
|
||||
|
|
@ -345,12 +345,12 @@
|
|||
<!-- Image message -->
|
||||
<div class="relative">
|
||||
{#if imageLoading}
|
||||
<div class="flex h-48 w-full items-center justify-center rounded-lg bg-black/10">
|
||||
<div class="flex h-48 w-full items-center justify-center rounded-lg bg-muted">
|
||||
<ImageIcon class="h-8 w-8 animate-pulse text-white/50" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if imageError}
|
||||
<div class="flex h-32 w-full items-center justify-center rounded-lg bg-black/10">
|
||||
<div class="flex h-32 w-full items-center justify-center rounded-lg bg-muted">
|
||||
<p class="text-sm text-white/70">Bild konnte nicht geladen werden</p>
|
||||
</div>
|
||||
{:else}
|
||||
|
|
@ -394,7 +394,7 @@
|
|||
<div
|
||||
class="flex items-center gap-3 rounded-lg {message.isOwn
|
||||
? 'bg-white/20'
|
||||
: 'bg-black/5 dark:bg-white/5'} p-3 min-w-[220px]"
|
||||
: 'bg-muted'} p-3 min-w-[220px]"
|
||||
>
|
||||
<!-- Hidden audio element -->
|
||||
{#if mediaUrl}
|
||||
|
|
@ -432,7 +432,7 @@
|
|||
<button
|
||||
class="relative h-1.5 w-full rounded-full {message.isOwn
|
||||
? 'bg-white/20'
|
||||
: 'bg-black/10 dark:bg-white/10'} overflow-hidden cursor-pointer"
|
||||
: 'bg-muted dark:bg-white/10'} overflow-hidden cursor-pointer"
|
||||
onclick={seekAudio}
|
||||
>
|
||||
<div
|
||||
|
|
@ -461,7 +461,7 @@
|
|||
rel="noopener noreferrer"
|
||||
class="flex items-center gap-3 rounded-lg {message.isOwn
|
||||
? 'bg-white/20 hover:bg-white/30'
|
||||
: 'bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10'} p-3 transition-colors"
|
||||
: 'bg-muted hover:bg-muted dark:hover:bg-white/10'} p-3 transition-colors"
|
||||
>
|
||||
<div class="rounded-lg {message.isOwn ? 'bg-white/20' : 'bg-primary/10'} p-2">
|
||||
<FileIcon class="h-5 w-5 {message.isOwn ? 'text-white' : 'text-primary'}" />
|
||||
|
|
@ -504,7 +504,7 @@
|
|||
rel="noopener noreferrer"
|
||||
class="mt-2 flex items-center gap-2 rounded-lg {message.isOwn
|
||||
? 'bg-white/10 hover:bg-white/20'
|
||||
: 'bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10'} p-2 transition-colors"
|
||||
: 'bg-muted hover:bg-muted dark:hover:bg-white/10'} p-2 transition-colors"
|
||||
>
|
||||
<img
|
||||
src="https://www.google.com/s2/favicons?domain={getDomain(firstUrl() || '')}&sz=32"
|
||||
|
|
@ -540,7 +540,7 @@
|
|||
class="flex items-center gap-1 px-2 py-0.5 rounded-full text-xs transition-colors
|
||||
{reaction.includesMe
|
||||
? 'bg-primary/20 border border-primary/40 text-primary'
|
||||
: 'bg-black/5 dark:bg-white/10 border border-black/10 dark:border-white/10 hover:bg-black/10 dark:hover:bg-white/20'}"
|
||||
: 'bg-muted border border-border hover:bg-surface-hover'}"
|
||||
title={reaction.users.join(', ')}
|
||||
onclick={() => handleReaction(reaction.key)}
|
||||
>
|
||||
|
|
@ -581,7 +581,7 @@
|
|||
<!-- Emoji reaction button -->
|
||||
<div class="relative">
|
||||
<button
|
||||
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
|
||||
title="Reaktion"
|
||||
onclick={() => (showEmojiPicker = !showEmojiPicker)}
|
||||
>
|
||||
|
|
@ -599,7 +599,7 @@
|
|||
></button>
|
||||
<!-- Emoji picker dropdown -->
|
||||
<div
|
||||
class="absolute z-50 rounded-xl bg-white dark:bg-zinc-800 border border-black/10 dark:border-white/10 shadow-xl
|
||||
class="absolute z-50 rounded-xl bg-surface-elevated border border-border shadow-xl
|
||||
left-0 top-full mt-2 lg:bottom-full lg:top-auto lg:mt-0 lg:mb-2
|
||||
{message.isOwn ? 'lg:right-0 lg:left-auto' : ''}
|
||||
{showFullPicker ? 'w-72' : ''}"
|
||||
|
|
@ -608,15 +608,13 @@
|
|||
<!-- Full emoji picker with categories -->
|
||||
<div class="p-2">
|
||||
<!-- Category tabs -->
|
||||
<div
|
||||
class="flex gap-1 mb-2 border-b border-black/10 dark:border-white/10 pb-2 overflow-x-auto"
|
||||
>
|
||||
<div class="flex gap-1 mb-2 border-b border-border pb-2 overflow-x-auto">
|
||||
{#each emojiCategories as category, i}
|
||||
<button
|
||||
class="px-2 py-1 text-xs rounded-md whitespace-nowrap transition-colors
|
||||
{selectedCategory === i
|
||||
? 'bg-violet-500 text-white'
|
||||
: 'hover:bg-black/5 dark:hover:bg-white/10 text-muted-foreground'}"
|
||||
: 'hover:bg-surface-hover text-muted-foreground'}"
|
||||
onclick={() => (selectedCategory = i)}
|
||||
>
|
||||
{category.name}
|
||||
|
|
@ -627,7 +625,7 @@
|
|||
<div class="grid grid-cols-8 gap-1 max-h-40 overflow-y-auto">
|
||||
{#each emojiCategories[selectedCategory].emojis as emoji}
|
||||
<button
|
||||
class="text-xl hover:scale-110 hover:bg-black/5 dark:hover:bg-white/10 rounded p-1 transition-all"
|
||||
class="text-xl hover:scale-110 hover:bg-surface-hover rounded p-1 transition-all"
|
||||
onclick={() => handleReaction(emoji)}
|
||||
>
|
||||
{emoji}
|
||||
|
|
@ -648,7 +646,7 @@
|
|||
{/each}
|
||||
<!-- Expand button -->
|
||||
<button
|
||||
class="ml-1 p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
class="ml-1 p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
|
||||
onclick={() => (showFullPicker = true)}
|
||||
title="Mehr Emojis"
|
||||
>
|
||||
|
|
@ -660,14 +658,14 @@
|
|||
{/if}
|
||||
</div>
|
||||
<button
|
||||
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
|
||||
title="Antworten"
|
||||
onclick={() => onReply?.(message)}
|
||||
>
|
||||
<ArrowBendUpLeft class="h-4 w-4 text-muted-foreground" />
|
||||
</button>
|
||||
<button
|
||||
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
|
||||
title="Weiterleiten"
|
||||
onclick={() => onForward?.(message)}
|
||||
>
|
||||
|
|
@ -675,7 +673,7 @@
|
|||
</button>
|
||||
{#if message.isOwn && message.type === 'm.text'}
|
||||
<button
|
||||
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
|
||||
title="Bearbeiten"
|
||||
onclick={() => onEdit?.(message)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -508,9 +508,7 @@
|
|||
<div class="p-3 pb-4 lg:pb-20 safe-area-bottom">
|
||||
<!-- Reply/Edit Preview -->
|
||||
{#if replyTo || editMessage}
|
||||
<div
|
||||
class="mb-2 flex items-center gap-2 rounded-xl bg-white/60 dark:bg-white/5 border border-black/5 dark:border-white/10 px-3 py-2"
|
||||
>
|
||||
<div class="mb-2 flex items-center gap-2 rounded-xl bg-surface border border-border px-3 py-2">
|
||||
<div class="flex-1">
|
||||
{#if editMessage}
|
||||
<p class="text-xs text-muted-foreground">Nachricht bearbeiten</p>
|
||||
|
|
@ -523,7 +521,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
<button
|
||||
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
|
||||
onclick={() => {
|
||||
if (editMessage) {
|
||||
onCancelEdit?.();
|
||||
|
|
@ -540,12 +538,10 @@
|
|||
|
||||
<!-- Upload Progress -->
|
||||
{#if uploading}
|
||||
<div
|
||||
class="mb-2 flex items-center gap-3 rounded-xl bg-white/60 dark:bg-white/5 border border-black/5 dark:border-white/10 px-3 py-2"
|
||||
>
|
||||
<div class="mb-2 flex items-center gap-3 rounded-xl bg-surface border border-border px-3 py-2">
|
||||
<CircleNotch class="h-4 w-4 animate-spin text-primary" />
|
||||
<div class="flex-1">
|
||||
<div class="h-1.5 overflow-hidden rounded-full bg-black/10 dark:bg-white/10">
|
||||
<div class="h-1.5 overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
class="h-full bg-primary transition-all duration-300"
|
||||
style="width: {uploadProgress}%"
|
||||
|
|
@ -578,12 +574,8 @@
|
|||
|
||||
<!-- @Mention Picker -->
|
||||
{#if showMentionPicker && mentionResults.length > 0}
|
||||
<div
|
||||
class="mb-2 rounded-xl bg-white dark:bg-zinc-800 border border-black/10 dark:border-white/10 shadow-xl overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="px-3 py-1.5 text-xs text-muted-foreground border-b border-black/5 dark:border-white/5"
|
||||
>
|
||||
<div class="mb-2 rounded-xl bg-surface-elevated border border-border shadow-xl overflow-hidden">
|
||||
<div class="px-3 py-1.5 text-xs text-muted-foreground border-b border-border">
|
||||
Erwähne jemanden
|
||||
</div>
|
||||
{#each mentionResults as member, i}
|
||||
|
|
@ -591,7 +583,7 @@
|
|||
class="flex items-center gap-3 w-full px-3 py-2 transition-colors text-left
|
||||
{i === selectedMentionIndex
|
||||
? 'bg-violet-500/10 dark:bg-violet-500/20'
|
||||
: 'hover:bg-black/5 dark:hover:bg-white/5'}"
|
||||
: 'hover:bg-surface-hover'}"
|
||||
onclick={() => insertMention(member)}
|
||||
>
|
||||
<!-- Avatar -->
|
||||
|
|
@ -623,7 +615,7 @@
|
|||
<!-- Attachment button (left, outside input) -->
|
||||
<div class="relative flex-shrink-0">
|
||||
<button
|
||||
class="p-2.5 rounded-full hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
class="p-2.5 rounded-full hover:bg-surface-hover transition-colors"
|
||||
title="Datei anhängen"
|
||||
disabled={uploading}
|
||||
onclick={() => (showAttachMenu = !showAttachMenu)}
|
||||
|
|
@ -640,14 +632,14 @@
|
|||
></button>
|
||||
<!-- Dropdown menu -->
|
||||
<div
|
||||
class="absolute bottom-full left-0 mb-2 z-50 w-44 rounded-xl bg-white dark:bg-zinc-800 border border-black/10 dark:border-white/10 p-1.5 shadow-xl"
|
||||
class="absolute bottom-full left-0 mb-2 z-50 w-44 rounded-xl bg-surface-elevated border border-border p-1.5 shadow-xl"
|
||||
>
|
||||
<button
|
||||
onclick={() => {
|
||||
openFilePicker();
|
||||
showAttachMenu = false;
|
||||
}}
|
||||
class="flex items-center gap-2 w-full px-3 py-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors text-sm"
|
||||
class="flex items-center gap-2 w-full px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors text-sm"
|
||||
>
|
||||
<Image class="h-4 w-4" />
|
||||
Bild oder Video
|
||||
|
|
@ -657,7 +649,7 @@
|
|||
openFilePicker();
|
||||
showAttachMenu = false;
|
||||
}}
|
||||
class="flex items-center gap-2 w-full px-3 py-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors text-sm"
|
||||
class="flex items-center gap-2 w-full px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors text-sm"
|
||||
>
|
||||
<FileIcon class="h-4 w-4" />
|
||||
Datei
|
||||
|
|
@ -677,7 +669,7 @@
|
|||
|
||||
<!-- Text input with emoji button inside -->
|
||||
<div
|
||||
class="relative flex-1 flex items-end rounded-full bg-white/80 dark:bg-white/10 backdrop-blur-xl border border-black/10 dark:border-white/20 px-4 py-1"
|
||||
class="relative flex-1 flex items-end rounded-full bg-surface border border-border px-4 py-1"
|
||||
>
|
||||
<textarea
|
||||
bind:this={textarea}
|
||||
|
|
@ -698,7 +690,7 @@
|
|||
></textarea>
|
||||
<!-- Emoji button inside input -->
|
||||
<button
|
||||
class="flex-shrink-0 p-1.5 rounded-full hover:bg-black/5 dark:hover:bg-white/10 transition-colors mb-1"
|
||||
class="flex-shrink-0 p-1.5 rounded-full hover:bg-surface-hover transition-colors mb-1"
|
||||
title="Emoji"
|
||||
onclick={handleEmojiClick}
|
||||
>
|
||||
|
|
@ -715,7 +707,7 @@
|
|||
></button>
|
||||
<!-- Picker -->
|
||||
<div
|
||||
class="absolute bottom-full right-0 mb-2 z-50 w-72 max-h-80 overflow-y-auto rounded-xl bg-white dark:bg-zinc-800 border border-black/10 dark:border-white/10 p-2 shadow-xl"
|
||||
class="absolute bottom-full right-0 mb-2 z-50 w-72 max-h-80 overflow-y-auto rounded-xl bg-surface-elevated border border-border p-2 shadow-xl"
|
||||
>
|
||||
<!-- Recent/Frequently used emojis -->
|
||||
{#if recentEmojis.length > 0}
|
||||
|
|
@ -726,7 +718,7 @@
|
|||
<div class="grid grid-cols-8 gap-1">
|
||||
{#each recentEmojis as emoji}
|
||||
<button
|
||||
class="p-1.5 text-xl hover:bg-black/5 dark:hover:bg-white/10 rounded-lg transition-colors"
|
||||
class="p-1.5 text-xl hover:bg-surface-hover rounded-lg transition-colors"
|
||||
onclick={() => insertEmoji(emoji)}
|
||||
>
|
||||
{emoji}
|
||||
|
|
@ -734,13 +726,13 @@
|
|||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-black/5 dark:border-white/10 my-2"></div>
|
||||
<div class="border-t border-border my-2"></div>
|
||||
{/if}
|
||||
<!-- All emojis -->
|
||||
<div class="grid grid-cols-8 gap-1">
|
||||
{#each commonEmojis as emoji}
|
||||
<button
|
||||
class="p-1.5 text-xl hover:bg-black/5 dark:hover:bg-white/10 rounded-lg transition-colors"
|
||||
class="p-1.5 text-xl hover:bg-surface-hover rounded-lg transition-colors"
|
||||
onclick={() => insertEmoji(emoji)}
|
||||
>
|
||||
{emoji}
|
||||
|
|
@ -772,7 +764,7 @@
|
|||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="flex-shrink-0 p-2.5 rounded-full hover:bg-black/5 dark:hover:bg-white/10 text-muted-foreground hover:text-primary transition-colors
|
||||
class="flex-shrink-0 p-2.5 rounded-full hover:bg-surface-hover text-muted-foreground hover:text-primary transition-colors
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onclick={startRecording}
|
||||
disabled={uploading}
|
||||
|
|
|
|||
|
|
@ -76,13 +76,11 @@
|
|||
</script>
|
||||
|
||||
{#if room}
|
||||
<header
|
||||
class="flex items-center gap-3 border-b border-black/10 dark:border-white/10 bg-white/50 dark:bg-white/5 backdrop-blur-sm px-4 py-3"
|
||||
>
|
||||
<header class="flex items-center gap-3 border-b border-border bg-surface-elevated px-4 py-3">
|
||||
<!-- Mobile back button or menu button -->
|
||||
{#if showBackButton}
|
||||
<button
|
||||
class="p-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
class="p-2 rounded-lg hover:bg-surface-hover transition-colors"
|
||||
onclick={onBackClick}
|
||||
aria-label="Zurück"
|
||||
>
|
||||
|
|
@ -90,7 +88,7 @@
|
|||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="p-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors lg:hidden"
|
||||
class="p-2 rounded-lg hover:bg-surface-hover transition-colors lg:hidden"
|
||||
onclick={onMenuClick}
|
||||
>
|
||||
<List class="h-5 w-5" />
|
||||
|
|
@ -112,8 +110,8 @@
|
|||
<!-- Online indicator for DMs -->
|
||||
{#if room.isDirect}
|
||||
<div
|
||||
class="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 rounded-full border-2 border-white dark:border-zinc-900
|
||||
{isOnline ? 'bg-green-500' : 'bg-zinc-400 dark:bg-zinc-600'}"
|
||||
class="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 rounded-full border-2 border-background
|
||||
{isOnline ? 'bg-green-500' : 'bg-muted-foreground'}"
|
||||
title={presenceText()}
|
||||
></div>
|
||||
{/if}
|
||||
|
|
@ -151,7 +149,7 @@
|
|||
<span class="w-2 h-2 rounded-full bg-green-500"></span>
|
||||
<span class="text-green-600 dark:text-green-400">Online</span>
|
||||
{:else}
|
||||
<span class="w-2 h-2 rounded-full bg-zinc-400"></span>
|
||||
<span class="w-2 h-2 rounded-full bg-muted-foreground"></span>
|
||||
<span>{presenceText() || 'Offline'}</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@
|
|||
<button
|
||||
class="flex w-full items-center gap-3 px-3 py-2.5 mb-1 rounded-xl transition-all duration-200
|
||||
{selected
|
||||
? 'bg-white dark:bg-white/15 shadow-md border border-black/5 dark:border-white/10'
|
||||
: 'hover:bg-white/60 dark:hover:bg-white/5 hover:-translate-y-0.5'}"
|
||||
? 'bg-surface-elevated shadow-md border border-border'
|
||||
: 'hover:bg-surface-hover hover:-translate-y-0.5'}"
|
||||
{onclick}
|
||||
>
|
||||
<!-- Avatar with online indicator -->
|
||||
|
|
@ -69,8 +69,8 @@
|
|||
<!-- Online indicator dot -->
|
||||
{#if room.isDirect}
|
||||
<div
|
||||
class="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 rounded-full border-2 border-white dark:border-zinc-900
|
||||
{isOnline ? 'bg-green-500' : 'bg-zinc-400 dark:bg-zinc-600'}"
|
||||
class="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 rounded-full border-2 border-background
|
||||
{isOnline ? 'bg-green-500' : 'bg-muted-foreground'}"
|
||||
title={lastActiveText()}
|
||||
></div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -63,8 +63,7 @@
|
|||
type="text"
|
||||
bind:value={search}
|
||||
placeholder="Chats durchsuchen..."
|
||||
class="w-full rounded-xl bg-white/70 dark:bg-white/10 backdrop-blur-xl
|
||||
border border-black/10 dark:border-white/20 px-4 py-2.5 pl-10
|
||||
class="w-full rounded-xl bg-surface border border-border px-4 py-2.5 pl-10
|
||||
text-sm font-medium text-foreground focus:ring-2 focus:ring-primary focus:outline-none
|
||||
placeholder:text-muted-foreground shadow-sm"
|
||||
/>
|
||||
|
|
@ -143,7 +142,7 @@
|
|||
>
|
||||
<ChatCircle class="h-3.5 w-3.5" />
|
||||
Direktnachrichten
|
||||
<span class="px-1.5 py-0.5 rounded-full bg-black/10 dark:bg-white/10 text-[10px]">
|
||||
<span class="px-1.5 py-0.5 rounded-full bg-muted text-[10px]">
|
||||
{matrixStore.directRooms.length}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -165,7 +164,7 @@
|
|||
>
|
||||
<Users class="h-3.5 w-3.5" />
|
||||
Räume
|
||||
<span class="px-1.5 py-0.5 rounded-full bg-black/10 dark:bg-white/10 text-[10px]">
|
||||
<span class="px-1.5 py-0.5 rounded-full bg-muted text-[10px]">
|
||||
{matrixStore.groupRooms.length}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -189,7 +188,7 @@
|
|||
</div>
|
||||
|
||||
<!-- New Room Button -->
|
||||
<div class="border-t border-black/10 dark:border-white/10 p-3 pb-4 lg:pb-20">
|
||||
<div class="border-t border-border p-3 pb-4 lg:pb-20">
|
||||
<button
|
||||
class="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl
|
||||
bg-gradient-to-r from-violet-500 to-purple-600 text-white font-medium
|
||||
|
|
|
|||
|
|
@ -79,7 +79,10 @@
|
|||
function highlightMatch(text: string, searchTerm: string): string {
|
||||
if (!searchTerm.trim()) return text;
|
||||
const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
||||
return text.replace(regex, '<mark class="bg-yellow-300/50 dark:bg-yellow-500/30 rounded px-0.5">$1</mark>');
|
||||
return text.replace(
|
||||
regex,
|
||||
'<mark class="bg-yellow-300/50 dark:bg-yellow-500/30 rounded px-0.5">$1</mark>'
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -95,12 +98,12 @@
|
|||
>
|
||||
<!-- Dialog -->
|
||||
<div
|
||||
class="w-full max-w-2xl rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hidden"
|
||||
class="w-full max-w-2xl rounded-2xl bg-surface-elevated shadow-2xl overflow-hidden"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
role="document"
|
||||
>
|
||||
<!-- Search Header -->
|
||||
<div class="flex items-center gap-3 p-4 border-b border-black/10 dark:border-white/10">
|
||||
<div class="flex items-center gap-3 p-4 border-b border-border">
|
||||
<MagnifyingGlass class="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
||||
<input
|
||||
bind:this={inputRef}
|
||||
|
|
@ -113,26 +116,23 @@
|
|||
{#if searching}
|
||||
<CircleNotch class="h-5 w-5 animate-spin text-muted-foreground" />
|
||||
{/if}
|
||||
<button
|
||||
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
onclick={onClose}
|
||||
>
|
||||
<button class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors" onclick={onClose}>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Scope Toggle -->
|
||||
<div class="flex gap-2 px-4 py-2 border-b border-black/5 dark:border-white/5 bg-muted/30">
|
||||
<div class="flex gap-2 px-4 py-2 border-b border-border bg-muted/30">
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors
|
||||
{searchScope === 'room' ? 'bg-primary text-primary-foreground' : 'hover:bg-black/5 dark:hover:bg-white/10'}"
|
||||
{searchScope === 'room' ? 'bg-primary text-primary-foreground' : 'hover:bg-surface-hover'}"
|
||||
onclick={() => (searchScope = 'room')}
|
||||
>
|
||||
Aktueller Raum
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors
|
||||
{searchScope === 'all' ? 'bg-primary text-primary-foreground' : 'hover:bg-black/5 dark:hover:bg-white/10'}"
|
||||
{searchScope === 'all' ? 'bg-primary text-primary-foreground' : 'hover:bg-surface-hover'}"
|
||||
onclick={() => (searchScope = 'all')}
|
||||
>
|
||||
Alle Räume
|
||||
|
|
@ -147,10 +147,10 @@
|
|||
<span>Suche läuft...</span>
|
||||
</div>
|
||||
{:else if searchResults.length > 0}
|
||||
<div class="divide-y divide-black/5 dark:divide-white/5">
|
||||
<div class="divide-y divide-border">
|
||||
{#each searchResults as result}
|
||||
<button
|
||||
class="w-full text-left px-4 py-3 hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
|
||||
class="w-full text-left px-4 py-3 hover:bg-surface-hover transition-colors"
|
||||
onclick={() => handleSelectResult(result)}
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
|
|
@ -158,7 +158,9 @@
|
|||
{#if searchScope === 'all'}
|
||||
<span class="text-xs text-muted-foreground">in {result.roomName}</span>
|
||||
{/if}
|
||||
<span class="text-xs text-muted-foreground ml-auto">{formatTime(result.timestamp)}</span>
|
||||
<span class="text-xs text-muted-foreground ml-auto"
|
||||
>{formatTime(result.timestamp)}</span
|
||||
>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground line-clamp-2">
|
||||
{@html highlightMatch(result.body, query)}
|
||||
|
|
|
|||
|
|
@ -37,12 +37,12 @@
|
|||
<img
|
||||
src={user.avatarUrl}
|
||||
alt={user.name}
|
||||
class="w-6 h-6 rounded-full border-2 border-white dark:border-zinc-900 object-cover"
|
||||
class="w-6 h-6 rounded-full border-2 border-background object-cover"
|
||||
style="z-index: {3 - i}"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="w-6 h-6 rounded-full border-2 border-white dark:border-zinc-900 bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center"
|
||||
class="w-6 h-6 rounded-full border-2 border-background bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center"
|
||||
style="z-index: {3 - i}"
|
||||
>
|
||||
<User class="w-3 h-3 text-white" />
|
||||
|
|
@ -52,10 +52,16 @@
|
|||
</div>
|
||||
|
||||
<!-- Animated dots -->
|
||||
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-black/5 dark:bg-white/10">
|
||||
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:0ms]"></span>
|
||||
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:150ms]"></span>
|
||||
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:300ms]"></span>
|
||||
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-muted">
|
||||
<span
|
||||
class="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:0ms]"
|
||||
></span>
|
||||
<span
|
||||
class="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:150ms]"
|
||||
></span>
|
||||
<span
|
||||
class="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:300ms]"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<!-- Text -->
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@
|
|||
type="text"
|
||||
bind:value={search}
|
||||
placeholder={$t('bots.search')}
|
||||
class="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white/5 border border-white/10 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition-all"
|
||||
class="w-full pl-10 pr-4 py-2.5 rounded-xl bg-surface border border-border text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary/50 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -104,7 +104,7 @@
|
|||
class="px-4 py-2 rounded-full text-sm font-medium transition-all whitespace-nowrap cursor-pointer
|
||||
{selectedCategory === category.id
|
||||
? 'bg-gradient-to-r from-violet-500 to-purple-600 text-white shadow-lg'
|
||||
: 'bg-white/5 text-muted-foreground hover:bg-white/10 hover:text-foreground border border-white/10'}"
|
||||
: 'bg-surface text-muted-foreground hover:bg-surface-hover hover:text-foreground border border-border'}"
|
||||
onclick={() => (selectedCategory = category.id as BotCategory)}
|
||||
>
|
||||
{category.label}
|
||||
|
|
|
|||
|
|
@ -183,9 +183,7 @@
|
|||
<!-- Mobile: Full-screen room list -->
|
||||
<div class="flex flex-col h-full bg-background safe-area-bottom">
|
||||
<!-- User Info / Status Bar -->
|
||||
<div
|
||||
class="border-b border-black/10 dark:border-white/10 px-4 py-3 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-xl safe-area-top"
|
||||
>
|
||||
<div class="border-b border-border px-4 py-3 bg-surface-elevated safe-area-top">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-foreground">Manalink</h1>
|
||||
|
|
@ -204,13 +202,13 @@
|
|||
<div class="flex items-center gap-1">
|
||||
<a
|
||||
href="/settings"
|
||||
class="p-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
class="p-2 rounded-lg hover:bg-surface-hover transition-colors"
|
||||
title="Einstellungen"
|
||||
>
|
||||
<Gear class="h-5 w-5" />
|
||||
</a>
|
||||
<button
|
||||
class="p-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
class="p-2 rounded-lg hover:bg-surface-hover transition-colors"
|
||||
title="Neuer Chat"
|
||||
onclick={() => (showCreateRoom = true)}
|
||||
>
|
||||
|
|
@ -229,23 +227,21 @@
|
|||
<!-- Desktop: Side-by-side layout -->
|
||||
<div class="chat-layout flex h-full min-h-0 overflow-hidden bg-background">
|
||||
<!-- Sidebar -->
|
||||
<aside
|
||||
class="flex flex-col border-r border-black/10 dark:border-white/10 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-xl w-80"
|
||||
>
|
||||
<aside class="flex flex-col border-r border-border bg-surface-elevated w-80">
|
||||
<!-- User Info / Status Bar -->
|
||||
<div class="border-b border-black/10 dark:border-white/10 px-4 py-3">
|
||||
<div class="border-b border-border px-4 py-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="truncate text-sm font-medium">{matrixStore.userId}</p>
|
||||
<div class="flex items-center gap-1">
|
||||
<a
|
||||
href="/settings"
|
||||
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
|
||||
title="Einstellungen"
|
||||
>
|
||||
<Gear class="h-4 w-4" />
|
||||
</a>
|
||||
<button
|
||||
class="p-1.5 rounded-lg hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
class="p-1.5 rounded-lg hover:bg-surface-hover transition-colors"
|
||||
title="Neuer Chat"
|
||||
onclick={() => (showCreateRoom = true)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -77,7 +77,14 @@ async function bootstrap() {
|
|||
app.use(cookieParser());
|
||||
|
||||
// Explicit body parsers for form-urlencoded (needed for OAuth2 token endpoint)
|
||||
app.use(bodyParser.json());
|
||||
// IMPORTANT: Skip JSON body parsing for Stripe webhooks to preserve rawBody for signature verification
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
if (req.path === '/api/v1/webhooks/stripe') {
|
||||
// Skip body parsing for Stripe webhooks - NestJS rawBody will be used instead
|
||||
return next();
|
||||
}
|
||||
bodyParser.json()(req, res, next);
|
||||
});
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
// CORS configuration
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue