mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01: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:
@@ -109,7 +109,7 @@ export function Feedback() {
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
"flex flex-col p-4 sm:max-w-2xl",
|
||||
)}
|
||||
>
|
||||
@@ -134,7 +134,7 @@ export function Feedback() {
|
||||
{...field}
|
||||
throttleDelay={500}
|
||||
className={cn(
|
||||
"border-muted-foreground/40 focus-within:border-muted-foreground/80 min-h-52 rounded-lg",
|
||||
"min-h-52 rounded-lg border-muted-foreground/40 focus-within:border-muted-foreground/80",
|
||||
{
|
||||
"border-destructive focus-within:border-destructive":
|
||||
form.formState.errors.content,
|
||||
|
||||
@@ -75,7 +75,7 @@ const JournalSectionHeader: React.FC<JournalHeaderProps> = ({
|
||||
<p className="text-xs">
|
||||
Journal
|
||||
{entriesCount > 0 && (
|
||||
<span className="text-muted-foreground ml-1">({entriesCount})</span>
|
||||
<span className="ml-1 text-muted-foreground">({entriesCount})</span>
|
||||
)}
|
||||
</p>
|
||||
</Link>
|
||||
@@ -104,7 +104,7 @@ const JournalEntryItem: React.FC<JournalEntryItemProps> = ({ entry }) => (
|
||||
href={`/journal/${entry.id}`}
|
||||
className="group/journal-entry relative flex min-w-0 flex-1"
|
||||
>
|
||||
<div className="relative flex h-8 w-full items-center gap-2 rounded-md p-1.5 font-medium">
|
||||
<div className="relative flex h-[30px] w-full items-center gap-2 rounded-md p-1.5 font-medium">
|
||||
<div className="flex max-w-full flex-1 items-center gap-1.5 truncate text-sm">
|
||||
<LaIcon name="FileText" className="opacity-60" />
|
||||
<p
|
||||
|
||||
28
web/app/components/sidebar/partials/link-collection.tsx
Normal file
28
web/app/components/sidebar/partials/link-collection.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as React from "react"
|
||||
import { useAccount } from "@/lib/providers/jazz-provider"
|
||||
import { NavItem } from "~/components/custom/nav-item"
|
||||
|
||||
export const LinkCollection: React.FC = () => {
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
personalLinks: [],
|
||||
topicsWantToLearn: [],
|
||||
topicsLearning: [],
|
||||
topicsLearned: [],
|
||||
},
|
||||
})
|
||||
|
||||
const linkCount = me?.root.personalLinks?.length || 0
|
||||
|
||||
const topicCount =
|
||||
(me?.root.topicsWantToLearn?.length || 0) +
|
||||
(me?.root.topicsLearning?.length || 0) +
|
||||
(me?.root.topicsLearned?.length || 0)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-px py-2">
|
||||
<NavItem to="/links" title="Links" icon="Link" count={linkCount} />
|
||||
<NavItem to="/topics" title="Topics" icon="Hash" count={topicCount} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { useAccount } from "@/lib/providers/jazz-provider"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { PersonalLinkLists } from "@/lib/schema/personal-link"
|
||||
import { LearningStateValue } from "~/lib/constants"
|
||||
import { LaIcon } from "~/components/custom/la-icon"
|
||||
|
||||
export const LinkSection: React.FC = () => {
|
||||
const { me } = useAccount({ root: { personalLinks: [] } })
|
||||
@@ -13,7 +14,7 @@ export const LinkSection: React.FC = () => {
|
||||
const linkCount = me.root.personalLinks?.length || 0
|
||||
|
||||
return (
|
||||
<div className="group/pages flex flex-col gap-px py-2">
|
||||
<div className="flex flex-col gap-px py-2">
|
||||
<LinkSectionHeader linkCount={linkCount} />
|
||||
<LinkList personalLinks={me.root.personalLinks} />
|
||||
</div>
|
||||
@@ -24,22 +25,41 @@ interface LinkSectionHeaderProps {
|
||||
linkCount: number
|
||||
}
|
||||
|
||||
const LinkSectionHeader: React.FC<LinkSectionHeaderProps> = ({ linkCount }) => (
|
||||
<Link
|
||||
to="/links"
|
||||
className={cn(
|
||||
"flex h-9 items-center gap-px rounded-md px-2 py-1 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-0 sm:h-[30px] sm:text-xs",
|
||||
)}
|
||||
activeProps={{
|
||||
className: "bg-accent text-accent-foreground",
|
||||
}}
|
||||
>
|
||||
Links
|
||||
{linkCount > 0 && (
|
||||
<span className="text-muted-foreground ml-1">{linkCount}</span>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
const LinkSectionHeader: React.FC<LinkSectionHeaderProps> = ({ linkCount }) => {
|
||||
return (
|
||||
<Link
|
||||
to="/links"
|
||||
className={cn(
|
||||
"flex h-[30px] items-center gap-px rounded-md px-2 text-sm font-medium hover:bg-[var(--item-hover)] focus-visible:outline-none focus-visible:ring-0",
|
||||
)}
|
||||
activeProps={{
|
||||
className:
|
||||
"bg-[var(--item-active)] data-[status='active']:hover:bg-[var(--item-active)]",
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<LaIcon name="Link" className="" />
|
||||
<span>Links</span>
|
||||
</div>
|
||||
<span className="flex flex-auto"></span>
|
||||
{linkCount > 0 && (
|
||||
<span
|
||||
className={cn("font-mono text-muted-foreground", {
|
||||
"text-foreground": isActive,
|
||||
})}
|
||||
>
|
||||
{linkCount}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
interface LinkListProps {
|
||||
personalLinks: PersonalLinkLists
|
||||
@@ -87,29 +107,34 @@ interface LinkListItemProps {
|
||||
}
|
||||
|
||||
const LinkListItem: React.FC<LinkListItemProps> = ({ label, state, count }) => (
|
||||
<div className="group/reorder-page relative">
|
||||
<div className="group/topic-link relative flex min-w-0 flex-1">
|
||||
<Link
|
||||
to="/links"
|
||||
search={{ state }}
|
||||
className={cn(
|
||||
"relative flex h-9 w-full items-center gap-2 rounded-md p-1.5 font-medium hover:bg-accent hover:text-accent-foreground sm:h-8",
|
||||
)}
|
||||
activeProps={{
|
||||
className: "bg-accent text-accent-foreground",
|
||||
}}
|
||||
>
|
||||
<div className="flex max-w-full flex-1 items-center gap-1.5 truncate text-sm">
|
||||
<p className="truncate opacity-95 group-hover/topic-link:opacity-100">
|
||||
{label}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
{count > 0 && (
|
||||
<span className="absolute right-2 top-1/2 z-[1] -translate-y-1/2 rounded p-1 text-sm">
|
||||
{count}
|
||||
</span>
|
||||
<div className="relative flex min-w-0 flex-1">
|
||||
<Link
|
||||
to="/links"
|
||||
search={{ state }}
|
||||
className={cn(
|
||||
"relative flex h-[30px] w-full items-center gap-2 rounded-md px-1.5 text-sm font-medium hover:bg-[var(--item-hover)]",
|
||||
)}
|
||||
</div>
|
||||
activeProps={{
|
||||
className:
|
||||
"bg-[var(--item-active)] data-[status='active']:hover:bg-[var(--item-active)]",
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<div className="flex max-w-full flex-1 items-center gap-1.5 truncate">
|
||||
<p className="truncate">{label}</p>
|
||||
</div>
|
||||
{count > 0 && (
|
||||
<span
|
||||
className={cn("font-mono text-muted-foreground", {
|
||||
"text-foreground": isActive,
|
||||
})}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { usePageActions } from "~/hooks/actions/use-page-actions"
|
||||
import { icons } from "lucide-react"
|
||||
import { ArrowIcon } from "~/components/icons/arrow-icon"
|
||||
|
||||
type SortOption = "title" | "recent"
|
||||
type ShowOption = 5 | 10 | 15 | 20 | 0
|
||||
@@ -44,56 +45,82 @@ const SHOWS: Option<ShowOption>[] = [
|
||||
|
||||
const pageSortAtom = atomWithStorage<SortOption>("pageSort", "title")
|
||||
const pageShowAtom = atomWithStorage<ShowOption>("pageShow", 5)
|
||||
const isExpandedAtom = atomWithStorage("isPageSectionExpanded", true)
|
||||
|
||||
export const PageSection: React.FC = () => {
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
personalPages: [],
|
||||
},
|
||||
})
|
||||
const { me } = useAccount({ root: { personalPages: [] } })
|
||||
const [sort] = useAtom(pageSortAtom)
|
||||
const [show] = useAtom(pageShowAtom)
|
||||
const [isExpanded, setIsExpanded] = useAtom(isExpandedAtom)
|
||||
|
||||
if (!me) return null
|
||||
|
||||
const pageCount = me.root.personalPages?.length || 0
|
||||
|
||||
return (
|
||||
<div className="group/pages flex flex-col gap-px py-2">
|
||||
<PageSectionHeader pageCount={pageCount} />
|
||||
<PageList personalPages={me.root.personalPages} sort={sort} show={show} />
|
||||
<div className="flex flex-col gap-px py-2">
|
||||
<PageSectionHeader
|
||||
pageCount={pageCount}
|
||||
isExpanded={isExpanded}
|
||||
onToggle={() => setIsExpanded(!isExpanded)}
|
||||
/>
|
||||
{isExpanded && (
|
||||
<PageList
|
||||
personalPages={me.root.personalPages}
|
||||
sort={sort}
|
||||
show={show}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface PageSectionHeaderProps {
|
||||
pageCount: number
|
||||
isExpanded: boolean
|
||||
onToggle: () => void
|
||||
}
|
||||
|
||||
const PageSectionHeader: React.FC<PageSectionHeaderProps> = ({ pageCount }) => (
|
||||
<Link
|
||||
to="/pages"
|
||||
const PageSectionHeader: React.FC<PageSectionHeaderProps> = ({
|
||||
pageCount,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-9 flex-1 items-center justify-start gap-px rounded-md px-2 py-1",
|
||||
"hover:bg-accent hover:text-accent-foreground sm:h-[30px]",
|
||||
"group/pages",
|
||||
"relative flex h-7 cursor-default items-center gap-px rounded-md py-0 font-medium text-muted-foreground hover:bg-[var(--item-hover)] focus-visible:outline-none focus-visible:ring-0",
|
||||
)}
|
||||
activeProps={{
|
||||
className: "bg-accent text-accent-foreground",
|
||||
}}
|
||||
>
|
||||
<div className="flex grow items-center justify-between">
|
||||
<p className="text-sm sm:text-xs">
|
||||
Pages
|
||||
{pageCount > 0 && (
|
||||
<span className="text-muted-foreground ml-1">{pageCount}</span>
|
||||
)}
|
||||
</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-7 w-full justify-start gap-1 px-2 py-0 text-xs hover:bg-inherit"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<span>Pages</span>
|
||||
{pageCount > 0 && <span className="text-xs">({pageCount})</span>}
|
||||
<ArrowIcon
|
||||
className={cn("size-3 transition-transform duration-200 ease-in-out", {
|
||||
"rotate-90": isExpanded,
|
||||
"opacity-0 group-hover/pages:opacity-100": !isExpanded,
|
||||
})}
|
||||
/>
|
||||
</Button>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute right-1 top-1/2 -translate-y-1/2",
|
||||
"transition-all duration-200 ease-in-out",
|
||||
{
|
||||
"opacity-100": isExpanded,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-px">
|
||||
<ShowAllForm />
|
||||
<NewPageButton />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
|
||||
const NewPageButton: React.FC = () => {
|
||||
@@ -122,11 +149,11 @@ const NewPageButton: React.FC = () => {
|
||||
variant="ghost"
|
||||
aria-label="New Page"
|
||||
className={cn(
|
||||
"flex size-5 items-center justify-center p-0.5 shadow-none",
|
||||
"hover:bg-accent-foreground/10",
|
||||
"flex size-5 cursor-default items-center justify-center p-0.5 shadow-none",
|
||||
"text-muted-foreground hover:bg-inherit hover:text-foreground",
|
||||
"opacity-0 transition-opacity duration-200",
|
||||
"group-hover/pages:opacity-100 group-has-[[data-state='open']]/pages:opacity-100",
|
||||
"data-[state='open']:opacity-100 focus-visible:outline-none focus-visible:ring-0",
|
||||
"focus-visible:outline-none focus-visible:ring-0 data-[state='open']:opacity-100",
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
@@ -168,29 +195,31 @@ interface PageListItemProps {
|
||||
|
||||
const PageListItem: React.FC<PageListItemProps> = ({ page }) => {
|
||||
return (
|
||||
<div className="group/reorder-page relative">
|
||||
<div className="group/sidebar-link relative flex min-w-0 flex-1">
|
||||
<Link
|
||||
to="/pages/$pageId"
|
||||
params={{ pageId: page.id }}
|
||||
className={cn(
|
||||
"relative flex h-9 w-full items-center gap-2 rounded-md p-1.5 font-medium sm:h-8",
|
||||
"group-hover/sidebar-link:bg-accent group-hover/sidebar-link:text-accent-foreground",
|
||||
)}
|
||||
activeOptions={{ exact: true }}
|
||||
activeProps={{
|
||||
className: "bg-accent text-accent-foreground",
|
||||
}}
|
||||
>
|
||||
<div className="flex max-w-[calc(100%-1rem)] flex-1 items-center gap-1.5 truncate text-sm">
|
||||
<LaIcon name="FileText" className="flex-shrink-0 opacity-60" />
|
||||
<p className="truncate opacity-95 group-hover/sidebar-link:opacity-100">
|
||||
{page.title || "Untitled"}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
to="/pages/$pageId"
|
||||
params={{ pageId: page.id }}
|
||||
className={cn(
|
||||
"group/p cursor-default text-[var(--less-foreground)]",
|
||||
"relative flex h-[30px] w-full items-center gap-2 rounded-md px-1.5 text-sm font-medium hover:bg-[var(--item-hover)]",
|
||||
)}
|
||||
activeProps={{
|
||||
className:
|
||||
"bg-[var(--item-active)] data-[status='active']:hover:bg-[var(--item-active)]",
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<div className="flex max-w-full flex-1 items-center gap-1.5 truncate">
|
||||
<LaIcon
|
||||
name="File"
|
||||
className={cn("flex-shrink-0 group-hover/p:text-foreground", {
|
||||
"text-foreground": isActive,
|
||||
"text-muted-foreground": !isActive,
|
||||
})}
|
||||
/>
|
||||
<p className="truncate">{page.title || "Untitled"}</p>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -212,11 +241,11 @@ const SubMenu = <T extends string | number>({
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<span className="flex items-center gap-2">
|
||||
<LaIcon name={icon} />
|
||||
<LaIcon name={icon} className="" />
|
||||
<span>{label}</span>
|
||||
</span>
|
||||
<span className="ml-auto flex items-center gap-1">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{options.find((option) => option.value === currentValue)?.label}
|
||||
</span>
|
||||
<LaIcon name="ChevronRight" />
|
||||
@@ -251,11 +280,11 @@ const ShowAllForm: React.FC = () => {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"flex size-5 items-center justify-center p-0.5 shadow-none",
|
||||
"hover:bg-accent-foreground/10",
|
||||
"flex size-5 cursor-default items-center justify-center p-0.5 shadow-none",
|
||||
"text-muted-foreground hover:bg-inherit hover:text-foreground",
|
||||
"opacity-0 transition-opacity duration-200",
|
||||
"group-hover/pages:opacity-100 group-has-[[data-state='open']]/pages:opacity-100",
|
||||
"data-[state='open']:opacity-100 focus-visible:outline-none focus-visible:ring-0",
|
||||
"focus-visible:outline-none focus-visible:ring-0 data-[state='open']:opacity-100",
|
||||
)}
|
||||
>
|
||||
<LaIcon name="Ellipsis" />
|
||||
|
||||
@@ -56,6 +56,7 @@ export const ProfileSection: React.FC = () => {
|
||||
signOut={signOut}
|
||||
setShowShortcut={setShowShortcut}
|
||||
/>
|
||||
<span className="flex flex-auto"></span>
|
||||
<Feedback />
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,12 +84,12 @@ const ProfileDropdown: React.FC<ProfileDropdownProps> = ({
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-label="Profile"
|
||||
className="hover:bg-accent focus-visible:ring-ring hover:text-accent-foreground flex h-auto items-center gap-1.5 truncate rounded py-1 pl-1 pr-1.5 focus-visible:outline-none focus-visible:ring-1"
|
||||
className="flex h-auto items-center gap-1.5 truncate rounded py-1 pl-1 pr-1.5 hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
>
|
||||
<Avatar className="size-6">
|
||||
<AvatarImage src={user.imageUrl} alt={user.fullName || ""} />
|
||||
</Avatar>
|
||||
<span className="truncate text-left text-sm font-medium -tracking-wider">
|
||||
<span className="truncate text-left text-sm font-medium -tracking-wide">
|
||||
{user.fullName}
|
||||
</span>
|
||||
<LaIcon
|
||||
|
||||
@@ -93,7 +93,7 @@ const TaskSectionHeader: React.FC<TaskSectionHeaderProps> = ({
|
||||
<Link
|
||||
to="/tasks"
|
||||
className={cn(
|
||||
"flex flex-1 min-h-[30px] gap-px items-center justify-start hover:bg-accent hover:text-accent-foreground rounded-md px-2 py-1 focus-visible:outline-none focus-visible:ring-0",
|
||||
"flex min-h-[30px] flex-1 items-center justify-start gap-px rounded-md px-2 py-1 hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-0",
|
||||
)}
|
||||
search={{ filter }}
|
||||
activeProps={{
|
||||
@@ -104,7 +104,7 @@ const TaskSectionHeader: React.FC<TaskSectionHeaderProps> = ({
|
||||
|
||||
<p className="text-sm">
|
||||
{title}
|
||||
{count > 0 && <span className="text-muted-foreground ml-1">{count}</span>}
|
||||
{count > 0 && <span className="ml-1 text-muted-foreground">{count}</span>}
|
||||
</p>
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { useAccount } from "@/lib/providers/jazz-provider"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { LaIcon } from "@/components/custom/la-icon"
|
||||
import { ListOfTopics } from "@/lib/schema"
|
||||
import { LEARNING_STATES, LearningStateValue } from "@/lib/constants"
|
||||
import { Link } from "@tanstack/react-router"
|
||||
|
||||
export const TopicSection: React.FC = () => {
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
topicsWantToLearn: [],
|
||||
topicsLearning: [],
|
||||
topicsLearned: [],
|
||||
},
|
||||
})
|
||||
|
||||
const topicCount =
|
||||
(me?.root.topicsWantToLearn?.length || 0) +
|
||||
(me?.root.topicsLearning?.length || 0) +
|
||||
(me?.root.topicsLearned?.length || 0)
|
||||
|
||||
if (!me) return null
|
||||
|
||||
return (
|
||||
<div className="group/topics flex flex-col gap-px py-2">
|
||||
<TopicSectionHeader topicCount={topicCount} />
|
||||
<List
|
||||
topicsWantToLearn={me.root.topicsWantToLearn}
|
||||
topicsLearning={me.root.topicsLearning}
|
||||
topicsLearned={me.root.topicsLearned}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface TopicSectionHeaderProps {
|
||||
topicCount: number
|
||||
}
|
||||
|
||||
const TopicSectionHeader: React.FC<TopicSectionHeaderProps> = ({
|
||||
topicCount,
|
||||
}) => (
|
||||
<Link
|
||||
to="/topics"
|
||||
className="flex h-9 items-center gap-px rounded-md px-2 py-1 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-0 sm:h-[30px] sm:text-xs"
|
||||
activeProps={{
|
||||
className: "bg-accent text-accent-foreground",
|
||||
}}
|
||||
>
|
||||
<p className="text-sm sm:text-xs">
|
||||
Topics
|
||||
{topicCount > 0 && (
|
||||
<span className="text-muted-foreground ml-1">{topicCount}</span>
|
||||
)}
|
||||
</p>
|
||||
</Link>
|
||||
)
|
||||
|
||||
interface ListProps {
|
||||
topicsWantToLearn: ListOfTopics
|
||||
topicsLearning: ListOfTopics
|
||||
topicsLearned: ListOfTopics
|
||||
}
|
||||
|
||||
const List: React.FC<ListProps> = ({
|
||||
topicsWantToLearn,
|
||||
topicsLearning,
|
||||
topicsLearned,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-px">
|
||||
<ListItem
|
||||
key={topicsWantToLearn.id}
|
||||
count={topicsWantToLearn.length}
|
||||
label="To Learn"
|
||||
value="wantToLearn"
|
||||
/>
|
||||
<ListItem
|
||||
key={topicsLearning.id}
|
||||
label="Learning"
|
||||
value="learning"
|
||||
count={topicsLearning.length}
|
||||
/>
|
||||
<ListItem
|
||||
key={topicsLearned.id}
|
||||
label="Learned"
|
||||
value="learned"
|
||||
count={topicsLearned.length}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ListItemProps {
|
||||
label: string
|
||||
value: LearningStateValue
|
||||
count: number
|
||||
}
|
||||
|
||||
const ListItem: React.FC<ListItemProps> = ({ label, value, count }) => {
|
||||
const le = LEARNING_STATES.find((l) => l.value === value)
|
||||
|
||||
if (!le) return null
|
||||
|
||||
return (
|
||||
<div className="group/reorder-page relative">
|
||||
<div className="group/topic-link relative flex min-w-0 flex-1">
|
||||
<Link
|
||||
to="/topics"
|
||||
search={{ learningState: value }}
|
||||
className={cn(
|
||||
"group-hover/topic-link:bg-accent relative flex h-9 w-full items-center gap-2 rounded-md p-1.5 font-medium sm:h-8",
|
||||
le.className,
|
||||
)}
|
||||
activeOptions={{ exact: true }}
|
||||
activeProps={{
|
||||
className: "bg-accent text-accent-foreground",
|
||||
}}
|
||||
>
|
||||
<div className="flex max-w-full flex-1 items-center gap-1.5 truncate text-sm">
|
||||
<LaIcon name={le.icon} className="flex-shrink-0 opacity-60" />
|
||||
<p
|
||||
className={cn(
|
||||
"truncate opacity-95 group-hover/topic-link:opacity-100",
|
||||
le.className,
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{count > 0 && (
|
||||
<span className="absolute right-2 top-1/2 z-[1] -translate-y-1/2 rounded p-1 text-sm">
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from "react"
|
||||
import { useMedia } from "@/hooks/use-media"
|
||||
import { useAtom } from "jotai"
|
||||
import { LogoIcon } from "@/components/icons/logo-icon"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { isCollapseAtom } from "@/store/sidebar"
|
||||
@@ -9,12 +8,12 @@ import { useAccountOrGuest } from "@/lib/providers/jazz-provider"
|
||||
import { LaIcon } from "@/components/custom/la-icon"
|
||||
import { Link, useLocation } from "@tanstack/react-router"
|
||||
|
||||
import { LinkSection } from "./partials/link-section"
|
||||
// import { LinkSection } from "./partials/link-section"
|
||||
import { PageSection } from "./partials/page-section"
|
||||
import { TopicSection } from "./partials/topic-section"
|
||||
import { ProfileSection } from "./partials/profile-section"
|
||||
import { JournalSection } from "./partials/journal-section"
|
||||
import { TaskSection } from "./partials/task-section"
|
||||
import { LinkCollection } from "./partials/link-collection"
|
||||
|
||||
interface SidebarContextType {
|
||||
isCollapsed: boolean
|
||||
@@ -64,14 +63,14 @@ const SidebarItem: React.FC<SidebarItemProps> = React.memo(
|
||||
)}
|
||||
>
|
||||
<Link
|
||||
className="text-secondary-foreground flex h-8 grow items-center truncate rounded-md pl-1.5 pr-1 text-sm font-medium"
|
||||
className="flex h-8 grow items-center truncate rounded-md pl-1.5 pr-1 text-sm font-medium text-secondary-foreground"
|
||||
to={url}
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon && (
|
||||
<span
|
||||
className={cn(
|
||||
"text-primary/60 group-hover:text-primary mr-2 size-4",
|
||||
"mr-2 size-4 text-primary/60 group-hover:text-primary",
|
||||
{ "text-primary": isActive },
|
||||
)}
|
||||
>
|
||||
@@ -102,7 +101,7 @@ const LogoAndSearch: React.FC = React.memo(() => {
|
||||
to={pathname === "/search" ? "/" : "/search"}
|
||||
className={cn(
|
||||
buttonVariants({ size: "sm", variant: "secondary" }),
|
||||
"text-primary/60 flex w-20 items-center justify-start py-4 pl-2",
|
||||
"flex w-20 items-center justify-start py-4 pl-2 text-primary/60",
|
||||
)}
|
||||
activeProps={{
|
||||
className: "text-md font-medium",
|
||||
@@ -126,14 +125,14 @@ const SidebarContent: React.FC = React.memo(() => {
|
||||
const { me } = useAccountOrGuest()
|
||||
|
||||
return (
|
||||
<nav className="bg-background relative flex h-full w-full shrink-0 flex-col">
|
||||
<nav className="relative flex h-full w-full shrink-0 flex-col bg-[var(--body-background)]">
|
||||
<div>
|
||||
<LogoAndSearch />
|
||||
</div>
|
||||
<div className="relative mb-0.5 mt-1.5 flex grow flex-col overflow-y-auto rounded-md px-3 outline-none">
|
||||
<div className="h-2 shrink-0" />
|
||||
{me._type === "Account" && <LinkSection />}
|
||||
{me._type === "Account" && <TopicSection />}
|
||||
{me._type === "Account" && <LinkCollection />}
|
||||
{/* {me._type === "Account" && <LinkSection />} */}
|
||||
{me._type === "Account" && <JournalSection />}
|
||||
{me._type === "Account" && <TaskSection />}
|
||||
{me._type === "Account" && <PageSection />}
|
||||
@@ -156,7 +155,7 @@ const Sidebar: React.FC = () => {
|
||||
)
|
||||
|
||||
const sidebarInnerClasses = cn(
|
||||
"h-full w-56 min-w-56 transition-transform duration-300 ease-in-out",
|
||||
"h-full w-60 min-w-60 transition-transform duration-300 ease-in-out",
|
||||
isCollapsed ? "-translate-x-full" : "translate-x-0",
|
||||
)
|
||||
|
||||
@@ -184,7 +183,7 @@ const Sidebar: React.FC = () => {
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(sidebarInnerClasses, "border-r-primary/5 border-r")}
|
||||
className={cn(sidebarInnerClasses, "border-r border-r-primary/5")}
|
||||
>
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<SidebarContent />
|
||||
|
||||
Reference in New Issue
Block a user