Rework #6
					 9 changed files with 54 additions and 33 deletions
				
			
		| 
						 | 
				
			
			@ -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…
	
	Add table
		
		Reference in a new issue