mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Remove E2EE and switch to plaintext messaging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
1 parent
43fa7033dc
commit
c48936686e
46 files changed
+738
-6874
No files matched your search
@@ -30,9 +30,11 @@
|
||||
"emoji-mart": "^5.6.0",
|
||||
"lucide-react": "^0.309.0",
|
||||
"openai": "^4.24.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.21.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-router-dom": "^6.21.0",
|
||||
|
||||
"recharts": "^3.6.0",
|
||||
"sonner": "^1.3.1",
|
||||
"tailwind-merge": "^2.2.0"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { EmojiPicker } from '@/components/ui/emoji-picker';
|
||||
import { IconPicker } from '@/components/ui/icon-picker';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -98,9 +98,9 @@ export function PackFormDialog({
|
||||
{/* Icon */}
|
||||
<div className="space-y-2">
|
||||
<Label>Icon</Label>
|
||||
<EmojiPicker
|
||||
<IconPicker
|
||||
value={formData.icon}
|
||||
onChange={(emoji) => setField('icon', emoji)}
|
||||
onChange={(iconName) => setField('icon', iconName)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { IconType } from 'react-icons';
|
||||
import {
|
||||
IoHeartOutline,
|
||||
IoHeartHalfOutline,
|
||||
IoHeartCircleOutline,
|
||||
IoChatbubblesOutline,
|
||||
IoChatboxOutline,
|
||||
IoMailOutline,
|
||||
IoFlowerOutline,
|
||||
IoWineOutline,
|
||||
IoRestaurantOutline,
|
||||
IoCafeOutline,
|
||||
IoAirplaneOutline,
|
||||
IoCarOutline,
|
||||
IoCompassOutline,
|
||||
IoMapOutline,
|
||||
IoHomeOutline,
|
||||
IoPeopleOutline,
|
||||
IoPersonOutline,
|
||||
IoEyeOffOutline,
|
||||
IoKeyOutline,
|
||||
IoLockClosedOutline,
|
||||
IoDiceOutline,
|
||||
IoGiftOutline,
|
||||
IoSparklesOutline,
|
||||
IoStarOutline,
|
||||
IoFlameOutline,
|
||||
IoFlashOutline,
|
||||
IoMoonOutline,
|
||||
IoSunnyOutline,
|
||||
IoFlagOutline,
|
||||
IoCalendarOutline,
|
||||
IoCheckboxOutline,
|
||||
IoTrophyOutline,
|
||||
IoLayersOutline,
|
||||
IoCubeOutline,
|
||||
IoFolderOutline,
|
||||
IoBookmarkOutline,
|
||||
IoBulbOutline,
|
||||
IoColorWandOutline,
|
||||
IoSyncOutline,
|
||||
IoRefreshOutline,
|
||||
IoBriefcaseOutline,
|
||||
} from 'react-icons/io5';
|
||||
import { Button } from './button';
|
||||
import { Input } from './input';
|
||||
|
||||
interface IconOption {
|
||||
name: string;
|
||||
Icon: IconType;
|
||||
}
|
||||
|
||||
const ICON_OPTIONS: IconOption[] = [
|
||||
// Relationships & Love
|
||||
{ name: 'heart-outline', Icon: IoHeartOutline },
|
||||
{ name: 'heart-half-outline', Icon: IoHeartHalfOutline },
|
||||
{ name: 'heart-circle-outline', Icon: IoHeartCircleOutline },
|
||||
|
||||
// Communication
|
||||
{ name: 'chatbubbles-outline', Icon: IoChatbubblesOutline },
|
||||
{ name: 'chatbox-outline', Icon: IoChatboxOutline },
|
||||
{ name: 'mail-outline', Icon: IoMailOutline },
|
||||
|
||||
// Romance & Dates
|
||||
{ name: 'flower-outline', Icon: IoFlowerOutline },
|
||||
{ name: 'wine-outline', Icon: IoWineOutline },
|
||||
{ name: 'restaurant-outline', Icon: IoRestaurantOutline },
|
||||
{ name: 'cafe-outline', Icon: IoCafeOutline },
|
||||
|
||||
// Adventure & Travel
|
||||
{ name: 'airplane-outline', Icon: IoAirplaneOutline },
|
||||
{ name: 'car-outline', Icon: IoCarOutline },
|
||||
{ name: 'compass-outline', Icon: IoCompassOutline },
|
||||
{ name: 'map-outline', Icon: IoMapOutline },
|
||||
|
||||
// Home & Family
|
||||
{ name: 'home-outline', Icon: IoHomeOutline },
|
||||
{ name: 'people-outline', Icon: IoPeopleOutline },
|
||||
{ name: 'person-outline', Icon: IoPersonOutline },
|
||||
|
||||
// Mystery & Secrets
|
||||
{ name: 'eye-off-outline', Icon: IoEyeOffOutline },
|
||||
{ name: 'key-outline', Icon: IoKeyOutline },
|
||||
{ name: 'lock-closed-outline', Icon: IoLockClosedOutline },
|
||||
|
||||
// Fun & Games
|
||||
{ name: 'dice-outline', Icon: IoDiceOutline },
|
||||
{ name: 'gift-outline', Icon: IoGiftOutline },
|
||||
{ name: 'sparkles-outline', Icon: IoSparklesOutline },
|
||||
{ name: 'star-outline', Icon: IoStarOutline },
|
||||
|
||||
// Intimacy
|
||||
{ name: 'flame-outline', Icon: IoFlameOutline },
|
||||
{ name: 'flash-outline', Icon: IoFlashOutline },
|
||||
{ name: 'moon-outline', Icon: IoMoonOutline },
|
||||
{ name: 'sunny-outline', Icon: IoSunnyOutline },
|
||||
|
||||
// Goals & Planning
|
||||
{ name: 'flag-outline', Icon: IoFlagOutline },
|
||||
{ name: 'calendar-outline', Icon: IoCalendarOutline },
|
||||
{ name: 'checkbox-outline', Icon: IoCheckboxOutline },
|
||||
{ name: 'trophy-outline', Icon: IoTrophyOutline },
|
||||
|
||||
// General
|
||||
{ name: 'layers-outline', Icon: IoLayersOutline },
|
||||
{ name: 'cube-outline', Icon: IoCubeOutline },
|
||||
{ name: 'folder-outline', Icon: IoFolderOutline },
|
||||
{ name: 'bookmark-outline', Icon: IoBookmarkOutline },
|
||||
{ name: 'bulb-outline', Icon: IoBulbOutline },
|
||||
{ name: 'color-wand-outline', Icon: IoColorWandOutline },
|
||||
{ name: 'sync-outline', Icon: IoSyncOutline },
|
||||
{ name: 'refresh-outline', Icon: IoRefreshOutline },
|
||||
{ name: 'briefcase-outline', Icon: IoBriefcaseOutline },
|
||||
];
|
||||
|
||||
const ICONS_BY_NAME = ICON_OPTIONS.reduce<Record<string, IconOption>>((acc, option) => {
|
||||
acc[option.name] = option;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
interface IconPreviewProps {
|
||||
value?: string | null;
|
||||
fallback?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function IconPreview({ value, fallback = '?', className }: IconPreviewProps) {
|
||||
const resolved = value ?? fallback;
|
||||
const Icon = resolved ? ICONS_BY_NAME[resolved]?.Icon : undefined;
|
||||
|
||||
if (Icon) {
|
||||
return <Icon className={className} />;
|
||||
}
|
||||
|
||||
return <span className={className}>{resolved}</span>;
|
||||
}
|
||||
|
||||
interface IconPickerProps {
|
||||
value: string;
|
||||
onChange: (iconName: string) => void;
|
||||
}
|
||||
|
||||
export function IconPicker({ value, onChange }: IconPickerProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (open) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const filteredIcons = useMemo(() => {
|
||||
const normalized = query.trim().toLowerCase();
|
||||
if (!normalized) return ICON_OPTIONS;
|
||||
return ICON_OPTIONS.filter((option) => option.name.includes(normalized));
|
||||
}, [query]);
|
||||
|
||||
const SelectedIcon = ICONS_BY_NAME[value]?.Icon;
|
||||
const buttonLabel = SelectedIcon ? value : value ? 'Legacy icon' : 'Select an icon';
|
||||
|
||||
return (
|
||||
<div className="relative" ref={containerRef}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="h-14 w-full justify-start gap-3 px-3"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{SelectedIcon ? (
|
||||
<SelectedIcon className="h-6 w-6" />
|
||||
) : (
|
||||
<span className="text-2xl">{value || '?'}</span>
|
||||
)}
|
||||
<span className="text-sm text-muted-foreground">{buttonLabel}</span>
|
||||
</Button>
|
||||
{open && (
|
||||
<div className="absolute top-full left-0 mt-2 w-[22rem] rounded-md border bg-background p-3 shadow-lg z-50">
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search icons..."
|
||||
className="mb-3"
|
||||
/>
|
||||
<div className="grid grid-cols-4 gap-2 max-h-64 overflow-y-auto pr-1">
|
||||
{filteredIcons.map(({ name, Icon }) => {
|
||||
const isSelected = name === value;
|
||||
return (
|
||||
<button
|
||||
key={name}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onChange(name);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={`flex flex-col items-center gap-1 rounded-md border p-2 text-xs transition-colors hover:bg-muted ${
|
||||
isSelected
|
||||
? 'border-primary ring-1 ring-primary/40'
|
||||
: 'border-border'
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
<span className="text-[10px] leading-tight text-muted-foreground">
|
||||
{name}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{filteredIcons.length === 0 && (
|
||||
<div className="col-span-4 text-xs text-muted-foreground">
|
||||
No icons found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -144,7 +144,8 @@ ${explicitInstruction}${crudeLangInstruction}${inspirationInstruction}${avoidIns
|
||||
Return a JSON object with an "ideas" array containing 5 objects, where each object has:
|
||||
- name: Pack name (catchy, 3-6 words) - should evoke activities/experiences, not questions
|
||||
- description: Brief description (1-2 sentences) focusing on the activities/experiences in the pack
|
||||
- icon: A single descriptive emoji
|
||||
- icon: An Ionicon name from this list: heart-outline, flame-outline, sparkles-outline, gift-outline, wine-outline, airplane-outline, home-outline, key-outline, flash-outline, sunny-outline, flower-outline, star-outline, dice-outline, compass-outline, bulb-outline
|
||||
|
||||
|
||||
Make them engaging and specific to "${categoryName}".`;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { EmojiPicker } from '@/components/ui/emoji-picker';
|
||||
import { IconPicker, IconPreview } from '@/components/ui/icon-picker';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -56,11 +56,11 @@ export function CategoriesPage() {
|
||||
|
||||
// Form state using the new hook
|
||||
const form = useEntityForm<CategoryFormData, Category>(
|
||||
{ name: '', description: '', icon: '📚' },
|
||||
{ name: '', description: '', icon: 'bookmark-outline' },
|
||||
(category) => ({
|
||||
name: category.name,
|
||||
description: category.description || '',
|
||||
icon: category.icon || '📚',
|
||||
icon: category.icon || 'bookmark-outline',
|
||||
})
|
||||
);
|
||||
|
||||
@@ -118,7 +118,7 @@ export function CategoriesPage() {
|
||||
form.openCreateWith({
|
||||
name: idea.name,
|
||||
description: idea.description || '',
|
||||
icon: idea.icon || '📚',
|
||||
icon: idea.icon || 'bookmark-outline',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -261,9 +261,9 @@ export function CategoriesPage() {
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Icon</Label>
|
||||
<EmojiPicker
|
||||
<IconPicker
|
||||
value={form.formData.icon}
|
||||
onChange={(emoji) => form.setField('icon', emoji)}
|
||||
onChange={(iconName) => form.setField('icon', iconName)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -363,7 +363,9 @@ export function CategoriesPage() {
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-4xl mb-2">{category.icon || '📚'}</div>
|
||||
<div className="text-4xl mb-2">
|
||||
<IconPreview value={category.icon} fallback="bookmark-outline" className="text-4xl" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
|
||||
<Button
|
||||
|
||||
@@ -12,6 +12,7 @@ import { toast } from 'sonner';
|
||||
import { AiGeneratorDialog } from '@/components/ai/AiGeneratorDialog';
|
||||
import { ExtractTopicsDialog } from '@/components/content/ExtractTopicsDialog';
|
||||
import { PackFormDialog, type PackFormData } from '@/components/content/PackFormDialog';
|
||||
import { IconPreview } from '@/components/ui/icon-picker';
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
@@ -65,7 +66,7 @@ export function PacksPage() {
|
||||
{
|
||||
name: '',
|
||||
description: '',
|
||||
icon: '💕',
|
||||
icon: 'heart-outline',
|
||||
is_premium: false,
|
||||
is_public: false,
|
||||
is_explicit: false,
|
||||
@@ -73,7 +74,7 @@ export function PacksPage() {
|
||||
(pack) => ({
|
||||
name: pack.name,
|
||||
description: pack.description || '',
|
||||
icon: pack.icon || '💕',
|
||||
icon: pack.icon || 'heart-outline',
|
||||
is_premium: pack.is_premium,
|
||||
is_public: pack.is_public,
|
||||
is_explicit: pack.is_explicit,
|
||||
@@ -265,7 +266,7 @@ export function PacksPage() {
|
||||
form.openCreateWith({
|
||||
name: result.name || '',
|
||||
description: result.description,
|
||||
icon: result.icon || '💕',
|
||||
icon: result.icon || 'heart-outline',
|
||||
is_premium: false,
|
||||
is_public: false,
|
||||
is_explicit: false,
|
||||
@@ -331,7 +332,9 @@ export function PacksPage() {
|
||||
<h1 className="text-3xl font-bold tracking-tight flex items-center gap-2">
|
||||
{category && (
|
||||
<>
|
||||
<span className="text-3xl">{category.icon}</span>
|
||||
<span className="text-3xl">
|
||||
<IconPreview value={category.icon} fallback="bookmark-outline" className="text-3xl" />
|
||||
</span>
|
||||
{category.name}:
|
||||
</>
|
||||
)}
|
||||
@@ -437,7 +440,9 @@ export function PacksPage() {
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-4xl mb-2">{pack.icon || '💕'}</div>
|
||||
<div className="text-4xl mb-2">
|
||||
<IconPreview value={pack.icon} fallback="heart-outline" className="text-4xl" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{pack.is_premium && (
|
||||
|
||||
Reference in new issue
Block a user