Migrate to sqlite
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s

This commit is contained in:
ryan 2025-10-02 11:42:40 -07:00
parent de63f0ce65
commit 3b3b1196b6
29 changed files with 2802 additions and 501 deletions

View File

@ -1 +1 @@
/nix/store/2l63479bllklr4bzi4vscn7ndj6lg614-nix-shell-env
/nix/store/gsxc3lz58zr7pdvvlihz39pqhidnh7aq-nix-shell-env

View File

@ -15,7 +15,7 @@ export CONFIG_SHELL
CXX='g++'
export CXX
HOSTTYPE='x86_64'
HOST_PATH='/nix/store/ddx7976jyll30xjbasghv9jailswprcp-bash-interactive-5.3p3/bin:/nix/store/q1zaii9cirbfpmwr7d86hpppql3kjcpf-git-2.51.0/bin:/nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev/bin:/nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/bin:/nix/store/967gn7p1p47ic924r2fx4rgbfp49fhsy-pnpm-10.15.1/bin:/nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/bin:/nix/store/jq2kbdw6ljv9i47jz23pm072cfyxwpfj-postgresql-17.6/bin:/nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/bin:/nix/store/1p5n2mzy33ayzc1scdnz82h53d192knh-claude-code-1.0.117/bin:/nix/store/8ksax0a2mxglr5hlkj2dzl556jx7xqn5-coreutils-9.7/bin:/nix/store/l964krgbp613d5jxga2vy5qdssj7zfzj-findutils-4.10.0/bin:/nix/store/s2fvny566vls74p4qm9v3fdqd741fh3f-diffutils-3.12/bin:/nix/store/pmhkmqy0vxk47r6ndh0azybhf6gs6k25-gnused-4.9/bin:/nix/store/vlckk0vnmawq9wwh7ndkrwxlpv4h29yh-gnugrep-3.12/bin:/nix/store/03nvbw411p097h6yxjghc33rbcrjfb9d-gawk-5.3.2/bin:/nix/store/8av8pfs7bnyc6hqj764ns4z1fnr9bva1-gnutar-1.35/bin:/nix/store/8gsxxh82rf957ffbsk0q9670nhvl5lia-gzip-1.14/bin:/nix/store/6yjb3zdj448rm8qsmpiq3f67kvj5683a-bzip2-1.0.8-bin/bin:/nix/store/aqdvlkh0jdwkc22hh5vr9sl6qlw5ha74-gnumake-4.4.1/bin:/nix/store/q7sqwn7i6w2b67adw0bmix29pxg85x3w-bash-5.3p3/bin:/nix/store/856i1ajaci3kmmp15rifacfz3jvn5l3q-patch-2.8/bin:/nix/store/y9kgzp85ykrhd7l691w4djx121qygy68-xz-5.8.1-bin/bin:/nix/store/v40ijzz8p2fpk9ihjck3a1ncqaqfmn3c-file-5.45/bin'
HOST_PATH='/nix/store/ddx7976jyll30xjbasghv9jailswprcp-bash-interactive-5.3p3/bin:/nix/store/q1zaii9cirbfpmwr7d86hpppql3kjcpf-git-2.51.0/bin:/nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev/bin:/nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/bin:/nix/store/967gn7p1p47ic924r2fx4rgbfp49fhsy-pnpm-10.15.1/bin:/nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/bin:/nix/store/jq2kbdw6ljv9i47jz23pm072cfyxwpfj-postgresql-17.6/bin:/nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/bin:/nix/store/1p5n2mzy33ayzc1scdnz82h53d192knh-claude-code-1.0.117/bin:/nix/store/gmwaym3dwkrb9987z8xg4njl2kmm2dvc-sqlite-3.50.2-bin/bin:/nix/store/8ksax0a2mxglr5hlkj2dzl556jx7xqn5-coreutils-9.7/bin:/nix/store/l964krgbp613d5jxga2vy5qdssj7zfzj-findutils-4.10.0/bin:/nix/store/s2fvny566vls74p4qm9v3fdqd741fh3f-diffutils-3.12/bin:/nix/store/pmhkmqy0vxk47r6ndh0azybhf6gs6k25-gnused-4.9/bin:/nix/store/vlckk0vnmawq9wwh7ndkrwxlpv4h29yh-gnugrep-3.12/bin:/nix/store/03nvbw411p097h6yxjghc33rbcrjfb9d-gawk-5.3.2/bin:/nix/store/8av8pfs7bnyc6hqj764ns4z1fnr9bva1-gnutar-1.35/bin:/nix/store/8gsxxh82rf957ffbsk0q9670nhvl5lia-gzip-1.14/bin:/nix/store/6yjb3zdj448rm8qsmpiq3f67kvj5683a-bzip2-1.0.8-bin/bin:/nix/store/aqdvlkh0jdwkc22hh5vr9sl6qlw5ha74-gnumake-4.4.1/bin:/nix/store/q7sqwn7i6w2b67adw0bmix29pxg85x3w-bash-5.3p3/bin:/nix/store/856i1ajaci3kmmp15rifacfz3jvn5l3q-patch-2.8/bin:/nix/store/y9kgzp85ykrhd7l691w4djx121qygy68-xz-5.8.1-bin/bin:/nix/store/v40ijzz8p2fpk9ihjck3a1ncqaqfmn3c-file-5.45/bin'
export HOST_PATH
IFS='
'
@ -37,13 +37,13 @@ NIX_CC='/nix/store/95k9rsn1zsw1yvir8mj824ldhf90i4qw-gcc-wrapper-14.3.0'
export NIX_CC
NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu='1'
export NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu
NIX_CFLAGS_COMPILE=' -frandom-seed=2l63479bll -isystem /nix/store/7zwa3r9agcyzf21d0792fvhrsl6gajiy-bash-interactive-5.3p3-dev/include -isystem /nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev/include -isystem /nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/include -isystem /nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/include -isystem /nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/include -isystem /nix/store/7zwa3r9agcyzf21d0792fvhrsl6gajiy-bash-interactive-5.3p3-dev/include -isystem /nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev/include -isystem /nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/include -isystem /nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/include -isystem /nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/include'
NIX_CFLAGS_COMPILE=' -frandom-seed=gsxc3lz58z -isystem /nix/store/7zwa3r9agcyzf21d0792fvhrsl6gajiy-bash-interactive-5.3p3-dev/include -isystem /nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev/include -isystem /nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/include -isystem /nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/include -isystem /nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/include -isystem /nix/store/w3ibvzff0yrpg8abrl8n2fxn0d9fpfpc-sqlite-3.50.2-dev/include -isystem /nix/store/7zwa3r9agcyzf21d0792fvhrsl6gajiy-bash-interactive-5.3p3-dev/include -isystem /nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev/include -isystem /nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/include -isystem /nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/include -isystem /nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/include -isystem /nix/store/w3ibvzff0yrpg8abrl8n2fxn0d9fpfpc-sqlite-3.50.2-dev/include'
export NIX_CFLAGS_COMPILE
NIX_ENFORCE_NO_NATIVE='1'
export NIX_ENFORCE_NO_NATIVE
NIX_HARDENING_ENABLE='bindnow format fortify fortify3 pic relro stackclashprotection stackprotector strictoverflow zerocallusedregs'
export NIX_HARDENING_ENABLE
NIX_LDFLAGS='-rpath /home/ryan/Documents/Code/hindki/outputs/out/lib -L/nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/lib -L/nix/store/msjxcqa4x2f52dyq10rbrbw6k0m0hi90-postgresql-17.6-lib/lib -L/nix/store/jq2kbdw6ljv9i47jz23pm072cfyxwpfj-postgresql-17.6/lib -L/nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/lib -L/nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/lib -L/nix/store/msjxcqa4x2f52dyq10rbrbw6k0m0hi90-postgresql-17.6-lib/lib -L/nix/store/jq2kbdw6ljv9i47jz23pm072cfyxwpfj-postgresql-17.6/lib -L/nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/lib'
NIX_LDFLAGS='-rpath /home/ryan/Documents/Code/hindki/outputs/out/lib -L/nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/lib -L/nix/store/msjxcqa4x2f52dyq10rbrbw6k0m0hi90-postgresql-17.6-lib/lib -L/nix/store/jq2kbdw6ljv9i47jz23pm072cfyxwpfj-postgresql-17.6/lib -L/nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/lib -L/nix/store/8bsyjdxv61ha88hjrpgl6nyfjxfqnphx-sqlite-3.50.2/lib -L/nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/lib -L/nix/store/msjxcqa4x2f52dyq10rbrbw6k0m0hi90-postgresql-17.6-lib/lib -L/nix/store/jq2kbdw6ljv9i47jz23pm072cfyxwpfj-postgresql-17.6/lib -L/nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/lib -L/nix/store/8bsyjdxv61ha88hjrpgl6nyfjxfqnphx-sqlite-3.50.2/lib'
export NIX_LDFLAGS
NIX_NO_SELF_RPATH='1'
NIX_STORE='/nix/store'
@ -60,7 +60,7 @@ OLDPWD=''
export OLDPWD
OPTERR='1'
OSTYPE='linux-gnu'
PATH='/nix/store/gx2l0rnp3qcnysdddkg9dqnh2mz6w08k-patchelf-0.15.2/bin:/nix/store/95k9rsn1zsw1yvir8mj824ldhf90i4qw-gcc-wrapper-14.3.0/bin:/nix/store/82kmz7r96navanrc2fgckh2bamiqrgsw-gcc-14.3.0/bin:/nix/store/4jxivbjpr86wmsziqlf7iljlwjlxz8bh-glibc-2.40-66-bin/bin:/nix/store/8ksax0a2mxglr5hlkj2dzl556jx7xqn5-coreutils-9.7/bin:/nix/store/l19cddv64i52rhcwahif8sgyrd3mhiqb-binutils-wrapper-2.44/bin:/nix/store/c43ry7z24x3jhnjlj4gpay8a4g2p3x1h-binutils-2.44/bin:/nix/store/ddx7976jyll30xjbasghv9jailswprcp-bash-interactive-5.3p3/bin:/nix/store/q1zaii9cirbfpmwr7d86hpppql3kjcpf-git-2.51.0/bin:/nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev/bin:/nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/bin:/nix/store/967gn7p1p47ic924r2fx4rgbfp49fhsy-pnpm-10.15.1/bin:/nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/bin:/nix/store/jq2kbdw6ljv9i47jz23pm072cfyxwpfj-postgresql-17.6/bin:/nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/bin:/nix/store/1p5n2mzy33ayzc1scdnz82h53d192knh-claude-code-1.0.117/bin:/nix/store/8ksax0a2mxglr5hlkj2dzl556jx7xqn5-coreutils-9.7/bin:/nix/store/l964krgbp613d5jxga2vy5qdssj7zfzj-findutils-4.10.0/bin:/nix/store/s2fvny566vls74p4qm9v3fdqd741fh3f-diffutils-3.12/bin:/nix/store/pmhkmqy0vxk47r6ndh0azybhf6gs6k25-gnused-4.9/bin:/nix/store/vlckk0vnmawq9wwh7ndkrwxlpv4h29yh-gnugrep-3.12/bin:/nix/store/03nvbw411p097h6yxjghc33rbcrjfb9d-gawk-5.3.2/bin:/nix/store/8av8pfs7bnyc6hqj764ns4z1fnr9bva1-gnutar-1.35/bin:/nix/store/8gsxxh82rf957ffbsk0q9670nhvl5lia-gzip-1.14/bin:/nix/store/6yjb3zdj448rm8qsmpiq3f67kvj5683a-bzip2-1.0.8-bin/bin:/nix/store/aqdvlkh0jdwkc22hh5vr9sl6qlw5ha74-gnumake-4.4.1/bin:/nix/store/q7sqwn7i6w2b67adw0bmix29pxg85x3w-bash-5.3p3/bin:/nix/store/856i1ajaci3kmmp15rifacfz3jvn5l3q-patch-2.8/bin:/nix/store/y9kgzp85ykrhd7l691w4djx121qygy68-xz-5.8.1-bin/bin:/nix/store/v40ijzz8p2fpk9ihjck3a1ncqaqfmn3c-file-5.45/bin'
PATH='/nix/store/gx2l0rnp3qcnysdddkg9dqnh2mz6w08k-patchelf-0.15.2/bin:/nix/store/95k9rsn1zsw1yvir8mj824ldhf90i4qw-gcc-wrapper-14.3.0/bin:/nix/store/82kmz7r96navanrc2fgckh2bamiqrgsw-gcc-14.3.0/bin:/nix/store/4jxivbjpr86wmsziqlf7iljlwjlxz8bh-glibc-2.40-66-bin/bin:/nix/store/8ksax0a2mxglr5hlkj2dzl556jx7xqn5-coreutils-9.7/bin:/nix/store/l19cddv64i52rhcwahif8sgyrd3mhiqb-binutils-wrapper-2.44/bin:/nix/store/c43ry7z24x3jhnjlj4gpay8a4g2p3x1h-binutils-2.44/bin:/nix/store/ddx7976jyll30xjbasghv9jailswprcp-bash-interactive-5.3p3/bin:/nix/store/q1zaii9cirbfpmwr7d86hpppql3kjcpf-git-2.51.0/bin:/nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev/bin:/nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/bin:/nix/store/967gn7p1p47ic924r2fx4rgbfp49fhsy-pnpm-10.15.1/bin:/nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/bin:/nix/store/jq2kbdw6ljv9i47jz23pm072cfyxwpfj-postgresql-17.6/bin:/nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/bin:/nix/store/1p5n2mzy33ayzc1scdnz82h53d192knh-claude-code-1.0.117/bin:/nix/store/gmwaym3dwkrb9987z8xg4njl2kmm2dvc-sqlite-3.50.2-bin/bin:/nix/store/8ksax0a2mxglr5hlkj2dzl556jx7xqn5-coreutils-9.7/bin:/nix/store/l964krgbp613d5jxga2vy5qdssj7zfzj-findutils-4.10.0/bin:/nix/store/s2fvny566vls74p4qm9v3fdqd741fh3f-diffutils-3.12/bin:/nix/store/pmhkmqy0vxk47r6ndh0azybhf6gs6k25-gnused-4.9/bin:/nix/store/vlckk0vnmawq9wwh7ndkrwxlpv4h29yh-gnugrep-3.12/bin:/nix/store/03nvbw411p097h6yxjghc33rbcrjfb9d-gawk-5.3.2/bin:/nix/store/8av8pfs7bnyc6hqj764ns4z1fnr9bva1-gnutar-1.35/bin:/nix/store/8gsxxh82rf957ffbsk0q9670nhvl5lia-gzip-1.14/bin:/nix/store/6yjb3zdj448rm8qsmpiq3f67kvj5683a-bzip2-1.0.8-bin/bin:/nix/store/aqdvlkh0jdwkc22hh5vr9sl6qlw5ha74-gnumake-4.4.1/bin:/nix/store/q7sqwn7i6w2b67adw0bmix29pxg85x3w-bash-5.3p3/bin:/nix/store/856i1ajaci3kmmp15rifacfz3jvn5l3q-patch-2.8/bin:/nix/store/y9kgzp85ykrhd7l691w4djx121qygy68-xz-5.8.1-bin/bin:/nix/store/v40ijzz8p2fpk9ihjck3a1ncqaqfmn3c-file-5.45/bin'
export PATH
PS4='+ '
RANLIB='ranlib'
@ -82,7 +82,7 @@ export XDG_DATA_DIRS
__structuredAttrs=''
export __structuredAttrs
_substituteStream_has_warned_replace_deprecation='false'
buildInputs='/nix/store/7zwa3r9agcyzf21d0792fvhrsl6gajiy-bash-interactive-5.3p3-dev /nix/store/008h0z2m22alg2v8kcdcw4v0f7c39lmm-glibc-locales-2.40-66 /nix/store/q1zaii9cirbfpmwr7d86hpppql3kjcpf-git-2.51.0 /nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev /nix/store/967gn7p1p47ic924r2fx4rgbfp49fhsy-pnpm-10.15.1 /nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev /nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0 /nix/store/1p5n2mzy33ayzc1scdnz82h53d192knh-claude-code-1.0.117'
buildInputs='/nix/store/7zwa3r9agcyzf21d0792fvhrsl6gajiy-bash-interactive-5.3p3-dev /nix/store/008h0z2m22alg2v8kcdcw4v0f7c39lmm-glibc-locales-2.40-66 /nix/store/q1zaii9cirbfpmwr7d86hpppql3kjcpf-git-2.51.0 /nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev /nix/store/967gn7p1p47ic924r2fx4rgbfp49fhsy-pnpm-10.15.1 /nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev /nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0 /nix/store/1p5n2mzy33ayzc1scdnz82h53d192knh-claude-code-1.0.117 /nix/store/w3ibvzff0yrpg8abrl8n2fxn0d9fpfpc-sqlite-3.50.2-dev'
export buildInputs
buildPhase='{ echo "------------------------------------------------------------";
echo " WARNING: the existence of this path is not guaranteed.";
@ -161,7 +161,7 @@ declare -a pkgsBuildBuild=()
declare -a pkgsBuildHost=('/nix/store/gx2l0rnp3qcnysdddkg9dqnh2mz6w08k-patchelf-0.15.2' '/nix/store/jwjq0fjgn7d00kswhaw2m8hbgws5vbi4-update-autotools-gnu-config-scripts-hook' '/nix/store/0y5xmdb7qfvimjwbq7ibg1xdgkgjwqng-no-broken-symlinks.sh' '/nix/store/cv1d7p48379km6a85h4zp6kr86brh32q-audit-tmpdir.sh' '/nix/store/85clx3b0xkdf58jn161iy80y5223ilbi-compress-man-pages.sh' '/nix/store/wgrbkkaldkrlrni33ccvm3b6vbxzb656-make-symlinks-relative.sh' '/nix/store/5yzw0vhkyszf2d179m0qfkgxmp5wjjx4-move-docs.sh' '/nix/store/fyaryjvghbkpfnsyw97hb3lyb37s1pd6-move-lib64.sh' '/nix/store/kd4xwxjpjxi71jkm6ka0np72if9rm3y0-move-sbin.sh' '/nix/store/pag6l61paj1dc9sv15l7bm5c17xn5kyk-move-systemd-user-units.sh' '/nix/store/cmzya9irvxzlkh7lfy6i82gbp0saxqj3-multiple-outputs.sh' '/nix/store/x8c40nfigps493a07sdr2pm5s9j1cdc0-patch-shebangs.sh' '/nix/store/cickvswrvann041nqxb0rxilc46svw1n-prune-libtool-files.sh' '/nix/store/xyff06pkhki3qy1ls77w10s0v79c9il0-reproducible-builds.sh' '/nix/store/z7k98578dfzi6l3hsvbivzm7hfqlk0zc-set-source-date-epoch-to-latest.sh' '/nix/store/pilsssjjdxvdphlg2h19p0bfx5q0jzkn-strip.sh' '/nix/store/95k9rsn1zsw1yvir8mj824ldhf90i4qw-gcc-wrapper-14.3.0' '/nix/store/l19cddv64i52rhcwahif8sgyrd3mhiqb-binutils-wrapper-2.44' )
declare -a pkgsBuildTarget=()
declare -a pkgsHostHost=()
declare -a pkgsHostTarget=('/nix/store/7zwa3r9agcyzf21d0792fvhrsl6gajiy-bash-interactive-5.3p3-dev' '/nix/store/ddx7976jyll30xjbasghv9jailswprcp-bash-interactive-5.3p3' '/nix/store/008h0z2m22alg2v8kcdcw4v0f7c39lmm-glibc-locales-2.40-66' '/nix/store/q1zaii9cirbfpmwr7d86hpppql3kjcpf-git-2.51.0' '/nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev' '/nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0' '/nix/store/967gn7p1p47ic924r2fx4rgbfp49fhsy-pnpm-10.15.1' '/nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev' '/nix/store/msjxcqa4x2f52dyq10rbrbw6k0m0hi90-postgresql-17.6-lib' '/nix/store/jq2kbdw6ljv9i47jz23pm072cfyxwpfj-postgresql-17.6' '/nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0' '/nix/store/1p5n2mzy33ayzc1scdnz82h53d192knh-claude-code-1.0.117' )
declare -a pkgsHostTarget=('/nix/store/7zwa3r9agcyzf21d0792fvhrsl6gajiy-bash-interactive-5.3p3-dev' '/nix/store/ddx7976jyll30xjbasghv9jailswprcp-bash-interactive-5.3p3' '/nix/store/008h0z2m22alg2v8kcdcw4v0f7c39lmm-glibc-locales-2.40-66' '/nix/store/q1zaii9cirbfpmwr7d86hpppql3kjcpf-git-2.51.0' '/nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev' '/nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0' '/nix/store/967gn7p1p47ic924r2fx4rgbfp49fhsy-pnpm-10.15.1' '/nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev' '/nix/store/msjxcqa4x2f52dyq10rbrbw6k0m0hi90-postgresql-17.6-lib' '/nix/store/jq2kbdw6ljv9i47jz23pm072cfyxwpfj-postgresql-17.6' '/nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0' '/nix/store/1p5n2mzy33ayzc1scdnz82h53d192knh-claude-code-1.0.117' '/nix/store/w3ibvzff0yrpg8abrl8n2fxn0d9fpfpc-sqlite-3.50.2-dev' '/nix/store/gmwaym3dwkrb9987z8xg4njl2kmm2dvc-sqlite-3.50.2-bin' '/nix/store/8bsyjdxv61ha88hjrpgl6nyfjxfqnphx-sqlite-3.50.2' )
declare -a pkgsTargetTarget=()
declare -a postFixupHooks=('noBrokenSymlinksInAllOutputs' '_makeSymlinksRelativeInAllOutputs' '_multioutPropagateDev' )
declare -a postUnpackHooks=('_updateSourceDateEpochFromSourceRoot' )

View File

@ -9,7 +9,6 @@
flake-utils.lib.eachDefaultSystem (
system:
let
inherit (pkgs.lib) optional optionals;
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
@ -28,6 +27,7 @@
postgresql
inotify-tools
claude-code
sqlite
];
};
}

25
hindki/.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# SQLite database
data.db
data.db-shm
data.db-wal
# build output
dist/
.astro/

View File

@ -0,0 +1,58 @@
# Type System Consolidation
All type definitions are now centralized in `src/types/types.ts` as the single source of truth.
## Core Types
### Domain Models (from database)
- **`Word`** - Main vocabulary word with all relations
- **`Example`** - Usage example for a word
- **`Tag`** - Category/tag for organizing words
- **`SeeAlso`** - Related reference (word or grammar page)
### Input Models (for creating records)
- **`NewWord`** - Data needed to create a word
- **`NewExample`** - Data needed to create an example
- **`NewTag`** - Data needed to create a tag
- **`NewSeeAlso`** - Data needed to create a see-also reference
## Type Rules
### Nullable vs Optional
- **Nullable (`| null`)**: Field exists in DB but can be null
- `type`, `gender`, `note` on `Word`
- `note` on `Example`, `Tag`, `SeeAlso`
- **Optional (`?`)**: Field may not exist at all
- `examples`, `tags`, `seeAlso` on `Word` (only included if non-empty)
- All fields on input types except required ones
### Gender Constraint
- `gender` is constrained to `"m" | "f" | null` (not free-form string)
### Reference Field
- `reference` on `SeeAlso` is **required** (`.notNull()` in schema)
- Should never be empty string
## Migration from Old System
### Removed Types
- Old `VocabWord` in storage.ts (now aliased to `Word`)
- Duplicate `Tag` in storage.ts
- `NewWord` duplicate in storage.ts
### Legacy Aliases (for backward compatibility)
```typescript
export type VocabWord = Word;
export const vocabWordSchema = wordSchema;
```
## Database Schema Types
Drizzle inferred types are exported from `src/lib/db/schema.ts`:
- `DBWord` - Raw database row for words table
- `DBExample` - Raw database row for examples table
- `DBTag` - Raw database row for tags table
- `DBSeeAlso` - Raw database row for see_also table
These should only be used internally by the storage layer. API/components should use the domain types from `types.ts`.

View File

@ -1,17 +1,16 @@
// @ts-check
import { defineConfig } from "astro/config";
import starlight from "@astrojs/starlight";
import YAML from "yaml";
import fs from "fs";
import react from "@astrojs/react";
import node from "@astrojs/node";
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import { tags } from './src/lib/db/schema.ts';
const vocabListFile = fs.readFileSync("src/vocab_list.yaml", "utf8");
const vocabListJson = YAML.parse(vocabListFile);
const categories = vocabListJson.map(
(/** @type {{ slug: string }} */ category) => category.slug,
);
// Load tags from database for sidebar
const sqlite = new Database('./data.db');
const db = drizzle(sqlite);
const tagsList = db.select().from(tags).all();
const vocabList = [
{
@ -19,9 +18,9 @@ const vocabList = [
link: `/vocabulary`,
},
].concat(
categories.map((/** @type {string} */ category) => ({
label: category.charAt(0).toUpperCase() + category.slice(1),
link: `/vocabulary/${category}`,
tagsList.map((tag) => ({
label: tag.name.charAt(0).toUpperCase() + tag.name.slice(1),
link: `/vocabulary/${tag.name}`,
})),
);

10
hindki/drizzle.config.ts Normal file
View File

@ -0,0 +1,10 @@
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/lib/db/schema.ts',
out: './drizzle',
dialect: 'sqlite',
dbCredentials: {
url: './data.db',
},
});

View File

@ -0,0 +1,42 @@
CREATE TABLE `examples` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`word_id` integer NOT NULL,
`hindi` text NOT NULL,
`english` text NOT NULL,
`note` text,
FOREIGN KEY (`word_id`) REFERENCES `words`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `see_also` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`word_id` integer NOT NULL,
`reference` text,
`note` text,
FOREIGN KEY (`word_id`) REFERENCES `words`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `tags` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`description` text
);
--> statement-breakpoint
CREATE UNIQUE INDEX `tags_name_unique` ON `tags` (`name`);--> statement-breakpoint
CREATE TABLE `word_tags` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`word_id` integer NOT NULL,
`tag_id` integer NOT NULL,
FOREIGN KEY (`word_id`) REFERENCES `words`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`tag_id`) REFERENCES `tags`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `words` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`hindi` text NOT NULL,
`english` text NOT NULL,
`type` text,
`gender` text,
`note` text,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);

View File

@ -0,0 +1,13 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_see_also` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`word_id` integer NOT NULL,
`reference` text NOT NULL,
`note` text,
FOREIGN KEY (`word_id`) REFERENCES `words`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
INSERT INTO `__new_see_also`("id", "word_id", "reference", "note") SELECT "id", "word_id", "reference", "note" FROM `see_also`;--> statement-breakpoint
DROP TABLE `see_also`;--> statement-breakpoint
ALTER TABLE `__new_see_also` RENAME TO `see_also`;--> statement-breakpoint
PRAGMA foreign_keys=ON;

View File

@ -0,0 +1,292 @@
{
"version": "6",
"dialect": "sqlite",
"id": "0c4cf3cf-109c-455f-9adb-40293d6825f0",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"examples": {
"name": "examples",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"word_id": {
"name": "word_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"hindi": {
"name": "hindi",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"english": {
"name": "english",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"note": {
"name": "note",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"examples_word_id_words_id_fk": {
"name": "examples_word_id_words_id_fk",
"tableFrom": "examples",
"tableTo": "words",
"columnsFrom": [
"word_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"see_also": {
"name": "see_also",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"word_id": {
"name": "word_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"reference": {
"name": "reference",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"note": {
"name": "note",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"see_also_word_id_words_id_fk": {
"name": "see_also_word_id_words_id_fk",
"tableFrom": "see_also",
"tableTo": "words",
"columnsFrom": [
"word_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"tags": {
"name": "tags",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"tags_name_unique": {
"name": "tags_name_unique",
"columns": [
"name"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"word_tags": {
"name": "word_tags",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"word_id": {
"name": "word_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"tag_id": {
"name": "tag_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"word_tags_word_id_words_id_fk": {
"name": "word_tags_word_id_words_id_fk",
"tableFrom": "word_tags",
"tableTo": "words",
"columnsFrom": [
"word_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"word_tags_tag_id_tags_id_fk": {
"name": "word_tags_tag_id_tags_id_fk",
"tableFrom": "word_tags",
"tableTo": "tags",
"columnsFrom": [
"tag_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"words": {
"name": "words",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"hindi": {
"name": "hindi",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"english": {
"name": "english",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"gender": {
"name": "gender",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"note": {
"name": "note",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -0,0 +1,292 @@
{
"version": "6",
"dialect": "sqlite",
"id": "e2f4339d-0cbb-4dd5-ba04-427b0f25625c",
"prevId": "0c4cf3cf-109c-455f-9adb-40293d6825f0",
"tables": {
"examples": {
"name": "examples",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"word_id": {
"name": "word_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"hindi": {
"name": "hindi",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"english": {
"name": "english",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"note": {
"name": "note",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"examples_word_id_words_id_fk": {
"name": "examples_word_id_words_id_fk",
"tableFrom": "examples",
"tableTo": "words",
"columnsFrom": [
"word_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"see_also": {
"name": "see_also",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"word_id": {
"name": "word_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"reference": {
"name": "reference",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"note": {
"name": "note",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"see_also_word_id_words_id_fk": {
"name": "see_also_word_id_words_id_fk",
"tableFrom": "see_also",
"tableTo": "words",
"columnsFrom": [
"word_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"tags": {
"name": "tags",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"tags_name_unique": {
"name": "tags_name_unique",
"columns": [
"name"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"word_tags": {
"name": "word_tags",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"word_id": {
"name": "word_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"tag_id": {
"name": "tag_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"word_tags_word_id_words_id_fk": {
"name": "word_tags_word_id_words_id_fk",
"tableFrom": "word_tags",
"tableTo": "words",
"columnsFrom": [
"word_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"word_tags_tag_id_tags_id_fk": {
"name": "word_tags_tag_id_tags_id_fk",
"tableFrom": "word_tags",
"tableTo": "tags",
"columnsFrom": [
"tag_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"words": {
"name": "words",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"hindi": {
"name": "hindi",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"english": {
"name": "english",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"gender": {
"name": "gender",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"note": {
"name": "note",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -0,0 +1,20 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1759426695406,
"tag": "0000_charming_screwball",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1759430283642,
"tag": "0001_shallow_sasquatch",
"breakpoints": true
}
]
}

1095
hindki/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,8 @@
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
"astro": "astro",
"migrate": "tsx scripts/migrate-yaml-to-db.ts"
},
"dependencies": {
"@astrojs/node": "^9.4.4",
@ -16,6 +17,8 @@
"@types/markdown-it": "^14.1.2",
"astro": "^5.6.1",
"astro-code-editor": "^0.1.1",
"better-sqlite3": "^12.4.1",
"drizzle-orm": "^0.44.6",
"markdown-it": "^14.1.0",
"markdown-it-mark": "^4.0.0",
"monaco-yaml": "^5.4.0",
@ -23,7 +26,10 @@
"yaml": "^2.8.1"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"drizzle-kit": "^0.31.5",
"prettier": "^3.6.2",
"prettier-plugin-astro": "^0.14.1"
"prettier-plugin-astro": "^0.14.1",
"tsx": "^4.20.6"
}
}

View File

@ -0,0 +1,111 @@
import fs from 'fs/promises';
import path from 'path';
import YAML from 'yaml';
import { db } from '../src/lib/db';
import { words, examples, tags, wordTags, seeAlso } from '../src/lib/db/schema';
interface YAMLWord {
english: string;
hindi: string;
type?: string;
gender?: string;
note?: string;
examples?: Array<{
english: string;
hindi: string;
note?: string;
}>;
see_also?: string[];
}
interface YAMLCategory {
slug: string;
about: string;
words: YAMLWord[];
}
async function migrate() {
try {
// Read YAML file
const yamlPath = path.join(process.cwd(), 'src', 'vocab_list.yaml');
const content = await fs.readFile(yamlPath, 'utf-8');
const categories: YAMLCategory[] = YAML.parse(content);
console.log(`Found ${categories.length} categories`);
// Create tags from categories
const tagMap = new Map<string, number>();
for (const category of categories) {
const [tag] = await db.insert(tags)
.values({
name: category.slug,
description: category.about,
})
.returning();
tagMap.set(category.slug, tag.id);
console.log(`Created tag: ${category.slug}`);
}
// Migrate words
let wordCount = 0;
for (const category of categories) {
const tagId = tagMap.get(category.slug)!;
for (const yamlWord of category.words) {
// Insert word
const [word] = await db.insert(words)
.values({
hindi: yamlWord.hindi,
english: yamlWord.english,
type: yamlWord.type || null,
gender: yamlWord.gender || null,
note: yamlWord.note || null,
})
.returning();
// Associate with tag
await db.insert(wordTags).values({
wordId: word.id,
tagId: tagId,
});
// Insert examples
if (yamlWord.examples) {
for (const example of yamlWord.examples) {
await db.insert(examples).values({
wordId: word.id,
hindi: example.hindi,
english: example.english,
note: example.note || null,
});
}
}
// Insert see_also references
if (yamlWord.see_also) {
for (const reference of yamlWord.see_also) {
await db.insert(seeAlso).values({
wordId: word.id,
reference: reference,
note: null,
});
}
}
wordCount++;
}
}
console.log(`\nMigration complete!`);
console.log(`- Created ${tagMap.size} tags`);
console.log(`- Migrated ${wordCount} words`);
} catch (error) {
console.error('Migration failed:', error);
process.exit(1);
}
}
migrate();

View File

@ -1,50 +1,60 @@
import React, { useState, useEffect } from 'react';
import type { VocabList, VocabWord } from '@/types/types';
import { z } from 'zod';
import type { Tag } from '@/types/types';
interface WordFormData {
english: string;
hindi: string;
type: string;
gender?: 'm' | 'f';
note?: string;
examples?: Array<{ english: string; hindi: string; note?: string }>;
seeAlso?: Array<{ reference: string; note?: string }>;
}
export default function AddVocabForm() {
const [categories, setCategories] = useState<VocabList[]>([]);
const [selectedCategory, setSelectedCategory] = useState('');
const [tags, setTags] = useState<Tag[]>([]);
const [selectedTagIds, setSelectedTagIds] = useState<number[]>([]);
const [loading, setLoading] = useState(false);
const [categoriesLoading, setCategoriesLoading] = useState(true);
const [tagsLoading, setTagsLoading] = useState(true);
const [message, setMessage] = useState('');
const [showNewCategory, setShowNewCategory] = useState(false);
const [newCategorySlug, setNewCategorySlug] = useState('');
const [newCategoryAbout, setNewCategoryAbout] = useState('');
const [formData, setFormData] = useState<VocabWord>({
const [showNewTag, setShowNewTag] = useState(false);
const [newTagName, setNewTagName] = useState('');
const [newTagDescription, setNewTagDescription] = useState('');
const [formData, setFormData] = useState<WordFormData>({
english: '',
hindi: '',
type: 'noun',
gender: undefined,
note: undefined,
examples: undefined,
see_also: undefined,
seeAlso: undefined,
});
const englishInputRef = React.useRef<HTMLInputElement>(null);
useEffect(() => {
// Get URL parameters
const urlParams = new URLSearchParams(window.location.search);
const catFromUrl = urlParams.get('cat');
const tagFromUrl = urlParams.get('tag');
const typeFromUrl = urlParams.get('type');
setCategoriesLoading(true);
fetch('/api/vocab.json')
setTagsLoading(true);
fetch('/api/tags.json')
.then(res => {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
return res.json();
})
.then(data => {
.then((data: Tag[]) => {
if (Array.isArray(data) && data.length > 0) {
setCategories(data);
setTags(data);
// Use category from URL if provided, otherwise first category
if (catFromUrl && data.some(cat => cat.slug === catFromUrl)) {
setSelectedCategory(catFromUrl);
} else if (!selectedCategory && data.length > 0) {
setSelectedCategory(data[0].slug);
// Use tag from URL if provided
if (tagFromUrl) {
const tag = data.find(t => t.name === tagFromUrl);
if (tag) {
setSelectedTagIds([tag.id]);
}
}
// Use type from URL if provided
@ -52,15 +62,15 @@ export default function AddVocabForm() {
setFormData(prev => ({ ...prev, type: typeFromUrl }));
}
} else {
setMessage('No categories found');
setMessage('No tags found');
}
})
.catch(err => {
console.error('Failed to load categories:', err);
setMessage(`Failed to load categories: ${err.message}`);
console.error('Failed to load tags:', err);
setMessage(`Failed to load tags: ${err.message}`);
})
.finally(() => {
setCategoriesLoading(false);
setTagsLoading(false);
});
}, []);
@ -70,22 +80,18 @@ export default function AddVocabForm() {
setMessage('');
try {
// Build word object with only non-empty fields
// Build word object
const word: any = {
english: formData.english,
hindi: formData.hindi,
type: formData.type,
type: formData.type || null,
gender: formData.gender || null,
note: formData.note?.trim() || null,
tagIds: selectedTagIds,
};
// Only add optional fields if they have values
if (formData.gender) {
word.gender = formData.gender;
}
if (formData.note && formData.note.trim()) {
word.note = formData.note;
}
// Add examples if present
if (formData.examples && formData.examples.length > 0) {
// Filter out empty examples
const validExamples = formData.examples.filter(
ex => ex.english.trim() || ex.hindi.trim()
);
@ -94,29 +100,23 @@ export default function AddVocabForm() {
}
}
if (formData.see_also && formData.see_also.length > 0) {
// Filter out empty see_also fields
const validSeeAlsos = formData.see_also.filter(
ex => ex.trim()
// Add see also if present
if (formData.seeAlso && formData.seeAlso.length > 0) {
const validSeeAlso = formData.seeAlso.filter(
sa => sa.reference.trim()
);
if (validSeeAlsos.length > 0) {
word.see_also = validSeeAlsos;
if (validSeeAlso.length > 0) {
word.seeAlso = validSeeAlso;
}
}
// If creating a new category, use the new category details
const categoryToUse = showNewCategory ? newCategorySlug : selectedCategory;
const requestBody: any = word;
const requestBody: any = {
category: categoryToUse,
word: word,
};
// Include new category information if creating one
if (showNewCategory) {
requestBody.newCategory = {
slug: newCategorySlug,
about: newCategoryAbout,
// Include new tag information if creating one
if (showNewTag) {
requestBody.newTag = {
name: newTagName,
description: newTagDescription || null,
};
}
@ -133,23 +133,31 @@ export default function AddVocabForm() {
if (response.ok) {
setMessage('Word added successfully!');
// Remember the current type and category for bulk entry
// Remember the current type and tags for bulk entry
const currentType = formData.type;
const currentCategory = showNewCategory ? newCategorySlug : selectedCategory;
const currentTagIds = showNewTag && result.word?.tags
? result.word.tags.map((t: Tag) => t.id)
: selectedTagIds;
// Update URL with current category and type to persist through reloads
const newUrl = `${window.location.pathname}?cat=${currentCategory}&type=${currentType}`;
window.history.replaceState({}, '', newUrl);
// Update URL with current tag and type to persist through reloads
const currentTagNames = showNewTag && result.word?.tags
? result.word.tags.map((t: Tag) => t.name)
: tags.filter(t => selectedTagIds.includes(t.id)).map(t => t.name);
// Reset form but keep the type
if (currentTagNames.length > 0) {
const newUrl = `${window.location.pathname}?tag=${currentTagNames[0]}&type=${currentType}`;
window.history.replaceState({}, '', newUrl);
}
// Reset form but keep the type and tags
setFormData({
english: '',
hindi: '',
gender: undefined,
type: currentType, // Keep the last used type
type: currentType,
note: '',
examples: [],
see_also: [],
seeAlso: [],
});
// Focus back on English input for quick bulk entry
@ -157,25 +165,23 @@ export default function AddVocabForm() {
englishInputRef.current?.focus();
}, 100);
// If we created a new category, refresh the categories list
if (showNewCategory) {
setShowNewCategory(false);
setNewCategorySlug('');
setNewCategoryAbout('');
// If we created a new tag, refresh the tags list
if (showNewTag) {
setShowNewTag(false);
setNewTagName('');
setNewTagDescription('');
// Refresh categories and set the new category as selected
fetch('/api/vocab.json')
// Refresh tags and select the new tag
fetch('/api/tags.json')
.then(res => res.json())
.then(data => {
.then((data: Tag[]) => {
if (Array.isArray(data) && data.length > 0) {
setCategories(data);
// Set the newly created category as selected
setSelectedCategory(currentCategory);
setTags(data);
setSelectedTagIds(currentTagIds);
}
});
} else {
// For existing categories, just keep it selected
setSelectedCategory(currentCategory);
setSelectedTagIds(currentTagIds);
}
} else {
setMessage(`Error: ${result.error}`);
@ -188,6 +194,14 @@ export default function AddVocabForm() {
}
};
const toggleTag = (tagId: number) => {
setSelectedTagIds(prev =>
prev.includes(tagId)
? prev.filter(id => id !== tagId)
: [...prev, tagId]
);
};
const addExample = () => {
setFormData({
...formData,
@ -195,7 +209,7 @@ export default function AddVocabForm() {
});
};
const updateExample = (index: number, field: 'english' | 'hindi', value: string) => {
const updateExample = (index: number, field: 'english' | 'hindi' | 'note', value: string) => {
const newExamples = [...(formData.examples || [])];
newExamples[index] = { ...newExamples[index], [field]: value };
setFormData({ ...formData, examples: newExamples });
@ -206,74 +220,68 @@ export default function AddVocabForm() {
setFormData({ ...formData, examples: newExamples });
};
// Same thing for see_also
const addSeeAlso = () => {
setFormData({
...formData,
see_also: [...(formData.see_also || []), ''],
seeAlso: [...(formData.seeAlso || []), { reference: '', note: '' }],
});
};
const updateSeeAlso = (index: number, value: string) => {
const newSeeAlso = [...(formData.see_also || [])];
newSeeAlso[index] = value;
setFormData({ ...formData, see_also: newSeeAlso });
}
const updateSeeAlso = (index: number, field: 'reference' | 'note', value: string) => {
const newSeeAlso = [...(formData.seeAlso || [])];
newSeeAlso[index] = { ...newSeeAlso[index], [field]: value };
setFormData({ ...formData, seeAlso: newSeeAlso });
};
const removeSeeAlso = (index: number) => {
const newSeeAlso = (formData.see_also || []).filter((_, i) => i !== index);
setFormData({ ...formData, see_also: newSeeAlso });
}
const newSeeAlso = (formData.seeAlso || []).filter((_, i) => i !== index);
setFormData({ ...formData, seeAlso: newSeeAlso });
};
return (
<div className="vocab-form">
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="category">Category:</label>
<select
id="category"
value={showNewCategory ? '__new__' : selectedCategory}
onChange={(e) => {
if (e.target.value === '__new__') {
setShowNewCategory(true);
} else {
setShowNewCategory(false);
setSelectedCategory(e.target.value);
}
}}
required={!showNewCategory}
>
{categories.map((cat) => (
<option key={cat.slug} value={cat.slug}>
{cat.slug.charAt(0).toUpperCase() + cat.slug.slice(1)} - {cat.about}
</option>
<label>Tags:</label>
<div className="tag-checkboxes">
{tags.map((tag) => (
<label key={tag.id} className="tag-checkbox">
<input
type="checkbox"
checked={selectedTagIds.includes(tag.id)}
onChange={() => toggleTag(tag.id)}
/>
{tag.name}
{tag.description && <span className="tag-desc"> - {tag.description}</span>}
</label>
))}
<option value="__new__">+ Create New Category</option>
</select>
</div>
<button type="button" onClick={() => setShowNewTag(!showNewTag)}>
{showNewTag ? 'Cancel' : '+ Create New Tag'}
</button>
</div>
{showNewCategory && (
{showNewTag && (
<>
<div className="form-group">
<label htmlFor="newCategorySlug">Category ID (slug):</label>
<label htmlFor="newTagName">Tag Name:</label>
<input
type="text"
id="newCategorySlug"
value={newCategorySlug}
onChange={(e) => setNewCategorySlug(e.target.value.toLowerCase().replace(/\s+/g, '-'))}
id="newTagName"
value={newTagName}
onChange={(e) => setNewTagName(e.target.value.toLowerCase().replace(/\s+/g, '-'))}
placeholder="e.g., food-and-drink"
required={showNewCategory}
required={showNewTag}
/>
</div>
<div className="form-group">
<label htmlFor="newCategoryAbout">Category Description:</label>
<label htmlFor="newTagDescription">Tag Description:</label>
<input
type="text"
id="newCategoryAbout"
value={newCategoryAbout}
onChange={(e) => setNewCategoryAbout(e.target.value)}
id="newTagDescription"
value={newTagDescription}
onChange={(e) => setNewTagDescription(e.target.value)}
placeholder="e.g., Words related to food and beverages"
required={showNewCategory}
/>
</div>
</>
@ -327,10 +335,11 @@ export default function AddVocabForm() {
id="gender"
value={formData.gender ?? ''}
onChange={(e) => {
// Use genderSchema from VocabWord type
const genderSchema = z.enum(['m', 'f']).optional();
const parsed = genderSchema.safeParse(e.target.value === '' ? undefined : e.target.value);
setFormData({ ...formData, gender: parsed.success ? parsed.data : undefined });
const value = e.target.value;
setFormData({
...formData,
gender: value === '' ? undefined : (value as 'm' | 'f')
});
}}
>
<option value="">N/A</option>
@ -343,7 +352,7 @@ export default function AddVocabForm() {
<label htmlFor="note">Note:</label>
<textarea
id="note"
value={formData.note}
value={formData.note || ''}
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
rows={3}
/>
@ -365,6 +374,12 @@ export default function AddVocabForm() {
value={example.hindi}
onChange={(e) => updateExample(index, 'hindi', e.target.value)}
/>
<input
type="text"
placeholder="Note (optional)"
value={example.note || ''}
onChange={(e) => updateExample(index, 'note', e.target.value)}
/>
<button type="button" onClick={() => removeExample(index)}>
Remove
</button>
@ -377,13 +392,19 @@ export default function AddVocabForm() {
<div className="form-group">
<label>See Also:</label>
{formData.see_also?.map((see_also, index) => (
{formData.seeAlso?.map((seeAlso, index) => (
<div key={index} className="see-also-group">
<input
type="text"
placeholder="Format: [text](link)"
value={see_also}
onChange={(e) => updateSeeAlso(index, e.target.value)}
placeholder="Reference (e.g., [text](#link))"
value={seeAlso.reference}
onChange={(e) => updateSeeAlso(index, 'reference', e.target.value)}
/>
<input
type="text"
placeholder="Note (optional)"
value={seeAlso.note || ''}
onChange={(e) => updateSeeAlso(index, 'note', e.target.value)}
/>
<button type="button" onClick={() => removeSeeAlso(index)}>
Remove
@ -433,14 +454,39 @@ export default function AddVocabForm() {
color: var(--sl-color-text);
}
.example-group {
display: grid;
grid-template-columns: 1fr 1fr auto;
.tag-checkboxes {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.example-group input {
.tag-checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: normal;
}
.tag-checkbox input[type="checkbox"] {
width: auto;
}
.tag-desc {
color: var(--sl-color-gray-3);
font-size: 0.9em;
}
.example-group,
.see-also-group {
display: grid;
grid-template-columns: 1fr 1fr 1fr auto;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.example-group input,
.see-also-group input {
width: auto;
}
@ -469,18 +515,6 @@ export default function AddVocabForm() {
background: var(--sl-color-red);
color: white;
}
.vocab-form .form-group {
display: flex;
flex-direction: row;
align-items: center;
gap: 1em;
}
.vocab-form .form-group label,
.vocab-form .form-group select {
margin: 0;
line-height: 1em;
}
`}</style>
</div>
);

View File

@ -1,79 +1,116 @@
import { useState, useEffect } from "react";
import type { VocabList, VocabWord } from "@/types/types";
import type { VocabWord, Tag } from "@/types/types";
import FlashCard from "./FlashCard";
export default function FlashCardActivity({ vocabList }: { vocabList: VocabList[] }) {
export default function FlashCardActivity() {
const [words, setWords] = useState<VocabWord[]>([]);
const [tags, setTags] = useState<Tag[]>([]);
const [currentWord, setCurrentWord] = useState<VocabWord | null>(null);
const [numFilteredWords, setNumFilteredWords] = useState(0);
const [selectedTagIds, setSelectedTagIds] = useState<number[]>([]);
const [selectedTypes, setSelectedTypes] = useState<string[]>(['noun', 'verb', 'adjective']);
const getSelectedFilters = () => {
const categoryCheckboxes = document.querySelectorAll<HTMLInputElement>(
'input[data-filter="category"]'
);
const typeCheckboxes = document.querySelectorAll<HTMLInputElement>(
'input[data-filter="type"]'
);
useEffect(() => {
// Fetch all words and tags
Promise.all([
fetch('/api/vocab.json').then(res => res.json()),
fetch('/api/tags.json').then(res => res.json()),
]).then(([wordsData, tagsData]) => {
setWords(wordsData);
setTags(tagsData);
// Default: select all tags
setSelectedTagIds(tagsData.map((t: Tag) => t.id));
});
}, []);
const selectedCategories = Array.from(categoryCheckboxes)
.filter(checkbox => checkbox.checked)
.map(checkbox => checkbox.value);
const selectedTypes = Array.from(typeCheckboxes)
.filter(checkbox => checkbox.checked)
.map(checkbox => checkbox.value);
return { selectedCategories, selectedTypes };
};
useEffect(() => {
// Update filtered words count and get a new random word when filters change
updateFilteredWordsAndRandomize();
}, [words, selectedTagIds, selectedTypes]);
const getFilteredWords = () => {
const { selectedCategories, selectedTypes } = getSelectedFilters();
const filteredWords = vocabList
.filter(list => selectedCategories.includes(list.slug))
.flatMap(list => list.words)
.filter(word => selectedTypes.includes(word.type));
return words.filter(word => {
// Filter by tags
const hasSelectedTag = word.tags?.some(tag => selectedTagIds.includes(tag.id));
if (!hasSelectedTag) return false;
if (filteredWords.length === 0) {
alert("No words match the selected filters.");
return;
// Filter by type
if (word.type && !selectedTypes.includes(word.type)) return false;
return true;
});
};
const updateFilteredWordsAndRandomize = () => {
const filteredWords = getFilteredWords();
setNumFilteredWords(filteredWords.length);
if (filteredWords.length > 0) {
const randomIndex = Math.floor(Math.random() * filteredWords.length);
setCurrentWord(filteredWords[randomIndex]);
} else {
setCurrentWord(null);
}
return filteredWords;
};
const getRandomWord = () => {
const filteredWords = getFilteredWords();
const randomIndex = Math.floor(Math.random() * filteredWords!.length);
setCurrentWord(filteredWords![randomIndex]);
setNumFilteredWords(filteredWords!.length);
};
const updateFilteredWordsCount = () => {
const filteredWords = getFilteredWords();
if (filteredWords) {
setNumFilteredWords(filteredWords.length);
if (filteredWords.length === 0) {
alert("No words match the selected filters.");
return;
}
const randomIndex = Math.floor(Math.random() * filteredWords.length);
setCurrentWord(filteredWords[randomIndex]);
};
useEffect(() => {
// Initialize with a random word
getRandomWord();
const toggleTag = (tagId: number) => {
setSelectedTagIds(prev =>
prev.includes(tagId)
? prev.filter(id => id !== tagId)
: [...prev, tagId]
);
};
// Listen for checkbox changes anywhere in the document
const handleCheckboxChange = (event: Event) => {
const target = event.target as HTMLElement;
if (target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'checkbox') {
updateFilteredWordsCount();
}
};
document.addEventListener('change', handleCheckboxChange);
return () => {
document.removeEventListener('change', handleCheckboxChange);
};
}, []);
const toggleType = (type: string) => {
setSelectedTypes(prev =>
prev.includes(type)
? prev.filter(t => t !== type)
: [...prev, type]
);
};
return (
<div className="flash-card-activity">
<div className="filters">
<div className="filter-group">
<h3>Tags</h3>
{tags.map(tag => (
<label key={tag.id} className="filter-checkbox">
<input
type="checkbox"
checked={selectedTagIds.includes(tag.id)}
onChange={() => toggleTag(tag.id)}
/>
{tag.name}
</label>
))}
</div>
<div className="filter-group">
<h3>Word Types</h3>
{['noun', 'verb', 'adjective', 'adverb', 'pronoun', 'conjunction', 'preposition', 'interjection'].map(type => (
<label key={type} className="filter-checkbox">
<input
type="checkbox"
checked={selectedTypes.includes(type)}
onChange={() => toggleType(type)}
/>
{type.charAt(0).toUpperCase() + type.slice(1)}
</label>
))}
</div>
</div>
{currentWord && (
<FlashCard word={currentWord} />
)}
@ -82,6 +119,43 @@ export default function FlashCardActivity({ vocabList }: { vocabList: VocabList[
<div>
{numFilteredWords} words match the selected filters.
</div>
<style>{`
.flash-card-activity {
margin: 2rem 0;
}
.filters {
display: flex;
gap: 2rem;
margin-bottom: 2rem;
padding: 1rem;
background: var(--sl-color-gray-6);
border-radius: 8px;
}
.filter-group {
flex: 1;
}
.filter-group h3 {
margin-top: 0;
margin-bottom: 0.5rem;
font-size: 1rem;
}
.filter-checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
font-weight: normal;
}
.filter-checkbox input[type="checkbox"] {
margin: 0;
}
`}</style>
</div>
);
}

View File

@ -2,24 +2,10 @@
import { Aside, Badge, Icon } from "@astrojs/starlight/components";
import AnchorHeading from "@astrojs/starlight/components/AnchorHeading.astro";
import { render, renderInline, highlight } from "@/lib/markdown";
import type { VocabWord } from "@/types/types";
interface Props {
word: {
english: string;
hindi: string;
gender?: "m" | "f";
note?: string;
examples?: Array<{
hindi: string;
english: string;
note?: string;
}>;
see_also?: string[];
tags?: string[];
};
}
const { word }: Props = Astro.props;
const { word } = Astro.props as { word: VocabWord };
const activeTag = Astro.params.tag;
const gender_lookup: Record<"m" | "f", ["note" | "tip", string]> = {
m: ["note", "masculine"],
@ -82,13 +68,12 @@ const gender_lookup: Record<"m" | "f", ["note" | "tip", string]> = {
)
}
{
word.see_also && (
word.seeAlso && (
<p>
<b>See also:</b>
{word.see_also.map((ref, i) => (
{word.seeAlso.map((ref, i) => (
<>
<span set:html={renderInline(ref)} />
{i < word.see_also!.length - 1 ? "; " : ""}
<span set:html={renderInline(ref.reference ?? "")} />{i < word.seeAlso!.length - 1 ? "; " : ""}
</>
))}
</p>
@ -97,9 +82,11 @@ const gender_lookup: Record<"m" | "f", ["note" | "tip", string]> = {
{
word.tags && (
<p>
{word.tags.map((tag, i) => (
{word.tags.filter((tag) => (
!activeTag || tag.name !== activeTag
)).map((tag, i) => (
<>
<Badge text={tag} class="tag-badge" />
<Badge text={tag.name} class="tag-badge" />
{i < word.tags!.length - 1 ? " " : ""}
</>
))}

View File

@ -1,15 +1,7 @@
import { defineCollection } from "astro:content";
import { docsLoader } from "@astrojs/starlight/loaders";
import { docsSchema } from "@astrojs/starlight/schema";
import { file } from "astro/loaders";
import { z } from "zod";
import type { VocabWord, VocabList } from "@/types/types";
import { vocabWordSchema, vocabListSchema } from "@/types/types";
export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
vocabList: defineCollection({
loader: file("src/vocab_list.yaml"),
schema: vocabListSchema,
}),
};

View File

@ -0,0 +1,8 @@
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import * as schema from './schema';
import path from 'path';
const dbPath = path.join(process.cwd(), 'data.db');
const sqlite = new Database(dbPath);
export const db = drizzle(sqlite, { schema });

View File

@ -0,0 +1,82 @@
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { relations } from 'drizzle-orm';
export const words = sqliteTable('words', {
id: integer('id').primaryKey({ autoIncrement: true }),
hindi: text('hindi').notNull(),
english: text('english').notNull(),
type: text('type'), // noun, verb, adjective, etc.
gender: text('gender'), // m, f, or null
note: text('note'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
});
export const examples = sqliteTable('examples', {
id: integer('id').primaryKey({ autoIncrement: true }),
wordId: integer('word_id').notNull().references(() => words.id, { onDelete: 'cascade' }),
hindi: text('hindi').notNull(),
english: text('english').notNull(),
note: text('note'),
});
export const tags = sqliteTable('tags', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull().unique(),
description: text('description'),
});
export const wordTags = sqliteTable('word_tags', {
id: integer('id').primaryKey({ autoIncrement: true }),
wordId: integer('word_id').notNull().references(() => words.id, { onDelete: 'cascade' }),
tagId: integer('tag_id').notNull().references(() => tags.id),
});
export const seeAlso = sqliteTable('see_also', {
id: integer('id').primaryKey({ autoIncrement: true }),
wordId: integer('word_id').notNull().references(() => words.id, { onDelete: 'cascade' }),
reference: text('reference').notNull(), // Link text like "[चीज़](#चीज़)" or "[Reflexive verbs](/grammar/reflexive-verbs)"
note: text('note'), // Optional additional context
});
// Relations
export const wordsRelations = relations(words, ({ many }) => ({
examples: many(examples),
tags: many(wordTags),
seeAlso: many(seeAlso),
}));
export const examplesRelations = relations(examples, ({ one }) => ({
word: one(words, {
fields: [examples.wordId],
references: [words.id],
}),
}));
export const tagsRelations = relations(tags, ({ many }) => ({
words: many(wordTags),
}));
export const wordTagsRelations = relations(wordTags, ({ one }) => ({
word: one(words, {
fields: [wordTags.wordId],
references: [words.id],
}),
tag: one(tags, {
fields: [wordTags.tagId],
references: [tags.id],
}),
}));
export const seeAlsoRelations = relations(seeAlso, ({ one }) => ({
word: one(words, {
fields: [seeAlso.wordId],
references: [words.id],
}),
}));
// Inferred types from schema
export type DBWord = typeof words.$inferSelect;
export type DBExample = typeof examples.$inferSelect;
export type DBTag = typeof tags.$inferSelect;
export type DBSeeAlso = typeof seeAlso.$inferSelect;

View File

@ -1,156 +1,173 @@
import fs from 'fs/promises';
import path from 'path';
import YAML from 'yaml';
import { db } from "./db";
import { words, examples, tags, wordTags, seeAlso } from "./db/schema";
import { eq } from "drizzle-orm";
import type { Word, Tag, NewWord } from "@/types/types";
interface StorageAdapter {
read(): Promise<any[]>;
write(data: any[]): Promise<void>;
}
class FileSystemAdapter implements StorageAdapter {
private filePath: string;
constructor(filePath: string) {
this.filePath = filePath;
}
async read(): Promise<any[]> {
const content = await fs.readFile(this.filePath, 'utf-8');
return YAML.parse(content);
}
async write(data: any[]): Promise<void> {
const yaml = YAML.stringify(data);
await fs.writeFile(this.filePath, yaml, 'utf-8');
}
}
class GitAdapter implements StorageAdapter {
private baseUrl: string;
private owner: string;
private repo: string;
private path: string;
private branch: string;
private token: string;
constructor(config: {
baseUrl?: string; // For Gitea, e.g., 'https://gitea.example.com'
owner: string;
repo: string;
path: string;
branch?: string;
token: string;
}) {
this.baseUrl = config.baseUrl || 'https://api.github.com';
this.owner = config.owner;
this.repo = config.repo;
this.path = config.path;
this.branch = config.branch || 'main';
this.token = config.token;
}
private get apiBase(): string {
// Remove trailing slash if present
const base = this.baseUrl.replace(/\/$/, '');
// Add /api/v1 for Gitea if not GitHub
if (!base.includes('api.github.com')) {
return base.includes('/api/v1') ? base : `${base}/api/v1`;
}
return base;
}
async read(): Promise<any[]> {
const response = await fetch(
`${this.apiBase}/repos/${this.owner}/${this.repo}/contents/${this.path}?ref=${this.branch}`,
{
headers: {
'Authorization': `Bearer ${this.token}`,
'Accept': 'application/json',
class SQLiteStorage {
async getAllWords(): Promise<Word[]> {
const allWords = await db.query.words.findMany({
with: {
examples: true,
tags: {
with: {
tag: true,
},
},
}
);
seeAlso: true,
},
});
if (!response.ok) {
throw new Error(`Failed to read from Git: ${response.statusText}`);
}
const data = await response.json();
const content = Buffer.from(data.content, 'base64').toString('utf-8');
return YAML.parse(content);
return allWords.map((word) => ({
id: word.id,
hindi: word.hindi,
english: word.english,
type: word.type,
gender: word.gender as "m" | "f" | null,
note: word.note,
examples: word.examples.length > 0 ? word.examples : undefined,
tags: word.tags.map((wt) => wt.tag),
seeAlso: word.seeAlso.length > 0 ? word.seeAlso : undefined,
}));
}
async write(data: any[]): Promise<void> {
// First, get the current file to get its SHA
const currentResponse = await fetch(
`${this.apiBase}/repos/${this.owner}/${this.repo}/contents/${this.path}?ref=${this.branch}`,
{
headers: {
'Authorization': `Bearer ${this.token}`,
'Accept': 'application/json',
async getWordById(id: number): Promise<Word | null> {
const word = await db.query.words.findFirst({
where: eq(words.id, id),
with: {
examples: true,
tags: {
with: {
tag: true,
},
},
}
);
seeAlso: true,
},
});
if (!currentResponse.ok) {
throw new Error(`Failed to get current file from Git: ${currentResponse.statusText}`);
if (!word) return null;
return {
id: word.id,
hindi: word.hindi,
english: word.english,
type: word.type,
gender: word.gender as "m" | "f" | null,
note: word.note,
examples: word.examples.length > 0 ? word.examples : undefined,
tags: word.tags.map((wt) => wt.tag),
seeAlso: word.seeAlso.length > 0 ? word.seeAlso : undefined,
};
}
async getWordsByTag(tagName: string): Promise<Word[]> {
const tag = await db.query.tags.findFirst({
where: eq(tags.name, tagName),
with: {
words: {
with: {
word: {
with: {
examples: true,
tags: {
with: {
tag: true,
},
},
seeAlso: true,
},
},
},
},
},
});
if (!tag) return [];
return tag.words.map((wt) => ({
id: wt.word.id,
hindi: wt.word.hindi,
english: wt.word.english,
type: wt.word.type,
gender: wt.word.gender as "m" | "f" | null,
note: wt.word.note,
examples: wt.word.examples.length > 0 ? wt.word.examples : undefined,
tags: wt.word.tags.map((t) => t.tag),
seeAlso: wt.word.seeAlso.length > 0 ? wt.word.seeAlso.map(sa => ({
...sa,
reference: sa.reference || '',
})) : undefined,
}));
}
async getAllTags(): Promise<Tag[]> {
return await db.query.tags.findMany();
}
async createWord(newWord: NewWord): Promise<Word> {
const [word] = await db
.insert(words)
.values({
hindi: newWord.hindi,
english: newWord.english,
type: newWord.type || null,
gender: newWord.gender || null,
note: newWord.note || null,
})
.returning();
// Add examples
if (newWord.examples) {
for (const example of newWord.examples) {
await db.insert(examples).values({
wordId: word.id,
hindi: example.hindi,
english: example.english,
note: example.note || null,
});
}
}
const currentData = await currentResponse.json();
const sha = currentData.sha;
// Now update the file
const yaml = YAML.stringify(data);
const content = Buffer.from(yaml).toString('base64');
const updateResponse = await fetch(
`${this.apiBase}/repos/${this.owner}/${this.repo}/contents/${this.path}`,
{
method: 'PUT',
headers: {
'Authorization': `Bearer ${this.token}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: 'Update vocab list via web form',
content: content,
sha: sha,
branch: this.branch,
}),
// Add tags
if (newWord.tagIds) {
for (const tagId of newWord.tagIds) {
await db.insert(wordTags).values({
wordId: word.id,
tagId: tagId,
});
}
);
if (!updateResponse.ok) {
const errorText = await updateResponse.text();
throw new Error(`Failed to update Git file: ${updateResponse.statusText} - ${errorText}`);
}
// Add see also references
if (newWord.seeAlso) {
for (const ref of newWord.seeAlso) {
await db.insert(seeAlso).values({
wordId: word.id,
reference: ref.reference,
note: ref.note || null,
});
}
}
// Fetch the complete word with all relations
const createdWord = await this.getWordById(word.id);
return createdWord!;
}
async createTag(name: string, description?: string): Promise<Tag> {
const [tag] = await db
.insert(tags)
.values({
name,
description: description || null,
})
.returning();
return tag;
}
async deleteWord(id: number): Promise<void> {
await db.delete(words).where(eq(words.id, id));
}
}
export function getStorageAdapter(): StorageAdapter {
const storageType = import.meta.env.VOCAB_STORAGE_TYPE || 'filesystem';
switch (storageType) {
case 'git':
case 'github':
case 'gitea':
if (!import.meta.env.GIT_TOKEN) {
throw new Error('GIT_TOKEN environment variable is required for Git storage');
}
return new GitAdapter({
baseUrl: import.meta.env.GIT_API_URL, // e.g., 'https://gitea.example.com' for Gitea
owner: import.meta.env.GIT_OWNER || 'your-username',
repo: import.meta.env.GIT_REPO || 'your-repo',
path: import.meta.env.GIT_PATH || 'src/vocab_list.yaml',
branch: import.meta.env.GIT_BRANCH || 'main',
token: import.meta.env.GIT_TOKEN,
});
case 'filesystem':
default:
const filePath = path.join(process.cwd(), 'src', 'vocab_list.yaml');
return new FileSystemAdapter(filePath);
}
}
export { StorageAdapter, FileSystemAdapter, GitAdapter };
export const storage = new SQLiteStorage();

View File

@ -0,0 +1,25 @@
import type { APIRoute } from 'astro';
import { storage } from '../../lib/storage';
export const prerender = false;
export const GET: APIRoute = async () => {
try {
const tags = await storage.getAllTags();
return new Response(JSON.stringify(tags), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
} catch (error) {
console.error('Error in GET /api/tags:', error);
return new Response(JSON.stringify({ error: 'Failed to read tags' }), {
status: 500,
headers: {
'Content-Type': 'application/json',
},
});
}
};

View File

@ -1,10 +1,18 @@
import type { APIRoute } from 'astro';
import { getStorageAdapter } from '../../lib/storage';
import { storage } from '../../lib/storage';
export const prerender = false;
export const GET: APIRoute = async () => {
export const GET: APIRoute = async ({ url }) => {
try {
const storage = getStorageAdapter();
const data = await storage.read();
const tag = url.searchParams.get('tag');
let data;
if (tag) {
data = await storage.getWordsByTag(tag);
} else {
data = await storage.getAllWords();
}
return new Response(JSON.stringify(data), {
status: 200,
@ -26,14 +34,10 @@ export const GET: APIRoute = async () => {
export const POST: APIRoute = async ({ request }) => {
try {
const data = await request.json();
const storage = getStorageAdapter();
// Read existing data
const vocabList = await storage.read();
// Validate the new entry
if (!data.category || !data.word) {
return new Response(JSON.stringify({ error: 'Missing required fields' }), {
// Validate the new word
if (!data.hindi || !data.english) {
return new Response(JSON.stringify({ error: 'Missing required fields: hindi and english' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
@ -41,11 +45,11 @@ export const POST: APIRoute = async ({ request }) => {
});
}
// Check if we're creating a new category
if (data.newCategory) {
// Validate new category data
if (!data.newCategory.slug || !data.newCategory.about) {
return new Response(JSON.stringify({ error: 'New category requires slug and description' }), {
// Check if creating a new tag
let tagIds = data.tagIds || [];
if (data.newTag) {
if (!data.newTag.name) {
return new Response(JSON.stringify({ error: 'New tag requires a name' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
@ -53,44 +57,22 @@ export const POST: APIRoute = async ({ request }) => {
});
}
// Check if category already exists
const existingCategory = vocabList.find((cat: any) => cat.slug === data.newCategory.slug);
if (existingCategory) {
return new Response(JSON.stringify({ error: 'Category already exists' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
},
});
}
// Create the new category with the first word
vocabList.push({
slug: data.newCategory.slug,
about: data.newCategory.about,
words: [data.word],
});
} else {
// Find the existing category to add to
const categoryIndex = vocabList.findIndex((cat: any) => cat.slug === data.category);
if (categoryIndex === -1) {
return new Response(JSON.stringify({ error: 'Category not found' }), {
status: 404,
headers: {
'Content-Type': 'application/json',
},
});
}
// Add the new word to the existing category
vocabList[categoryIndex].words.push(data.word);
const newTag = await storage.createTag(data.newTag.name, data.newTag.description);
tagIds.push(newTag.id);
}
// Write back using the storage adapter
await storage.write(vocabList);
const newWord = await storage.createWord({
hindi: data.hindi,
english: data.english,
type: data.type,
gender: data.gender,
note: data.note,
examples: data.examples,
tagIds,
seeAlso: data.seeAlso,
});
return new Response(JSON.stringify({ success: true, message: 'Word added successfully' }), {
return new Response(JSON.stringify({ success: true, word: newWord }), {
status: 200,
headers: {
'Content-Type': 'application/json',
@ -98,7 +80,7 @@ export const POST: APIRoute = async ({ request }) => {
});
} catch (error) {
console.error('Error in POST /api/vocab:', error);
return new Response(JSON.stringify({ error: 'Failed to update vocab list' }), {
return new Response(JSON.stringify({ error: 'Failed to create word' }), {
status: 500,
headers: {
'Content-Type': 'application/json',

View File

@ -1,12 +1,8 @@
---
import FlashCardActivity from "@/components/FlashCardActivity";
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
import { getCollection } from "astro:content";
const vocabListCollection = await getCollection("vocabList");
const vocabList = vocabListCollection.map((category) => ({
...category.data,
slug: category.id,
}));
export const prerender = false;
---
<StarlightPage
@ -16,5 +12,5 @@ const vocabList = vocabListCollection.map((category) => ({
prev: false,
}}
>
<FlashCardActivity vocabList={vocabList} client:load />
<FlashCardActivity client:load />
</StarlightPage>

View File

@ -0,0 +1,71 @@
---
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
import AnchorHeading from '@astrojs/starlight/components/AnchorHeading.astro';
import VocabWord from '@/components/VocabWord.astro';
import markdownit from 'markdown-it'
import markdownItMark from 'markdown-it-mark'
import { storage } from '@/lib/storage';
const md = markdownit().use(markdownItMark);
export const prerender = false;
function titlecase(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
const { tag } = Astro.params;
if (!tag) {
return Astro.redirect('/vocabulary');
}
const allTags = await storage.getAllTags();
const currentTag = allTags.find(t => t.name === tag);
if (!currentTag) {
return Astro.redirect('/vocabulary');
}
const words = await storage.getWordsByTag(tag);
const wordtypes = [...new Set(words.map((word) => word.type).filter(Boolean))];
const wordsByType = wordtypes.map((type) => ({
type: titlecase(type!) + "s",
words: words.filter((word) => word.type === type),
}));
const headings = wordsByType.flatMap(({type, words}) => {
return [{
text: type,
depth: 2,
slug: type.toLowerCase().replace(/\s+/g, '-'),
}].concat(words.map((word) => ({
text: word.hindi,
depth: 3,
slug: word.hindi.toLowerCase().replace(/\s+/g, '-'),
})));
});
---
<StarlightPage
frontmatter={{ title: "Vocabulary: " + titlecase(tag) }}
headings={headings}
>
{currentTag.description && <div set:html={md.render(currentTag.description)}/>}
{
wordsByType.map(({type, words}) => (
<div class="word-type-section">
<AnchorHeading level="3" id={type}>{type}</AnchorHeading>
<ul class="part-of-speech-list">
{
words.map((word) => (
<VocabWord {word} />
))
}
</ul>
</div>
))
}
</StarlightPage>

View File

@ -75,8 +75,7 @@
font-weight: 300;
font-style: normal;
font-display: swap;
unicode-range:
U+0900-097F, U+200C-200D;
unicode-range: U+0900-097F, U+200C-200D;
/* Devanagari block + zero-width joiners */
}
@ -234,6 +233,7 @@ h6,
.tag-badge {
font-family: var(--font-sans);
background: transparent;
color: var(--sl-color-text);
}
.word-entry {
@ -267,20 +267,27 @@ mark {
}
/* Override the exact Starlight rule that's causing issues */
.sl-markdown-content [yaml-editor] :not(a, strong, em, del, span, input, code, br)+ :not(a, strong, em, del, span, input, code, br, :where(.not-content *)) {
.sl-markdown-content
[yaml-editor]
:not(a, strong, em, del, span, input, code, br)
+ :not(a, strong, em, del, span, input, code, br, :where(.not-content *)) {
margin-top: 0 !important;
}
/* Also target if the YAMLEditor itself is within sl-markdown-content */
.sl-markdown-content :not(a, strong, em, del, span, input, code, br)+[yaml-editor]:not(a,
strong,
em,
del,
span,
input,
code,
br,
:where(.not-content *)) {
.sl-markdown-content
:not(a, strong, em, del, span, input, code, br)
+ [yaml-editor]:not(
a,
strong,
em,
del,
span,
input,
code,
br,
:where(.not-content *)
) {
margin-top: 0 !important;
}

View File

@ -1,48 +1,113 @@
// Export interfaces and types
import { z } from "zod";
export interface VocabWord {
// Core domain types matching database schema
export interface Word {
id: number;
english: string;
hindi: string;
type: string;
gender?: "m" | "f" | undefined;
note?: string;
examples?: Array<{
english: string;
hindi: string;
note?: string;
}>;
see_also?: string[];
type?: string | null;
gender?: "m" | "f" | null;
note?: string | null;
examples?: Example[];
tags?: Tag[];
seeAlso?: SeeAlso[];
}
export interface Example {
id: number;
english: string;
hindi: string;
note?: string | null;
}
export interface Tag {
id: number;
name: string;
description?: string | null;
}
export interface SeeAlso {
id: number;
reference: string;
note?: string | null;
}
// Input types for creating new records
export interface NewWord {
hindi: string;
english: string;
type?: string;
gender?: "m" | "f";
note?: string;
examples?: NewExample[];
tagIds?: number[];
seeAlso?: NewSeeAlso[];
}
export interface NewExample {
hindi: string;
english: string;
note?: string;
}
export interface NewTag {
name: string;
description?: string;
}
export interface NewSeeAlso {
reference: string;
note?: string;
}
// Zod schemas for validation
export const exampleSchema = z.object({
id: z.number(),
english: z.string(),
hindi: z.string(),
note: z.string().optional().nullable(),
});
export const tagSchema = z.object({
id: z.number(),
name: z.string(),
description: z.string().optional().nullable(),
});
export const seeAlsoSchema = z.object({
id: z.number(),
reference: z.string(),
note: z.string().optional().nullable(),
});
export const wordSchema = z.object({
id: z.number(),
english: z.string(),
hindi: z.string(),
type: z.string().optional().nullable(),
gender: z.enum(["m", "f"]).optional().nullable(),
note: z.string().optional().nullable(),
examples: z.array(exampleSchema).optional(),
tags: z.array(tagSchema).optional(),
seeAlso: z.array(seeAlsoSchema).optional(),
});
export type WordSchema = z.infer<typeof wordSchema>;
export type ExampleSchema = z.infer<typeof exampleSchema>;
export type TagSchema = z.infer<typeof tagSchema>;
export type SeeAlsoSchema = z.infer<typeof seeAlsoSchema>;
// Legacy aliases for backward compatibility
export type VocabWord = Word;
export const vocabWordSchema = wordSchema;
export type VocabWordSchema = WordSchema;
// Legacy type for backward compatibility during migration
export interface VocabList {
slug: string;
about: string;
words: VocabWord[];
words: Word[];
}
export const vocabWordSchema = z.object({
english: z.string(),
hindi: z.string(),
type: z.string(),
gender: z.enum(["m", "f"]).optional(),
note: z.string().optional(),
examples: z
.array(
z.object({
english: z.string(),
hindi: z.string(),
note: z.string().optional(),
})
)
.optional(),
see_also: z.array(z.string()).optional(),
});
export type VocabWordSchema = z.infer<typeof vocabWordSchema>;
export const vocabListSchema = z.object({
slug: z.string(),
about: z.string(),
words: z.array(vocabWordSchema),
});
export type VocabListSchema = z.infer<typeof vocabListSchema>;