mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-02 02:19:43 +00:00
Update
This commit is contained in:
1 parent
456459bb00
commit
5b97bc6f56
60 files changed
+1440
-1974
No files matched your search
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -10,8 +11,8 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Sparkles, Loader2, RefreshCw, Check } from 'lucide-react';
|
||||
import { generatePack, generateQuestions, suggestCategories, suggestPacks, type GeneratedPack, type GeneratedQuestion, type GeneratedCategoryIdea, type GeneratedPackIdea } from '@/lib/openai';
|
||||
import { Sparkles, Loader2, RefreshCw, Check, CheckSquare, Square } from 'lucide-react';
|
||||
import { generatePack, generateQuestions, suggestCategories, suggestPacks, TONE_LEVELS, INTENSITY_LEVELS, type GeneratedPack, type GeneratedQuestion, type GeneratedCategoryIdea, type GeneratedPackIdea, type ToneLevel } from '@/lib/openai';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -22,8 +23,10 @@ interface AiGeneratorDialogProps {
|
||||
context?: {
|
||||
categoryName?: string;
|
||||
packName?: string;
|
||||
packDescription?: string | null;
|
||||
existingCategories?: string[];
|
||||
existingPacks?: string[];
|
||||
isExplicit?: boolean;
|
||||
};
|
||||
onGenerated: (result: any) => void;
|
||||
}
|
||||
@@ -37,7 +40,16 @@ export function AiGeneratorDialog({
|
||||
}: AiGeneratorDialogProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [count, setCount] = useState(10);
|
||||
const [isExplicit, setIsExplicit] = useState(false);
|
||||
// Default tone based on pack's explicit flag (5 for explicit packs, 3 for non-explicit)
|
||||
const [tone, setTone] = useState<ToneLevel>(context?.isExplicit ? 5 : 3);
|
||||
const isExplicit = tone >= 4; // For other generators that still use boolean
|
||||
|
||||
// Update tone when dialog opens with new context
|
||||
useEffect(() => {
|
||||
if (open && type === 'questions') {
|
||||
setTone(context?.isExplicit ? 5 : 3);
|
||||
}
|
||||
}, [open, context?.isExplicit, type]);
|
||||
|
||||
// Generated results
|
||||
const [generatedPack, setGeneratedPack] = useState<GeneratedPack | null>(null);
|
||||
@@ -45,6 +57,9 @@ export function AiGeneratorDialog({
|
||||
const [generatedIdeas, setGeneratedIdeas] = useState<GeneratedCategoryIdea[]>([]);
|
||||
const [generatedPackIdeas, setGeneratedPackIdeas] = useState<GeneratedPackIdea[]>([]);
|
||||
|
||||
// Selection state for questions
|
||||
const [selectedQuestionIndices, setSelectedQuestionIndices] = useState<Set<number>>(new Set());
|
||||
|
||||
const handleGenerate = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -56,8 +71,10 @@ export function AiGeneratorDialog({
|
||||
toast.error('Pack name is required');
|
||||
return;
|
||||
}
|
||||
const result = await generateQuestions(context.packName, count, undefined, isExplicit);
|
||||
const result = await generateQuestions(context.packName, count, undefined, tone, context.packDescription || undefined);
|
||||
setGeneratedQuestions(result);
|
||||
// Select all questions by default
|
||||
setSelectedQuestionIndices(new Set(result.map((_, i) => i)));
|
||||
} else if (type === 'category-ideas') {
|
||||
const result = await suggestCategories(context?.existingCategories || [], isExplicit);
|
||||
setGeneratedIdeas(result);
|
||||
@@ -81,16 +98,43 @@ export function AiGeneratorDialog({
|
||||
if (type === 'pack' && generatedPack) {
|
||||
onGenerated(generatedPack);
|
||||
} else if (type === 'questions' && generatedQuestions.length > 0) {
|
||||
onGenerated(generatedQuestions);
|
||||
// Only pass selected questions
|
||||
const selectedQuestions = generatedQuestions.filter((_, i) => selectedQuestionIndices.has(i));
|
||||
if (selectedQuestions.length === 0) {
|
||||
toast.error('Please select at least one question');
|
||||
return;
|
||||
}
|
||||
onGenerated(selectedQuestions);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleQuestionSelection = (index: number) => {
|
||||
setSelectedQuestionIndices(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(index)) {
|
||||
newSet.delete(index);
|
||||
} else {
|
||||
newSet.add(index);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const selectAllQuestions = () => {
|
||||
setSelectedQuestionIndices(new Set(generatedQuestions.map((_, i) => i)));
|
||||
};
|
||||
|
||||
const deselectAllQuestions = () => {
|
||||
setSelectedQuestionIndices(new Set());
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
// Reset state
|
||||
setGeneratedPack(null);
|
||||
setGeneratedQuestions([]);
|
||||
setGeneratedIdeas([]);
|
||||
setGeneratedPackIdeas([]);
|
||||
setSelectedQuestionIndices(new Set());
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
@@ -130,13 +174,38 @@ export function AiGeneratorDialog({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Explicit Toggle */}
|
||||
{(type === 'category-ideas' || type === 'category-pack-ideas' || type === 'pack' || type === 'questions') && (
|
||||
{/* Tone Selector for Questions */}
|
||||
{type === 'questions' && (
|
||||
<div className="space-y-2">
|
||||
<Label>Content Tone</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{TONE_LEVELS.map((t) => (
|
||||
<Button
|
||||
key={t.level}
|
||||
type="button"
|
||||
variant={tone === t.level ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setTone(t.level as ToneLevel)}
|
||||
className={tone === t.level ? 'ring-2 ring-offset-1' : ''}
|
||||
title={t.description}
|
||||
>
|
||||
{t.level}. {t.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{TONE_LEVELS.find(t => t.level === tone)?.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Explicit Toggle for other generators */}
|
||||
{(type === 'category-ideas' || type === 'category-pack-ideas' || type === 'pack') && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="explicit-mode"
|
||||
checked={isExplicit}
|
||||
onCheckedChange={setIsExplicit}
|
||||
onCheckedChange={(checked) => setTone(checked ? 5 : 3)}
|
||||
/>
|
||||
<Label htmlFor="explicit-mode">Suggest Explicit Ideas</Label>
|
||||
</div>
|
||||
@@ -149,8 +218,11 @@ export function AiGeneratorDialog({
|
||||
</div>
|
||||
)}
|
||||
{context?.packName && (
|
||||
<div className="rounded-md bg-muted p-3 text-sm">
|
||||
<strong>Pack:</strong> {context.packName}
|
||||
<div className="rounded-md bg-muted p-3 text-sm space-y-1">
|
||||
<div><strong>Pack:</strong> {context.packName}</div>
|
||||
{context.packDescription && (
|
||||
<div className="text-muted-foreground text-xs">{context.packDescription}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -185,19 +257,66 @@ export function AiGeneratorDialog({
|
||||
{/* Generated Questions Preview */}
|
||||
{generatedQuestions.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-medium">Generated Questions ({generatedQuestions.length})</h4>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-medium">
|
||||
Generated Questions ({selectedQuestionIndices.size}/{generatedQuestions.length} selected)
|
||||
</h4>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={selectAllQuestions}
|
||||
disabled={selectedQuestionIndices.size === generatedQuestions.length}
|
||||
>
|
||||
<CheckSquare className="mr-1 h-3 w-3" />
|
||||
All
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={deselectAllQuestions}
|
||||
disabled={selectedQuestionIndices.size === 0}
|
||||
>
|
||||
<Square className="mr-1 h-3 w-3" />
|
||||
None
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||
{generatedQuestions.map((q, i) => (
|
||||
<div key={i} className="rounded-lg border bg-card p-3 text-sm">
|
||||
<p>{q.text}</p>
|
||||
{q.partner_text && (
|
||||
<p className="text-muted-foreground text-xs mt-1">
|
||||
Partner: {q.partner_text}
|
||||
</p>
|
||||
)}
|
||||
<span className="inline-block mt-1 text-xs px-2 py-0.5 rounded bg-muted">
|
||||
{q.intensity} - {['Light', 'Mild', 'Moderate', 'Spicy', 'Very Intense'][q.intensity - 1]}
|
||||
</span>
|
||||
<div
|
||||
key={i}
|
||||
className={`rounded-lg border bg-card p-3 text-sm cursor-pointer transition-colors ${
|
||||
selectedQuestionIndices.has(i)
|
||||
? 'border-primary ring-1 ring-primary/20'
|
||||
: 'opacity-60 hover:opacity-80'
|
||||
}`}
|
||||
onClick={() => toggleQuestionSelection(i)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
checked={selectedQuestionIndices.has(i)}
|
||||
onCheckedChange={() => toggleQuestionSelection(i)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<p>{q.text}</p>
|
||||
{q.partner_text && (
|
||||
<p className="text-muted-foreground text-xs mt-1">
|
||||
Partner: {q.partner_text}
|
||||
</p>
|
||||
)}
|
||||
<span
|
||||
className={`inline-block mt-1 text-xs px-2 py-0.5 rounded text-white ${INTENSITY_LEVELS[q.intensity - 1]?.color || 'bg-muted'}`}
|
||||
title={INTENSITY_LEVELS[q.intensity - 1]?.description}
|
||||
>
|
||||
{q.intensity} - {INTENSITY_LEVELS[q.intensity - 1]?.label || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -217,7 +336,10 @@ export function AiGeneratorDialog({
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl">{idea.icon}</span>
|
||||
<span className="font-medium">{idea.name}</span>
|
||||
<div>
|
||||
<span className="font-medium block">{idea.name}</span>
|
||||
<span className="text-xs text-muted-foreground">{idea.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" variant="ghost">Use</Button>
|
||||
</div>
|
||||
@@ -262,9 +384,9 @@ export function AiGeneratorDialog({
|
||||
Regenerate
|
||||
</Button>
|
||||
{type !== 'category-ideas' && type !== 'category-pack-ideas' && (
|
||||
<Button onClick={handleUse}>
|
||||
<Button onClick={handleUse} disabled={type === 'questions' && selectedQuestionIndices.size === 0}>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
Use This
|
||||
{type === 'questions' ? `Use Selected (${selectedQuestionIndices.size})` : 'Use This'}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Loader2, ShieldCheck, ArrowRight, Check } from 'lucide-react';
|
||||
import { analyzeQuestionTargets, TargetAnalysis } from '@/lib/openai';
|
||||
import { supabase } from '@/config';
|
||||
import { auditedSupabase } from '@/hooks/useAuditedSupabase';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// Define local compatible interface
|
||||
@@ -22,6 +22,7 @@ interface Question {
|
||||
partner_text: string | null;
|
||||
intensity: number;
|
||||
allowed_couple_genders: string[] | null;
|
||||
target_user_genders: string[] | null;
|
||||
}
|
||||
|
||||
interface FixTargetsDialogProps {
|
||||
@@ -38,13 +39,58 @@ export function FixTargetsDialog({ open, onOpenChange, questions, onUpdated }: F
|
||||
const [step, setStep] = useState<'initial' | 'review'>('initial');
|
||||
const [applying, setApplying] = useState(false);
|
||||
|
||||
// Check if couple targets represent "All" (null, empty, or all three values)
|
||||
const isAllCoupleTargets = (targets: string[] | null): boolean => {
|
||||
if (!targets || targets.length === 0) return true;
|
||||
if (targets.length === 3) {
|
||||
const sorted = [...targets].sort();
|
||||
return sorted[0] === 'female+female' && sorted[1] === 'female+male' && sorted[2] === 'male+male';
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Check if initiator targets represent "All" (null or empty)
|
||||
const isAllInitiator = (targets: string[] | null): boolean => {
|
||||
return !targets || targets.length === 0;
|
||||
};
|
||||
|
||||
// Check if two arrays are equal
|
||||
const arraysEqual = (a: string[] | null, b: string[] | null): boolean => {
|
||||
if (!a && !b) return true;
|
||||
if (!a || !b) return false;
|
||||
if (a.length !== b.length) return false;
|
||||
const sortedA = [...a].sort();
|
||||
const sortedB = [...b].sort();
|
||||
return sortedA.every((val, i) => val === sortedB[i]);
|
||||
};
|
||||
|
||||
const handleAnalyze = async () => {
|
||||
setAnalyzing(true);
|
||||
try {
|
||||
const results = await analyzeQuestionTargets(questions);
|
||||
setSuggestions(results);
|
||||
// Filter out redundant suggestions where nothing meaningful changes
|
||||
const meaningfulResults = results.filter(suggestion => {
|
||||
const question = questions.find(q => q.id === suggestion.id);
|
||||
if (!question) return false;
|
||||
|
||||
// Check if couple targets change meaningfully
|
||||
const currentCoupleIsAll = isAllCoupleTargets(question.allowed_couple_genders);
|
||||
const suggestedCoupleIsAll = isAllCoupleTargets(suggestion.suggested_targets);
|
||||
const coupleChanged = !(currentCoupleIsAll && suggestedCoupleIsAll) &&
|
||||
!arraysEqual(question.allowed_couple_genders, suggestion.suggested_targets);
|
||||
|
||||
// Check if initiator changes meaningfully
|
||||
const currentInitiatorIsAll = isAllInitiator(question.target_user_genders);
|
||||
const suggestedInitiatorIsAll = isAllInitiator(suggestion.suggested_initiator);
|
||||
const initiatorChanged = !(currentInitiatorIsAll && suggestedInitiatorIsAll) &&
|
||||
!arraysEqual(question.target_user_genders, suggestion.suggested_initiator);
|
||||
|
||||
// Include if either changed
|
||||
return coupleChanged || initiatorChanged;
|
||||
});
|
||||
setSuggestions(meaningfulResults);
|
||||
// Default select all
|
||||
setSelectedIds(new Set(results.map(r => r.id)));
|
||||
setSelectedIds(new Set(meaningfulResults.map(r => r.id)));
|
||||
setStep('review');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -63,12 +109,12 @@ export function FixTargetsDialog({ open, onOpenChange, questions, onUpdated }: F
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply updates in parallel
|
||||
// Apply updates in parallel with audit logging
|
||||
await Promise.all(updates.map(update =>
|
||||
supabase
|
||||
.from('questions')
|
||||
.update({ allowed_couple_genders: update.suggested_targets && update.suggested_targets.length > 0 ? update.suggested_targets : null })
|
||||
.eq('id', update.id)
|
||||
auditedSupabase.update('questions', update.id, {
|
||||
allowed_couple_genders: update.suggested_targets && update.suggested_targets.length > 0 ? update.suggested_targets : null,
|
||||
target_user_genders: update.suggested_initiator && update.suggested_initiator.length > 0 ? update.suggested_initiator : null,
|
||||
})
|
||||
));
|
||||
|
||||
toast.success(`Updated ${updates.length} questions`);
|
||||
@@ -103,6 +149,11 @@ export function FixTargetsDialog({ open, onOpenChange, questions, onUpdated }: F
|
||||
return targets.map(t => t.replace('male+male', 'M+M').replace('female+male', 'M+F').replace('female+female', 'F+F')).join(', ');
|
||||
};
|
||||
|
||||
const formatInitiator = (targets: string[] | null) => {
|
||||
if (!targets || targets.length === 0) return 'Any';
|
||||
return targets.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(', ');
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col">
|
||||
@@ -163,8 +214,8 @@ export function FixTargetsDialog({ open, onOpenChange, questions, onUpdated }: F
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>Question</TableHead>
|
||||
<TableHead>Current</TableHead>
|
||||
<TableHead>Suggested</TableHead>
|
||||
<TableHead>Couples</TableHead>
|
||||
<TableHead>Initiator</TableHead>
|
||||
<TableHead>Reason</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -190,19 +241,28 @@ export function FixTargetsDialog({ open, onOpenChange, questions, onUpdated }: F
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs whitespace-nowrap">
|
||||
<Badge variant="outline" className="text-muted-foreground">
|
||||
{formatTargets(question.allowed_couple_genders)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs whitespace-nowrap">
|
||||
<TableCell className="text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<Badge variant="outline" className="text-muted-foreground">
|
||||
{formatTargets(question.allowed_couple_genders)}
|
||||
</Badge>
|
||||
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
||||
<Badge variant="secondary" className="bg-indigo-100 text-indigo-700 hover:bg-indigo-100">
|
||||
{formatTargets(suggestion.suggested_targets)}
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<Badge variant="outline" className="text-muted-foreground border-orange-300">
|
||||
{formatInitiator(question.target_user_genders)}
|
||||
</Badge>
|
||||
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
||||
<Badge variant="secondary" className="bg-orange-100 text-orange-700 hover:bg-orange-100">
|
||||
{formatInitiator(suggestion.suggested_initiator)}
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">
|
||||
{suggestion.reason}
|
||||
</TableCell>
|
||||
|
||||
@@ -1,15 +1,55 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import { ChevronRight, Home } from 'lucide-react';
|
||||
import { supabase } from '@/config';
|
||||
|
||||
interface BreadcrumbItem {
|
||||
label: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
interface PackInfo {
|
||||
category_id: string | null;
|
||||
category_name: string | null;
|
||||
}
|
||||
|
||||
// Generate breadcrumbs based on current path and params
|
||||
function useBreadcrumbs(): BreadcrumbItem[] {
|
||||
function useBreadcrumbs(): { breadcrumbs: BreadcrumbItem[]; loading: boolean } {
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
const [packInfo, setPackInfo] = useState<PackInfo | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Fetch pack info when on questions page to get category_id
|
||||
useEffect(() => {
|
||||
const fetchPackInfo = async () => {
|
||||
if (params.packId && !params.categoryId) {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('question_packs')
|
||||
.select('category_id, categories(name)')
|
||||
.eq('id', params.packId)
|
||||
.single();
|
||||
|
||||
if (data) {
|
||||
setPackInfo({
|
||||
category_id: data.category_id,
|
||||
category_name: (data.categories as any)?.name || null,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch pack info for breadcrumbs:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
setPackInfo(null);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPackInfo();
|
||||
}, [params.packId, params.categoryId]);
|
||||
|
||||
const pathSegments = location.pathname.split('/').filter(Boolean);
|
||||
const breadcrumbs: BreadcrumbItem[] = [];
|
||||
@@ -23,6 +63,13 @@ function useBreadcrumbs(): BreadcrumbItem[] {
|
||||
breadcrumbs.push({ label: 'Categories', href: '/categories' });
|
||||
} else if (segment === 'packs' && params.categoryId) {
|
||||
breadcrumbs.push({ label: 'Packs', href: currentPath });
|
||||
} else if (segment === 'packs' && params.packId && packInfo?.category_id) {
|
||||
// On questions page, link back to category's packs
|
||||
breadcrumbs.push({ label: 'Categories', href: '/categories' });
|
||||
breadcrumbs.push({
|
||||
label: packInfo.category_name || 'Packs',
|
||||
href: `/categories/${packInfo.category_id}/packs`
|
||||
});
|
||||
} else if (segment === 'questions' && params.packId) {
|
||||
breadcrumbs.push({ label: 'Questions', href: currentPath });
|
||||
} else if (segment === 'users') {
|
||||
@@ -44,13 +91,13 @@ function useBreadcrumbs(): BreadcrumbItem[] {
|
||||
breadcrumbs[breadcrumbs.length - 1].href = undefined;
|
||||
}
|
||||
|
||||
return breadcrumbs;
|
||||
return { breadcrumbs, loading };
|
||||
}
|
||||
|
||||
export function Breadcrumbs() {
|
||||
const breadcrumbs = useBreadcrumbs();
|
||||
const { breadcrumbs, loading } = useBreadcrumbs();
|
||||
|
||||
if (breadcrumbs.length === 0) return null;
|
||||
if (breadcrumbs.length === 0 && !loading) return null;
|
||||
|
||||
return (
|
||||
<nav className="flex items-center space-x-1 text-sm text-muted-foreground">
|
||||
|
||||
Reference in new issue
Block a user