Rework #6
@ -4,6 +4,7 @@ import TagBadge from "@/components/gallery/TagBadge.tsx";
|
||||
import { Fragment, useLayoutEffect, useRef, useState } from "react";
|
||||
import { LazyImage } from "./LazyImage.tsx";
|
||||
import StyledLink from "./StyledLink.tsx";
|
||||
import React from "react";
|
||||
|
||||
function clipTagsWhenOverflow(tags: string[], limit: number) {
|
||||
let l = 0;
|
||||
@ -17,7 +18,7 @@ function clipTagsWhenOverflow(tags: string[], limit: number) {
|
||||
return tags;
|
||||
}
|
||||
|
||||
export function GalleryCard({
|
||||
function GalleryCardImpl({
|
||||
doc: x
|
||||
}: { doc: Document; }) {
|
||||
const ref = useRef<HTMLUListElement>(null);
|
||||
@ -85,3 +86,5 @@ export function GalleryCard({
|
||||
</div>
|
||||
</Card>;
|
||||
}
|
||||
|
||||
export const GalleryCard = React.memo(GalleryCardImpl);
|
@ -40,7 +40,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
<ResizablePanel minSize={minSize} collapsible maxSize={minSize}>
|
||||
<NavList />
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizableHandle withHandle className="z-20" />
|
||||
<ResizablePanel >
|
||||
{children}
|
||||
</ResizablePanel>
|
||||
|
@ -32,7 +32,7 @@ export function NavItem({
|
||||
export function NavList() {
|
||||
const loginInfo = useLogin();
|
||||
|
||||
return <aside className="h-screen flex flex-col">
|
||||
return <aside className="h-dvh flex flex-col">
|
||||
<nav className="flex flex-col items-center gap-4 px-2 sm:py-5 flex-1">
|
||||
<NavItem icon={<MagnifyingGlassIcon className="h-5 w-5" />} to="/search" name="Search" />
|
||||
<NavItem icon={<ActivityLogIcon className="h-5 w-5" />} to="/tags" name="Tags" />
|
||||
|
@ -1,7 +1,11 @@
|
||||
export const BASE_API_URL = 'http://localhost:5173/';
|
||||
export const BASE_API_URL = import.meta.env.VITE_API_URL ?? window.location.origin;
|
||||
|
||||
export function makeApiUrl(pathnameAndQueryparam: string) {
|
||||
return new URL(pathnameAndQueryparam, BASE_API_URL).toString();
|
||||
}
|
||||
|
||||
export async function fetcher(url: string) {
|
||||
const u = new URL(url, BASE_API_URL);
|
||||
const u = makeApiUrl(url);
|
||||
const res = await fetch(u);
|
||||
return res.json();
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import useSWRInifinite from "swr/infinite";
|
||||
import type { Document } from "dbtype/api";
|
||||
import { fetcher } from "./fetcher";
|
||||
import useSWR from "swr";
|
||||
|
||||
interface SearchParams {
|
||||
word?: string;
|
||||
@ -9,10 +10,22 @@ interface SearchParams {
|
||||
cursor?: number;
|
||||
}
|
||||
|
||||
export function useSearchGallery({
|
||||
function makeSearchParams({
|
||||
word, tags, limit, cursor,
|
||||
}: SearchParams) {
|
||||
|
||||
}: SearchParams){
|
||||
const search = new URLSearchParams();
|
||||
if (word) search.set("word", word);
|
||||
if (tags) search.set("allow_tag", tags);
|
||||
if (limit) search.set("limit", limit.toString());
|
||||
if (cursor) search.set("cursor", cursor.toString());
|
||||
return search;
|
||||
}
|
||||
|
||||
export function useSearchGallery(searchParams: SearchParams = {}) {
|
||||
return useSWR<Document[]>(`/api/doc/search?${makeSearchParams(searchParams).toString()}`, fetcher);
|
||||
}
|
||||
|
||||
export function useSearchGalleryInfinite(searchParams: SearchParams = {}) {
|
||||
return useSWRInifinite<
|
||||
{
|
||||
data: Document[];
|
||||
@ -22,11 +35,7 @@ export function useSearchGallery({
|
||||
>((index, previous) => {
|
||||
if (!previous && index > 0) return null;
|
||||
if (previous && !previous.hasMore) return null;
|
||||
const search = new URLSearchParams();
|
||||
if (word) search.set("word", word);
|
||||
if (tags) search.set("allow_tag", tags);
|
||||
if (limit) search.set("limit", limit.toString());
|
||||
if (cursor) search.set("cursor", cursor.toString());
|
||||
const search = makeSearchParams(searchParams)
|
||||
if (index === 0) {
|
||||
return `/api/doc/search?${search.toString()}`;
|
||||
}
|
||||
@ -35,6 +44,7 @@ export function useSearchGallery({
|
||||
search.set("cursor", last.id.toString());
|
||||
return `/api/doc/search?${search.toString()}`;
|
||||
}, async (url) => {
|
||||
const limit = searchParams.limit;
|
||||
const res = await fetcher(url);
|
||||
return {
|
||||
data: res,
|
||||
|
@ -2,7 +2,7 @@ import { useLocation, useSearch } from "wouter";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { GalleryCard } from "@/components/gallery/GalleryCard.tsx";
|
||||
import TagBadge from "@/components/gallery/TagBadge.tsx";
|
||||
import { useSearchGallery } from "../hook/useSearchGallery.ts";
|
||||
import { useSearchGalleryInfinite } from "../hook/useSearchGallery.ts";
|
||||
import { Spinner } from "../components/Spinner.tsx";
|
||||
import TagInput from "@/components/gallery/TagInput.tsx";
|
||||
import { useState } from "react";
|
||||
@ -14,7 +14,7 @@ export default function Gallery() {
|
||||
const tags = searchParams.get("allow_tag") ?? undefined;
|
||||
const limit = searchParams.get("limit");
|
||||
const cursor = searchParams.get("cursor");
|
||||
const { data, error, isLoading, size, setSize } = useSearchGallery({
|
||||
const { data, error, isLoading, size, setSize } = useSearchGalleryInfinite({
|
||||
word, tags,
|
||||
limit: limit ? Number.parseInt(limit) : undefined,
|
||||
cursor: cursor ? Number.parseInt(cursor) : undefined
|
||||
@ -42,9 +42,9 @@ export default function Gallery() {
|
||||
data?.length === 0 && <div className="p-4 text-3xl">No results</div>
|
||||
}
|
||||
{
|
||||
// TODO: date based grouping
|
||||
// TODO: date based grouping
|
||||
data?.map((docs) => {
|
||||
return docs.data.map((x) => {
|
||||
return docs?.data?.map((x) => {
|
||||
return (
|
||||
<GalleryCard doc={x} key={x.id} />
|
||||
);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useEffect } from "react";
|
||||
import { type TernaryDarkMode, useTernaryDarkMode, useMediaQuery } from "usehooks-ts";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { atom, useAtomValue, setAtomValue } from "../lib/atom.ts";
|
||||
import { BASE_API_URL } from "../hook/fetcher.ts";
|
||||
import { makeApiUrl } from "../hook/fetcher.ts";
|
||||
|
||||
type LoginLocalStorage = {
|
||||
username: string;
|
||||
@ -24,7 +24,7 @@ function getUserSessions() {
|
||||
}
|
||||
|
||||
export async function refresh() {
|
||||
const u = new URL("/api/user/refresh", BASE_API_URL);
|
||||
const u = makeApiUrl("/api/user/refresh");
|
||||
const res = await fetch(u, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
@ -50,7 +50,7 @@ export async function refresh() {
|
||||
}
|
||||
|
||||
export const doLogout = async () => {
|
||||
const u = new URL("/api/user/logout", BASE_API_URL);
|
||||
const u = makeApiUrl("/api/user/logout");
|
||||
const req = await fetch(u, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
@ -81,7 +81,7 @@ export const doLogin = async (userLoginInfo: {
|
||||
username: string;
|
||||
password: string;
|
||||
}): Promise<string | LoginLocalStorage> => {
|
||||
const u = new URL("/api/user/login", BASE_API_URL);
|
||||
const u = makeApiUrl("/api/user/login");
|
||||
const res = await fetch(u, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(userLoginInfo),
|
||||
|
@ -61,10 +61,10 @@ class SqliteDocumentAccessor implements DocumentAccessor {
|
||||
return await this.kysely.transaction().execute(async (trx) => {
|
||||
const { tags, additional, ...rest } = c;
|
||||
const id_lst = await trx.insertInto("document").values({
|
||||
additional: JSON.stringify(additional),
|
||||
created_at: Date.now(),
|
||||
...rest,
|
||||
})
|
||||
additional: JSON.stringify(additional),
|
||||
created_at: Date.now(),
|
||||
...rest,
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirst() as { id: number };
|
||||
const id = id_lst.id;
|
||||
@ -150,7 +150,7 @@ class SqliteDocumentAccessor implements DocumentAccessor {
|
||||
.selectFrom("document")
|
||||
.selectAll()
|
||||
.$if(allow_tag.length > 0, (qb) => {
|
||||
return allow_tag.reduce((prevQb ,tag, index) => {
|
||||
return allow_tag.reduce((prevQb, tag, index) => {
|
||||
return prevQb.innerJoin(`doc_tag_relation as tags_${index}`, `tags_${index}.doc_id`, "document.id")
|
||||
.where(`tags_${index}.tag_name`, "=", tag);
|
||||
}, qb) as unknown as typeof qb;
|
||||
@ -161,11 +161,16 @@ class SqliteDocumentAccessor implements DocumentAccessor {
|
||||
.$if(!use_offset && cursor !== undefined, (qb) => qb.where("id", "<", cursor as number))
|
||||
.limit(limit)
|
||||
.$if(eager_loading, (qb) => {
|
||||
return qb.select(eb => jsonArrayFrom(
|
||||
eb.selectFrom("doc_tag_relation")
|
||||
.select(["doc_tag_relation.tag_name"])
|
||||
.whereRef("document.id", "=", "doc_tag_relation.doc_id")
|
||||
).as("tags")).withPlugin(new MyParseJSONResultsPlugin("tags"))
|
||||
return qb.select(eb =>
|
||||
eb.selectFrom(e =>
|
||||
e.selectFrom("doc_tag_relation")
|
||||
.select(["doc_tag_relation.tag_name"])
|
||||
.whereRef("document.id", "=", "doc_tag_relation.doc_id")
|
||||
.as("agg")
|
||||
).select(e => e.fn.agg<string>("json_group_array", ["agg.tag_name"])
|
||||
.as("tags_list")
|
||||
).as("tags")
|
||||
)
|
||||
})
|
||||
.orderBy("id", "desc")
|
||||
.execute();
|
||||
@ -173,7 +178,7 @@ class SqliteDocumentAccessor implements DocumentAccessor {
|
||||
...x,
|
||||
content_hash: x.content_hash ?? "",
|
||||
additional: x.additional !== null ? (JSON.parse(x.additional)) : {},
|
||||
tags: x.tags?.map((x: { tag_name: string }) => x.tag_name) ?? [],
|
||||
tags: JSON.parse(x.tags ?? "[]"), //?.map((x: { tag_name: string }) => x.tag_name) ?? [],
|
||||
}));
|
||||
}
|
||||
async findByPath(path: string, filename?: string): Promise<Document[]> {
|
||||
|
Loading…
Reference in New Issue
Block a user