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:
@@ -17,28 +17,28 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
|
||||
console.error(error)
|
||||
|
||||
return (
|
||||
<div className="min-w-0 flex-1 p-4 flex flex-col items-center justify-center gap-6">
|
||||
<div className="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 p-4">
|
||||
<ErrorComponent error={error} />
|
||||
<div className="flex gap-2 items-center flex-wrap">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
router.invalidate()
|
||||
}}
|
||||
className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded text-white uppercase font-extrabold`}
|
||||
className={`rounded bg-gray-600 px-2 py-1 font-extrabold uppercase text-white dark:bg-gray-700`}
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
{isRoot ? (
|
||||
<Link
|
||||
to="/"
|
||||
className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded text-white uppercase font-extrabold`}
|
||||
className={`rounded bg-gray-600 px-2 py-1 font-extrabold uppercase text-white dark:bg-gray-700`}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
to="/"
|
||||
className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded text-white uppercase font-extrabold`}
|
||||
className={`rounded bg-gray-600 px-2 py-1 font-extrabold uppercase text-white dark:bg-gray-700`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
window.history.back()
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import * as React from "react"
|
||||
import { useKeyDown, KeyFilter, Options } from "@/hooks/use-key-down"
|
||||
import { useAccountOrGuest } from "@/lib/providers/jazz-provider"
|
||||
import { isModKey, isServer } from "@/lib/utils"
|
||||
import { useAtom } from "jotai"
|
||||
import { usePageActions } from "~/hooks/actions/use-page-actions"
|
||||
import { useAuth } from "@clerk/tanstack-start"
|
||||
import { useNavigate } from "@tanstack/react-router"
|
||||
import queryString from "query-string"
|
||||
import { commandPaletteOpenAtom } from "~/store/any-store"
|
||||
import { isModKey, isServer } from "@shared/utils"
|
||||
|
||||
type RegisterKeyDownProps = {
|
||||
trigger: KeyFilter
|
||||
|
||||
@@ -6,16 +6,16 @@ export function NotFound({ children }: { children?: any }) {
|
||||
<div className="text-gray-600 dark:text-gray-400">
|
||||
{children || <p>The page you are looking for does not exist.</p>}
|
||||
</div>
|
||||
<p className="flex items-center gap-2 flex-wrap">
|
||||
<p className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
onClick={() => window.history.back()}
|
||||
className="bg-emerald-500 text-white px-2 py-1 rounded uppercase font-black text-sm"
|
||||
className="rounded bg-emerald-500 px-2 py-1 text-sm font-black uppercase text-white"
|
||||
>
|
||||
Go back
|
||||
</button>
|
||||
<Link
|
||||
to="/"
|
||||
className="bg-cyan-600 text-white px-2 py-1 rounded uppercase font-black text-sm"
|
||||
className="rounded bg-cyan-600 px-2 py-1 text-sm font-black uppercase text-white"
|
||||
>
|
||||
Start Over
|
||||
</Link>
|
||||
|
||||
@@ -42,7 +42,7 @@ export function Onboarding() {
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogDescription asChild>
|
||||
<div className="text-foreground/70 space-y-4 text-base leading-5">
|
||||
<div className="space-y-4 text-base leading-5 text-foreground/70">
|
||||
<p>
|
||||
Learn Anything is a learning platform that organizes knowledge in
|
||||
a social way. You can create pages, add links, track learning
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { icons } from "lucide-react"
|
||||
import { LaAccount } from "@/lib/schema"
|
||||
import { HTMLLikeElement } from "@/lib/utils"
|
||||
import { useCommandActions } from "~/hooks/use-command-actions"
|
||||
import { useCommandActions } from "~/hooks/actions/use-command-actions"
|
||||
|
||||
export type CommandAction = string | (() => void)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { CommandGroup } from "./command-group"
|
||||
import { CommandAction, createCommandGroups } from "./command-data"
|
||||
import { useAccount, useAccountOrGuest } from "@/lib/providers/jazz-provider"
|
||||
import { useAtom } from "jotai"
|
||||
import { useCommandActions } from "~/hooks/use-command-actions"
|
||||
import { useCommandActions } from "~/hooks/actions/use-command-actions"
|
||||
import {
|
||||
filterItems,
|
||||
getTopics,
|
||||
|
||||
@@ -15,7 +15,7 @@ export const ContentHeader = React.forwardRef<
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
"flex min-h-10 min-w-0 shrink-0 items-center gap-3 pl-8 pr-6 transition-opacity max-lg:px-4",
|
||||
"flex min-h-10 min-w-0 shrink-0 items-center gap-3 border-b border-b-[var(--la-border-new)] px-6 py-3 transition-opacity max-lg:px-4",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
@@ -48,7 +48,7 @@ export const SidebarToggleButton: React.FC = () => {
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
aria-label="Menu"
|
||||
className="text-primary/60"
|
||||
className="-ml-2 cursor-default text-muted-foreground hover:bg-transparent"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<LaIcon name="PanelLeft" />
|
||||
|
||||
@@ -66,7 +66,7 @@ export const LearningStateSelector: React.FC<LearningStateSelectorProps> = ({
|
||||
type="button"
|
||||
role="combobox"
|
||||
variant="secondary"
|
||||
className={cn("gap-x-2 text-sm", className)}
|
||||
className={cn("h-7 gap-x-2 text-sm", className)}
|
||||
>
|
||||
{iconName && (
|
||||
<LaIcon
|
||||
|
||||
71
web/app/components/custom/nav-item.tsx
Normal file
71
web/app/components/custom/nav-item.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Link } from "@tanstack/react-router"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { LaIcon } from "~/components/custom/la-icon"
|
||||
import { icons } from "lucide-react"
|
||||
import type { NavigateOptions } from "@tanstack/react-router"
|
||||
|
||||
interface NavItemProps extends NavigateOptions {
|
||||
title: string
|
||||
count: number
|
||||
icon: keyof typeof icons
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function NavItem({
|
||||
title,
|
||||
count,
|
||||
icon,
|
||||
className,
|
||||
...linkProps
|
||||
}: NavItemProps) {
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
"group/p",
|
||||
"flex h-[30px] cursor-default items-center gap-px rounded-md px-2 text-sm font-medium",
|
||||
"text-[var(--less-foreground)] hover:bg-[var(--item-hover)] focus-visible:outline-none focus-visible:ring-0",
|
||||
className,
|
||||
)}
|
||||
activeProps={{
|
||||
className:
|
||||
'bg-[var(--item-active)] data-[status="active"]:hover:bg-[var(--item-active)]',
|
||||
}}
|
||||
{...linkProps}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<LaIcon
|
||||
name={icon}
|
||||
className={cn("group-hover/p:text-foreground", {
|
||||
"text-foreground": isActive,
|
||||
"text-muted-foreground": !isActive,
|
||||
})}
|
||||
/>
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
<span className="flex-grow" />
|
||||
{count > 0 && <BadgeCount count={count} isActive={isActive} />}
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
interface BadgeCountProps {
|
||||
count: number
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
function BadgeCount({ count, isActive }: BadgeCountProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn("font-mono", {
|
||||
"text-muted-foreground": !isActive,
|
||||
"text-foreground": isActive,
|
||||
})}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ const TextareaAutosize = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
return (
|
||||
<BaseTextareaAutosize
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[60px] w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
27
web/app/components/icons/arrow-icon.tsx
Normal file
27
web/app/components/icons/arrow-icon.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
export const ArrowIcon = ({
|
||||
className,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M18.1125 12.4381C18.4579 12.2481 18.4579 11.7519 18.1125 11.5619L8.74096 6.40753C8.40773 6.22425 8 6.46533 8 6.84564V17.1544C8 17.5347 8.40773 17.7757 8.74096 17.5925L18.1125 12.4381Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -66,7 +66,7 @@ const ShortcutKey: React.FC<{ keyChar: string }> = ({ keyChar }) => (
|
||||
const ShortcutItem: React.FC<ShortcutItem> = ({ label, keys, then }) => (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<dt className="flex grow items-center">
|
||||
<span className="text-muted-foreground text-left text-sm">{label}</span>
|
||||
<span className="text-left text-sm text-muted-foreground">{label}</span>
|
||||
</dt>
|
||||
<dd className="flex items-end">
|
||||
<span className="text-left">
|
||||
@@ -79,7 +79,7 @@ const ShortcutItem: React.FC<ShortcutItem> = ({ label, keys, then }) => (
|
||||
))}
|
||||
{then && (
|
||||
<>
|
||||
<span className="text-muted-foreground text-xs">then</span>
|
||||
<span className="text-xs text-muted-foreground">then</span>
|
||||
{then.map((key, index) => (
|
||||
<ShortcutKey key={`then-${index}`} keyChar={key} />
|
||||
))}
|
||||
@@ -149,7 +149,7 @@ export function Shortcut() {
|
||||
"size-6 p-0",
|
||||
)}
|
||||
>
|
||||
<LaIcon name="X" className="text-muted-foreground size-5" />
|
||||
<LaIcon name="X" className="size-5 text-muted-foreground" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</header>
|
||||
@@ -158,12 +158,12 @@ export function Shortcut() {
|
||||
<form className="relative flex items-center">
|
||||
<LaIcon
|
||||
name="Search"
|
||||
className="text-muted-foreground absolute left-3 size-4"
|
||||
className="absolute left-3 size-4 text-muted-foreground"
|
||||
/>
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder="Search shortcuts"
|
||||
className="border-muted-foreground/50 focus-visible:border-muted-foreground h-10 pl-10 focus-visible:ring-0"
|
||||
className="h-10 border-muted-foreground/50 pl-10 focus-visible:border-muted-foreground focus-visible:ring-0"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -19,7 +19,7 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -19,7 +19,7 @@ const SheetOverlay = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user