Compare commits

...

3 commits

11 changed files with 6476 additions and 4286 deletions

View file

@ -3,12 +3,8 @@
"tasks": [ "tasks": [
{ {
"name": "run-prettier", "name": "run-prettier",
"command": "pnpm", "command": "npx",
"args": [ "args": ["prettier", "-w", "${staged}"],
"prettier",
"-w",
"${staged}"
],
"include": [ "include": [
"Foxnouns.Frontend/**/*.ts", "Foxnouns.Frontend/**/*.ts",
"Foxnouns.Frontend/**/*.json", "Foxnouns.Frontend/**/*.json",
@ -22,13 +18,8 @@
{ {
"name": "run-csharpier", "name": "run-csharpier",
"command": "dotnet", "command": "dotnet",
"args": [ "args": ["csharpier", "${staged}"],
"csharpier", "include": ["**/*.cs"]
"${staged}"
],
"include": [
"**/*.cs"
]
} }
] ]
} }

View file

@ -1,9 +1,5 @@
FROM docker.io/node:23-slim FROM docker.io/node:23-slim
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
COPY ./Foxnouns.Frontend /app COPY ./Foxnouns.Frontend /app
COPY ./docker/frontend.env /app/.env.local COPY ./docker/frontend.env /app/.env.local
WORKDIR /app WORKDIR /app
@ -11,7 +7,7 @@ WORKDIR /app
ENV PRIVATE_API_HOST=http://rate:5003/api ENV PRIVATE_API_HOST=http://rate:5003/api
ENV PRIVATE_INTERNAL_API_HOST=http://backend:5000/api ENV PRIVATE_INTERNAL_API_HOST=http://backend:5000/api
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile RUN npm ci
RUN pnpm run build RUN npm run build
CMD ["pnpm", "node", "build/index.js"] CMD ["node", "build/index.js"]

View file

@ -1,6 +1,6 @@
// This script regenerates the list of icons for the frontend (Foxnouns.Frontend/src/lib/icons.ts) // This script regenerates the list of icons for the frontend (Foxnouns.Frontend/src/lib/icons.ts)
// and the backend (Foxnouns.Backend/Utils/BootstrapIcons.Icons.cs) from the currently installed version of Bootstrap Icons. // and the backend (Foxnouns.Backend/Utils/BootstrapIcons.Icons.cs) from the currently installed version of Bootstrap Icons.
// Run with `pnpm node icons.js` in the frontend directory. // Run with `node icons.js` in the frontend directory.
import { writeFileSync } from "fs"; import { writeFileSync } from "fs";
import icons from "bootstrap-icons/font/bootstrap-icons.json" with { type: "json" }; import icons from "bootstrap-icons/font/bootstrap-icons.json" with { type: "json" };

6354
Foxnouns.Frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -12,43 +12,42 @@
"lint": "prettier --check . && eslint ." "lint": "prettier --check . && eslint ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-node": "^5.2.10", "@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/kit": "^2.12.1", "@sveltejs/kit": "^2.20.4",
"@sveltejs/vite-plugin-svelte": "^5.0.2", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@sveltestrap/sveltestrap": "^7.1.0", "@sveltestrap/sveltestrap": "^7.1.0",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.6.2",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/sanitize-html": "^2.13.0", "@types/sanitize-html": "^2.15.0",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.5",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"eslint": "^9.17.0", "eslint": "^9.24.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.1", "eslint-plugin-svelte": "^2.46.1",
"globals": "^15.13.0", "globals": "^16.0.0",
"prettier": "^3.4.2", "prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.2", "prettier-plugin-svelte": "^3.3.3",
"sass": "^1.83.0", "sass": "^1.86.3",
"svelte": "^5.14.3", "svelte": "^5.25.7",
"svelte-bootstrap-icons": "^3.1.1", "svelte-bootstrap-icons": "^3.1.2",
"svelte-check": "^4.1.1", "svelte-check": "^4.1.5",
"svelte-easy-crop": "^4.0.0", "svelte-easy-crop": "^4.0.1",
"sveltekit-i18n": "^2.4.2", "sveltekit-i18n": "^2.4.2",
"typescript": "^5.7.2", "typescript": "^5.8.3",
"typescript-eslint": "^8.18.1", "typescript-eslint": "^8.29.0",
"vite": "^6.0.3" "vite": "^6.2.5"
}, },
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c",
"dependencies": { "dependencies": {
"@fontsource/firago": "^5.1.0", "@fontsource/firago": "^5.2.5",
"@sentry/sveltekit": "^8.52.0", "@sentry/sveltekit": "^9.11.0",
"base64-arraybuffer": "^1.0.2", "base64-arraybuffer": "^1.0.2",
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"luxon": "^3.5.0", "luxon": "^3.6.1",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"minidenticons": "^4.2.1", "minidenticons": "^4.2.1",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"sanitize-html": "^2.13.1", "sanitize-html": "^2.15.0",
"svelte-tippy": "^1.3.2", "svelte-tippy": "^1.3.2",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"tslog": "^4.9.3" "tslog": "^4.9.3"

File diff suppressed because it is too large Load diff

View file

@ -5,8 +5,11 @@
currentPage: number; currentPage: number;
pageCount: number; pageCount: number;
center?: boolean; center?: boolean;
listAllPages?: boolean;
}; };
let { currentPage = $bindable(), pageCount, center }: Props = $props(); let { currentPage = $bindable(), pageCount, center, listAllPages }: Props = $props();
let allPages = $derived(listAllPages === undefined || listAllPages);
let prevPage = $derived(currentPage > 0 ? currentPage - 1 : 0); let prevPage = $derived(currentPage > 0 ? currentPage - 1 : 0);
let nextPage = $derived(currentPage < pageCount - 1 ? currentPage + 1 : pageCount - 1); let nextPage = $derived(currentPage < pageCount - 1 ? currentPage + 1 : pageCount - 1);
@ -21,11 +24,27 @@
<PaginationItem> <PaginationItem>
<PaginationLink previous onclick={() => (currentPage = prevPage)} /> <PaginationLink previous onclick={() => (currentPage = prevPage)} />
</PaginationItem> </PaginationItem>
{#if allPages}
{#each new Array(pageCount) as _, page} {#each new Array(pageCount) as _, page}
<PaginationItem active={page === currentPage}> <PaginationItem active={page === currentPage}>
<PaginationLink onclick={() => (currentPage = page)}>{page + 1}</PaginationLink> <PaginationLink onclick={() => (currentPage = page)}>{page + 1}</PaginationLink>
</PaginationItem> </PaginationItem>
{/each} {/each}
{:else}
{#if currentPage !== 0}
<PaginationItem onclick={() => (currentPage = prevPage)}>
<PaginationLink>{currentPage}</PaginationLink>
</PaginationItem>
{/if}
<PaginationItem active>
<PaginationLink>{currentPage + 1}</PaginationLink>
</PaginationItem>
{#if currentPage !== pageCount - 1}
<PaginationItem onclick={() => (currentPage = nextPage)}>
<PaginationLink>{currentPage + 2}</PaginationLink>
</PaginationItem>
{/if}
{/if}
<PaginationItem> <PaginationItem>
<PaginationLink next onclick={() => (currentPage = nextPage)} /> <PaginationLink next onclick={() => (currentPage = nextPage)} />
</PaginationItem> </PaginationItem>

View file

@ -4,23 +4,35 @@
import Search from "svelte-bootstrap-icons/lib/Search.svelte"; import Search from "svelte-bootstrap-icons/lib/Search.svelte";
import FlagButton from "./FlagButton.svelte"; import FlagButton from "./FlagButton.svelte";
import { t } from "$lib/i18n"; import { t } from "$lib/i18n";
import paginate from "$lib/paginate";
import ClientPaginator from "$components/ClientPaginator.svelte";
type Props = { flags: PrideFlag[]; select(flag: PrideFlag): void }; type Props = { flags: PrideFlag[]; select(flag: PrideFlag): void };
let { flags, select }: Props = $props(); let { flags, select }: Props = $props();
const FLAGS_PER_PAGE = 5;
let query = $state(""); let query = $state("");
let filteredFlags = $derived(search(query)); let filteredFlags = $derived(search(query));
let arr: PrideFlag[] = $state([]);
let currentPage = $state(0);
let pageCount = $state(0);
$effect(() => {
const pages = paginate(filteredFlags, currentPage, FLAGS_PER_PAGE);
arr = pages.data;
pageCount = pages.pageCount;
});
function search(q: string) { function search(q: string) {
if (!q) return flags.slice(0, 20); return flags.filter((f) => f.name.toLowerCase().indexOf(q.toLowerCase()) !== -1);
return flags.filter((f) => f.name.toLowerCase().indexOf(q.toLowerCase()) !== -1).slice(0, 20);
} }
</script> </script>
<input class="form-control" placeholder={$t("editor.flag-search-placeholder")} bind:value={query} /> <input class="form-control" placeholder={$t("editor.flag-search-placeholder")} bind:value={query} />
<div class="mt-3"> <div class="mt-3">
{#each filteredFlags as flag (flag.id)} {#each arr as flag (flag.id)}
<FlagButton {flag} onclick={() => select(flag)} padding /> <FlagButton {flag} onclick={() => select(flag)} padding />
{:else} {:else}
<div class="text-secondary text-center"> <div class="text-secondary text-center">
@ -36,6 +48,7 @@
</p> </p>
</div> </div>
{/each} {/each}
<ClientPaginator bind:currentPage {pageCount} listAllPages={false} />
{#if flags.length > 0} {#if flags.length > 0}
<p class="text-secondary mt-2"> <p class="text-secondary mt-2">
<InfoCircleFill aria-hidden /> <InfoCircleFill aria-hidden />

View file

@ -13,6 +13,7 @@
import ApiError from "$api/error"; import ApiError from "$api/error";
import log from "$lib/log"; import log from "$lib/log";
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte"; import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
import Search from "svelte-bootstrap-icons/lib/Search.svelte";
type Props = { data: PageData; form: ActionData }; type Props = { data: PageData; form: ActionData };
let { data, form }: Props = $props(); let { data, form }: Props = $props();
@ -26,13 +27,17 @@
const FLAGS_PER_PAGE = 50; const FLAGS_PER_PAGE = 50;
$effect(() => { $effect(() => {
const pages = paginate(flags, currentPage, FLAGS_PER_PAGE); const filteredFlags = search
? flags.filter((f) => f.name.toLocaleLowerCase().indexOf(search.toLocaleLowerCase()) !== -1)
: flags;
const pages = paginate(filteredFlags, currentPage, FLAGS_PER_PAGE);
arr = pages.data; arr = pages.data;
pageCount = pages.pageCount; pageCount = pages.pageCount;
}); });
let lastEditedFlag: string | null = $state(null); let lastEditedFlag: string | null = $state(null);
let ok: { ok: boolean; error: RawApiError | null } | null = $state(null); let ok: { ok: boolean; error: RawApiError | null } | null = $state(null);
let search: string = $state("");
const update = async ( const update = async (
id: string, id: string,
@ -109,9 +114,32 @@
})} })}
</h4> </h4>
<div class="mb-3">
<input
class="form-control"
type="text"
bind:value={search}
placeholder={$t("editor.flag-search-placeholder")}
/>
</div>
<ClientPaginator center bind:currentPage {pageCount} /> <ClientPaginator center bind:currentPage {pageCount} />
<Accordion class="mb-3"> {#if arr.length === 0}
<div class="text-secondary text-center">
<p>
<Search class="no-flags-icon" height={64} width={64} aria-hidden />
</p>
<p>
{#if search}
{$t("editor.flag-search-no-flags")}
{:else}
{$t("editor.flag-search-no-account-flags")}
{/if}
</p>
</div>
{:else}
<Accordion class="mb-3">
{#each arr as flag (flag.id)} {#each arr as flag (flag.id)}
<AccordionItem> <AccordionItem>
<span slot="header"> <span slot="header">
@ -123,6 +151,7 @@
<FlagEditor {flag} {update} {deleteFlag} /> <FlagEditor {flag} {update} {deleteFlag} />
</AccordionItem> </AccordionItem>
{/each} {/each}
</Accordion> </Accordion>
{/if}
<ClientPaginator center bind:currentPage {pageCount} /> <ClientPaginator center bind:currentPage {pageCount} />

View file

@ -25,12 +25,12 @@ echo "Building Node.js frontend"
cd "$ROOT_DIR/Foxnouns.Frontend" cd "$ROOT_DIR/Foxnouns.Frontend"
[ -d "$ROOT_DIR/Foxnouns.Frontend/build" ] && rm -r "$ROOT_DIR/Foxnouns.Frontend/build" [ -d "$ROOT_DIR/Foxnouns.Frontend/build" ] && rm -r "$ROOT_DIR/Foxnouns.Frontend/build"
pnpm install npm ci
pnpm build npm run build
mkdir "$ROOT_DIR/build/fe" mkdir "$ROOT_DIR/build/fe"
cp -r build .env* package.json pnpm-lock.yaml "$ROOT_DIR/build/fe" cp -r build .env* package.json package-lock.json "$ROOT_DIR/build/fe"
cd "$ROOT_DIR/build/fe" cd "$ROOT_DIR/build/fe"
pnpm install -P NODE_ENV=production npm ci
echo "Finished building Foxnouns.NET" echo "Finished building Foxnouns.NET"

View file

@ -4,8 +4,7 @@
}, },
"scripts": { "scripts": {
"watch:be": "dotnet watch --no-hot-reload --project Foxnouns.Backend -- --migrate-and-start", "watch:be": "dotnet watch --no-hot-reload --project Foxnouns.Backend -- --migrate-and-start",
"dev": "concurrently -n .net,node,rate -c magenta,yellow,blue -i 'pnpm watch:be' 'cd Foxnouns.Frontend && pnpm dev' 'cd rate && go run -v .'", "dev": "concurrently -n .net,node,rate -c magenta,yellow,blue -i 'npm run watch:be' 'cd Foxnouns.Frontend && npm run dev' 'cd rate && go run -v .'",
"format": "dotnet csharpier . && cd Foxnouns.Frontend && pnpm format" "format": "dotnet csharpier . && cd Foxnouns.Frontend && npm run format"
}, }
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c"
} }