Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 2 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@
"@tanstack/react-query": "^5.82.2",
"@tanstack/react-query-devtools": "^5.82.2",
"@tanstack/react-router": "^1.119.0",
"@tanstack/zod-adapter": "^1.150.0",
"@tursodatabase/sync-wasm": "^0.3.2",
"@uidotdev/usehooks": "^2.4.1",
"better-auth": "1.4.9",
"date-fns": "^3.6.0",
"dompurify": "^3.3.0",
"drizzle-orm": "^0.45.1",
"drizzle-orm": "1.0.0-beta.10-4a43a22",
Comment on lines +37 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

@tanstack/zod-adapter 1.150.0 compatibility requirements zod 4 @tanstack/react-router``

💡 Result:

Short answer:

  • @tanstack/zod-adapter@1.150.0 is intended for Zod v3 (peerDependency: zod ^3.23.8) and requires a TanStack Router / React Router version >= 1.43.2. [1][2]
  • If you want to use Zod v4: recent Zod releases (>= v4.0.6) added behavior that makes the adapter unnecessary for many use-cases (the repo issue notes Zod‑4 compatibility/workarounds and that Zod >= 4.0.6 works natively). If you rely on any adapter-specific helpers (e.g. fallback behavior), check the issue discussion and test with your exact versions. [2][3]

Sources:

  1. TanStack release listing showing @tanstack/zod-adapter@1.150.0. [1]
  2. @tanstack/zod-adapter package peerDependency info / docs (zod: ^3.23.8, @tanstack/react-router: >=1.43.2). [2]
  3. GitHub issue discussing Zod v4 support and notes about Zod >= 4.0.6. [3]

🌐 Web query:

drizzle-orm 1.0.0-beta.10-4a43a22 compatibility drizzle-zod 0.8.3 zod

💡 Result:

Short answer: Generally compatible — drizzle-zod@0.8.3 supports Zod v3 (>=3.25.1) and v4 and declares drizzle-orm >=0.36.0 as a peer, so it should work with a drizzle-orm 1.0.0-beta build. [1][2][3]

Notes / caveats:

  • drizzle-zod docs state requirement: Drizzle ORM >= 0.36.0 and Zod >= 3.25.1 (with v4 supported). [2]
  • npm shows drizzle-zod@0.8.3 published recently. [3]
  • There are GitHub issues about peer-dependency bumps for beta drizzle-orm builds (some beta tags can cause npm peer-dep resolution warnings); if you use a specific 1.0.0-beta commit tag you may see peerDep warnings and might need to align package tags or use package-manager overrides/resolutions. [4]

If you want, tell me the exact drizzle-orm beta tag you’re using (full version string) and your package manager (npm/pnpm/yarn) and I’ll check for any known breakages or provide a concrete install/resolution command.

References:
[1] Drizzle docs — drizzle-zod page (requirements).
[2] Drizzle docs / changelog notes about zod support.
[3] npm: drizzle-zod (v0.8.3).
[4] GitHub issue re: peer dependency bumps for beta builds.


Remove or downgrade @tanstack/zod-adapter — it requires Zod v3 but the project uses Zod v4.

@tanstack/zod-adapter@1.150.0 declares a peer dependency of zod@^3.23.8 and is incompatible with zod@4.0.17. This will cause peer-dependency resolution warnings or failures. Since Zod v4 (>= 4.0.6) added native support for the adapter's functionality, consider removing the adapter entirely or using a newer adapter version if one exists for Zod v4.

The drizzle-orm@1.0.0-beta.10-4a43a22 is compatible with drizzle-zod@0.8.3 and zod@4.0.17.

🤖 Prompt for AI Agents
In `@apps/web/package.json` around lines 37 - 43, The package.json currently
includes `@tanstack/zod-adapter` which requires zod@^3; remove the
"@tanstack/zod-adapter" entry or replace it with a version that supports Zod v4
(if one exists) so peer-dependency resolution no longer conflicts with
zod@4.0.17; alternatively, delete the dependency and rely on native Zod v4
features (or update any imports/usages referencing `@tanstack/zod-adapter`) to
ensure code compiles with zod@4 used by drizzle-zod/drizzle-orm.

"input-otp": "1.4.2",
"jotai": "^2.7.0",
"lucide-react": "^0.507.0",
Expand Down
74 changes: 46 additions & 28 deletions apps/web/src/components/TagsCombobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { cn } from "@bahar/design-system";
import { useDebounce } from "@uidotdev/usehooks";
import { Check, ChevronDown, X } from "lucide-react";
import {
type FC,
type KeyboardEvent,
type ReactNode,
startTransition,
Expand All @@ -11,6 +12,46 @@ import {
useState,
} from "react";

export const TagPill: FC<{
disabled?: boolean;
onClick?: () => void;
tagValue: string;
}> = ({ tagValue, disabled, onClick }) => {
return (
<button
className="inline-flex max-w-[calc(100%-8px)] items-center gap-1.5 rounded border bg-transparent py-1 pr-1.5 pl-2.5 text-sm active:bg-muted/50"
disabled={disabled}
key={tagValue}
onClick={(e) => {
e.stopPropagation();
// On touch devices (mobile), clicking anywhere on the pill removes it
if (window.matchMedia("(pointer: coarse)").matches) {
onClick?.();
}
}}
type="button"
>
<span className="truncate">{tagValue}</span>
<button
className="size-4 shrink-0 cursor-pointer rounded-sm opacity-70 transition-opacity hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
onClick?.();
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.stopPropagation();
onClick?.();
}
}}
type="button"
>
<X className="size-3.5" />
</button>
</button>
);
Comment thread
Shunseii marked this conversation as resolved.
};

export interface TagsComboboxProps<T> {
/** Selected tag values */
value: string[];
Expand Down Expand Up @@ -210,7 +251,6 @@ export function TagsCombobox<T>({

return (
<div className={cn("relative", className)} ref={containerRef}>
{/* Anchor - Tags + Input */}
<div
className={cn(
"relative flex min-h-10 w-full flex-row flex-wrap items-center gap-1.5 rounded-md border border-input bg-background py-2 text-sm shadow-xs",
Expand All @@ -225,36 +265,14 @@ export function TagsCombobox<T>({
}}
>
{value.map((tagValue) => (
<button
className="inline-flex max-w-[calc(100%-8px)] cursor-pointer items-center gap-1.5 rounded border bg-transparent py-1 pr-1.5 pl-2.5 text-sm active:bg-muted/50"
<TagPill
disabled={disabled}
key={tagValue}
onClick={(e) => {
e.stopPropagation();
// On touch devices (mobile), clicking anywhere on the pill removes it
if (window.matchMedia("(pointer: coarse)").matches) {
removeTag(tagValue);
}
onClick={() => {
removeTag(tagValue);
}}
type="button"
>
<span className="truncate">{tagValue}</span>
<span
className="size-4 shrink-0 rounded-sm opacity-70 transition-opacity hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
removeTag(tagValue);
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.stopPropagation();
removeTag(tagValue);
}
}}
>
<X className="size-3.5" />
</span>
</button>
tagValue={tagValue}
/>
))}

<input
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/features/decks/DeckDialogContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export const DeckDialogContent = ({
if (!isEditing) {
form.reset();
}
} catch (err) {
} catch (_err) {
if (isEditing) {
toast.error(t`There was an error updating the deck`, {
description: t`Your deck was not updated. Please try again.`,
Expand Down Expand Up @@ -309,7 +309,7 @@ export const DeckDialogContent = ({
/>
</FormControl>

<FormLabel className="!mt-0 cursor-pointer font-normal text-sm">
<FormLabel className="mt-0! cursor-pointer font-normal text-sm">
{getTypeLabel(type)}
</FormLabel>
</FormItem>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { Button } from "@bahar/web-ui/components/button";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@bahar/web-ui/components/select";
import { Separator } from "@bahar/web-ui/components/separator";
import { Trans } from "@lingui/react/macro";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { ArrowDownUp, FunnelXIcon, SlidersHorizontal } from "lucide-react";
import { TagsFilter } from "@/components/features/dictionary/filters/TagsFilter";
import { TagPill } from "@/components/TagsCombobox";

type SortOption = "relevance" | "updatedAt" | "createdAt";

const SortOptionLabel = ({ option }: { option: SortOption }) => {
switch (option) {
case "relevance":
return <Trans>Relevance</Trans>;
case "updatedAt":
return <Trans>Recently updated</Trans>;
case "createdAt":
return <Trans>Recently added</Trans>;
}
};

const sortOptions: SortOption[] = ["relevance", "updatedAt", "createdAt"];

export const DictionaryFilters = () => {
const navigate = useNavigate();
const { tags: filteredTags, sort } = useSearch({
from: "/_authorized-layout/_search-layout",
});

const hasActiveFilters = !!filteredTags?.length && filteredTags.length > 0;
Comment thread
Shunseii marked this conversation as resolved.
Outdated

const clearAllFilters = () => {
navigate({ to: "/" });
};

return (
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-4">
<section className="flex flex-row items-center gap-x-4">
<div className="flex items-center gap-1.5 text-muted-foreground">
<SlidersHorizontal className="h-4 w-4" />
<span className="font-medium text-sm">
<Trans>Filters</Trans>
</span>
</div>

<Separator className="shrink bg-linear-to-r from-border/50 via-border to-border/50" />
</section>

<section className="flex flex-col gap-y-2">
<p className="font-medium text-muted-foreground text-sm">
<Trans>Tags</Trans>
</p>

<TagsFilter />

{filteredTags?.length ? (
<ul className="flex flex-wrap gap-2">
{filteredTags.map((tag) => (
<TagPill
key={tag}
onClick={() => {
const shouldRemove = filteredTags?.some((t) => t === tag);

if (shouldRemove) {
const newTags = filteredTags?.filter((t) => t !== tag);

navigate({
to: "/",
search: {
tags: newTags?.length ? newTags : undefined,
sort,
},
});
} else {
navigate({
to: "/",
search: {
tags: [...(filteredTags ?? []), tag],
sort,
},
});
}
Comment on lines +76 to +99

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n apps/web/src/components/features/dictionary/filters/DictionaryFilters.tsx

Repository: Shunseii/bahar

Length of output: 6473


🏁 Script executed:

# Search for other uses of navigate with search updaters in the codebase
rg -A 8 'navigate\(' apps/web/src --type tsx --type ts | head -100

Repository: Shunseii/bahar

Length of output: 85


🏁 Script executed:

# Search for navigate with search updater patterns in the codebase
rg -A 8 'navigate\(' apps/web/src | grep -A 8 'search:' | head -80

Repository: Shunseii/bahar

Length of output: 4208


🏁 Script executed:

# Check TagsFilter.tsx to see if it has similar pattern and context
cat -n apps/web/src/components/features/dictionary/filters/TagsFilter.tsx | head -150

Repository: Shunseii/bahar

Length of output: 4266


🏁 Script executed:

# Look for TanstackRouter documentation or check if there are any other patterns
# that might show how the search updater works
rg 'search:.*prev.*=>' apps/web/src -A 5 | head -60

Repository: Shunseii/bahar

Length of output: 3297


Use prev.tags inside the search updater to avoid stale tag toggles.

The handler captures render-time filteredTags which can be stale if multiple tag clicks happen quickly. Computing from prev.tags ensures atomic updates.

✅ Proposed fix
-                  onClick={() => {
-                    const shouldRemove = filteredTags?.some((t) => t === tag);
-
-                    if (shouldRemove) {
-                      const newTags = filteredTags?.filter((t) => t !== tag);
-
-                      navigate({
-                        to: "/",
-                        search: (prev) => ({
-                          ...prev,
-                          tags: newTags?.length ? newTags : undefined,
-                        }),
-                      });
-                    } else {
-                      navigate({
-                        to: "/",
-                        search: (prev) => ({
-                          ...prev,
-                          tags: [...(filteredTags ?? []), tag],
-                        }),
-                      });
-                    }
-                  }}
+                  onClick={() => {
+                    navigate({
+                      to: "/",
+                      search: (prev) => {
+                        const prevTags = prev.tags ?? [];
+                        const nextTags = prevTags.includes(tag)
+                          ? prevTags.filter((t) => t !== tag)
+                          : [...prevTags, tag];
+
+                        return {
+                          ...prev,
+                          tags: nextTags.length ? nextTags : undefined,
+                        };
+                      },
+                    });
+                  }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<TagPill
key={tag}
onClick={() => {
const shouldRemove = filteredTags?.some((t) => t === tag);
if (shouldRemove) {
const newTags = filteredTags?.filter((t) => t !== tag);
navigate({
to: "/",
search: (prev) => ({
...prev,
tags: newTags?.length ? newTags : undefined,
}),
});
} else {
navigate({
to: "/",
search: (prev) => ({
...prev,
tags: [...(filteredTags ?? []), tag],
}),
});
}
<TagPill
key={tag}
onClick={() => {
navigate({
to: "/",
search: (prev) => {
const prevTags = prev.tags ?? [];
const nextTags = prevTags.includes(tag)
? prevTags.filter((t) => t !== tag)
: [...prevTags, tag];
return {
...prev,
tags: nextTags.length ? nextTags : undefined,
};
},
});
}}
🤖 Prompt for AI Agents
In `@apps/web/src/components/features/dictionary/filters/DictionaryFilters.tsx`
around lines 74 - 97, The TagPill onClick handler in DictionaryFilters is using
the render-time variable filteredTags, which can be stale; update the navigate
search updater to compute the new tags from prev.tags instead (inside the
search: (prev) => { ... }) and perform the toggle logic there (check if
prev.tags includes the clicked tag, filter it out or append it accordingly), so
updates are atomic and avoid lost toggles when clicking tags rapidly.

}}
tagValue={tag}
/>
))}
</ul>
) : null}
</section>

<section className="flex flex-row items-center gap-x-4">
<div className="flex items-center gap-1.5 text-muted-foreground">
<ArrowDownUp className="h-4 w-4" />
<span className="whitespace-nowrap font-medium text-sm">
<Trans>Sort by</Trans>
</span>
</div>

<Separator className="shrink bg-linear-to-r from-border/50 via-border to-border/50" />
</section>

<section className="flex flex-col gap-y-2">
<Select
onValueChange={(value: SortOption) => {
navigate({
to: "/",
search: {
tags: filteredTags?.length ? filteredTags : undefined,
sort: value === "relevance" ? undefined : value,
},
});
}}
value={sort ?? "relevance"}
>
<SelectTrigger className="w-max min-w-[200px] gap-x-2">
<SelectValue />
</SelectTrigger>

<SelectContent>
<SelectGroup>
{sortOptions.map((option) => (
<SelectItem
className="cursor-pointer"
key={option}
value={option}
>
<SortOptionLabel option={option} />
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</section>

{hasActiveFilters && (
<Button
className="h-8 w-max px-2 text-muted-foreground hover:text-foreground"
onClick={clearAllFilters}
size="sm"
variant="ghost"
>
<FunnelXIcon />

<Trans>Clear all filters</Trans>
</Button>
)}
</div>
</div>
);
};
Loading