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:
Aslam
2024-10-18 21:18:20 +07:00
committed by GitHub
parent c93c634a77
commit a440828f8c
158 changed files with 2808 additions and 1064 deletions

0
q&a
View File

View File

@@ -3,7 +3,7 @@ import tsConfigPaths from "vite-tsconfig-paths"
export default defineConfig({
vite: {
plugins: () => [
plugins: [
tsConfigPaths({
projects: ["./tsconfig.json"],
}),

View File

@@ -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()

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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)

View File

@@ -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,

View File

@@ -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" />

View File

@@ -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

View 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>
)
}

View File

@@ -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}

View 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>
)
}

View File

@@ -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)}
/>

View File

@@ -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,

View File

@@ -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

View 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>
)
}

View File

@@ -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>
)

View File

@@ -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" />

View File

@@ -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

View File

@@ -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>
)

View File

@@ -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>
)
}

View File

@@ -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 />

View File

@@ -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}

View File

@@ -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}

View File

@@ -3,7 +3,7 @@ import { ensureUrlProtocol } from "@/lib/utils"
import { useTheme } from "next-themes"
import { toast } from "sonner"
import { LaAccount } from "@/lib/schema"
import { usePageActions } from "./actions/use-page-actions"
import { usePageActions } from "./use-page-actions"
import { useNavigate } from "@tanstack/react-router"
export const useCommandActions = () => {

View File

@@ -1,5 +1,6 @@
import * as React from "react"
import { isModKey, isServer, isTextInput } from "@/lib/utils"
import { isTextInput } from "@/lib/utils"
import { isModKey, isServer } from "@shared/utils"
export type KeyFilter = ((event: KeyboardEvent) => boolean) | string
export type Options = { allowInInput?: boolean }

View File

@@ -1,5 +1,5 @@
import { isClient } from "@shared/utils"
import * as React from "react"
import { isClient } from "~/lib/utils"
export function useTouchSensor() {
const [isTouchDevice, setIsTouchDevice] = React.useState(false)

View File

@@ -28,13 +28,13 @@ export const LEARNING_STATES: LearningState[] = [
label: "Learning",
value: "learning",
icon: "GraduationCap",
className: "text-[#D29752]",
className: "text-yellow-600 hover:text-yellow-700",
},
{
label: "Learned",
value: "learned",
icon: "Check",
className: "text-[#708F51]",
className: "text-green-700 hover:text-green-800",
},
] as const

View File

@@ -0,0 +1,6 @@
import { co, CoMap, Encoders } from "jazz-tools"
export class BaseModel extends CoMap {
createdAt = co.encoded(Encoders.Date)
updatedAt = co.encoded(Encoders.Date)
}

View File

@@ -0,0 +1,26 @@
import { co, CoList, ImageDefinition } from "jazz-tools"
import { BaseModel } from "./base"
export class Image extends BaseModel {
fileName = co.optional.string
fileSize = co.optional.number
width = co.optional.number
height = co.optional.number
aspectRatio = co.optional.number
mimeType = co.optional.string
lastModified = co.optional.string
url = co.optional.string
content = co.optional.ref(ImageDefinition)
}
export class ImageLists extends CoList.Of(co.ref(Image)) {}
export class Folder extends BaseModel {
name = co.string
description = co.optional.string
icon = co.optional.string
images = co.optional.ref(ImageLists)
parent = co.optional.ref(Folder)
}
export class FolderLists extends CoList.Of(co.ref(Folder)) {}

View File

@@ -4,6 +4,7 @@ import { PersonalLinkLists } from "./personal-link"
import { ListOfTopics } from "./master/topic"
import { ListOfTasks } from "./task"
import { JournalEntryLists } from "./journal"
import { FolderLists, ImageLists } from "./folder"
declare module "jazz-tools" {
interface Profile {
@@ -12,6 +13,7 @@ declare module "jazz-tools" {
}
export class UserRoot extends CoMap {
version = co.optional.number
name = co.string
username = co.string
avatar = co.optional.string
@@ -28,6 +30,9 @@ export class UserRoot extends CoMap {
tasks = co.ref(ListOfTasks)
journalEntries = co.ref(JournalEntryLists)
folders = co.ref(FolderLists)
images = co.ref(ImageLists)
}
export class LaAccount extends Account {
@@ -38,9 +43,6 @@ export class LaAccount extends Account {
this: LaAccount,
creationProps?: { name: string; avatarUrl?: string },
) {
// since we dont have a custom AuthProvider yet.
// and still using the DemoAuth. the creationProps will only accept name.
// so just do default profile create provided by jazz-tools
super.migrate(creationProps)
if (!this._refs.root && creationProps) {
@@ -62,6 +64,11 @@ export class LaAccount extends Account {
tasks: ListOfTasks.create([], { owner: this }),
journalEntries: JournalEntryLists.create([], { owner: this }),
folders: FolderLists.create([], { owner: this }),
images: ImageLists.create([], { owner: this }),
version: 1,
},
{ owner: this },
)
@@ -72,3 +79,6 @@ export class LaAccount extends Account {
export * from "./master/topic"
export * from "./personal-link"
export * from "./personal-page"
export * from "./task"
export * from "./journal"
export * from "./folder"

View File

@@ -1,11 +1,10 @@
import { co, CoList, CoMap, Encoders } from "jazz-tools"
import { co, CoList, Encoders } from "jazz-tools"
import { BaseModel } from "./base"
export class JournalEntry extends CoMap {
export class JournalEntry extends BaseModel {
title = co.string
content = co.json()
date = co.encoded(Encoders.Date)
createdAt = co.encoded(Encoders.Date)
updatedAt = co.encoded(Encoders.Date)
}
export class JournalEntryLists extends CoList.Of(co.ref(JournalEntry)) {}

View File

@@ -1,14 +1,10 @@
import { co, CoList, CoMap, Encoders } from "jazz-tools"
import { co, CoList } from "jazz-tools"
import { Link, Topic } from "./master/topic"
class BaseModel extends CoMap {
createdAt = co.encoded(Encoders.Date)
updatedAt = co.encoded(Encoders.Date)
}
import { BaseModel } from "./base"
export class PersonalLink extends BaseModel {
url = co.string
icon = co.optional.string // is an icon URL
icon = co.optional.string
link = co.optional.ref(Link)
title = co.string
slug = co.string

View File

@@ -1,21 +1,13 @@
import { co, CoList, CoMap, Encoders } from "jazz-tools"
import { co, CoList } from "jazz-tools"
import { Topic } from "./master/topic"
import { BaseModel } from "./base"
/*
* Page, content that user can write to. Similar to Notion/Reflect page. It holds ProseMirror editor content + metadata.
* - slug: make it unique
* - Public Access, url should be learn-anything.xyz/@user/slug
* - if public, certain members (can do read/write access accordingly), personal (end to end encrypted, only accessed by user)
*/
export class PersonalPage extends CoMap {
export class PersonalPage extends BaseModel {
title = co.optional.string
slug = co.optional.string // is used only when `public: true` for sharing, `@user/page-slug`
slug = co.optional.string
public = co.boolean
content = co.optional.json()
topic = co.optional.ref(Topic)
createdAt = co.encoded(Encoders.Date)
updatedAt = co.encoded(Encoders.Date)
// backlinks = co.optional.ref() // other PersonalPages linking to this page TODO: add, think through how to do it well, efficiently
}
export class PersonalPageLists extends CoList.Of(co.ref(PersonalPage)) {}

View File

@@ -1,11 +1,10 @@
import { co, CoList, CoMap, Encoders } from "jazz-tools"
import { co, CoList, Encoders } from "jazz-tools"
import { BaseModel } from "./base"
export class Task extends CoMap {
export class Task extends BaseModel {
title = co.string
description = co.optional.string
status = co.literal("todo", "in_progress", "done")
createdAt = co.encoded(Encoders.Date)
updatedAt = co.encoded(Encoders.Date)
dueDate = co.optional.encoded(Encoders.Date)
}

View File

@@ -24,10 +24,6 @@ export function shuffleArray<T>(array: T[]): T[] {
return shuffled
}
export const isClient = () => typeof window !== "undefined"
export const isServer = () => !isClient()
const inputs = ["input", "select", "button", "textarea"] // detect if node is a text input element
export function isTextInput(element: Element): boolean {
@@ -75,7 +71,6 @@ export function calendarFormatDate(date: Date): string {
}
export * from "./force-graph"
export * from "./keyboard"
export * from "./env"
export * from "./slug"
export * from "./url"

View File

@@ -1,18 +1,3 @@
export function isValidUrl(string: string): boolean {
try {
new URL(string)
return true
} catch (_) {
return false
}
}
export function isUrl(text: string): boolean {
const pattern: RegExp =
/^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/
return pattern.test(text)
}
export function ensureUrlProtocol(
url: string,
defaultProtocol: string = "https://",

View File

@@ -169,14 +169,14 @@ declare module '@tanstack/react-router' {
parentRoute: typeof LayoutImport
}
'/_layout/(auth)': {
id: '/_layout/'
id: '/_layout/(auth)'
path: '/'
fullPath: '/'
preLoaderRoute: typeof LayoutauthImport
parentRoute: typeof LayoutImport
}
'/_layout/(auth)/_auth': {
id: '/_layout/_auth'
id: '/_layout/(auth)/_auth'
path: '/'
fullPath: '/'
preLoaderRoute: typeof LayoutauthAuthImport
@@ -190,28 +190,28 @@ declare module '@tanstack/react-router' {
parentRoute: typeof LayoutPagesImport
}
'/_layout/(landing)/': {
id: '/_layout/'
id: '/_layout/(landing)/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof LayoutlandingIndexImport
parentRoute: typeof LayoutImport
}
'/_layout/_pages/(topic)/$': {
id: '/_layout/_pages/$'
id: '/_layout/_pages/(topic)/$'
path: '/$'
fullPath: '/$'
preLoaderRoute: typeof LayoutPagestopicSplatImport
parentRoute: typeof LayoutPagesImport
}
'/_layout/(auth)/_auth/sign-in/$': {
id: '/_layout/_auth/sign-in/$'
id: '/_layout/(auth)/_auth/sign-in/$'
path: '/sign-in/$'
fullPath: '/sign-in/$'
preLoaderRoute: typeof LayoutauthAuthSignInSplatImport
parentRoute: typeof LayoutauthAuthImport
}
'/_layout/(auth)/_auth/sign-up/$': {
id: '/_layout/_auth/sign-up/$'
id: '/_layout/(auth)/_auth/sign-up/$'
path: '/sign-up/$'
fullPath: '/sign-up/$'
preLoaderRoute: typeof LayoutauthAuthSignUpSplatImport
@@ -432,12 +432,13 @@ export interface FileRoutesById {
__root__: typeof rootRoute
'/_layout': typeof LayoutRouteWithChildren
'/_layout/_pages': typeof LayoutPagesRouteWithChildren
'/_layout/': typeof LayoutlandingIndexRoute
'/_layout/_auth': typeof LayoutauthAuthRouteWithChildren
'/_layout/(auth)': typeof LayoutauthRouteWithChildren
'/_layout/(auth)/_auth': typeof LayoutauthAuthRouteWithChildren
'/_layout/_pages/_protected': typeof LayoutPagesProtectedRouteWithChildren
'/_layout/_pages/$': typeof LayoutPagestopicSplatRoute
'/_layout/_auth/sign-in/$': typeof LayoutauthAuthSignInSplatRoute
'/_layout/_auth/sign-up/$': typeof LayoutauthAuthSignUpSplatRoute
'/_layout/(landing)/': typeof LayoutlandingIndexRoute
'/_layout/_pages/(topic)/$': typeof LayoutPagestopicSplatRoute
'/_layout/(auth)/_auth/sign-in/$': typeof LayoutauthAuthSignInSplatRoute
'/_layout/(auth)/_auth/sign-up/$': typeof LayoutauthAuthSignUpSplatRoute
'/_layout/_pages/_protected/journals/': typeof LayoutPagesProtectedJournalsIndexRoute
'/_layout/_pages/_protected/links/': typeof LayoutPagesProtectedLinksIndexRoute
'/_layout/_pages/_protected/onboarding/': typeof LayoutPagesProtectedOnboardingIndexRoute
@@ -492,12 +493,13 @@ export interface FileRouteTypes {
| '__root__'
| '/_layout'
| '/_layout/_pages'
| '/_layout/'
| '/_layout/_auth'
| '/_layout/(auth)'
| '/_layout/(auth)/_auth'
| '/_layout/_pages/_protected'
| '/_layout/_pages/$'
| '/_layout/_auth/sign-in/$'
| '/_layout/_auth/sign-up/$'
| '/_layout/(landing)/'
| '/_layout/_pages/(topic)/$'
| '/_layout/(auth)/_auth/sign-in/$'
| '/_layout/(auth)/_auth/sign-up/$'
| '/_layout/_pages/_protected/journals/'
| '/_layout/_pages/_protected/links/'
| '/_layout/_pages/_protected/onboarding/'
@@ -539,8 +541,8 @@ export const routeTree = rootRoute
"filePath": "_layout.tsx",
"children": [
"/_layout/_pages",
"/_layout/",
"/_layout/"
"/_layout/(auth)",
"/_layout/(landing)/"
]
},
"/_layout/_pages": {
@@ -548,19 +550,22 @@ export const routeTree = rootRoute
"parent": "/_layout",
"children": [
"/_layout/_pages/_protected",
"/_layout/_pages/$"
"/_layout/_pages/(topic)/$"
]
},
"/_layout/": {
"filePath": "_layout/(landing)/index.tsx",
"parent": "/_layout"
},
"/_layout/_auth": {
"filePath": "_layout/(auth)/_auth.tsx",
"parent": "/_layout/",
"/_layout/(auth)": {
"filePath": "_layout/(auth)",
"parent": "/_layout",
"children": [
"/_layout/_auth/sign-in/$",
"/_layout/_auth/sign-up/$"
"/_layout/(auth)/_auth"
]
},
"/_layout/(auth)/_auth": {
"filePath": "_layout/(auth)/_auth.tsx",
"parent": "/_layout/(auth)",
"children": [
"/_layout/(auth)/_auth/sign-in/$",
"/_layout/(auth)/_auth/sign-up/$"
]
},
"/_layout/_pages/_protected": {
@@ -580,17 +585,21 @@ export const routeTree = rootRoute
"/_layout/_pages/_protected/pages/$pageId/"
]
},
"/_layout/_pages/$": {
"/_layout/(landing)/": {
"filePath": "_layout/(landing)/index.tsx",
"parent": "/_layout"
},
"/_layout/_pages/(topic)/$": {
"filePath": "_layout/_pages/(topic)/$.tsx",
"parent": "/_layout/_pages"
},
"/_layout/_auth/sign-in/$": {
"/_layout/(auth)/_auth/sign-in/$": {
"filePath": "_layout/(auth)/_auth.sign-in.$.tsx",
"parent": "/_layout/_auth"
"parent": "/_layout/(auth)/_auth"
},
"/_layout/_auth/sign-up/$": {
"/_layout/(auth)/_auth/sign-up/$": {
"filePath": "_layout/(auth)/_auth.sign-up.$.tsx",
"parent": "/_layout/_auth"
"parent": "/_layout/(auth)/_auth"
},
"/_layout/_pages/_protected/journals/": {
"filePath": "_layout/_pages/_protected/journals/index.tsx",

View File

@@ -124,7 +124,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
<React.Suspense>
<TanStackRouterDevtools position="bottom-right" />
<ReactQueryDevtools buttonPosition="bottom-left" />
<ReactQueryDevtools buttonPosition="bottom-right" />
</React.Suspense>
<ScrollRestoration />

View File

@@ -8,7 +8,7 @@ export const Route = createFileRoute("/_layout")({
function LayoutComponent() {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
<ClerkProvider>
<Outlet />
</ClerkProvider>

View File

@@ -1,6 +1,11 @@
import { createFileRoute, Outlet } from "@tanstack/react-router"
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"
export const Route = createFileRoute("/_layout/(auth)/_auth")({
beforeLoad({ context }) {
if (context.auth) {
throw redirect({ to: "/links", replace: true })
}
},
component: () => (
<main className="h-full">
<Outlet />

View File

@@ -104,10 +104,13 @@ export function Autocomplete({
return (
<Command
className={cn("relative mx-auto max-w-md overflow-visible shadow-md", {
"rounded-lg border": !open,
"rounded-none rounded-t-lg border-l border-r border-t": open,
})}
className={cn(
"relative mx-auto max-w-md overflow-visible bg-background shadow-md",
{
"rounded-lg border": !open,
"rounded-none rounded-t-lg border-l border-r border-t": open,
},
)}
>
<div className="relative flex items-center">
<CommandPrimitive.Input
@@ -125,7 +128,7 @@ export function Autocomplete({
}}
placeholder={filteredTopics[0]?.prettyName}
className={cn(
"placeholder:text-muted-foreground flex-1 bg-transparent min-h-10 px-3 py-1 sm:py-3 sm:px-4 outline-none",
"min-h-10 flex-1 bg-transparent px-3 py-1 outline-none placeholder:text-muted-foreground sm:px-4 sm:py-3",
)}
autoFocus
/>
@@ -138,7 +141,7 @@ export function Autocomplete({
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.1 }}
className="bg-background absolute left-0 right-0 z-10 -mx-px rounded-b-lg border-b border-l border-r border-t shadow-lg"
className="absolute left-0 right-0 z-10 -mx-px rounded-b-lg border-b border-l border-r border-t bg-background shadow-lg"
>
<CommandList className="max-h-56">
<CommandGroup className="my-2">
@@ -150,7 +153,7 @@ export function Autocomplete({
className="min-h-10 rounded-none px-3 py-1.5"
>
<span>{topic.prettyName}</span>
<span className="text-muted-foreground/80 ml-auto text-xs">
<span className="ml-auto text-xs text-muted-foreground/80">
{topic.connectedTopics.length > 0 &&
topic.connectedTopics.join(", ")}
</span>

View File

@@ -25,7 +25,7 @@ function LandingComponent() {
}
return (
<div className="relative h-full w-screen">
<div className="relative h-full w-screen bg-background">
<ForceGraphClient
raw_nodes={GraphData}
onNodeClick={handleTopicSelect}
@@ -40,7 +40,7 @@ function LandingComponent() {
>
<motion.h1
className={cn(
"mb-2 text-center text-5xl font-bold tracking-tight sm:mb-4 md:text-7xl font-raleway",
"mb-2 text-center font-raleway text-5xl font-bold tracking-tight sm:mb-4 md:text-7xl",
)}
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}

View File

@@ -33,7 +33,7 @@ function LayoutContent() {
return (
<>
<Toaster expand={false} />
<div className="flex min-h-full size-full flex-row items-stretch overflow-hidden">
<div className="flex size-full min-h-full flex-row items-stretch overflow-hidden">
<Sidebar />
<Shortcut />
<GlobalKeyboardHandler />
@@ -49,7 +49,7 @@ function LayoutContent() {
function MainContent() {
return (
<div className="relative flex min-w-0 flex-1 flex-col">
<main className="relative flex flex-auto flex-col place-items-stretch overflow-auto lg:my-2 lg:mr-2 lg:rounded-md lg:border">
<main className="relative flex flex-auto flex-col place-items-stretch overflow-auto bg-[var(--container-background)] lg:my-2 lg:mr-2 lg:rounded-md lg:border">
<Outlet />
</main>
</div>

View File

@@ -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">

View File

@@ -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>
</>
)
})

View File

@@ -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>

View File

@@ -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`,

View File

@@ -1,7 +1,7 @@
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"
export const Route = createFileRoute("/_layout/_pages/_protected")({
beforeLoad: async ({ context, location, cause }) => {
beforeLoad: async ({ context, location }) => {
if (!context?.auth?.userId) {
throw redirect({
to: "/sign-in/$",

View File

@@ -106,7 +106,7 @@ export function QuestionThread({ question, onClose }: QuestionThreadProps) {
>
<div className="flex items-center justify-between pb-1">
<div className="flex items-center">
<div className="bg-accent mr-2 h-6 w-6 rounded-full"></div>
<div className="mr-2 h-6 w-6 rounded-full bg-accent"></div>
<span className="text-sm">{answer.author}</span>
</div>
<div className="flex items-center">
@@ -147,16 +147,16 @@ export function QuestionThread({ question, onClose }: QuestionThreadProps) {
)
return (
<div className="border-accent bg-background fixed bottom-0 right-0 top-0 z-50 flex h-full w-[40%] flex-col border-l">
<div className="border-accent flex w-full justify-between border-b p-4">
<div className="fixed bottom-0 right-0 top-0 z-50 flex h-full w-[40%] flex-col border-l border-accent bg-background">
<div className="flex w-full justify-between border-b border-accent p-4">
<div className="flex w-full flex-col">
<div className="mb-2 flex w-full items-center justify-between">
<div className="flex items-center gap-3">
<div className="bg-accent h-8 w-8 rounded-full"></div>
<div className="h-8 w-8 rounded-full bg-accent"></div>
<h2 className="opacity-70">{question.author}</h2>
</div>
<button
className="bg-accent rounded-full p-1.5 opacity-50 hover:opacity-80"
className="rounded-full bg-accent p-1.5 opacity-50 hover:opacity-80"
onClick={onClose}
>
<LaIcon name="X" className="text-primary" />
@@ -167,7 +167,7 @@ export function QuestionThread({ question, onClose }: QuestionThreadProps) {
</div>
</div>
<div className="flex-grow overflow-y-auto">{renderAnswers(answers)}</div>
<div className="border-accent border-t p-4">
<div className="border-t border-accent p-4">
<form className="relative" onSubmit={sendAnswer}>
<div className="relative flex items-center">
<input
@@ -176,7 +176,7 @@ export function QuestionThread({ question, onClose }: QuestionThreadProps) {
value={newAnswer}
onChange={changeInput}
placeholder="Answer the question..."
className="bg-input w-full rounded p-2 text-opacity-70 placeholder:text-opacity-50 focus:outline-none focus:ring-0"
className="w-full rounded bg-input p-2 text-opacity-70 placeholder:text-opacity-50 focus:outline-none focus:ring-0"
/>
</div>
<button className="absolute right-2 top-1/2 -translate-y-1/2 transform opacity-50 hover:opacity-90">

View File

@@ -28,7 +28,7 @@ export const GuideCommunityToggle: React.FC<GuideCommunityToggleProps> = ({
}
return (
<div className="bg-accent/70 relative flex h-8 w-48 items-center rounded-md">
<div className="relative flex h-8 w-48 items-center rounded-md bg-accent/70">
<div
className="absolute h-8 w-[calc(50%-4px)] rounded-md transition-all duration-300 ease-in-out"
style={{ left: view === "guide" ? "2px" : "calc(50% + 2px)" }}
@@ -36,7 +36,7 @@ export const GuideCommunityToggle: React.FC<GuideCommunityToggleProps> = ({
<button
className={cn(
"relative z-10 h-full flex-1 rounded-md text-sm font-medium transition-colors",
view === "guide" ? "text-primary bg-accent" : "text-primary/50",
view === "guide" ? "bg-accent text-primary" : "text-primary/50",
)}
onClick={() => handleToggle("guide")}
>
@@ -45,7 +45,7 @@ export const GuideCommunityToggle: React.FC<GuideCommunityToggleProps> = ({
<button
className={cn(
"relative z-10 h-full flex-1 rounded-md text-sm font-medium transition-colors",
view === "community" ? "text-primary bg-accent" : "text-primary/50",
view === "community" ? "bg-accent text-primary" : "text-primary/50",
)}
onClick={() => handleToggle("community")}
>

View File

@@ -45,7 +45,7 @@ function CommunityTopicComponent() {
return (
<div className="flex h-full flex-auto flex-col">
<ContentHeader className="px-6 py-4">
<ContentHeader>
<div className="flex min-w-0 shrink-0 items-center gap-1.5">
<SidebarToggleButton />
<div className="flex min-h-0 flex-col items-start">

View File

@@ -7,7 +7,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { cn, getShortcutKeys } from "@/lib/utils"
import { cn } from "@/lib/utils"
import { LaIcon } from "@/components/custom/la-icon"
import { useAtom } from "jotai"
import { useConfirm } from "@omit/react-confirm-dialog"
@@ -17,6 +17,7 @@ import { ID } from "jazz-tools"
import { globalLinkFormExceptionRefsAtom } from "./-link-form"
import { useLinkActions } from "~/hooks/actions/use-link-actions"
import { useNavigate, useSearch } from "@tanstack/react-router"
import { getShortcutKeys } from "@shared/utils"
interface ToolbarButtonProps
extends React.ComponentPropsWithoutRef<typeof Button> {
@@ -146,7 +147,7 @@ export const LinkBottomBar: React.FC = () => {
const shortcutText = getShortcutKeys(["c"])
return (
<div className="bg-background min-h-11 border-t">
<div className="min-h-11 border-t">
<AnimatePresence mode="wait">
{editId && (
<motion.div

View File

@@ -27,7 +27,7 @@ export const DescriptionInput: React.FC<DescriptionInputProps> = () => {
{...field}
autoComplete="off"
placeholder="Description"
className="placeholder:text-muted-foreground/70 resize-none overflow-y-auto border-none p-1.5 text-[13px] font-medium shadow-none focus-visible:ring-0"
className="resize-none overflow-y-auto border-none p-1.5 text-sm font-medium shadow-none placeholder:text-muted-foreground/70 focus-visible:ring-0"
/>
</FormControl>
</FormItem>

View File

@@ -1,77 +1,37 @@
import * as React from "react"
import { Button } from "@/components/ui/button"
import { useMedia } from "@/hooks/use-media"
import {
ContentHeader,
SidebarToggleButton,
} from "@/components/custom/content-header"
import { useMedia } from "@/hooks/use-media"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { useNavigate, useSearch } from "@tanstack/react-router"
import { FancySwitch, OptionValue } from "@omit/react-fancy-switch"
import { cn } from "@/lib/utils"
import { LEARNING_STATES, LearningStateValue } from "@/lib/constants"
import { useAtom } from "jotai"
import { linkSortAtom } from "@/store/link"
import { LEARNING_STATES, LearningStateValue } from "@/lib/constants"
import { FancySwitch } from "@omit/react-fancy-switch"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { LaIcon } from "@/components/custom/la-icon"
import { useNavigate, useSearch } from "@tanstack/react-router"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
const ALL_STATES = [
{ label: "All", value: "all", icon: "List", className: "text-foreground" },
...LEARNING_STATES,
]
export const LinkHeader = React.memo(() => {
const isTablet = useMedia("(max-width: 1024px)")
return (
<>
<ContentHeader className="px-6 max-lg:px-4 lg:py-4">
<div className="flex min-w-0 shrink-0 items-center gap-1.5">
<SidebarToggleButton />
<div className="flex min-h-0 items-center">
<span className="truncate text-left font-bold lg:text-xl">
Links
</span>
</div>
</div>
{!isTablet && <LearningTab />}
<div className="flex flex-auto"></div>
<FilterAndSort />
</ContentHeader>
{isTablet && (
<div className="flex flex-row items-start justify-between border-b px-6 pb-4 pt-2 max-lg:pl-4">
<LearningTab />
</div>
)}
</>
)
})
LinkHeader.displayName = "LinkHeader"
const LearningTab = React.memo(() => {
const LearningTab: React.FC = React.memo(() => {
const navigate = useNavigate()
const { state } = useSearch({
from: "/_layout/_pages/_protected/links/",
})
const { state } = useSearch({ from: "/_layout/_pages/_protected/links/" })
const handleTabChange = React.useCallback(
async (value: string) => {
(value: OptionValue) => {
if (value !== state) {
navigate({
to: "/links",
@@ -85,30 +45,22 @@ const LearningTab = React.memo(() => {
return (
<FancySwitch
value={state}
onChange={(value) => {
handleTabChange(value as string)
}}
onChange={handleTabChange}
options={ALL_STATES}
className="bg-muted flex rounded-lg"
className="flex rounded-md"
highlighterClassName="bg-muted-foreground/10 rounded-md"
radioClassName={cn(
"relative mx-2 flex h-8 cursor-pointer items-center justify-center rounded-full px-1 text-sm text-secondary-foreground/60 data-[checked]:text-secondary-foreground font-medium transition-colors focus:outline-none",
"relative mx-2 flex h-6 cursor-pointer items-center justify-center rounded-full px-1 text-sm text-muted-foreground data-[checked]:text-foreground data-[checked]:font-medium transition-colors focus:outline-none",
)}
highlighterIncludeMargin={true}
/>
)
})
LearningTab.displayName = "LearningTab"
const FilterAndSort = React.memo(() => {
const FilterAndSort: React.FC = React.memo(() => {
const [sort, setSort] = useAtom(linkSortAtom)
const [sortOpen, setSortOpen] = React.useState(false)
const getFilterText = React.useCallback(() => {
return sort.charAt(0).toUpperCase() + sort.slice(1)
}, [sort])
const handleSortChange = React.useCallback(
(value: string) => {
setSort(value)
@@ -120,40 +72,80 @@ const FilterAndSort = React.memo(() => {
return (
<div className="flex w-auto items-center justify-end">
<div className="flex items-center gap-2">
<Popover open={sortOpen} onOpenChange={setSortOpen}>
<PopoverTrigger asChild>
<DropdownMenu open={sortOpen} onOpenChange={setSortOpen}>
<DropdownMenuTrigger asChild>
<Button
size="sm"
type="button"
variant="secondary"
className="min-w-8 gap-x-2 text-sm max-sm:p-0"
variant="ghost"
className="h-7 gap-x-2 text-sm max-sm:p-0"
>
<LaIcon name="ListFilter" className="text-primary/60" />
<span className="hidden md:block">Filter: {getFilterText()}</span>
<LaIcon name="ChevronDown" />
<span className="hidden md:block">Display</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-72" align="end">
<div className="flex flex-col">
<div className="flex min-w-8 flex-row items-center">
<Label>Sort by</Label>
<div className="flex flex-auto flex-row items-center justify-end">
<Select value={sort} onValueChange={handleSortChange}>
<SelectTrigger className="h-6 w-auto">
<SelectValue placeholder="Select"></SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="title">Title</SelectItem>
<SelectItem value="manual">Manual</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</PopoverContent>
</Popover>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" side="top">
<DropdownMenuLabel className="text-xs font-medium text-muted-foreground">
Display
</DropdownMenuLabel>
<DropdownMenuItem>
<LaIcon name="List" className="mr-2 h-4 w-4" />
<span>List</span>
<LaIcon name="Check" className="ml-auto h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel className="text-xs font-medium text-muted-foreground">
Ordering
</DropdownMenuLabel>
<DropdownMenuItem onClick={() => handleSortChange("title")}>
<span>Title</span>
{sort === "title" && (
<LaIcon name="Check" className="ml-auto h-4 w-4" />
)}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleSortChange("manual")}>
<span>Manual</span>
{sort === "manual" && (
<LaIcon name="Check" className="ml-auto h-4 w-4" />
)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
)
})
export const LinkHeader: React.FC = React.memo(() => {
const isTablet = useMedia("(max-width: 1024px)")
return (
<>
<ContentHeader>
<div className="flex min-w-0 shrink-0 items-center gap-1.5">
<SidebarToggleButton />
<div className="flex min-h-0 items-center">
<span className="truncate text-left font-semibold lg:text-lg">
Links
</span>
</div>
</div>
{!isTablet && <LearningTab />}
<div className="flex flex-auto" />
<FilterAndSort />
</ContentHeader>
{isTablet && (
<div className="flex flex-row items-start justify-between border-b px-6 py-2 max-lg:pl-4">
<LearningTab />
</div>
)}
</>
)
})
LinkHeader.displayName = "LinkHeader"
LearningTab.displayName = "LearningTab"
FilterAndSort.displayName = "FilterAndSort"

View File

@@ -115,15 +115,15 @@ export const LinkItem = React.forwardRef<HTMLDivElement, LinkItemProps>(
data-disabled={disabled}
data-active={isActive}
className={cn(
"w-full overflow-visible border-b-[0.5px] border-transparent outline-none",
"data-[active='true']:bg-[var(--link-background-muted)] data-[keyboard-active='true']:focus-visible:shadow-[var(--link-shadow)_0px_0px_0px_1px_inset]",
"w-full cursor-default overflow-visible border-b-[0.5px] border-transparent outline-none",
"data-[active='true']:bg-[var(--link-background-muted-new)] data-[keyboard-active='true']:focus-visible:shadow-[var(--link-shadow)_0px_0px_0px_1px_inset]",
)}
onKeyDown={handleKeyDown}
>
<div
className={cn(
"w-full grow overflow-visible outline-none",
"flex items-center gap-x-2 py-2 max-lg:px-4 sm:px-5 sm:py-2",
"flex items-center gap-x-2 py-2 sm:px-5 sm:py-2 max-lg:px-4",
)}
>
<Popover
@@ -137,8 +137,8 @@ export const LinkItem = React.forwardRef<HTMLDivElement, LinkItemProps>(
size="sm"
type="button"
role="combobox"
variant="secondary"
className="size-7 shrink-0 p-0"
variant="ghost"
className="size-7 shrink-0 cursor-default p-0 text-muted-foreground/75 hover:bg-inherit hover:text-foreground"
onClick={(e) => e.stopPropagation()}
onDoubleClick={(e) => e.stopPropagation()}
>
@@ -148,7 +148,7 @@ export const LinkItem = React.forwardRef<HTMLDivElement, LinkItemProps>(
className={cn(selectedLearningState.className)}
/>
) : (
<LaIcon name="Circle" />
<LaIcon name="Circle" strokeWidth={2.5} />
)}
</Button>
</PopoverTrigger>
@@ -167,22 +167,22 @@ export const LinkItem = React.forwardRef<HTMLDivElement, LinkItemProps>(
</Popover>
<div className="flex min-w-0 flex-col items-start gap-y-1.5 overflow-hidden md:flex-row md:items-center md:gap-x-2">
<div className="flex items-center gap-x-1">
<div className="flex items-center gap-x-1.5">
{personalLink.icon && (
<img
src={personalLink.icon as string}
alt={personalLink.title}
className="size-5 shrink-0 rounded-full"
className="size-4 shrink-0 rounded-full"
width={16}
height={16}
/>
)}
<p className="text-primary hover:text-primary line-clamp-1 text-sm font-medium">
<p className="line-clamp-1 text-sm font-medium text-primary hover:text-primary">
{personalLink.title}
</p>
</div>
{personalLink.url && (
<div className="text-muted-foreground flex min-w-0 shrink items-center gap-x-1">
<div className="flex min-w-0 shrink items-center gap-x-1 text-muted-foreground">
<LaIcon
name="Link"
aria-hidden="true"
@@ -192,7 +192,7 @@ export const LinkItem = React.forwardRef<HTMLDivElement, LinkItemProps>(
to={ensureUrlProtocol(personalLink.url)}
target="_blank"
onClick={(e) => e.stopPropagation()}
className="hover:text-primary mr-1 truncate text-xs"
className="mr-1 truncate text-xs hover:text-primary"
>
{personalLink.url}
</Link>
@@ -204,14 +204,17 @@ export const LinkItem = React.forwardRef<HTMLDivElement, LinkItemProps>(
<div className="flex shrink-0 items-center justify-end">
{personalLink.topic && (
<Badge variant="secondary" className="border-muted-foreground/25">
<Badge
variant="secondary"
className="border-muted-foreground/25 font-medium"
>
{personalLink.topic.prettyName}
</Badge>
)}
</div>
</div>
<div className="relative h-[0.5px] w-full after:absolute after:left-0 after:right-0 after:block after:h-full after:bg-[var(--link-border-after)]"></div>
<div className="relative h-[0.5px] w-full after:absolute after:left-0 after:right-0 after:block after:h-full after:bg-[var(--la-border-new)]"></div>
</div>
)
},

View File

@@ -19,7 +19,8 @@ import { DescriptionInput } from "./-description-input"
import { UrlBadge } from "./-url-badge"
import { NotesSection } from "./-notes-section"
import { useOnClickOutside } from "~/hooks/use-on-click-outside"
import TopicSelector, {
import {
TopicSelector,
topicSelectorAtom,
} from "~/components/custom/topic-selector"
import { createServerFn } from "@tanstack/start"
@@ -291,7 +292,7 @@ export const LinkForm: React.FC<LinkFormProps> = ({
>
<div
className={cn(
"bg-muted/30 relative rounded-md border",
"relative rounded-md border bg-muted/30",
isFetching && "opacity-50",
)}
>
@@ -369,7 +370,7 @@ export const LinkForm: React.FC<LinkFormProps> = ({
{isFetching ? (
<div className="flex w-auto items-center justify-end gap-x-2">
<span className="text-muted-foreground flex items-center text-sm">
<span className="flex items-center text-sm text-muted-foreground">
<svg
className="mr-2 h-4 w-4 animate-spin"
viewBox="0 0 24 24"

View File

@@ -27,12 +27,12 @@ import { linkSortAtom } from "@/store/link"
import { LinkItem } from "./-item"
import { useConfirm } from "@omit/react-confirm-dialog"
import { useKeyDown } from "@/hooks/use-key-down"
import { isModKey } from "@/lib/utils"
import { useTouchSensor } from "~/hooks/use-touch-sensor"
import { useActiveItemScroll } from "~/hooks/use-active-item-scroll"
import { isDeleteConfirmShownAtom } from "."
import { useLinkActions } from "~/hooks/actions/use-link-actions"
import { useNavigate, useSearch } from "@tanstack/react-router"
import { isModKey } from "@shared/utils"
interface LinkListProps {}

View File

@@ -35,7 +35,7 @@ export const NotesSection: React.FC = () => {
autoComplete="off"
placeholder="Notes"
className={cn(
"placeholder:text-muted-foreground/70 border-none pl-8 shadow-none focus-visible:ring-0",
"border-none pl-8 shadow-none placeholder:text-muted-foreground/70 focus-visible:ring-0",
)}
/>
</>

View File

@@ -31,7 +31,7 @@ export const TitleInput: React.FC<TitleInputProps> = ({ urlFetched }) => {
maxLength={100}
autoFocus
placeholder="Title"
className="placeholder:text-muted-foreground/70 h-8 border-none p-1.5 text-[15px] font-semibold shadow-none focus-visible:ring-0"
className="h-8 border-none p-1.5 text-[15px] font-semibold shadow-none placeholder:text-muted-foreground/70 focus-visible:ring-0"
/>
</FormControl>
</FormItem>

View File

@@ -27,9 +27,9 @@ export const UrlBadge: React.FC<UrlBadgeProps> = ({
size="icon"
type="button"
onClick={handleResetUrl}
className="text-muted-foreground hover:text-foreground ml-2 size-4 rounded-full bg-transparent hover:bg-transparent"
className="ml-2 size-4 rounded-full bg-transparent text-muted-foreground hover:bg-transparent hover:text-foreground"
>
<LaIcon name="X" className="size-3.5" />
<LaIcon name="X" className="" />
</Button>
</Badge>
</div>

View File

@@ -65,14 +65,14 @@ export const UrlInput: React.FC<UrlInputProps> = ({
maxLength={100}
autoFocus
placeholder="Paste a link or write a link"
className="placeholder:text-muted-foreground/70 h-8 border-none p-1.5 text-[15px] font-semibold shadow-none focus-visible:ring-0"
className="h-8 border-none p-1.5 text-[15px] font-semibold shadow-none placeholder:text-muted-foreground/70 focus-visible:ring-0"
onKeyDown={handleKeyDown}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>
</TooltipTrigger>
<TooltipContent align="center" side="top">
<TooltipArrow className="text-primary fill-current" />
<TooltipArrow className="fill-current text-primary" />
<span>
Press <kbd className="px-1.5">Enter</kbd> to fetch metadata
</span>

View File

@@ -60,7 +60,7 @@ const StepItem = ({
done: boolean
}) => (
<div className="flex items-start space-x-4 py-4">
<div className="border-foreground/20 w-6 flex-shrink-0 items-center justify-center rounded-3xl border text-center opacity-70">
<div className="w-6 flex-shrink-0 items-center justify-center rounded-3xl border border-foreground/20 text-center opacity-70">
{number}
</div>
<div className="flex-grow space-y-2">

View File

@@ -23,7 +23,7 @@ export const DetailPageHeader: React.FC<DetailPageHeaderProps> = ({
return (
<>
<ContentHeader className="lg:min-h-0">
<ContentHeader>
<div className="flex min-w-0 gap-2">
<SidebarToggleButton />
</div>
@@ -45,7 +45,7 @@ export const DetailPageHeader: React.FC<DetailPageHeaderProps> = ({
)}
/>
<Button size="sm" variant="outline" onClick={handleDelete}>
<LaIcon name="Trash" className="mr-2 size-3.5" />
<LaIcon name="Trash" className="mr-2" />
Delete
</Button>
</div>

View File

@@ -15,14 +15,14 @@ import { Button } from "@/components/ui/button"
import { LaIcon } from "@/components/custom/la-icon"
import { useConfirm } from "@omit/react-confirm-dialog"
import { usePageActions } from "~/hooks/actions/use-page-actions"
import { Paragraph } from "@shared/la-editor/extensions/paragraph"
import { StarterKit } from "@shared/la-editor/extensions/starter-kit"
import { LAEditor, LAEditorRef } from "@shared/la-editor"
import { Paragraph } from "@shared/editor/extensions/paragraph"
import { StarterKit } from "@shared/editor/extensions/starter-kit"
import { LaEditor } from "@shared/editor"
export const Route = createFileRoute(
"/_layout/_pages/_protected/pages/$pageId/",
)({
component: () => <PageDetailComponent />,
component: PageDetailComponent,
})
const TITLE_PLACEHOLDER = "Untitled"
@@ -73,20 +73,22 @@ function PageDetailComponent() {
)
}
const SidebarActions = ({
page,
handleDelete,
}: {
page: PersonalPage
handleDelete: () => void
}) => (
<div className="relative min-w-56 max-w-72 border-l">
<div className="flex">
<div className="flex h-10 flex-auto flex-row items-center justify-between px-5">
<span className="text-left text-[13px] font-medium">Page actions</span>
</div>
<div className="absolute bottom-0 left-0 right-0 top-10 space-y-3 overflow-y-auto px-4 py-1.5">
<div className="flex flex-row">
const SidebarActions = React.memo(
({
page,
handleDelete,
}: {
page: PersonalPage
handleDelete: () => void
}) => (
<div className="relative min-w-56 max-w-72 border-l bg-[var(--body-background)]">
<div className="flex">
<div className="flex h-10 flex-auto flex-row items-center justify-between px-5">
<span className="text-left text-[13px] font-medium text-muted-foreground">
Page actions
</span>
</div>
<div className="absolute bottom-0 left-0 right-0 top-10 space-y-3 overflow-y-auto px-4 py-1.5">
<TopicSelector
value={page.topic?.name}
onTopicChange={(topic) => {
@@ -101,52 +103,40 @@ const SidebarActions = ({
</span>
)}
/>
</div>
<div className="flex flex-row">
<Button
size="sm"
variant="ghost"
onClick={handleDelete}
className="-ml-1.5"
>
<LaIcon name="Trash" className="mr-2 size-3.5" />
<LaIcon name="Trash" className="mr-2" />
<span className="text-sm">Delete</span>
</Button>
</div>
</div>
</div>
</div>
),
)
const DetailPageForm = ({ page }: { page: PersonalPage }) => {
SidebarActions.displayName = "SidebarActions"
const DetailPageForm = React.memo(({ page }: { page: PersonalPage }) => {
const titleEditorRef = React.useRef<Editor | null>(null)
const contentEditorRef = React.useRef<LAEditorRef>(null)
const isTitleInitialMount = React.useRef(true)
const isContentInitialMount = React.useRef(true)
const isInitialFocusApplied = React.useRef(false)
const contentEditorRef = React.useRef<Editor | null>(null)
const updatePageContent = React.useCallback(
(content: Content, model: PersonalPage) => {
if (isContentInitialMount.current) {
isContentInitialMount.current = false
return
}
model.content = content
model.updatedAt = new Date()
(content: Content) => {
page.content = content
page.updatedAt = new Date()
},
[],
[page],
)
const handleUpdateTitle = React.useCallback(
(editor: Editor) => {
if (isTitleInitialMount.current) {
isTitleInitialMount.current = false
return
}
const newTitle = editor.getText()
if (newTitle !== page.title) {
const slug = generateUniqueSlug(page.title?.toString() || "")
const slug = generateUniqueSlug(newTitle || "")
page.title = newTitle
page.slug = slug
page.updatedAt = new Date()
@@ -164,22 +154,18 @@ const DetailPageForm = ({ page }: { page: PersonalPage }) => {
const { selection } = state
const { $anchor } = selection
switch (event.key) {
case "ArrowRight":
case "ArrowDown":
if ($anchor.pos === state.doc.content.size - 1) {
event.preventDefault()
contentEditorRef.current?.editor?.commands.focus("start")
return true
}
break
case "Enter":
if (!event.shiftKey) {
event.preventDefault()
contentEditorRef.current?.editor?.commands.focus("start")
return true
}
break
if (
(event.key === "ArrowRight" || event.key === "ArrowDown") &&
$anchor.pos === state.doc.content.size - 1
) {
event.preventDefault()
contentEditorRef.current?.commands.focus("start")
return true
}
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault()
contentEditorRef.current?.commands.focus("start")
return true
}
return false
},
@@ -188,7 +174,7 @@ const DetailPageForm = ({ page }: { page: PersonalPage }) => {
const handleContentKeyDown = React.useCallback(
(view: EditorView, event: KeyboardEvent) => {
const editor = contentEditorRef.current?.editor
const editor = contentEditorRef.current
if (!editor) return false
const { state } = editor
@@ -239,34 +225,21 @@ const DetailPageForm = ({ page }: { page: PersonalPage }) => {
},
onCreate: ({ editor }) => {
if (page.title) editor.commands.setContent(`<p>${page.title}</p>`)
titleEditorRef.current = editor
},
onBlur: ({ editor }) => handleUpdateTitle(editor),
onUpdate: ({ editor }) => handleUpdateTitle(editor),
})
React.useEffect(() => {
if (titleEditor) {
titleEditorRef.current = titleEditor
}
}, [titleEditor])
React.useEffect(() => {
isTitleInitialMount.current = true
isContentInitialMount.current = true
if (
!isInitialFocusApplied.current &&
titleEditor &&
contentEditorRef.current?.editor
) {
isInitialFocusApplied.current = true
if (!page.title) {
titleEditor?.commands.focus()
} else {
contentEditorRef.current.editor.commands.focus()
const handleCreate = React.useCallback(
({ editor }: { editor: Editor }) => {
if (page.content) {
editor.commands.setContent(page.content as Content)
}
}
}, [page.title, titleEditor])
contentEditorRef.current = editor
},
[page.content],
)
return (
<div className="relative flex grow flex-col overflow-y-auto [scrollbar-gutter:stable]">
@@ -275,21 +248,21 @@ const DetailPageForm = ({ page }: { page: PersonalPage }) => {
<div className="mb-2 mt-8 py-1.5">
<EditorContent
editor={titleEditor}
className="la-editor no-command grow cursor-text select-text text-2xl font-semibold leading-[calc(1.33333)] tracking-[-0.00625rem]"
className="title-editor no-command grow cursor-text select-text text-2xl font-semibold leading-[calc(1.33333)] tracking-[-0.00625rem]"
/>
</div>
<div className="flex flex-auto flex-col">
<div className="relative flex h-full max-w-full grow flex-col items-stretch p-0">
<LAEditor
ref={contentEditorRef}
<LaEditor
editorClassName="-mx-3.5 px-3.5 py-2.5 flex-auto focus:outline-none"
value={page.content}
value={page.content as Content}
placeholder="Add content..."
output="json"
throttleDelay={3000}
onUpdate={(c) => updatePageContent(c, page)}
handleKeyDown={handleContentKeyDown}
onBlur={(c) => updatePageContent(c, page)}
editorProps={{ handleKeyDown: handleContentKeyDown }}
onCreate={handleCreate}
onUpdate={updatePageContent}
onBlur={updatePageContent}
/>
</div>
</div>
@@ -297,4 +270,6 @@ const DetailPageForm = ({ page }: { page: PersonalPage }) => {
</div>
</div>
)
}
})
DetailPageForm.displayName = "DetailPageForm"

View File

@@ -24,7 +24,7 @@ export const PageHeader: React.FC<PageHeaderProps> = React.memo(() => {
}
return (
<ContentHeader className="px-6 py-4 max-lg:px-4">
<ContentHeader>
<HeaderTitle />
<div className="flex flex-auto" />
<NewPageButton onClick={handleNewPageClick} />

View File

@@ -36,8 +36,8 @@ export const PageItem = React.forwardRef<HTMLAnchorElement, PageItemProps>(
tabIndex={isActive ? 0 : -1}
className={cn(
"relative block cursor-default outline-none",
"min-h-12 py-2 max-lg:px-4 sm:px-6",
"data-[active='true']:bg-[var(--link-background-muted)] data-[keyboard-active='true']:focus-visible:shadow-[var(--link-shadow)_0px_0px_0px_1px_inset]",
"min-h-12 py-2 sm:px-6 max-lg:px-4",
"data-[active='true']:bg-[var(--link-background-muted-new)] data-[keyboard-active='true']:focus-visible:shadow-[var(--link-shadow)_0px_0px_0px_1px_inset]",
)}
to={`/pages/${page.id}`}
aria-selected={isActive}
@@ -47,7 +47,7 @@ export const PageItem = React.forwardRef<HTMLAnchorElement, PageItemProps>(
>
<div className="flex h-full items-center gap-4">
<Column.Wrapper style={columnStyles.title}>
<Column.Text className="truncate text-[13px] font-medium">
<Column.Text className="truncate text-sm font-medium">
{page.title || "Untitled"}
</Column.Text>
</Column.Wrapper>
@@ -64,7 +64,7 @@ export const PageItem = React.forwardRef<HTMLAnchorElement, PageItemProps>(
style={columnStyles.updated}
className="flex justify-end"
>
<Column.Text className="text-[13px]">
<Column.Text className="text-sm">
{format(new Date(page.updatedAt), "d MMM yyyy")}
</Column.Text>
</Column.Wrapper>

View File

@@ -54,7 +54,7 @@ export const PageList: React.FC<PageListProps> = () => {
<div className="flex h-full w-full flex-col overflow-hidden border-t">
{!isTablet && <ColumnHeader />}
<Primitive.div
className="divide-primary/5 flex flex-1 flex-col divide-y overflow-y-auto outline-none [scrollbar-gutter:stable]"
className="flex flex-1 flex-col divide-y divide-primary/5 overflow-y-auto outline-none [scrollbar-gutter:stable]"
tabIndex={-1}
role="list"
>
@@ -110,7 +110,7 @@ export const ColumnHeader: React.FC = () => {
const columnStyles = useColumnStyles()
return (
<div className="flex h-8 shrink-0 grow-0 flex-row gap-4 border-b max-lg:px-4 sm:px-6">
<div className="flex h-8 shrink-0 grow-0 flex-row gap-4 border-b sm:px-6 max-lg:px-4">
<Column.Wrapper style={columnStyles.title}>
<Column.Text>Title</Column.Text>
</Column.Wrapper>

View File

@@ -137,7 +137,7 @@ function ProfileComponent() {
<Input
value={newName}
onChange={changeName}
className="border-result mb-3 mr-3 text-[25px] font-semibold"
className="mb-3 mr-3 border-result text-[25px] font-semibold"
/>
{error && (
<p className="text-red-500 text-opacity-70">{error}</p>

View File

@@ -28,7 +28,7 @@ const SearchTitle: React.FC<SearchTitleProps> = ({ title, count }) => (
<div className="flex w-full items-center">
<h2 className="text-md font-semibold">{title}</h2>
<div className="mx-4 flex-grow">
<div className="bg-result h-px"></div>
<div className="h-px bg-result"></div>
</div>
<span className="text-base font-light text-opacity-55">{count}</span>
</div>
@@ -41,7 +41,7 @@ const SearchItem: React.FC<SearchItemProps> = ({
subtitle,
topic,
}) => (
<div className="hover:bg-result group flex min-w-0 items-center gap-x-4 rounded-md p-2">
<div className="group flex min-w-0 items-center gap-x-4 rounded-md p-2 hover:bg-result">
<LaIcon
name={icon as "Square"}
className="size-4 flex-shrink-0 opacity-0 transition-opacity duration-200 group-hover:opacity-50"
@@ -50,7 +50,7 @@ const SearchItem: React.FC<SearchItemProps> = ({
<Link
to={href}
onClick={(e) => e.stopPropagation()}
className="hover:text-primary text-sm font-medium hover:opacity-70"
className="text-sm font-medium hover:text-primary hover:opacity-70"
>
{title}
</Link>
@@ -58,7 +58,7 @@ const SearchItem: React.FC<SearchItemProps> = ({
<Link
to={href}
onClick={(e) => e.stopPropagation()}
className="text-muted-foreground ml-2 truncate text-xs hover:underline"
className="ml-2 truncate text-xs text-muted-foreground hover:underline"
>
{subtitle}
</Link>
@@ -138,7 +138,7 @@ const SearchComponent = () => {
<div className="relative my-5 flex w-full items-center space-x-2">
<LaIcon
name="Search"
className="text-foreground absolute left-4 size-4 flex-shrink-0"
className="absolute left-4 size-4 flex-shrink-0 text-foreground"
/>
<input
autoFocus
@@ -146,12 +146,12 @@ const SearchComponent = () => {
value={searchText}
onChange={handleSearch}
placeholder="Search topics, links, pages"
className="dark:bg-input w-full rounded-lg border border-neutral-300 p-2 pl-8 focus:outline-none dark:border-neutral-600"
className="w-full rounded-lg border border-neutral-300 p-2 pl-8 focus:outline-none dark:border-neutral-600 dark:bg-input"
/>
{searchText && (
<LaIcon
name="X"
className="text-foreground/50 absolute right-3 size-4 flex-shrink-0 cursor-pointer"
className="absolute right-3 size-4 flex-shrink-0 cursor-pointer text-foreground/50"
onClick={clearSearch}
/>
)}

View File

@@ -115,7 +115,7 @@ export const TaskForm: React.FC = () => {
<div
ref={formRef}
onSubmit={handleSubmit}
className="bg-result flex w-full items-center justify-between rounded-lg px-2 py-1"
className="flex w-full items-center justify-between rounded-lg bg-result px-2 py-1"
>
<div className="flex min-w-0 flex-1 items-center">
<Checkbox

View File

@@ -58,7 +58,7 @@ export const TaskItem: React.FC<TaskItemProps> = ({
: "No due date"
return (
<li className="bg-result transitiion-opacity flex items-center justify-between rounded-lg p-2 px-3 hover:opacity-60">
<li className="transitiion-opacity flex items-center justify-between rounded-lg bg-result p-2 px-3 hover:opacity-60">
<div className="flex flex-grow flex-row items-center gap-3">
<Checkbox
checked={task.status === "done"}
@@ -77,7 +77,7 @@ export const TaskItem: React.FC<TaskItemProps> = ({
<p
className={
task.status === "done"
? "text-foreground flex-grow line-through"
? "flex-grow text-foreground line-through"
: "flex-grow"
}
onClick={clickTitle}
@@ -86,7 +86,7 @@ export const TaskItem: React.FC<TaskItemProps> = ({
</p>
)}
</div>
<span className="text-muted-foreground text-xs">{formattedDate}</span>
<span className="text-xs text-muted-foreground">{formattedDate}</span>
</li>
)
}

View File

@@ -13,7 +13,7 @@ export const TopicHeader: React.FC<TopicHeaderProps> = React.memo(() => {
if (!me) return null
return (
<ContentHeader className="px-6 py-4 max-lg:px-4">
<ContentHeader>
<HeaderTitle />
<div className="flex flex-auto" />
</ContentHeader>
@@ -26,7 +26,9 @@ const HeaderTitle: React.FC = () => (
<div className="flex min-w-0 shrink-0 items-center gap-1.5">
<SidebarToggleButton />
<div className="flex min-h-0 items-center">
<span className="truncate text-left font-bold lg:text-xl">Topics</span>
<span className="truncate text-left font-semibold lg:text-lg">
Topics
</span>
</div>
</div>
)

View File

@@ -142,8 +142,8 @@ export const TopicItem = React.forwardRef<HTMLAnchorElement, TopicItemProps>(
tabIndex={isActive ? 0 : -1}
className={cn(
"relative block cursor-default outline-none",
"min-h-12 py-2 max-lg:px-4 sm:px-6",
"data-[active='true']:bg-[var(--link-background-muted)] data-[keyboard-active='true']:focus-visible:shadow-[var(--link-shadow)_0px_0px_0px_1px_inset]",
"min-h-12 py-2 sm:px-6 max-lg:px-4",
"data-[active='true']:bg-[var(--link-background-muted-new)] data-[keyboard-active='true']:focus-visible:shadow-[var(--link-shadow)_0px_0px_0px_1px_inset]",
)}
aria-selected={isActive}
data-active={isActive}
@@ -155,7 +155,7 @@ export const TopicItem = React.forwardRef<HTMLAnchorElement, TopicItemProps>(
tabIndex={isActive ? 0 : -1}
>
<Column.Wrapper style={columnStyles.title}>
<Column.Text className="truncate text-[13px] font-medium">
<Column.Text className="truncate text-sm font-medium">
{topic.prettyName}
</Column.Text>
</Column.Wrapper>

View File

@@ -94,10 +94,10 @@ export const MainTopicList: React.FC<MainTopicListProps> = ({ me }) => {
})
return (
<div className="flex h-full w-full flex-col overflow-hidden border-t">
<div className="flex h-full w-full flex-col overflow-hidden">
{!isTablet && <ColumnHeader />}
<Primitive.div
className="divide-primary/5 flex flex-1 flex-col divide-y overflow-y-auto outline-none [scrollbar-gutter:stable]"
className="flex flex-1 flex-col divide-y divide-primary/5 overflow-y-auto outline-none [scrollbar-gutter:stable]"
tabIndex={-1}
role="list"
>
@@ -144,7 +144,7 @@ export const ColumnHeader: React.FC = () => {
const columnStyles = useColumnStyles()
return (
<div className="flex h-8 shrink-0 grow-0 flex-row gap-4 border-b max-lg:px-4 sm:px-6">
<div className="flex h-8 shrink-0 grow-0 flex-row gap-4 border-b sm:px-6 max-lg:px-4">
<Column.Wrapper style={columnStyles.title}>
<Column.Text>Name</Column.Text>
</Column.Wrapper>

View File

@@ -43,7 +43,7 @@
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover: 220, 5.66%, 10.39%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
@@ -55,7 +55,7 @@
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--border: 240 3.7% 20%;
--input: 220 9% 10%;
--result: 0 0% 7%;
--ring: 240 4.9% 83.9%;
@@ -77,7 +77,7 @@
}
body,
div#root {
@apply bg-background text-foreground size-full font-sans antialiased;
@apply size-full bg-[var(--body-background)] font-sans text-foreground antialiased;
}
}

View File

@@ -116,11 +116,11 @@
.la [cmdk-group-heading] {
font-size: 13px;
height: 30px;
@apply text-muted-foreground flex items-center px-2;
@apply flex items-center px-2 text-muted-foreground;
}
.la [cmdk-empty] {
@apply text-muted-foreground flex h-16 items-center justify-center whitespace-pre-wrap text-sm;
@apply flex h-16 items-center justify-center whitespace-pre-wrap text-sm text-muted-foreground;
}
.la [cmdk-item] {

View File

@@ -1,11 +1,42 @@
:root {
--link-background-muted: hsl(0, 0%, 97.3%);
--link-border-after: hsl(0, 0%, 91%);
--link-background-muted-new: hsl(0, 0%, 97.3%);
--la-border: hsl(0, 0%, 91%);
--la-border-new: hsl(0, 0%, 91%);
--link-shadow: hsl(240, 5.6%, 82.5%);
--less-foreground: hsl(240 10% 3.9%);
--item-active: rgb(228, 228, 229);
--item-hover: rgb(237, 237, 239);
--body-background: rgb(248, 248, 249);
--container-background: rgb(255, 255, 255);
}
.dark {
--link-background-muted: hsl(220, 6.7%, 8.8%);
--link-border-after: hsl(230, 10%, 11.8%);
--link-background-muted-new: rgb(28, 29, 32);
--la-border: hsl(230, 10%, 11.8%);
--la-border-new: hsl(230, 10%, 15%);
--link-shadow: hsl(234.9, 27.1%, 25.3%);
--less-foreground: #e5e7eb;
--item-active: rgb(53, 54, 57);
--item-hover: rgb(42, 43, 46);
--body-background: rgb(31, 32, 35);
--container-background: rgb(24, 25, 28);
}
.title-editor .ProseMirror .is-empty::before {
@apply pointer-events-none float-left h-0 w-full text-[var(--la-secondary)];
}
.title-editor:not(.no-command)
.ProseMirror.ProseMirror-focused
> p.has-focus.is-empty::before {
content: "Type / for commands...";
}
.title-editor .ProseMirror > p.is-editor-empty::before {
content: attr(data-placeholder);
@apply pointer-events-none float-left h-0 text-[var(--la-secondary)];
}

View File

@@ -13,7 +13,7 @@
},
"dependencies": {
"@clerk/tanstack-start": "0.4.1",
"@clerk/themes": "^2.1.35",
"@clerk/themes": "^2.1.37",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",
@@ -39,12 +39,12 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.3",
"@tanstack/react-query": "^5.59.0",
"@tanstack/react-router": "^1.62.0",
"@tanstack/react-router-with-query": "^1.62.0",
"@tanstack/react-query": "^5.59.15",
"@tanstack/react-virtual": "^3.10.8",
"@tanstack/router-zod-adapter": "^1.62.0",
"@tanstack/start": "^1.62.0",
"@tanstack/react-router": "^1.70.1",
"@tanstack/react-router-with-query": "^1.70.1",
"@tanstack/router-zod-adapter": "^1.70.1",
"@tanstack/start": "^1.70.1",
"@tiptap/core": "^2.8.0",
"@tiptap/extension-code-block-lowlight": "^2.8.0",
"@tiptap/extension-color": "^2.8.0",
@@ -67,50 +67,59 @@
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^4.1.0",
"dotenv": "^16.4.5",
"framer-motion": "^11.11.1",
"jazz-react": "^0.8.2",
"jazz-react-auth-clerk": "^0.8.2",
"jazz-tools": "^0.8.2",
"jotai": "^2.10.0",
"framer-motion": "^11.11.9",
"jazz-browser-media-images": "^0.8.7",
"jazz-react": "^0.8.7",
"jazz-react-auth-clerk": "^0.8.7",
"jazz-tools": "^0.8.5",
"jotai": "^2.10.1",
"lowlight": "^3.1.0",
"lucide-react": "^0.446.0",
"next-themes": "^0.3.0",
"query-string": "^9.1.0",
"query-string": "^9.1.1",
"react": "^18.3.1",
"react-day-picker": "8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-textarea-autosize": "^8.5.3",
"react-medium-image-zoom": "^5.2.10",
"react-textarea-autosize": "^8.5.4",
"ronin": "^4.3.1",
"slugify": "^1.6.6",
"sonner": "^1.5.0",
"streaming-markdown": "^0.0.14",
"tailwind-merge": "^2.5.3",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"uuid": "^10.0.0",
"vinxi": "0.4.3",
"zod": "^3.23.8"
},
"devDependencies": {
"@ronin/learn-anything": "^0.0.0-3456082797916",
"@ronin/learn-anything": "^0.0.0-3457754034220",
"@tailwindcss/typography": "^0.5.15",
"@tanstack/react-query-devtools": "^5.59.0",
"@tanstack/router-devtools": "^1.62.0",
"@types/node": "^22.7.4",
"@tanstack/react-query-devtools": "^5.59.15",
"@tanstack/router-devtools": "^1.70.1",
"@types/node": "^22.7.6",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
"@types/react-dom": "^18.3.1",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitejs/plugin-react": "^4.3.2",
"autoprefixer": "^10.4.20",
"dotenv": "^16.4.5",
"eslint": "^8.57.1",
"eslint-plugin-react-hooks": "^4.6.2",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.13",
"typescript": "^5.6.2",
"vite-tsconfig-paths": "^5.0.1"
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3",
"vite-tsconfig-paths": "^5.0.1",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.5.14"
},
"prettier": {
"plugins": [
"prettier-plugin-tailwindcss"
],
"semi": false
}
}

View File

@@ -0,0 +1,37 @@
import * as React from "react"
import { cn } from "@/lib/utils"
interface SpinnerProps extends React.SVGProps<SVGSVGElement> {}
const SpinnerComponent = React.forwardRef<SVGSVGElement, SpinnerProps>(
function Spinner({ className, ...props }, ref) {
return (
<svg
ref={ref}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className={cn("animate-spin", className)}
{...props}
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
)
},
)
SpinnerComponent.displayName = "Spinner"
export const Spinner = React.memo(SpinnerComponent)

View File

@@ -0,0 +1,90 @@
import { useTextmenuCommands } from "../../hooks/use-text-menu-commands"
import { PopoverWrapper } from "../ui/popover-wrapper"
import { useTextmenuStates } from "../../hooks/use-text-menu-states"
import { BubbleMenu as TiptapBubbleMenu, Editor } from "@tiptap/react"
import { ToolbarButton } from "../ui/toolbar-button"
import { Icon } from "../ui/icon"
export type BubbleMenuProps = {
editor: Editor
}
export const BubbleMenu = ({ editor }: BubbleMenuProps) => {
const commands = useTextmenuCommands(editor)
const states = useTextmenuStates(editor)
return (
<TiptapBubbleMenu
tippyOptions={{
// duration: [0, 999999],
popperOptions: { placement: "top-start" },
}}
editor={editor}
pluginKey="textMenu"
shouldShow={states.shouldShow}
updateDelay={100}
>
<PopoverWrapper>
<div className="flex space-x-1">
<ToolbarButton
value="bold"
aria-label="Bold"
onPressedChange={commands.onBold}
isActive={states.isBold}
>
<Icon name="Bold" strokeWidth={2.5} />
</ToolbarButton>
<ToolbarButton
value="italic"
aria-label="Italic"
onClick={commands.onItalic}
isActive={states.isItalic}
>
<Icon name="Italic" strokeWidth={2.5} />
</ToolbarButton>
<ToolbarButton
value="strikethrough"
aria-label="Strikethrough"
onClick={commands.onStrike}
isActive={states.isStrike}
>
<Icon name="Strikethrough" strokeWidth={2.5} />
</ToolbarButton>
<ToolbarButton
value="quote"
aria-label="Quote"
onClick={commands.onCode}
isActive={states.isCode}
>
<Icon name="Quote" strokeWidth={2.5} />
</ToolbarButton>
<ToolbarButton
value="inline code"
aria-label="Inline code"
onClick={commands.onCode}
isActive={states.isCode}
>
<Icon name="Braces" strokeWidth={2.5} />
</ToolbarButton>
<ToolbarButton
value="code block"
aria-label="Code block"
onClick={commands.onCodeBlock}
>
<Icon name="Code" strokeWidth={2.5} />
</ToolbarButton>
{/* <ToolbarButton value="list" aria-label="List">
<Icon name="List" strokeWidth={2.5} />
</ToolbarButton> */}
</div>
</PopoverWrapper>
</TiptapBubbleMenu>
)
}
export default BubbleMenu

View File

@@ -10,7 +10,7 @@ export const PopoverWrapper = React.forwardRef<
return (
<div
className={cn(
"bg-popover text-popover-foreground rounded-lg border shadow-sm",
"rounded-lg border bg-popover text-popover-foreground shadow-sm",
className,
)}
{...props}

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { getShortcutKey } from "@/lib/utils"
import { getShortcutKey } from "@shared/utils"
export interface ShortcutKeyWrapperProps
extends React.HTMLAttributes<HTMLSpanElement> {

View File

@@ -0,0 +1,43 @@
import * as React from "react"
import "./styles/index.css"
import { EditorContent } from "@tiptap/react"
import { Content } from "@tiptap/core"
import { BubbleMenu } from "./components/bubble-menu"
import { cn } from "@/lib/utils"
import { useLaEditor, UseLaEditorProps } from "./hooks/use-la-editor"
export interface LaEditorProps extends UseLaEditorProps {
value?: Content
className?: string
editorContentClassName?: string
}
export const LaEditor = React.memo(
React.forwardRef<HTMLDivElement, LaEditorProps>(
({ className, editorContentClassName, ...props }, ref) => {
const editor = useLaEditor(props)
if (!editor) {
return null
}
return (
<div
className={cn("relative flex h-full w-full grow flex-col", className)}
ref={ref}
>
<EditorContent
editor={editor}
className={cn("la-editor", editorContentClassName)}
/>
<BubbleMenu editor={editor} />
</div>
)
},
),
)
LaEditor.displayName = "LaEditor"
export default LaEditor

View File

@@ -0,0 +1,116 @@
import { type Editor, Extension } from "@tiptap/core"
import { Plugin, PluginKey } from "@tiptap/pm/state"
import type { FileError, FileValidationOptions } from "@shared/editor/lib/utils"
import { filterFiles } from "@shared/editor/lib/utils"
type FileHandlePluginOptions = {
key?: PluginKey
editor: Editor
onPaste?: (editor: Editor, files: File[], pasteContent?: string) => void
onDrop?: (editor: Editor, files: File[], pos: number) => void
onValidationError?: (errors: FileError[]) => void
} & FileValidationOptions
const FileHandlePlugin = (options: FileHandlePluginOptions) => {
const {
key,
editor,
onPaste,
onDrop,
onValidationError,
allowedMimeTypes,
maxFileSize,
} = options
return new Plugin({
key: key || new PluginKey("fileHandler"),
props: {
handleDrop(view, event) {
event.preventDefault()
event.stopPropagation()
const { dataTransfer } = event
if (!dataTransfer?.files.length) {
return
}
const pos = view.posAtCoords({
left: event.clientX,
top: event.clientY,
})
const [validFiles, errors] = filterFiles(
Array.from(dataTransfer.files),
{
allowedMimeTypes,
maxFileSize,
allowBase64: options.allowBase64,
},
)
if (errors.length > 0 && onValidationError) {
onValidationError(errors)
}
if (validFiles.length > 0 && onDrop) {
onDrop(editor, validFiles, pos?.pos ?? 0)
}
},
handlePaste(_, event) {
event.preventDefault()
event.stopPropagation()
const { clipboardData } = event
if (!clipboardData?.files.length) {
return
}
const [validFiles, errors] = filterFiles(
Array.from(clipboardData.files),
{
allowedMimeTypes,
maxFileSize,
allowBase64: options.allowBase64,
},
)
const html = clipboardData.getData("text/html")
if (errors.length > 0 && onValidationError) {
onValidationError(errors)
}
if (validFiles.length > 0 && onPaste) {
onPaste(editor, validFiles, html)
}
},
},
})
}
export const FileHandler = Extension.create<
Omit<FileHandlePluginOptions, "key" | "editor">
>({
name: "fileHandler",
addOptions() {
return {
allowBase64: false,
allowedMimeTypes: [],
maxFileSize: 0,
}
},
addProseMirrorPlugins() {
return [
FileHandlePlugin({
key: new PluginKey(this.name),
editor: this.editor,
...this.options,
}),
]
},
})

Some files were not shown because too many files have changed in this diff Show More