Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 144 additions & 31 deletions src/features/discover/components/DiscoverFilters.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Filter, Search } from "lucide-react";
import { Filter, Search, X } from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import { useState, useRef, useEffect, useMemo } from "react";

import { ROLE_OPTIONS, STATUS_OPTIONS } from "../constants/discover.constants";

Expand All @@ -7,7 +9,8 @@ interface DiscoverFiltersProps {
selectedRole: string;
selectedStatus: string;
selectedTag: string;
popularTags: string[];
topTags: string[];
allTags: string[];
setSearchQuery: (value: string) => void;
setSelectedRole: (value: string) => void;
setSelectedStatus: (value: string) => void;
Expand All @@ -20,13 +23,43 @@ export function DiscoverFilters({
selectedRole,
selectedStatus,
selectedTag,
popularTags,
topTags,
allTags,
setSearchQuery,
setSelectedRole,
setSelectedStatus,
setSelectedTag,
clearSelectedTag,
}: DiscoverFiltersProps) {
const [tagSearch, setTagSearch] = useState("");
const [isTagDropdownOpen, setIsTagDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);

const trimmedTagSearch = tagSearch.trim();

const visibleTags = useMemo(() => {
if (!trimmedTagSearch) return allTags;
return allTags.filter((tag) => tag.toLowerCase().includes(trimmedTagSearch.toLowerCase()));
}, [allTags, trimmedTagSearch]);

useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setIsTagDropdownOpen(false);
setTagSearch("");
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);

const handleTagSelect = (tag: string) => {
setSelectedTag(tag === selectedTag ? "" : tag);
setTagSearch("");
setIsTagDropdownOpen(false);
};

return (
<div className="bg-white border-4 border-neo-black p-6 shadow-[8px_8px_0_0_#000] space-y-6">
<h3 className="font-heading font-black text-2xl uppercase tracking-tight flex items-center gap-2">
Expand Down Expand Up @@ -87,42 +120,122 @@ export function DiscoverFilters({
</div>
</div>

{popularTags.length > 0 && (
<div className="pt-4 border-t-2 border-dashed border-neo-black flex flex-wrap items-center gap-3">
<span className="text-[10px] font-black uppercase text-neo-black/60">
Filtrar por Paixão:
</span>
<div className="pt-4 border-t-2 border-dashed border-neo-black flex flex-wrap items-center gap-3">
<span className="text-[10px] font-black uppercase text-neo-black/60 shrink-0">
Filtrar por Skills:
</span>

{popularTags.map((tag) => {
const isSelected = selectedTag === tag;
{/* ── Top 3 Tags (Quick Filters) ── */}
{topTags.map((tag) => {
const isSelected = selectedTag === tag;

return (
return (
<button
key={tag}
type="button"
onClick={() => setSelectedTag(isSelected ? "" : tag)}
className={`px-3 py-1.5 text-[10px] font-heading font-black uppercase border-2 border-neo-black transition-all hover:-translate-y-0.5 active:translate-y-0 shadow-[2px_2px_0_0_#000] active:shadow-none ${
isSelected
? "bg-neo-cyan text-neo-black"
: "bg-white text-neo-black hover:bg-neo-bg-alt"
}`}
>
{tag}
</button>
);
})}

{/* ── Search Bar (Autocomplete) ── */}
<div ref={dropdownRef} className="relative flex-1 min-w-[200px] max-w-sm">
<div className="relative">
<Search className="absolute left-3 top-2.5 w-4 h-4 text-neo-black/60" strokeWidth={3} />
<input
ref={inputRef}
type="text"
value={tagSearch}
onChange={(e) => {
setTagSearch(e.target.value);
setIsTagDropdownOpen(true);
}}
onFocus={() => setIsTagDropdownOpen(true)}
placeholder="BUSCAR MAIS SKILLS..."
className="w-full pl-9 pr-8 py-2 bg-neo-bg border-2 border-neo-black font-black text-[11px] uppercase tracking-widest focus:bg-white transition-all outline-none"
/>
{tagSearch && (
<button
key={tag}
type="button"
onClick={() => setSelectedTag(isSelected ? "" : tag)}
className={`px-3 py-1 text-[10px] font-heading font-black uppercase border-2 transition-all hover:-translate-y-0.5 active:translate-y-0 shadow-[2px_2px_0_0_#000] active:shadow-none ${
isSelected
? "bg-neo-cyan text-neo-black"
: "bg-white text-neo-black hover:bg-neo-bg-alt"
}`}
onClick={() => {
setTagSearch("");
inputRef.current?.focus();
}}
className="absolute right-2 top-2.5 hover:opacity-70 transition-opacity"
>
{tag}
<X className="w-4 h-4 text-neo-black/60" strokeWidth={3} />
</button>
);
})}
)}
</div>
Comment thread
YnotMax marked this conversation as resolved.

{selectedTag && (
<button
type="button"
onClick={clearSelectedTag}
className="text-[9px] font-black text-neo-pink uppercase hover:underline"
>
[Limpar Tag]
</button>
)}
<AnimatePresence>
{isTagDropdownOpen && (
<motion.div
initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.1 }}
className="absolute z-50 left-0 right-0 top-full mt-1 border-4 border-neo-black bg-white shadow-[4px_4px_0_0_#000] max-h-48 overflow-y-auto"
>
{visibleTags.length > 0 ? (
visibleTags.map((tag) => {
const isSelected = selectedTag === tag;
return (
<button
key={tag}
type="button"
onMouseDown={(e) => {
e.preventDefault();
handleTagSelect(tag);
}}
className={`w-full text-left px-4 py-2.5 font-black text-[11px] uppercase tracking-widest border-b-2 border-neo-black/10 last:border-0 transition-colors ${
isSelected ? "bg-neo-cyan text-neo-black" : "hover:bg-neo-cyan/20"
}`}
>
{tag}
</button>
);
})
) : (
<div className="p-3">
<p className="text-[10px] font-black uppercase tracking-widest text-neo-black/40">
Nenhuma skill encontrada.
</p>
</div>
)}
</motion.div>
)}
</AnimatePresence>
</div>
)}

{/* Selected tag indicator if it's not in the top 3 */}
{selectedTag && !topTags.includes(selectedTag) && (
<button
type="button"
onClick={clearSelectedTag}
className="px-3 py-1.5 text-[10px] font-heading font-black uppercase bg-neo-cyan text-neo-black border-2 border-neo-black transition-all shadow-[2px_2px_0_0_#000] flex items-center gap-1"
>
{selectedTag} <X className="w-3 h-3" strokeWidth={3} />
</button>
)}

{selectedTag && (
<button
type="button"
onClick={clearSelectedTag}
className="text-[9px] font-black text-neo-pink uppercase hover:underline shrink-0"
>
[Limpar Filtro]
</button>
)}
</div>
Comment thread
YnotMax marked this conversation as resolved.
</div>
);
}
5 changes: 3 additions & 2 deletions src/features/discover/hooks/useDiscoverFilters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function useDiscoverFilters(profiles: Profile[]) {
clearSelectedTag,
} = useDiscoverFiltersStore();

const popularTags = useMemo(() => getPopularTags(profiles), [profiles]);
const { topTags, allTags } = useMemo(() => getPopularTags(profiles), [profiles]);

const filteredProfiles = useMemo(
() =>
Expand All @@ -35,7 +35,8 @@ export function useDiscoverFilters(profiles: Profile[]) {
selectedRole,
selectedStatus,
selectedTag,
popularTags,
topTags,
allTags,
filteredProfiles,
setSearchQuery,
setSelectedRole,
Expand Down
17 changes: 12 additions & 5 deletions src/features/discover/model/discover.selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,26 @@ export function sortProfiles(profiles: Profile[], currentUserId?: string): Profi
return sortByCurrentUserAndName(profiles, currentUserId);
}

export function getPopularTags(profiles: Profile[]): string[] {
export function getPopularTags(profiles: Profile[]): { topTags: string[]; allTags: string[] } {
const counts: Record<string, number> = {};

profiles.forEach((profile) => {
profile.canvas?.loves?.forEach((tag) => {
counts[tag] = (counts[tag] || 0) + 1;
});
profile.canvas?.comfort?.forEach((tag) => {
counts[tag] = (counts[tag] || 0) + 1;
});
});

return Object.entries(counts)
.sort((a, b) => b[1] - a[1])
.slice(0, 8)
.map(([tag]) => tag);
const sortedByCount = Object.entries(counts).sort((a, b) => b[1] - a[1]);

return {
topTags: sortedByCount.slice(0, 3).map(([tag]) => tag),
allTags: Object.keys(counts).sort((a, b) =>
a.localeCompare(b, "pt-BR", { sensitivity: "base" }),
),
};
Comment thread
YnotMax marked this conversation as resolved.
}

export function filterProfiles(
Expand Down
Loading