first commit

This commit is contained in:
2026-07-17 21:48:37 +05:30
commit f85599dac5
124 changed files with 10775 additions and 0 deletions
@@ -0,0 +1,96 @@
"use client";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { EMOJIS, FREQUENT, searchEmojis, type EmojiCategory } from "@/lib/emoji";
import { Search } from "lucide-react";
const CATS: EmojiCategory[] = ["Smileys", "Gestures", "Activity", "Animals", "Food", "Travel", "Objects", "Symbols"];
export function EmojiPicker({
onPick,
onClose,
align = "left",
}: {
onPick: (emoji: string) => void;
onClose: () => void;
align?: "left" | "right";
}) {
const ref = useRef<HTMLDivElement>(null);
const [q, setQ] = useState("");
const [pos, setPos] = useState<{ v: "up" | "down"; h: "left" | "right" }>({ v: "up", h: align });
// auto-place: flip up/down + left/right based on available space (tooltip-style)
useLayoutEffect(() => {
const wrap = (ref.current?.offsetParent as HTMLElement | null) ?? ref.current;
const r = wrap?.getBoundingClientRect();
if (!r) return;
const H = 300, W = 300;
const v = r.top < H + 12 && window.innerHeight - r.bottom > r.top ? "down" : "up";
const h = window.innerWidth - r.left < W ? "right" : "left";
setPos({ v, h });
}, []);
useEffect(() => {
const onDoc = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) onClose(); };
const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") { e.stopPropagation(); onClose(); } };
const t = setTimeout(() => document.addEventListener("mousedown", onDoc), 0);
document.addEventListener("keydown", onKey);
return () => { clearTimeout(t); document.removeEventListener("mousedown", onDoc); document.removeEventListener("keydown", onKey); };
}, [onClose]);
const results = useMemo(() => (q ? searchEmojis(q, 64) : null), [q]);
return (
<div
ref={ref}
className={`absolute z-40 w-[300px] rounded-control border border-border bg-elevated p-2 shadow-pop animate-slide-up ${pos.v === "up" ? "bottom-9" : "top-9"} ${pos.h === "right" ? "right-0" : "left-0"}`}
>
<div className="mb-2 flex items-center gap-2 rounded-control border border-border bg-panel px-2">
<Search size={14} className="text-muted" />
<input
autoFocus
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="Search emoji…"
className="w-full bg-transparent py-1.5 text-[13px] outline-none placeholder:text-dim"
/>
</div>
<div className="max-h-[240px] overflow-y-auto pr-0.5">
{results ? (
<Grid list={results.map((e) => e.e)} onPick={onPick} />
) : (
<>
<CatLabel>Frequently used</CatLabel>
<Grid list={FREQUENT} onPick={onPick} />
{CATS.map((c) => (
<div key={c}>
<CatLabel>{c}</CatLabel>
<Grid list={EMOJIS.filter((e) => e.c === c).map((e) => e.e)} onPick={onPick} />
</div>
))}
</>
)}
{results && results.length === 0 && <div className="py-6 text-center text-[12.5px] text-muted">No emoji found</div>}
</div>
<div className="mt-1 border-t border-border px-1 pt-1 text-[10.5px] text-dim">
Tip: type <code className="text-accent">:tick:</code> in a message for
</div>
</div>
);
}
function CatLabel({ children }: { children: React.ReactNode }) {
return <div className="px-1 pb-0.5 pt-1.5 text-[10px] font-bold uppercase tracking-wide text-dim">{children}</div>;
}
function Grid({ list, onPick }: { list: string[]; onPick: (e: string) => void }) {
return (
<div className="grid grid-cols-8 gap-0.5">
{list.map((e, i) => (
<button key={e + i} onClick={() => onPick(e)} className="grid h-8 w-8 place-items-center rounded-md text-lg hover:bg-hover" title={e}>
{e}
</button>
))}
</div>
);
}