Migrate to sqlite
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
This commit is contained in:
parent
de63f0ce65
commit
3b3b1196b6
@ -1 +1 @@
|
|||||||
/nix/store/2l63479bllklr4bzi4vscn7ndj6lg614-nix-shell-env
|
/nix/store/gsxc3lz58zr7pdvvlihz39pqhidnh7aq-nix-shell-env
|
||||||
@ -15,7 +15,7 @@ export CONFIG_SHELL
|
|||||||
CXX='g++'
|
CXX='g++'
|
||||||
export CXX
|
export CXX
|
||||||
HOSTTYPE='x86_64'
|
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
|
export HOST_PATH
|
||||||
IFS='
|
IFS='
|
||||||
'
|
'
|
||||||
@ -37,13 +37,13 @@ NIX_CC='/nix/store/95k9rsn1zsw1yvir8mj824ldhf90i4qw-gcc-wrapper-14.3.0'
|
|||||||
export NIX_CC
|
export NIX_CC
|
||||||
NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu='1'
|
NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu='1'
|
||||||
export NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu
|
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
|
export NIX_CFLAGS_COMPILE
|
||||||
NIX_ENFORCE_NO_NATIVE='1'
|
NIX_ENFORCE_NO_NATIVE='1'
|
||||||
export NIX_ENFORCE_NO_NATIVE
|
export NIX_ENFORCE_NO_NATIVE
|
||||||
NIX_HARDENING_ENABLE='bindnow format fortify fortify3 pic relro stackclashprotection stackprotector strictoverflow zerocallusedregs'
|
NIX_HARDENING_ENABLE='bindnow format fortify fortify3 pic relro stackclashprotection stackprotector strictoverflow zerocallusedregs'
|
||||||
export NIX_HARDENING_ENABLE
|
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
|
export NIX_LDFLAGS
|
||||||
NIX_NO_SELF_RPATH='1'
|
NIX_NO_SELF_RPATH='1'
|
||||||
NIX_STORE='/nix/store'
|
NIX_STORE='/nix/store'
|
||||||
@ -60,7 +60,7 @@ OLDPWD=''
|
|||||||
export OLDPWD
|
export OLDPWD
|
||||||
OPTERR='1'
|
OPTERR='1'
|
||||||
OSTYPE='linux-gnu'
|
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
|
export PATH
|
||||||
PS4='+ '
|
PS4='+ '
|
||||||
RANLIB='ranlib'
|
RANLIB='ranlib'
|
||||||
@ -82,7 +82,7 @@ export XDG_DATA_DIRS
|
|||||||
__structuredAttrs=''
|
__structuredAttrs=''
|
||||||
export __structuredAttrs
|
export __structuredAttrs
|
||||||
_substituteStream_has_warned_replace_deprecation='false'
|
_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
|
export buildInputs
|
||||||
buildPhase='{ echo "------------------------------------------------------------";
|
buildPhase='{ echo "------------------------------------------------------------";
|
||||||
echo " WARNING: the existence of this path is not guaranteed.";
|
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 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 pkgsBuildTarget=()
|
||||||
declare -a pkgsHostHost=()
|
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 pkgsTargetTarget=()
|
||||||
declare -a postFixupHooks=('noBrokenSymlinksInAllOutputs' '_makeSymlinksRelativeInAllOutputs' '_multioutPropagateDev' )
|
declare -a postFixupHooks=('noBrokenSymlinksInAllOutputs' '_makeSymlinksRelativeInAllOutputs' '_multioutPropagateDev' )
|
||||||
declare -a postUnpackHooks=('_updateSourceDateEpochFromSourceRoot' )
|
declare -a postUnpackHooks=('_updateSourceDateEpochFromSourceRoot' )
|
||||||
|
|||||||
@ -9,7 +9,6 @@
|
|||||||
flake-utils.lib.eachDefaultSystem (
|
flake-utils.lib.eachDefaultSystem (
|
||||||
system:
|
system:
|
||||||
let
|
let
|
||||||
inherit (pkgs.lib) optional optionals;
|
|
||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
inherit system;
|
inherit system;
|
||||||
config.allowUnfree = true;
|
config.allowUnfree = true;
|
||||||
@ -28,6 +27,7 @@
|
|||||||
postgresql
|
postgresql
|
||||||
inotify-tools
|
inotify-tools
|
||||||
claude-code
|
claude-code
|
||||||
|
sqlite
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
25
hindki/.gitignore
vendored
Normal file
25
hindki/.gitignore
vendored
Normal 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/
|
||||||
58
hindki/TYPE_CONSOLIDATION.md
Normal file
58
hindki/TYPE_CONSOLIDATION.md
Normal 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`.
|
||||||
@ -1,17 +1,16 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
import { defineConfig } from "astro/config";
|
import { defineConfig } from "astro/config";
|
||||||
import starlight from "@astrojs/starlight";
|
import starlight from "@astrojs/starlight";
|
||||||
import YAML from "yaml";
|
|
||||||
import fs from "fs";
|
|
||||||
import react from "@astrojs/react";
|
import react from "@astrojs/react";
|
||||||
|
|
||||||
import node from "@astrojs/node";
|
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");
|
// Load tags from database for sidebar
|
||||||
const vocabListJson = YAML.parse(vocabListFile);
|
const sqlite = new Database('./data.db');
|
||||||
const categories = vocabListJson.map(
|
const db = drizzle(sqlite);
|
||||||
(/** @type {{ slug: string }} */ category) => category.slug,
|
const tagsList = db.select().from(tags).all();
|
||||||
);
|
|
||||||
|
|
||||||
const vocabList = [
|
const vocabList = [
|
||||||
{
|
{
|
||||||
@ -19,9 +18,9 @@ const vocabList = [
|
|||||||
link: `/vocabulary`,
|
link: `/vocabulary`,
|
||||||
},
|
},
|
||||||
].concat(
|
].concat(
|
||||||
categories.map((/** @type {string} */ category) => ({
|
tagsList.map((tag) => ({
|
||||||
label: category.charAt(0).toUpperCase() + category.slice(1),
|
label: tag.name.charAt(0).toUpperCase() + tag.name.slice(1),
|
||||||
link: `/vocabulary/${category}`,
|
link: `/vocabulary/${tag.name}`,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
10
hindki/drizzle.config.ts
Normal file
10
hindki/drizzle.config.ts
Normal 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',
|
||||||
|
},
|
||||||
|
});
|
||||||
42
hindki/drizzle/0000_charming_screwball.sql
Normal file
42
hindki/drizzle/0000_charming_screwball.sql
Normal 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
|
||||||
|
);
|
||||||
13
hindki/drizzle/0001_shallow_sasquatch.sql
Normal file
13
hindki/drizzle/0001_shallow_sasquatch.sql
Normal 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;
|
||||||
292
hindki/drizzle/meta/0000_snapshot.json
Normal file
292
hindki/drizzle/meta/0000_snapshot.json
Normal 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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
292
hindki/drizzle/meta/0001_snapshot.json
Normal file
292
hindki/drizzle/meta/0001_snapshot.json
Normal 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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
hindki/drizzle/meta/_journal.json
Normal file
20
hindki/drizzle/meta/_journal.json
Normal 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
1095
hindki/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,8 @@
|
|||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro"
|
"astro": "astro",
|
||||||
|
"migrate": "tsx scripts/migrate-yaml-to-db.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/node": "^9.4.4",
|
"@astrojs/node": "^9.4.4",
|
||||||
@ -16,6 +17,8 @@
|
|||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"astro": "^5.6.1",
|
"astro": "^5.6.1",
|
||||||
"astro-code-editor": "^0.1.1",
|
"astro-code-editor": "^0.1.1",
|
||||||
|
"better-sqlite3": "^12.4.1",
|
||||||
|
"drizzle-orm": "^0.44.6",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"markdown-it-mark": "^4.0.0",
|
"markdown-it-mark": "^4.0.0",
|
||||||
"monaco-yaml": "^5.4.0",
|
"monaco-yaml": "^5.4.0",
|
||||||
@ -23,7 +26,10 @@
|
|||||||
"yaml": "^2.8.1"
|
"yaml": "^2.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
"drizzle-kit": "^0.31.5",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-astro": "^0.14.1"
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
|
"tsx": "^4.20.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
111
hindki/scripts/migrate-yaml-to-db.ts
Normal file
111
hindki/scripts/migrate-yaml-to-db.ts
Normal 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();
|
||||||
@ -1,50 +1,60 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import type { VocabList, VocabWord } from '@/types/types';
|
import type { Tag } from '@/types/types';
|
||||||
import { z } from 'zod';
|
|
||||||
|
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() {
|
export default function AddVocabForm() {
|
||||||
const [categories, setCategories] = useState<VocabList[]>([]);
|
const [tags, setTags] = useState<Tag[]>([]);
|
||||||
const [selectedCategory, setSelectedCategory] = useState('');
|
const [selectedTagIds, setSelectedTagIds] = useState<number[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [categoriesLoading, setCategoriesLoading] = useState(true);
|
const [tagsLoading, setTagsLoading] = useState(true);
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const [showNewCategory, setShowNewCategory] = useState(false);
|
const [showNewTag, setShowNewTag] = useState(false);
|
||||||
const [newCategorySlug, setNewCategorySlug] = useState('');
|
const [newTagName, setNewTagName] = useState('');
|
||||||
const [newCategoryAbout, setNewCategoryAbout] = useState('');
|
const [newTagDescription, setNewTagDescription] = useState('');
|
||||||
const [formData, setFormData] = useState<VocabWord>({
|
const [formData, setFormData] = useState<WordFormData>({
|
||||||
english: '',
|
english: '',
|
||||||
hindi: '',
|
hindi: '',
|
||||||
type: 'noun',
|
type: 'noun',
|
||||||
gender: undefined,
|
gender: undefined,
|
||||||
note: undefined,
|
note: undefined,
|
||||||
examples: undefined,
|
examples: undefined,
|
||||||
see_also: undefined,
|
seeAlso: undefined,
|
||||||
});
|
});
|
||||||
const englishInputRef = React.useRef<HTMLInputElement>(null);
|
const englishInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Get URL parameters
|
// Get URL parameters
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const catFromUrl = urlParams.get('cat');
|
const tagFromUrl = urlParams.get('tag');
|
||||||
const typeFromUrl = urlParams.get('type');
|
const typeFromUrl = urlParams.get('type');
|
||||||
|
|
||||||
setCategoriesLoading(true);
|
setTagsLoading(true);
|
||||||
fetch('/api/vocab.json')
|
fetch('/api/tags.json')
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`HTTP error! status: ${res.status}`);
|
throw new Error(`HTTP error! status: ${res.status}`);
|
||||||
}
|
}
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then((data: Tag[]) => {
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
setCategories(data);
|
setTags(data);
|
||||||
|
|
||||||
// Use category from URL if provided, otherwise first category
|
// Use tag from URL if provided
|
||||||
if (catFromUrl && data.some(cat => cat.slug === catFromUrl)) {
|
if (tagFromUrl) {
|
||||||
setSelectedCategory(catFromUrl);
|
const tag = data.find(t => t.name === tagFromUrl);
|
||||||
} else if (!selectedCategory && data.length > 0) {
|
if (tag) {
|
||||||
setSelectedCategory(data[0].slug);
|
setSelectedTagIds([tag.id]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use type from URL if provided
|
// Use type from URL if provided
|
||||||
@ -52,15 +62,15 @@ export default function AddVocabForm() {
|
|||||||
setFormData(prev => ({ ...prev, type: typeFromUrl }));
|
setFormData(prev => ({ ...prev, type: typeFromUrl }));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setMessage('No categories found');
|
setMessage('No tags found');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('Failed to load categories:', err);
|
console.error('Failed to load tags:', err);
|
||||||
setMessage(`Failed to load categories: ${err.message}`);
|
setMessage(`Failed to load tags: ${err.message}`);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setCategoriesLoading(false);
|
setTagsLoading(false);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -70,22 +80,18 @@ export default function AddVocabForm() {
|
|||||||
setMessage('');
|
setMessage('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build word object with only non-empty fields
|
// Build word object
|
||||||
const word: any = {
|
const word: any = {
|
||||||
english: formData.english,
|
english: formData.english,
|
||||||
hindi: formData.hindi,
|
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
|
// Add examples if present
|
||||||
if (formData.gender) {
|
|
||||||
word.gender = formData.gender;
|
|
||||||
}
|
|
||||||
if (formData.note && formData.note.trim()) {
|
|
||||||
word.note = formData.note;
|
|
||||||
}
|
|
||||||
if (formData.examples && formData.examples.length > 0) {
|
if (formData.examples && formData.examples.length > 0) {
|
||||||
// Filter out empty examples
|
|
||||||
const validExamples = formData.examples.filter(
|
const validExamples = formData.examples.filter(
|
||||||
ex => ex.english.trim() || ex.hindi.trim()
|
ex => ex.english.trim() || ex.hindi.trim()
|
||||||
);
|
);
|
||||||
@ -94,29 +100,23 @@ export default function AddVocabForm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formData.see_also && formData.see_also.length > 0) {
|
// Add see also if present
|
||||||
// Filter out empty see_also fields
|
if (formData.seeAlso && formData.seeAlso.length > 0) {
|
||||||
const validSeeAlsos = formData.see_also.filter(
|
const validSeeAlso = formData.seeAlso.filter(
|
||||||
ex => ex.trim()
|
sa => sa.reference.trim()
|
||||||
);
|
);
|
||||||
if (validSeeAlsos.length > 0) {
|
if (validSeeAlso.length > 0) {
|
||||||
word.see_also = validSeeAlsos;
|
word.seeAlso = validSeeAlso;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If creating a new category, use the new category details
|
const requestBody: any = word;
|
||||||
const categoryToUse = showNewCategory ? newCategorySlug : selectedCategory;
|
|
||||||
|
|
||||||
const requestBody: any = {
|
// Include new tag information if creating one
|
||||||
category: categoryToUse,
|
if (showNewTag) {
|
||||||
word: word,
|
requestBody.newTag = {
|
||||||
};
|
name: newTagName,
|
||||||
|
description: newTagDescription || null,
|
||||||
// Include new category information if creating one
|
|
||||||
if (showNewCategory) {
|
|
||||||
requestBody.newCategory = {
|
|
||||||
slug: newCategorySlug,
|
|
||||||
about: newCategoryAbout,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,23 +133,31 @@ export default function AddVocabForm() {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setMessage('Word added successfully!');
|
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 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
|
// Update URL with current tag and type to persist through reloads
|
||||||
const newUrl = `${window.location.pathname}?cat=${currentCategory}&type=${currentType}`;
|
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);
|
||||||
|
|
||||||
|
if (currentTagNames.length > 0) {
|
||||||
|
const newUrl = `${window.location.pathname}?tag=${currentTagNames[0]}&type=${currentType}`;
|
||||||
window.history.replaceState({}, '', newUrl);
|
window.history.replaceState({}, '', newUrl);
|
||||||
|
}
|
||||||
|
|
||||||
// Reset form but keep the type
|
// Reset form but keep the type and tags
|
||||||
setFormData({
|
setFormData({
|
||||||
english: '',
|
english: '',
|
||||||
hindi: '',
|
hindi: '',
|
||||||
gender: undefined,
|
gender: undefined,
|
||||||
type: currentType, // Keep the last used type
|
type: currentType,
|
||||||
note: '',
|
note: '',
|
||||||
examples: [],
|
examples: [],
|
||||||
see_also: [],
|
seeAlso: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Focus back on English input for quick bulk entry
|
// Focus back on English input for quick bulk entry
|
||||||
@ -157,25 +165,23 @@ export default function AddVocabForm() {
|
|||||||
englishInputRef.current?.focus();
|
englishInputRef.current?.focus();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
// If we created a new category, refresh the categories list
|
// If we created a new tag, refresh the tags list
|
||||||
if (showNewCategory) {
|
if (showNewTag) {
|
||||||
setShowNewCategory(false);
|
setShowNewTag(false);
|
||||||
setNewCategorySlug('');
|
setNewTagName('');
|
||||||
setNewCategoryAbout('');
|
setNewTagDescription('');
|
||||||
|
|
||||||
// Refresh categories and set the new category as selected
|
// Refresh tags and select the new tag
|
||||||
fetch('/api/vocab.json')
|
fetch('/api/tags.json')
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then((data: Tag[]) => {
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
setCategories(data);
|
setTags(data);
|
||||||
// Set the newly created category as selected
|
setSelectedTagIds(currentTagIds);
|
||||||
setSelectedCategory(currentCategory);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// For existing categories, just keep it selected
|
setSelectedTagIds(currentTagIds);
|
||||||
setSelectedCategory(currentCategory);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setMessage(`Error: ${result.error}`);
|
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 = () => {
|
const addExample = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...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 || [])];
|
const newExamples = [...(formData.examples || [])];
|
||||||
newExamples[index] = { ...newExamples[index], [field]: value };
|
newExamples[index] = { ...newExamples[index], [field]: value };
|
||||||
setFormData({ ...formData, examples: newExamples });
|
setFormData({ ...formData, examples: newExamples });
|
||||||
@ -206,74 +220,68 @@ export default function AddVocabForm() {
|
|||||||
setFormData({ ...formData, examples: newExamples });
|
setFormData({ ...formData, examples: newExamples });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Same thing for see_also
|
|
||||||
const addSeeAlso = () => {
|
const addSeeAlso = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
see_also: [...(formData.see_also || []), ''],
|
seeAlso: [...(formData.seeAlso || []), { reference: '', note: '' }],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSeeAlso = (index: number, value: string) => {
|
const updateSeeAlso = (index: number, field: 'reference' | 'note', value: string) => {
|
||||||
const newSeeAlso = [...(formData.see_also || [])];
|
const newSeeAlso = [...(formData.seeAlso || [])];
|
||||||
newSeeAlso[index] = value;
|
newSeeAlso[index] = { ...newSeeAlso[index], [field]: value };
|
||||||
setFormData({ ...formData, see_also: newSeeAlso });
|
setFormData({ ...formData, seeAlso: newSeeAlso });
|
||||||
}
|
};
|
||||||
|
|
||||||
const removeSeeAlso = (index: number) => {
|
const removeSeeAlso = (index: number) => {
|
||||||
const newSeeAlso = (formData.see_also || []).filter((_, i) => i !== index);
|
const newSeeAlso = (formData.seeAlso || []).filter((_, i) => i !== index);
|
||||||
setFormData({ ...formData, see_also: newSeeAlso });
|
setFormData({ ...formData, seeAlso: newSeeAlso });
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vocab-form">
|
<div className="vocab-form">
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="category">Category:</label>
|
<label>Tags:</label>
|
||||||
<select
|
<div className="tag-checkboxes">
|
||||||
id="category"
|
{tags.map((tag) => (
|
||||||
value={showNewCategory ? '__new__' : selectedCategory}
|
<label key={tag.id} className="tag-checkbox">
|
||||||
onChange={(e) => {
|
<input
|
||||||
if (e.target.value === '__new__') {
|
type="checkbox"
|
||||||
setShowNewCategory(true);
|
checked={selectedTagIds.includes(tag.id)}
|
||||||
} else {
|
onChange={() => toggleTag(tag.id)}
|
||||||
setShowNewCategory(false);
|
/>
|
||||||
setSelectedCategory(e.target.value);
|
{tag.name}
|
||||||
}
|
{tag.description && <span className="tag-desc"> - {tag.description}</span>}
|
||||||
}}
|
</label>
|
||||||
required={!showNewCategory}
|
|
||||||
>
|
|
||||||
{categories.map((cat) => (
|
|
||||||
<option key={cat.slug} value={cat.slug}>
|
|
||||||
{cat.slug.charAt(0).toUpperCase() + cat.slug.slice(1)} - {cat.about}
|
|
||||||
</option>
|
|
||||||
))}
|
))}
|
||||||
<option value="__new__">+ Create New Category</option>
|
</div>
|
||||||
</select>
|
<button type="button" onClick={() => setShowNewTag(!showNewTag)}>
|
||||||
|
{showNewTag ? 'Cancel' : '+ Create New Tag'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showNewCategory && (
|
{showNewTag && (
|
||||||
<>
|
<>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="newCategorySlug">Category ID (slug):</label>
|
<label htmlFor="newTagName">Tag Name:</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="newCategorySlug"
|
id="newTagName"
|
||||||
value={newCategorySlug}
|
value={newTagName}
|
||||||
onChange={(e) => setNewCategorySlug(e.target.value.toLowerCase().replace(/\s+/g, '-'))}
|
onChange={(e) => setNewTagName(e.target.value.toLowerCase().replace(/\s+/g, '-'))}
|
||||||
placeholder="e.g., food-and-drink"
|
placeholder="e.g., food-and-drink"
|
||||||
required={showNewCategory}
|
required={showNewTag}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="newCategoryAbout">Category Description:</label>
|
<label htmlFor="newTagDescription">Tag Description:</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="newCategoryAbout"
|
id="newTagDescription"
|
||||||
value={newCategoryAbout}
|
value={newTagDescription}
|
||||||
onChange={(e) => setNewCategoryAbout(e.target.value)}
|
onChange={(e) => setNewTagDescription(e.target.value)}
|
||||||
placeholder="e.g., Words related to food and beverages"
|
placeholder="e.g., Words related to food and beverages"
|
||||||
required={showNewCategory}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -327,10 +335,11 @@ export default function AddVocabForm() {
|
|||||||
id="gender"
|
id="gender"
|
||||||
value={formData.gender ?? ''}
|
value={formData.gender ?? ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
// Use genderSchema from VocabWord type
|
const value = e.target.value;
|
||||||
const genderSchema = z.enum(['m', 'f']).optional();
|
setFormData({
|
||||||
const parsed = genderSchema.safeParse(e.target.value === '' ? undefined : e.target.value);
|
...formData,
|
||||||
setFormData({ ...formData, gender: parsed.success ? parsed.data : undefined });
|
gender: value === '' ? undefined : (value as 'm' | 'f')
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">N/A</option>
|
<option value="">N/A</option>
|
||||||
@ -343,7 +352,7 @@ export default function AddVocabForm() {
|
|||||||
<label htmlFor="note">Note:</label>
|
<label htmlFor="note">Note:</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="note"
|
id="note"
|
||||||
value={formData.note}
|
value={formData.note || ''}
|
||||||
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
@ -365,6 +374,12 @@ export default function AddVocabForm() {
|
|||||||
value={example.hindi}
|
value={example.hindi}
|
||||||
onChange={(e) => updateExample(index, 'hindi', e.target.value)}
|
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)}>
|
<button type="button" onClick={() => removeExample(index)}>
|
||||||
Remove
|
Remove
|
||||||
</button>
|
</button>
|
||||||
@ -377,13 +392,19 @@ export default function AddVocabForm() {
|
|||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>See Also:</label>
|
<label>See Also:</label>
|
||||||
{formData.see_also?.map((see_also, index) => (
|
{formData.seeAlso?.map((seeAlso, index) => (
|
||||||
<div key={index} className="see-also-group">
|
<div key={index} className="see-also-group">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Format: [text](link)"
|
placeholder="Reference (e.g., [text](#link))"
|
||||||
value={see_also}
|
value={seeAlso.reference}
|
||||||
onChange={(e) => updateSeeAlso(index, e.target.value)}
|
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)}>
|
<button type="button" onClick={() => removeSeeAlso(index)}>
|
||||||
Remove
|
Remove
|
||||||
@ -433,14 +454,39 @@ export default function AddVocabForm() {
|
|||||||
color: var(--sl-color-text);
|
color: var(--sl-color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.example-group {
|
.tag-checkboxes {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr 1fr auto;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-bottom: 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;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -469,18 +515,6 @@ export default function AddVocabForm() {
|
|||||||
background: var(--sl-color-red);
|
background: var(--sl-color-red);
|
||||||
color: white;
|
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>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,79 +1,116 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import type { VocabList, VocabWord } from "@/types/types";
|
import type { VocabWord, Tag } from "@/types/types";
|
||||||
import FlashCard from "./FlashCard";
|
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 [currentWord, setCurrentWord] = useState<VocabWord | null>(null);
|
||||||
const [numFilteredWords, setNumFilteredWords] = useState(0);
|
const [numFilteredWords, setNumFilteredWords] = useState(0);
|
||||||
|
const [selectedTagIds, setSelectedTagIds] = useState<number[]>([]);
|
||||||
|
const [selectedTypes, setSelectedTypes] = useState<string[]>(['noun', 'verb', 'adjective']);
|
||||||
|
|
||||||
const getSelectedFilters = () => {
|
useEffect(() => {
|
||||||
const categoryCheckboxes = document.querySelectorAll<HTMLInputElement>(
|
// Fetch all words and tags
|
||||||
'input[data-filter="category"]'
|
Promise.all([
|
||||||
);
|
fetch('/api/vocab.json').then(res => res.json()),
|
||||||
const typeCheckboxes = document.querySelectorAll<HTMLInputElement>(
|
fetch('/api/tags.json').then(res => res.json()),
|
||||||
'input[data-filter="type"]'
|
]).then(([wordsData, tagsData]) => {
|
||||||
);
|
setWords(wordsData);
|
||||||
|
setTags(tagsData);
|
||||||
|
// Default: select all tags
|
||||||
|
setSelectedTagIds(tagsData.map((t: Tag) => t.id));
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const selectedCategories = Array.from(categoryCheckboxes)
|
useEffect(() => {
|
||||||
.filter(checkbox => checkbox.checked)
|
// Update filtered words count and get a new random word when filters change
|
||||||
.map(checkbox => checkbox.value);
|
updateFilteredWordsAndRandomize();
|
||||||
|
}, [words, selectedTagIds, selectedTypes]);
|
||||||
const selectedTypes = Array.from(typeCheckboxes)
|
|
||||||
.filter(checkbox => checkbox.checked)
|
|
||||||
.map(checkbox => checkbox.value);
|
|
||||||
|
|
||||||
return { selectedCategories, selectedTypes };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFilteredWords = () => {
|
const getFilteredWords = () => {
|
||||||
const { selectedCategories, selectedTypes } = getSelectedFilters();
|
return words.filter(word => {
|
||||||
const filteredWords = vocabList
|
// Filter by tags
|
||||||
.filter(list => selectedCategories.includes(list.slug))
|
const hasSelectedTag = word.tags?.some(tag => selectedTagIds.includes(tag.id));
|
||||||
.flatMap(list => list.words)
|
if (!hasSelectedTag) return false;
|
||||||
.filter(word => selectedTypes.includes(word.type));
|
|
||||||
|
|
||||||
if (filteredWords.length === 0) {
|
// Filter by type
|
||||||
alert("No words match the selected filters.");
|
if (word.type && !selectedTypes.includes(word.type)) return false;
|
||||||
return;
|
|
||||||
|
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 getRandomWord = () => {
|
||||||
const filteredWords = getFilteredWords();
|
const filteredWords = getFilteredWords();
|
||||||
const randomIndex = Math.floor(Math.random() * filteredWords!.length);
|
if (filteredWords.length === 0) {
|
||||||
setCurrentWord(filteredWords![randomIndex]);
|
alert("No words match the selected filters.");
|
||||||
setNumFilteredWords(filteredWords!.length);
|
return;
|
||||||
};
|
|
||||||
|
|
||||||
const updateFilteredWordsCount = () => {
|
|
||||||
const filteredWords = getFilteredWords();
|
|
||||||
if (filteredWords) {
|
|
||||||
setNumFilteredWords(filteredWords.length);
|
|
||||||
}
|
}
|
||||||
|
const randomIndex = Math.floor(Math.random() * filteredWords.length);
|
||||||
|
setCurrentWord(filteredWords[randomIndex]);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const toggleTag = (tagId: number) => {
|
||||||
// Initialize with a random word
|
setSelectedTagIds(prev =>
|
||||||
getRandomWord();
|
prev.includes(tagId)
|
||||||
|
? prev.filter(id => id !== tagId)
|
||||||
// Listen for checkbox changes anywhere in the document
|
: [...prev, tagId]
|
||||||
const handleCheckboxChange = (event: Event) => {
|
);
|
||||||
const target = event.target as HTMLElement;
|
|
||||||
if (target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'checkbox') {
|
|
||||||
updateFilteredWordsCount();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('change', handleCheckboxChange);
|
const toggleType = (type: string) => {
|
||||||
|
setSelectedTypes(prev =>
|
||||||
return () => {
|
prev.includes(type)
|
||||||
document.removeEventListener('change', handleCheckboxChange);
|
? prev.filter(t => t !== type)
|
||||||
|
: [...prev, type]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flash-card-activity">
|
<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 && (
|
{currentWord && (
|
||||||
<FlashCard word={currentWord} />
|
<FlashCard word={currentWord} />
|
||||||
)}
|
)}
|
||||||
@ -82,6 +119,43 @@ export default function FlashCardActivity({ vocabList }: { vocabList: VocabList[
|
|||||||
<div>
|
<div>
|
||||||
{numFilteredWords} words match the selected filters.
|
{numFilteredWords} words match the selected filters.
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,24 +2,10 @@
|
|||||||
import { Aside, Badge, Icon } from "@astrojs/starlight/components";
|
import { Aside, Badge, Icon } from "@astrojs/starlight/components";
|
||||||
import AnchorHeading from "@astrojs/starlight/components/AnchorHeading.astro";
|
import AnchorHeading from "@astrojs/starlight/components/AnchorHeading.astro";
|
||||||
import { render, renderInline, highlight } from "@/lib/markdown";
|
import { render, renderInline, highlight } from "@/lib/markdown";
|
||||||
|
import type { VocabWord } from "@/types/types";
|
||||||
|
|
||||||
interface Props {
|
const { word } = Astro.props as { word: VocabWord };
|
||||||
word: {
|
const activeTag = Astro.params.tag;
|
||||||
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 gender_lookup: Record<"m" | "f", ["note" | "tip", string]> = {
|
const gender_lookup: Record<"m" | "f", ["note" | "tip", string]> = {
|
||||||
m: ["note", "masculine"],
|
m: ["note", "masculine"],
|
||||||
@ -82,13 +68,12 @@ const gender_lookup: Record<"m" | "f", ["note" | "tip", string]> = {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
word.see_also && (
|
word.seeAlso && (
|
||||||
<p>
|
<p>
|
||||||
<b>See also:</b>
|
<b>See also:</b>
|
||||||
{word.see_also.map((ref, i) => (
|
{word.seeAlso.map((ref, i) => (
|
||||||
<>
|
<>
|
||||||
<span set:html={renderInline(ref)} />
|
<span set:html={renderInline(ref.reference ?? "")} />{i < word.seeAlso!.length - 1 ? "; " : ""}
|
||||||
{i < word.see_also!.length - 1 ? "; " : ""}
|
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
</p>
|
</p>
|
||||||
@ -97,9 +82,11 @@ const gender_lookup: Record<"m" | "f", ["note" | "tip", string]> = {
|
|||||||
{
|
{
|
||||||
word.tags && (
|
word.tags && (
|
||||||
<p>
|
<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 ? " " : ""}
|
{i < word.tags!.length - 1 ? " " : ""}
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,15 +1,7 @@
|
|||||||
import { defineCollection } from "astro:content";
|
import { defineCollection } from "astro:content";
|
||||||
import { docsLoader } from "@astrojs/starlight/loaders";
|
import { docsLoader } from "@astrojs/starlight/loaders";
|
||||||
import { docsSchema } from "@astrojs/starlight/schema";
|
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 = {
|
export const collections = {
|
||||||
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
|
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
|
||||||
vocabList: defineCollection({
|
|
||||||
loader: file("src/vocab_list.yaml"),
|
|
||||||
schema: vocabListSchema,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|||||||
8
hindki/src/lib/db/index.ts
Normal file
8
hindki/src/lib/db/index.ts
Normal 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 });
|
||||||
82
hindki/src/lib/db/schema.ts
Normal file
82
hindki/src/lib/db/schema.ts
Normal 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;
|
||||||
@ -1,156 +1,173 @@
|
|||||||
import fs from 'fs/promises';
|
import { db } from "./db";
|
||||||
import path from 'path';
|
import { words, examples, tags, wordTags, seeAlso } from "./db/schema";
|
||||||
import YAML from 'yaml';
|
import { eq } from "drizzle-orm";
|
||||||
|
import type { Word, Tag, NewWord } from "@/types/types";
|
||||||
|
|
||||||
interface StorageAdapter {
|
class SQLiteStorage {
|
||||||
read(): Promise<any[]>;
|
async getAllWords(): Promise<Word[]> {
|
||||||
write(data: any[]): Promise<void>;
|
const allWords = await db.query.words.findMany({
|
||||||
}
|
with: {
|
||||||
|
examples: true,
|
||||||
class FileSystemAdapter implements StorageAdapter {
|
tags: {
|
||||||
private filePath: string;
|
with: {
|
||||||
|
tag: true,
|
||||||
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',
|
|
||||||
},
|
},
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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',
|
|
||||||
},
|
},
|
||||||
}
|
seeAlso: true,
|
||||||
);
|
|
||||||
|
|
||||||
if (!currentResponse.ok) {
|
|
||||||
throw new Error(`Failed to get current file from Git: ${currentResponse.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!updateResponse.ok) {
|
|
||||||
const errorText = await updateResponse.text();
|
|
||||||
throw new Error(`Failed to update Git file: ${updateResponse.statusText} - ${errorText}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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':
|
return allWords.map((word) => ({
|
||||||
default:
|
id: word.id,
|
||||||
const filePath = path.join(process.cwd(), 'src', 'vocab_list.yaml');
|
hindi: word.hindi,
|
||||||
return new FileSystemAdapter(filePath);
|
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 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 (!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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tags
|
||||||
|
if (newWord.tagIds) {
|
||||||
|
for (const tagId of newWord.tagIds) {
|
||||||
|
await db.insert(wordTags).values({
|
||||||
|
wordId: word.id,
|
||||||
|
tagId: tagId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 { StorageAdapter, FileSystemAdapter, GitAdapter };
|
export const storage = new SQLiteStorage();
|
||||||
|
|||||||
25
hindki/src/pages/api/tags.json.ts
Normal file
25
hindki/src/pages/api/tags.json.ts
Normal 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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,10 +1,18 @@
|
|||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import { getStorageAdapter } from '../../lib/storage';
|
import { storage } from '../../lib/storage';
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
export const GET: APIRoute = async () => {
|
|
||||||
|
export const GET: APIRoute = async ({ url }) => {
|
||||||
try {
|
try {
|
||||||
const storage = getStorageAdapter();
|
const tag = url.searchParams.get('tag');
|
||||||
const data = await storage.read();
|
|
||||||
|
let data;
|
||||||
|
if (tag) {
|
||||||
|
data = await storage.getWordsByTag(tag);
|
||||||
|
} else {
|
||||||
|
data = await storage.getAllWords();
|
||||||
|
}
|
||||||
|
|
||||||
return new Response(JSON.stringify(data), {
|
return new Response(JSON.stringify(data), {
|
||||||
status: 200,
|
status: 200,
|
||||||
@ -26,14 +34,10 @@ export const GET: APIRoute = async () => {
|
|||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
const data = await request.json();
|
const data = await request.json();
|
||||||
const storage = getStorageAdapter();
|
|
||||||
|
|
||||||
// Read existing data
|
// Validate the new word
|
||||||
const vocabList = await storage.read();
|
if (!data.hindi || !data.english) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Missing required fields: hindi and english' }), {
|
||||||
// Validate the new entry
|
|
||||||
if (!data.category || !data.word) {
|
|
||||||
return new Response(JSON.stringify({ error: 'Missing required fields' }), {
|
|
||||||
status: 400,
|
status: 400,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -41,11 +45,11 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're creating a new category
|
// Check if creating a new tag
|
||||||
if (data.newCategory) {
|
let tagIds = data.tagIds || [];
|
||||||
// Validate new category data
|
if (data.newTag) {
|
||||||
if (!data.newCategory.slug || !data.newCategory.about) {
|
if (!data.newTag.name) {
|
||||||
return new Response(JSON.stringify({ error: 'New category requires slug and description' }), {
|
return new Response(JSON.stringify({ error: 'New tag requires a name' }), {
|
||||||
status: 400,
|
status: 400,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -53,44 +57,22 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if category already exists
|
const newTag = await storage.createTag(data.newTag.name, data.newTag.description);
|
||||||
const existingCategory = vocabList.find((cat: any) => cat.slug === data.newCategory.slug);
|
tagIds.push(newTag.id);
|
||||||
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
|
const newWord = await storage.createWord({
|
||||||
vocabList.push({
|
hindi: data.hindi,
|
||||||
slug: data.newCategory.slug,
|
english: data.english,
|
||||||
about: data.newCategory.about,
|
type: data.type,
|
||||||
words: [data.word],
|
gender: data.gender,
|
||||||
|
note: data.note,
|
||||||
|
examples: data.examples,
|
||||||
|
tagIds,
|
||||||
|
seeAlso: data.seeAlso,
|
||||||
});
|
});
|
||||||
} 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({ success: true, word: newWord }), {
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write back using the storage adapter
|
|
||||||
await storage.write(vocabList);
|
|
||||||
|
|
||||||
return new Response(JSON.stringify({ success: true, message: 'Word added successfully' }), {
|
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -98,7 +80,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in POST /api/vocab:', 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,
|
status: 500,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
---
|
---
|
||||||
import FlashCardActivity from "@/components/FlashCardActivity";
|
import FlashCardActivity from "@/components/FlashCardActivity";
|
||||||
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
|
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
|
||||||
import { getCollection } from "astro:content";
|
|
||||||
const vocabListCollection = await getCollection("vocabList");
|
export const prerender = false;
|
||||||
const vocabList = vocabListCollection.map((category) => ({
|
|
||||||
...category.data,
|
|
||||||
slug: category.id,
|
|
||||||
}));
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<StarlightPage
|
<StarlightPage
|
||||||
@ -16,5 +12,5 @@ const vocabList = vocabListCollection.map((category) => ({
|
|||||||
prev: false,
|
prev: false,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FlashCardActivity vocabList={vocabList} client:load />
|
<FlashCardActivity client:load />
|
||||||
</StarlightPage>
|
</StarlightPage>
|
||||||
|
|||||||
71
hindki/src/pages/vocabulary/[tag].astro
Normal file
71
hindki/src/pages/vocabulary/[tag].astro
Normal 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>
|
||||||
@ -75,8 +75,7 @@
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
unicode-range:
|
unicode-range: U+0900-097F, U+200C-200D;
|
||||||
U+0900-097F, U+200C-200D;
|
|
||||||
/* Devanagari block + zero-width joiners */
|
/* Devanagari block + zero-width joiners */
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,6 +233,7 @@ h6,
|
|||||||
.tag-badge {
|
.tag-badge {
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
color: var(--sl-color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.word-entry {
|
.word-entry {
|
||||||
@ -267,12 +267,18 @@ mark {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Override the exact Starlight rule that's causing issues */
|
/* 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;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Also target if the YAMLEditor itself is within sl-markdown-content */
|
/* 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,
|
.sl-markdown-content
|
||||||
|
:not(a, strong, em, del, span, input, code, br)
|
||||||
|
+ [yaml-editor]:not(
|
||||||
|
a,
|
||||||
strong,
|
strong,
|
||||||
em,
|
em,
|
||||||
del,
|
del,
|
||||||
@ -280,7 +286,8 @@ mark {
|
|||||||
input,
|
input,
|
||||||
code,
|
code,
|
||||||
br,
|
br,
|
||||||
:where(.not-content *)) {
|
:where(.not-content *)
|
||||||
|
) {
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,48 +1,113 @@
|
|||||||
// Export interfaces and types
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export interface VocabWord {
|
// Core domain types matching database schema
|
||||||
|
|
||||||
|
export interface Word {
|
||||||
|
id: number;
|
||||||
english: string;
|
english: string;
|
||||||
hindi: string;
|
hindi: string;
|
||||||
type: string;
|
type?: string | null;
|
||||||
gender?: "m" | "f" | undefined;
|
gender?: "m" | "f" | null;
|
||||||
note?: string;
|
note?: string | null;
|
||||||
examples?: Array<{
|
examples?: Example[];
|
||||||
english: string;
|
tags?: Tag[];
|
||||||
hindi: string;
|
seeAlso?: SeeAlso[];
|
||||||
note?: string;
|
|
||||||
}>;
|
|
||||||
see_also?: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
export interface VocabList {
|
||||||
slug: string;
|
slug: string;
|
||||||
about: 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>;
|
|
||||||
Loading…
Reference in New Issue
Block a user