Compare commits
3 commits
b0431ff962
...
a89a5b3494
Author | SHA1 | Date | |
---|---|---|---|
a89a5b3494 | |||
1adb26e8b8 | |||
35c5b520db |
11 changed files with 6476 additions and 4286 deletions
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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
6354
Foxnouns.Frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||||
|
|
4210
Foxnouns.Frontend/pnpm-lock.yaml
generated
4210
Foxnouns.Frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -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>
|
||||||
{#each new Array(pageCount) as _, page}
|
{#if allPages}
|
||||||
<PaginationItem active={page === currentPage}>
|
{#each new Array(pageCount) as _, page}
|
||||||
<PaginationLink onclick={() => (currentPage = page)}>{page + 1}</PaginationLink>
|
<PaginationItem active={page === currentPage}>
|
||||||
|
<PaginationLink onclick={() => (currentPage = page)}>{page + 1}</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
{#if currentPage !== 0}
|
||||||
|
<PaginationItem onclick={() => (currentPage = prevPage)}>
|
||||||
|
<PaginationLink>{currentPage}</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
{/if}
|
||||||
|
<PaginationItem active>
|
||||||
|
<PaginationLink>{currentPage + 1}</PaginationLink>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
{/each}
|
{#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>
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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,20 +114,44 @@
|
||||||
})}
|
})}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<ClientPaginator center bind:currentPage {pageCount} />
|
<div class="mb-3">
|
||||||
|
<input
|
||||||
<Accordion class="mb-3">
|
class="form-control"
|
||||||
{#each arr as flag (flag.id)}
|
type="text"
|
||||||
<AccordionItem>
|
bind:value={search}
|
||||||
<span slot="header">
|
placeholder={$t("editor.flag-search-placeholder")}
|
||||||
<EditorFlagImage {flag} />
|
/>
|
||||||
{flag.name}
|
</div>
|
||||||
</span>
|
|
||||||
|
<ClientPaginator center bind:currentPage {pageCount} />
|
||||||
{#if lastEditedFlag === flag.id}<FormStatusMarker form={ok} />{/if}
|
|
||||||
<FlagEditor {flag} {update} {deleteFlag} />
|
{#if arr.length === 0}
|
||||||
</AccordionItem>
|
<div class="text-secondary text-center">
|
||||||
{/each}
|
<p>
|
||||||
</Accordion>
|
<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)}
|
||||||
|
<AccordionItem>
|
||||||
|
<span slot="header">
|
||||||
|
<EditorFlagImage {flag} />
|
||||||
|
{flag.name}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{#if lastEditedFlag === flag.id}<FormStatusMarker form={ok} />{/if}
|
||||||
|
<FlagEditor {flag} {update} {deleteFlag} />
|
||||||
|
</AccordionItem>
|
||||||
|
{/each}
|
||||||
|
</Accordion>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<ClientPaginator center bind:currentPage {pageCount} />
|
<ClientPaginator center bind:currentPage {pageCount} />
|
||||||
|
|
8
build.sh
8
build.sh
|
@ -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"
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue