mirror of
https://github.com/linsa-io/linsa.git
synced 2026-04-27 02:38:45 +02:00
chore: Enhancement + New Feature (#185)
* wip * wip page * chore: style * wip pages * wip pages * chore: toggle * chore: link * feat: topic search * chore: page section * refactor: apply tailwind class ordering * fix: handle loggedIn user for guest route * feat: folder & image schema * chore: move utils to shared * refactor: tailwind class ordering * feat: img ext for editor * refactor: remove qa * fix: tanstack start * fix: wrong import * chore: use toast * chore: schema
This commit is contained in:
@@ -16,7 +16,6 @@ export const Route = createFileRoute("/_layout/_pages/(topic)/$")({
|
||||
export const openPopoverForIdAtom = atom<string | null>(null)
|
||||
|
||||
export function TopicDetailComponent() {
|
||||
console.log("TopicDetailComponent")
|
||||
const params = useParams({ from: "/_layout/_pages/$" })
|
||||
const { me } = useAccountOrGuest({ root: { personalLinks: [] } })
|
||||
|
||||
@@ -30,34 +29,65 @@ export function TopicDetailComponent() {
|
||||
latestGlobalGuide: { sections: [] },
|
||||
})
|
||||
const [activeIndex, setActiveIndex] = React.useState(-1)
|
||||
const [searchQuery, setSearchQuery] = React.useState("")
|
||||
|
||||
const topicExists = GraphData.find((node) => {
|
||||
return node.name === params._splat
|
||||
})
|
||||
const topicExists = React.useMemo(
|
||||
() => GraphData.find((node) => node.name === params._splat),
|
||||
[params._splat],
|
||||
)
|
||||
|
||||
const latestGlobalGuide = React.useMemo(
|
||||
() => topic?.latestGlobalGuide,
|
||||
[topic?.latestGlobalGuide],
|
||||
)
|
||||
|
||||
const flattenedItems = React.useMemo(
|
||||
() =>
|
||||
latestGlobalGuide?.sections.flatMap((section) => [
|
||||
{ type: "section" as const, data: section },
|
||||
...(section?.links?.map((link) => ({
|
||||
type: "link" as const,
|
||||
data: link,
|
||||
})) || []),
|
||||
]) || [],
|
||||
[latestGlobalGuide],
|
||||
)
|
||||
|
||||
const filteredItems = React.useMemo(() => {
|
||||
if (!searchQuery) return flattenedItems
|
||||
return flattenedItems.filter((item) => {
|
||||
if (item.type === "section") {
|
||||
return item.data?.title
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase())
|
||||
}
|
||||
if (item.type === "link") {
|
||||
return (
|
||||
item.data?.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
item.data?.url.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
}
|
||||
return false
|
||||
})
|
||||
}, [flattenedItems, searchQuery])
|
||||
|
||||
if (!topicExists) {
|
||||
return <NotFoundPlaceholder />
|
||||
}
|
||||
|
||||
const flattenedItems = topic?.latestGlobalGuide?.sections.flatMap(
|
||||
(section) => [
|
||||
{ type: "section" as const, data: section },
|
||||
...(section?.links?.map((link) => ({
|
||||
type: "link" as const,
|
||||
data: link,
|
||||
})) || []),
|
||||
],
|
||||
)
|
||||
|
||||
if (!topic || !me || !flattenedItems) {
|
||||
if (!topic || !me) {
|
||||
return <TopicDetailSkeleton />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopicDetailHeader topic={topic} />
|
||||
<TopicDetailHeader
|
||||
topic={topic}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
/>
|
||||
<TopicDetailList
|
||||
items={flattenedItems}
|
||||
items={filteredItems}
|
||||
topic={topic}
|
||||
activeIndex={activeIndex}
|
||||
setActiveIndex={setActiveIndex}
|
||||
@@ -88,7 +118,7 @@ function TopicDetailSkeleton() {
|
||||
<Skeleton className="h-8 w-8 rounded-full" />
|
||||
<Skeleton className="h-6 w-48" />
|
||||
</div>
|
||||
<Skeleton className="h-9 w-36" />
|
||||
<Skeleton className="h-7 w-28" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 p-6 max-lg:px-4">
|
||||
|
||||
@@ -10,13 +10,19 @@ import { LearningStateValue } from "@/lib/constants"
|
||||
import { useMedia } from "@/hooks/use-media"
|
||||
import { useClerk } from "@clerk/tanstack-start"
|
||||
import { useLocation } from "@tanstack/react-router"
|
||||
import { Input } from "~/components/ui/input"
|
||||
import { LaIcon } from "~/components/custom/la-icon"
|
||||
|
||||
interface TopicDetailHeaderProps {
|
||||
topic: Topic
|
||||
searchQuery: string
|
||||
setSearchQuery: (query: string) => void
|
||||
}
|
||||
|
||||
export const TopicDetailHeader = React.memo(function TopicDetailHeader({
|
||||
topic,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
}: TopicDetailHeaderProps) {
|
||||
const clerk = useClerk()
|
||||
const { pathname } = useLocation()
|
||||
@@ -111,28 +117,51 @@ export const TopicDetailHeader = React.memo(function TopicDetailHeader({
|
||||
topicLists[learningState]?.push(topic)
|
||||
}
|
||||
|
||||
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchQuery(event.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentHeader className="px-6 py-5 max-lg:px-4">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1.5">
|
||||
<SidebarToggleButton />
|
||||
<div className="flex min-h-0 min-w-0 flex-1 items-center">
|
||||
<h1 className="truncate text-left font-bold lg:text-xl">
|
||||
{topic.prettyName}
|
||||
</h1>
|
||||
<>
|
||||
<ContentHeader>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1.5">
|
||||
<SidebarToggleButton />
|
||||
<div className="flex min-h-0 min-w-0 flex-1 items-center">
|
||||
<h1 className="truncate text-left font-semibold lg:text-lg">
|
||||
{topic.prettyName}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-auto"></div>
|
||||
{/* <GuideCommunityToggle topicName={topic.name} /> */}
|
||||
|
||||
<LearningStateSelector
|
||||
showSearch={false}
|
||||
value={p?.learningState || ""}
|
||||
onChange={handleAddToProfile}
|
||||
defaultLabel={isMobile ? "" : "Add to profile"}
|
||||
defaultIcon="Circle"
|
||||
/>
|
||||
</ContentHeader>
|
||||
<div className="flex min-h-10 flex-row items-center justify-between border-b border-b-[var(--la-border-new)] px-6 py-2 max-lg:px-4">
|
||||
<div className="flex flex-1 flex-row items-center gap-2">
|
||||
<span className="text-tertiary flex h-5 w-5 items-center justify-center">
|
||||
<LaIcon name="Search" className="text-muted-foreground" />
|
||||
</span>
|
||||
<Input
|
||||
className="h-6 flex-1 border-none bg-transparent p-0 focus-visible:ring-0"
|
||||
placeholder="Search..."
|
||||
role="searchbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
value={searchQuery}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-auto"></div>
|
||||
{/* <GuideCommunityToggle topicName={topic.name} /> */}
|
||||
|
||||
<LearningStateSelector
|
||||
showSearch={false}
|
||||
value={p?.learningState || ""}
|
||||
onChange={handleAddToProfile}
|
||||
defaultLabel={isMobile ? "" : "Add to profile"}
|
||||
defaultIcon="Circle"
|
||||
/>
|
||||
</ContentHeader>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -178,11 +178,8 @@ export const LinkItem = React.memo(
|
||||
size="sm"
|
||||
type="button"
|
||||
role="combobox"
|
||||
variant="secondary"
|
||||
className={cn(
|
||||
"size-7 shrink-0 p-0",
|
||||
"hover:bg-accent-foreground/10",
|
||||
)}
|
||||
variant="ghost"
|
||||
className="h-auto shrink-0 cursor-default p-0 text-muted-foreground/75 hover:bg-inherit hover:text-foreground"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{selectedLearningState?.icon ? (
|
||||
@@ -215,7 +212,7 @@ export const LinkItem = React.memo(
|
||||
<div className="gap-x-2 space-y-0.5 xl:flex xl:flex-row">
|
||||
<p
|
||||
className={cn(
|
||||
"text-primary hover:text-primary line-clamp-1 text-sm font-medium",
|
||||
"line-clamp-1 text-sm font-medium text-primary hover:text-primary",
|
||||
isActive && "font-bold",
|
||||
)}
|
||||
>
|
||||
@@ -226,14 +223,14 @@ export const LinkItem = React.memo(
|
||||
<LaIcon
|
||||
name="Link"
|
||||
aria-hidden="true"
|
||||
className="text-muted-foreground group-hover:text-primary size-3.5 flex-none"
|
||||
className="flex-none text-muted-foreground group-hover:text-primary"
|
||||
/>
|
||||
|
||||
<Link
|
||||
to={ensureUrlProtocol(link.url)}
|
||||
target="_blank"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="text-muted-foreground hover:text-primary text-xs"
|
||||
className="text-xs text-muted-foreground hover:text-primary"
|
||||
>
|
||||
<span className="line-clamp-1">{link.url}</span>
|
||||
</Link>
|
||||
|
||||
@@ -51,10 +51,10 @@ export function TopicDetailList({
|
||||
className="flex flex-col"
|
||||
>
|
||||
<div className="flex items-center gap-4 px-6 py-2 max-lg:px-4">
|
||||
<p className="text-foreground text-sm font-medium">
|
||||
<p className="text-[13px] font-medium text-muted-foreground">
|
||||
{item.data?.title}
|
||||
</p>
|
||||
<div className="flex-1 border-b" />
|
||||
<div className="flex-1 border-b border-[var(--la-border-new)]" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -82,7 +82,7 @@ export function TopicDetailList({
|
||||
)
|
||||
|
||||
return (
|
||||
<div ref={parentRef} className="flex-1 overflow-auto">
|
||||
<div ref={parentRef} className="flex-1 overflow-auto py-4">
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
|
||||
Reference in New Issue
Block a user