Skip to content

Commit

Permalink
Added categories and updated UI to support them
Browse files Browse the repository at this point in the history
  • Loading branch information
jordan-dalby committed Oct 27, 2024
1 parent 554fbf3 commit 72d3496
Show file tree
Hide file tree
Showing 13 changed files with 666 additions and 63 deletions.
167 changes: 167 additions & 0 deletions client/src/components/common/CategoryList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React, { useState, useRef, useEffect } from 'react';
import { ChevronDown, ChevronUp } from 'lucide-react';

interface CategoryListProps {
categories: string[];
onCategoryClick: (category: string) => void;
className?: string;
showAll?: boolean;
}

const CategoryList = ({
categories,
onCategoryClick,
className = '',
showAll = false
}: CategoryListProps) => {
const [isExpanded, setIsExpanded] = useState(false);
const [visibleCount, setVisibleCount] = useState(categories.length);
const containerRef = useRef<HTMLDivElement>(null);
const measureRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (showAll) return;

const calculateVisibleCount = () => {
const container = containerRef.current;
const measure = measureRef.current;
if (!container || !measure || categories.length === 0) return;

measure.style.visibility = 'hidden';
measure.style.display = 'flex';

const containerWidth = container.offsetWidth;
const items = Array.from(measure.children) as HTMLElement[];
let currentWidth = 0;
let count = 0;

const moreButtonWidth = items[items.length - 1].offsetWidth + 8;

for (let i = 0; i < items.length - 1; i++) {
const itemWidth = items[i].offsetWidth + 8;
if (currentWidth + itemWidth + moreButtonWidth > containerWidth) break;
currentWidth += itemWidth;
count++;
}

measure.style.display = 'none';

if (count > 0 && count !== visibleCount) {
setVisibleCount(count);
}
};

calculateVisibleCount();

window.addEventListener('resize', calculateVisibleCount);
return () => window.removeEventListener('resize', calculateVisibleCount);
}, [categories, visibleCount, showAll]);

const handleCategoryClick = (e: React.MouseEvent, category: string) => {
e.stopPropagation();
onCategoryClick(category);
};

const handleExpandClick = (e: React.MouseEvent) => {
e.stopPropagation();
setIsExpanded(true);
};

const handleCollapseClick = (e: React.MouseEvent) => {
e.stopPropagation();
setIsExpanded(false);
};

const getCategoryColor = (name: string) => {
const colors = [
'bg-blue-500/20 text-blue-200 hover:bg-blue-500/30',
'bg-green-500/20 text-green-200 hover:bg-green-500/30',
'bg-purple-500/20 text-purple-200 hover:bg-purple-500/30',
'bg-orange-500/20 text-orange-200 hover:bg-orange-500/30',
'bg-pink-500/20 text-pink-200 hover:bg-pink-500/30',
];

const hash = name.split('').reduce((acc, char) => {
return char.charCodeAt(0) + ((acc << 5) - acc);
}, 0);

return colors[Math.abs(hash) % colors.length];
};

const visibleCategories = showAll || isExpanded
? categories
: categories.slice(0, visibleCount);

const hasMoreCategories = !showAll && categories.length > visibleCount;
const moreCount = categories.length - visibleCount;

return (
<div className={`relative ${className}`}>
<div ref={containerRef} className="flex flex-wrap items-center gap-2">
{visibleCategories.map((category) => (
<button
key={category}
onClick={(e) => handleCategoryClick(e, category)}
className={`px-2 py-1 rounded-md text-xs font-medium
transition-colors duration-200
${getCategoryColor(category)}`}
>
{category}
</button>
))}

{hasMoreCategories && !isExpanded && (
<button
onClick={handleExpandClick}
className="flex items-center gap-1 px-2 py-1 rounded-md text-xs
font-medium bg-gray-700 text-gray-300 hover:bg-gray-600
transition-colors duration-200"
>
<span>{moreCount} more</span>
<ChevronDown size={14} />
</button>
)}

{isExpanded && hasMoreCategories && (
<button
onClick={handleCollapseClick}
className="flex items-center gap-1 px-2 py-1 rounded-md text-xs
font-medium bg-gray-700 text-gray-300 hover:bg-gray-600
transition-colors duration-200"
>
<span>Show less</span>
<ChevronUp size={14} />
</button>
)}
</div>

{!showAll && (
<div
ref={measureRef}
className="absolute flex flex-wrap items-center gap-2"
aria-hidden="true"
style={{ visibility: 'hidden', position: 'absolute', top: 0, left: 0 }}
>
{categories.map((category) => (
<button
key={category}
className={`px-2 py-1 rounded-md text-xs font-medium
${getCategoryColor(category)}`}
>
{category}
</button>
))}
<button
className="flex items-center gap-1 px-2 py-1 rounded-md text-xs
font-medium bg-gray-700 text-gray-300"
>
<span>99 more</span>
<ChevronDown size={14} />
</button>
</div>
)}
</div>
);
};

export default CategoryList;
34 changes: 33 additions & 1 deletion client/src/components/settings/SettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,18 @@ const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose, settings
const [showCodePreview, setShowCodePreview] = useState(settings.showCodePreview);
const [previewLines, setPreviewLines] = useState(settings.previewLines);
const [includeCodeInSearch, setIncludeCodeInSearch] = useState(settings.includeCodeInSearch);
const [showCategories, setShowCategories] = useState(settings.showCategories);
const [expandCategories, setExpandCategories] = useState(settings.expandCategories);

const handleSave = () => {
onSettingsChange({ compactView, showCodePreview, previewLines, includeCodeInSearch });
onSettingsChange({
compactView,
showCodePreview,
previewLines,
includeCodeInSearch,
showCategories,
expandCategories
});
onClose();
};

Expand Down Expand Up @@ -61,6 +70,29 @@ const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose, settings
className="form-checkbox h-5 w-5 text-blue-600 rounded focus:ring-blue-500"
/>
</div>
<div className="flex items-center justify-between">
<label htmlFor="showCategories" className="text-gray-300">Show Categories</label>
<input
type="checkbox"
id="showCategories"
checked={showCategories}
onChange={(e) => setShowCategories(e.target.checked)}
className="form-checkbox h-5 w-5 text-blue-600 rounded focus:ring-blue-500"
/>
</div>

{showCategories && (
<div className="flex items-center justify-between">
<label htmlFor="expandCategories" className="text-gray-300">Expand Categories</label>
<input
type="checkbox"
id="expandCategories"
checked={expandCategories}
onChange={(e) => setExpandCategories(e.target.checked)}
className="form-checkbox h-5 w-5 text-blue-600 rounded focus:ring-blue-500"
/>
</div>
)}
</div>
<div className="mt-6 flex justify-end">
<button
Expand Down
72 changes: 70 additions & 2 deletions client/src/components/snippets/EditSnippetModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,35 @@ const EditSnippetModal: React.FC<EditSnippetModalProps> = ({ isOpen, onClose, on
const [key, setKey] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [supportedLanguages, setSupportedLanguages] = useState<string[]>([]);
const [categories, setCategories] = useState<string[]>([]);
const [categoryInput, setCategoryInput] = useState('');

useEffect(() => {
if (snippetToEdit) {
setCategories(snippetToEdit.categories || []);
} else {
setCategories([]);
}
}, [snippetToEdit]);

const handleCategoryInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if ((e.key === 'Enter' || e.key === ',') && categoryInput.trim()) {
e.preventDefault();
addCategory(categoryInput.trim());
}
};

const addCategory = (category: string) => {
const normalizedCategory = category.toLowerCase().replace(/,/g, '').trim();
if (normalizedCategory && categories.length < 20 && !categories.includes(normalizedCategory)) {
setCategories([...categories, normalizedCategory]);
}
setCategoryInput('');
};

const removeCategory = (categoryToRemove: string) => {
setCategories(categories.filter(cat => cat !== categoryToRemove));
};

useEffect(() => {
if (isOpen) {
Expand Down Expand Up @@ -173,8 +202,9 @@ const EditSnippetModal: React.FC<EditSnippetModalProps> = ({ isOpen, onClose, on
const snippetData = {
title: title.slice(0, 255),
language: language.slice(0, 50),
description,
code
description: description,
code: code,
categories: categories
};

try {
Expand Down Expand Up @@ -232,6 +262,44 @@ const EditSnippetModal: React.FC<EditSnippetModalProps> = ({ isOpen, onClose, on
></textarea>
</div>

<div>
<label htmlFor="categories" className="block text-sm font-medium text-gray-300">
Categories (max 20)
</label>
<div className="mt-1">
<input
type="text"
id="categories"
value={categoryInput}
onChange={(e) => setCategoryInput(e.target.value)}
onKeyDown={handleCategoryInputKeyDown}
className="mt-1 block w-full rounded-md bg-gray-700 border border-gray-600 text-white p-2 text-sm"
placeholder="Type a category and press Enter or comma"
disabled={categories.length >= 20}
/>
<p className="text-sm text-gray-400 mt-1">
{categories.length}/20 categories
</p>
<div className="flex flex-wrap gap-2 mt-2">
{categories.map((category, index) => (
<div
key={index}
className="flex items-center gap-1 px-2 py-1 rounded-md bg-gray-700 text-sm"
>
<span>{category}</span>
<button
type="button"
onClick={() => removeCategory(category)}
className="text-gray-400 hover:text-white"
>
×
</button>
</div>
))}
</div>
</div>
</div>

<div>
<label htmlFor="code" className="block text-sm font-medium text-gray-300">Code</label>
<div className="mt-1 rounded-md bg-gray-800 border border-gray-600 overflow-hidden">
Expand Down
Loading

0 comments on commit 72d3496

Please sign in to comment.