2026-01-07 09:31:02 +00:00
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
import { useAudio } from '../../../context/AudioContext';
|
|
|
|
|
import { Card } from '../../ui/card';
|
|
|
|
|
import { Button } from '../../ui/button';
|
2026-02-09 22:23:09 +00:00
|
|
|
import { EmptyState } from '../../ui/empty-state';
|
2026-01-07 09:31:02 +00:00
|
|
|
import { SaveQueueAsPlaylistModal } from './SaveQueueAsPlaylistModal';
|
2026-01-13 18:47:57 +00:00
|
|
|
import {
|
|
|
|
|
Play,
|
|
|
|
|
Pause,
|
|
|
|
|
X,
|
|
|
|
|
GripVertical,
|
|
|
|
|
Trash2,
|
|
|
|
|
Save,
|
|
|
|
|
ListMusic,
|
|
|
|
|
} from 'lucide-react';
|
2026-01-26 13:12:17 +00:00
|
|
|
import { useToast } from '../../../components/feedback/ToastProvider';
|
2026-01-07 09:31:02 +00:00
|
|
|
|
|
|
|
|
export const QueueView: React.FC = () => {
|
2026-01-13 18:47:57 +00:00
|
|
|
const {
|
|
|
|
|
queue,
|
|
|
|
|
currentTrack,
|
|
|
|
|
reorderQueue,
|
|
|
|
|
removeFromQueue,
|
|
|
|
|
clearQueue,
|
|
|
|
|
playTrack,
|
|
|
|
|
isPlaying,
|
|
|
|
|
togglePlay,
|
|
|
|
|
autoplay,
|
|
|
|
|
toggleAutoplay,
|
|
|
|
|
} = useAudio();
|
|
|
|
|
const { addToast } = useToast();
|
|
|
|
|
const [showSaveModal, setShowSaveModal] = useState(false);
|
|
|
|
|
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
feat(ui): remaining polish — DnD feedback, typography headings, lightbox, share dialog
Includes changes from previous session that weren't fully staged:
- PlaylistDetailView + QueueView: drag-over visual feedback
- PlaylistTrackListSortableItem: DnD opacity + shadow + insertion line
- ImageViewerModal: zoom toggle, keyboard nav, image counter, loading skeleton
- Badge: dismissible, pulse, dot-only enhancements
- ShareDialog: useCopyToClipboard integration
- SessionsPage, NotificationsPage, SettingsPage, DashboardPage: typography utility classes
- index.css: like-bounce, shake, empty-state-in, marquee, typography utilities
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 22:54:39 +00:00
|
|
|
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
const handleDragStart = (e: React.DragEvent, index: number) => {
|
|
|
|
|
setDraggedIndex(index);
|
|
|
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
|
|
|
// Transparent ghost image
|
|
|
|
|
const ghost = document.createElement('div');
|
|
|
|
|
ghost.style.opacity = '0';
|
|
|
|
|
document.body.appendChild(ghost);
|
|
|
|
|
e.dataTransfer.setDragImage(ghost, 0, 0);
|
|
|
|
|
setTimeout(() => document.body.removeChild(ghost), 0);
|
|
|
|
|
};
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
const handleDragOver = (e: React.DragEvent, index: number) => {
|
|
|
|
|
e.preventDefault();
|
feat(ui): remaining polish — DnD feedback, typography headings, lightbox, share dialog
Includes changes from previous session that weren't fully staged:
- PlaylistDetailView + QueueView: drag-over visual feedback
- PlaylistTrackListSortableItem: DnD opacity + shadow + insertion line
- ImageViewerModal: zoom toggle, keyboard nav, image counter, loading skeleton
- Badge: dismissible, pulse, dot-only enhancements
- ShareDialog: useCopyToClipboard integration
- SessionsPage, NotificationsPage, SettingsPage, DashboardPage: typography utility classes
- index.css: like-bounce, shake, empty-state-in, marquee, typography utilities
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 22:54:39 +00:00
|
|
|
setDragOverIndex(index);
|
2026-01-13 18:47:57 +00:00
|
|
|
if (draggedIndex === null || draggedIndex === index) return;
|
|
|
|
|
reorderQueue(draggedIndex, index);
|
|
|
|
|
setDraggedIndex(index);
|
|
|
|
|
};
|
2026-01-07 09:31:02 +00:00
|
|
|
|
feat(ui): remaining polish — DnD feedback, typography headings, lightbox, share dialog
Includes changes from previous session that weren't fully staged:
- PlaylistDetailView + QueueView: drag-over visual feedback
- PlaylistTrackListSortableItem: DnD opacity + shadow + insertion line
- ImageViewerModal: zoom toggle, keyboard nav, image counter, loading skeleton
- Badge: dismissible, pulse, dot-only enhancements
- ShareDialog: useCopyToClipboard integration
- SessionsPage, NotificationsPage, SettingsPage, DashboardPage: typography utility classes
- index.css: like-bounce, shake, empty-state-in, marquee, typography utilities
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 22:54:39 +00:00
|
|
|
const handleDragEnd = () => {
|
|
|
|
|
setDraggedIndex(null);
|
|
|
|
|
setDragOverIndex(null);
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
const handleSavePlaylist = (name: string, _isPublic: boolean) => {
|
|
|
|
|
addToast(`Queue saved as "${name}"`, 'success');
|
|
|
|
|
// Logic to actually save would connect to backend/context here
|
|
|
|
|
};
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
return (
|
|
|
|
|
<div className="max-w-4xl mx-auto space-y-6 animate-fadeIn pb-20">
|
ui(tokens): migrate border-kodo-steel to border-border (86 files, 269 instances)
Replace legacy hardcoded border-kodo-steel (RGB 59,69,84, theme-unaware)
with semantic border-border token across 86 user-facing components.
Covers UI primitives (checkbox, badge, modal, table, textarea, alert,
radio-group, avatar), all modals, settings views, social features,
playlist views, inventory, chat, commerce, and cloud file browser.
Only story/test files retain the legacy token.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 23:07:00 +00:00
|
|
|
<div className="flex flex-col md:flex-row justify-between items-end border-b border-border/50 pb-6 gap-4">
|
2026-01-13 18:47:57 +00:00
|
|
|
<div>
|
2026-02-12 00:49:07 +00:00
|
|
|
<h1 className="text-3xl font-heading font-bold text-foreground mb-2">
|
2026-01-13 18:47:57 +00:00
|
|
|
PLAY QUEUE
|
|
|
|
|
</h1>
|
2026-02-08 23:04:51 +00:00
|
|
|
<p className="text-muted-foreground font-mono text-sm">
|
2026-01-13 18:47:57 +00:00
|
|
|
{queue.length} tracks upcoming
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
aesthetic-improvements: align spacing to 8px grid (Action 11.2.1.3)
- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid
- Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px)
- Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation
- Modified files across all components to ensure consistent 8px grid alignment
- Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
2026-01-16 10:50:46 +00:00
|
|
|
<div className="flex gap-4">
|
2026-01-13 18:47:57 +00:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={() => setShowSaveModal(true)}
|
|
|
|
|
icon={<Save className="w-4 h-4" />}
|
|
|
|
|
>
|
|
|
|
|
Save Queue
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
2026-02-08 23:17:14 +00:00
|
|
|
className="text-destructive hover:bg-destructive/10"
|
2026-01-13 18:47:57 +00:00
|
|
|
onClick={clearQueue}
|
|
|
|
|
icon={<Trash2 className="w-4 h-4" />}
|
|
|
|
|
>
|
|
|
|
|
Clear
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Current Track */}
|
|
|
|
|
{currentTrack && (
|
|
|
|
|
<div>
|
2026-02-08 23:04:51 +00:00
|
|
|
<h3 className="text-xs font-bold text-muted-foreground uppercase tracking-widest mb-3">
|
2026-01-13 18:47:57 +00:00
|
|
|
Now Playing
|
|
|
|
|
</h3>
|
|
|
|
|
<Card
|
2026-01-26 13:12:17 +00:00
|
|
|
variant="glass"
|
refactor: Phase 3a — Global color class migration to SUMI semantics
- Replace all kodo-* color classes across ~100 TSX files:
kodo-void → background, kodo-ink → card, kodo-graphite → muted,
kodo-steel → muted-foreground, kodo-cyan → primary, kodo-magenta → destructive,
kodo-lime → success, kodo-red → destructive, kodo-gold → warning
- Replace cyan-500, magenta-500, lime-500 default Tailwind colors with
semantic equivalents (primary, destructive, success)
- Fix WaveformVisualizer hardcoded hex colors to SUMI values
- Delete global-effects.css (conflicting, redundant with index.css)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 00:51:49 +00:00
|
|
|
className="flex items-center gap-4 p-4 border-l-4 border-l-primary"
|
2026-01-13 18:47:57 +00:00
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className="relative w-16 h-16 rounded overflow-hidden flex-shrink-0 group cursor-pointer"
|
|
|
|
|
onClick={togglePlay}
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
src={currentTrack.coverUrl}
|
|
|
|
|
className="w-full h-full object-cover"
|
|
|
|
|
/>
|
refactor(tokens): complete design token migration to semantic system
Sprint 3.1 — Default colors → semantic (~15 files, ~99 replacements):
- lime-500 → success, red-500 → destructive, cyan-500 → primary
Sprint 3.2 — Hardcoded colors → semantic (~13 files, ~99 replacements):
- text-white → text-foreground, bg-black → bg-background, bg-white → bg-card
Sprint 3.3 — Legacy kodo-* → semantic (~27 files, ~122 replacements):
- bg-kodo-ink → bg-card, bg-kodo-void → bg-background, text-kodo-steel → text-muted-foreground
- Preserved kodo-cyan/magenta/lime/gold palette accents and gradients
Sprint 3.4 — Arbitrary values → Tailwind scale (5 replacements):
- min-h-[600px] → min-h-layout-page, min-h-[400px] → min-h-layout-page-sm
- left-[50%] → left-1/2, min-h-[80px] → min-h-20, min-h-[40px] → min-h-10
Sprint 3.5 — Border-radius standardization (4 replacements):
- Modal/dialog skeletons: rounded-lg → rounded-xl (convention)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 22:05:09 +00:00
|
|
|
<div className="absolute inset-0 bg-background/40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
2026-01-13 18:47:57 +00:00
|
|
|
{isPlaying ? (
|
2026-02-08 23:23:00 +00:00
|
|
|
<Pause className="w-6 h-6 text-foreground" />
|
2026-01-13 18:47:57 +00:00
|
|
|
) : (
|
2026-02-08 23:23:00 +00:00
|
|
|
<Play className="w-6 h-6 text-foreground fill-current ml-1" />
|
2026-01-13 18:47:57 +00:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{isPlaying && (
|
|
|
|
|
<div className="absolute bottom-1 right-1 flex gap-0.5 items-end h-3">
|
ui(tokens): migrate kodo-cyan to primary (51 files, 88 instances)
Replace legacy text-kodo-cyan/border-kodo-cyan/bg-kodo-cyan with semantic
text-primary/border-primary/bg-primary across 51 components.
The brand primary color now uses the design system token, enabling proper
theme adaptation. Covers UI primitives, search, dashboard, chat, playlists,
settings, social, marketplace, and auth components.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 23:19:12 +00:00
|
|
|
<div className="w-1 bg-primary animate-[bounce_1s_infinite] h-full"></div>
|
|
|
|
|
<div className="w-1 bg-primary animate-[bounce_1.2s_infinite] h-2/3"></div>
|
|
|
|
|
<div className="w-1 bg-primary animate-[bounce_0.8s_infinite] h-full"></div>
|
2026-01-07 10:15:48 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
)}
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
<div className="flex-1">
|
2026-02-08 23:23:00 +00:00
|
|
|
<h2 className="text-xl font-bold text-foreground">
|
2026-01-13 18:47:57 +00:00
|
|
|
{currentTrack.title}
|
|
|
|
|
</h2>
|
refactor(tokens): complete design token migration to semantic system
Sprint 3.1 — Default colors → semantic (~15 files, ~99 replacements):
- lime-500 → success, red-500 → destructive, cyan-500 → primary
Sprint 3.2 — Hardcoded colors → semantic (~13 files, ~99 replacements):
- text-white → text-foreground, bg-black → bg-background, bg-white → bg-card
Sprint 3.3 — Legacy kodo-* → semantic (~27 files, ~122 replacements):
- bg-kodo-ink → bg-card, bg-kodo-void → bg-background, text-kodo-steel → text-muted-foreground
- Preserved kodo-cyan/magenta/lime/gold palette accents and gradients
Sprint 3.4 — Arbitrary values → Tailwind scale (5 replacements):
- min-h-[600px] → min-h-layout-page, min-h-[400px] → min-h-layout-page-sm
- left-[50%] → left-1/2, min-h-[80px] → min-h-20, min-h-[40px] → min-h-10
Sprint 3.5 — Border-radius standardization (4 replacements):
- Modal/dialog skeletons: rounded-lg → rounded-xl (convention)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 22:05:09 +00:00
|
|
|
<p className="text-muted-foreground">{currentTrack.artist}</p>
|
2026-01-13 18:47:57 +00:00
|
|
|
</div>
|
2026-02-08 23:04:51 +00:00
|
|
|
<div className="text-muted-foreground font-mono text-sm hidden md:block">
|
2026-01-13 18:47:57 +00:00
|
|
|
{currentTrack.duration}
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
{/* Up Next */}
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex justify-between items-center mb-3">
|
2026-02-08 23:04:51 +00:00
|
|
|
<h3 className="text-xs font-bold text-muted-foreground uppercase tracking-widest">
|
2026-01-13 18:47:57 +00:00
|
|
|
Up Next
|
|
|
|
|
</h3>
|
|
|
|
|
<div
|
|
|
|
|
className="flex items-center gap-2 cursor-pointer group"
|
|
|
|
|
onClick={toggleAutoplay}
|
|
|
|
|
>
|
|
|
|
|
<span
|
2026-02-08 23:14:40 +00:00
|
|
|
className={`text-xs font-bold ${autoplay ? 'text-success' : 'text-muted-foreground'}`}
|
2026-01-13 18:47:57 +00:00
|
|
|
>
|
|
|
|
|
Autoplay
|
|
|
|
|
</span>
|
|
|
|
|
<div
|
2026-02-08 23:17:14 +00:00
|
|
|
className={`w-8 h-4 rounded-full relative transition-colors ${autoplay ? 'bg-success' : 'bg-muted'}`}
|
2026-01-13 18:47:57 +00:00
|
|
|
>
|
|
|
|
|
<div
|
2026-02-10 08:45:30 +00:00
|
|
|
className={`absolute top-0.5 w-3 h-3 bg-background rounded-full transition-all ${autoplay ? 'left-4.5' : 'left-0.5'}`}
|
2026-01-13 18:47:57 +00:00
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
<div className="space-y-2">
|
|
|
|
|
{queue.length === 0 ? (
|
2026-02-09 22:23:09 +00:00
|
|
|
<EmptyState
|
|
|
|
|
variant="card"
|
|
|
|
|
icon={<ListMusic className="w-full h-full" />}
|
|
|
|
|
title="Nothing in your queue"
|
|
|
|
|
description={
|
|
|
|
|
autoplay
|
|
|
|
|
? 'Autoplay is on — we\u2019ll pick something for you.'
|
|
|
|
|
: 'Start playing music and add tracks to build your queue.'
|
|
|
|
|
}
|
|
|
|
|
size="md"
|
|
|
|
|
/>
|
2026-01-13 18:47:57 +00:00
|
|
|
) : (
|
|
|
|
|
queue.map((track, i) => (
|
|
|
|
|
<div
|
|
|
|
|
key={`${track.id}-${i}`}
|
|
|
|
|
draggable
|
|
|
|
|
onDragStart={(e) => handleDragStart(e, i)}
|
|
|
|
|
onDragOver={(e) => handleDragOver(e, i)}
|
feat(ui): remaining polish — DnD feedback, typography headings, lightbox, share dialog
Includes changes from previous session that weren't fully staged:
- PlaylistDetailView + QueueView: drag-over visual feedback
- PlaylistTrackListSortableItem: DnD opacity + shadow + insertion line
- ImageViewerModal: zoom toggle, keyboard nav, image counter, loading skeleton
- Badge: dismissible, pulse, dot-only enhancements
- ShareDialog: useCopyToClipboard integration
- SessionsPage, NotificationsPage, SettingsPage, DashboardPage: typography utility classes
- index.css: like-bounce, shake, empty-state-in, marquee, typography utilities
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 22:54:39 +00:00
|
|
|
onDragEnd={handleDragEnd}
|
|
|
|
|
className={`flex items-center gap-4 p-4 bg-card rounded-lg border border-transparent hover:border-border transition-all group ${draggedIndex === i ? 'opacity-50 border-primary shadow-lg scale-[1.02] cursor-grabbing' : ''} ${dragOverIndex === i && draggedIndex !== null && draggedIndex !== i ? 'border-t-2 border-t-primary bg-primary/5' : ''}`}
|
2026-01-13 18:47:57 +00:00
|
|
|
>
|
2026-02-08 23:23:00 +00:00
|
|
|
<div className="text-muted-foreground cursor-grab active:cursor-grabbing hover:text-foreground p-1">
|
2026-01-13 18:47:57 +00:00
|
|
|
<GripVertical className="w-5 h-5" />
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
<div className="w-10 h-10 rounded overflow-hidden flex-shrink-0 relative">
|
|
|
|
|
<img
|
|
|
|
|
src={track.coverUrl}
|
|
|
|
|
className="w-full h-full object-cover"
|
|
|
|
|
/>
|
|
|
|
|
<div
|
refactor(tokens): complete design token migration to semantic system
Sprint 3.1 — Default colors → semantic (~15 files, ~99 replacements):
- lime-500 → success, red-500 → destructive, cyan-500 → primary
Sprint 3.2 — Hardcoded colors → semantic (~13 files, ~99 replacements):
- text-white → text-foreground, bg-black → bg-background, bg-white → bg-card
Sprint 3.3 — Legacy kodo-* → semantic (~27 files, ~122 replacements):
- bg-kodo-ink → bg-card, bg-kodo-void → bg-background, text-kodo-steel → text-muted-foreground
- Preserved kodo-cyan/magenta/lime/gold palette accents and gradients
Sprint 3.4 — Arbitrary values → Tailwind scale (5 replacements):
- min-h-[600px] → min-h-layout-page, min-h-[400px] → min-h-layout-page-sm
- left-[50%] → left-1/2, min-h-[80px] → min-h-20, min-h-[40px] → min-h-10
Sprint 3.5 — Border-radius standardization (4 replacements):
- Modal/dialog skeletons: rounded-lg → rounded-xl (convention)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 22:05:09 +00:00
|
|
|
className="absolute inset-0 bg-background/50 hidden group-hover:flex items-center justify-center cursor-pointer"
|
2026-01-13 18:47:57 +00:00
|
|
|
onClick={() => playTrack(track)}
|
|
|
|
|
>
|
2026-02-08 23:23:00 +00:00
|
|
|
<Play className="w-4 h-4 text-foreground fill-current" />
|
2026-01-13 18:47:57 +00:00
|
|
|
</div>
|
2026-01-07 10:15:48 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
<div className="flex-1 min-w-0">
|
2026-02-08 23:23:00 +00:00
|
|
|
<div className="text-sm font-bold text-foreground truncate">
|
2026-01-13 18:47:57 +00:00
|
|
|
{track.title}
|
|
|
|
|
</div>
|
2026-02-08 23:04:51 +00:00
|
|
|
<div className="text-xs text-muted-foreground truncate">
|
2026-01-13 18:47:57 +00:00
|
|
|
{track.artist}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-08 23:04:51 +00:00
|
|
|
<div className="text-muted-foreground font-mono text-xs hidden sm:block">
|
2026-01-13 18:47:57 +00:00
|
|
|
{track.duration}
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
2026-02-08 23:14:40 +00:00
|
|
|
className="p-2 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity"
|
2026-01-13 18:47:57 +00:00
|
|
|
onClick={() => removeFromQueue(track.id)}
|
|
|
|
|
>
|
|
|
|
|
<X className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
))
|
|
|
|
|
)}
|
2026-01-07 10:15:48 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{showSaveModal && (
|
|
|
|
|
<SaveQueueAsPlaylistModal
|
|
|
|
|
onClose={() => setShowSaveModal(false)}
|
|
|
|
|
onSave={handleSavePlaylist}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|