feat: SSR, metadata, and more (#199)

This commit is contained in:
Jason Rasmussen
2025-10-15 13:01:18 -04:00
committed by GitHub
parent e3cc9cdd8a
commit 5f6652e050
14 changed files with 246 additions and 246 deletions
+6 -19
View File
@@ -8,7 +8,7 @@
"name": "data.immich.app",
"version": "0.0.1",
"dependencies": {
"@immich/ui": "^0.35.0",
"@immich/ui": "^0.36.0",
"@mdi/js": "^7.4.47",
"luxon": "^3.5.0",
"simple-icons": "^15.11.0",
@@ -707,9 +707,9 @@
}
},
"node_modules/@immich/ui": {
"version": "0.35.0",
"resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.35.0.tgz",
"integrity": "sha512-dyjYLzJUNzR1aIQZEqPwfHutjuvK5t3wy3pp19Vm5bcQ4ira4Gk75flCgd9oEON5mlaxtlaUJVMeNor66bK9pg==",
"version": "0.36.0",
"resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.36.0.tgz",
"integrity": "sha512-9Ucj2hvMtPgXDIcfCyZPDDs5F49ooueUiPn/sJyppo+xKPEFjMdwYlebPM5nv7rbQ8VFIviWRIWOPV4cPKTy8g==",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@mdi/js": "^7.4.47",
@@ -1162,7 +1162,6 @@
"integrity": "sha512-J1fd80WokLzIm6EAV7z7C2+/C02qVAX645LZomARARTRJkbbJSY1Jln3wtBZYibUB8c9/5Z6xqLAV39VdbtWCQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@sveltejs/acorn-typescript": "^1.0.5",
@@ -1202,7 +1201,6 @@
"integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
"debug": "^4.4.1",
@@ -1241,6 +1239,7 @@
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
@@ -1640,7 +1639,6 @@
"integrity": "sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -1691,7 +1689,6 @@
"integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.0",
"@typescript-eslint/types": "8.46.0",
@@ -2024,7 +2021,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2452,7 +2448,6 @@
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3586,7 +3581,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -3614,7 +3608,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -3745,7 +3738,6 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -4073,7 +4065,6 @@
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.39.11.tgz",
"integrity": "sha512-8MxWVm2+3YwrFbPaxOlT1bbMi6OTenrAgks6soZfiaS8Fptk4EVyRIFhJc3RpO264EeSNwgjWAdki0ufg4zkGw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -4203,7 +4194,6 @@
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
"integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
@@ -4232,8 +4222,7 @@
"version": "4.1.14",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz",
"integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.2.2",
@@ -4380,7 +4369,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -4448,7 +4436,6 @@
"integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
+1 -1
View File
@@ -40,7 +40,7 @@
},
"type": "module",
"dependencies": {
"@immich/ui": "^0.35.0",
"@immich/ui": "^0.36.0",
"@mdi/js": "^7.4.47",
"luxon": "^3.5.0",
"simple-icons": "^15.11.0",
+34 -2
View File
@@ -1,13 +1,45 @@
<!doctype html>
<html lang="en">
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Immich - Data</title>
<meta name="theme-color" content="currentColor" />
<meta name="description" content="Graphs and charts for data around Immich" />
%sveltekit.head%
<style>
/* prevent FOUC */
html {
height: 100%;
width: 100%;
}
body,
html {
margin: 0;
padding: 0;
}
</style>
<script>
/**
* Prevent FOUC on page load.
*/
const THEME_KEY = 'immich-ui-theme';
let value;
try {
value = JSON.parse(localStorage.getItem(THEME_KEY)).value;
if (value !== 'light' && value !== 'dark') {
value = undefined;
}
} catch {
// noop
}
if (value === 'light') {
document.documentElement.classList.remove('dark');
document.documentElement.classList.add('light');
}
</script>
</head>
<body data-sveltekit-preload-data="hover" class="bg-light text-dark">
<div style="display: contents">%sveltekit.body%</div>
@@ -0,0 +1,33 @@
<script lang="ts">
import { Card, CardBody, CardHeader, CardTitle, Link, Text } from '@immich/ui';
import { DateTime } from 'luxon';
import type { Snippet } from 'svelte';
type Props = {
title: string;
href: string;
date: DateTime | Snippet;
};
const { title, href, date }: Props = $props();
</script>
<Card color="secondary">
<CardHeader class="text-center">
<CardTitle>
<Link {href}>{title}</Link>
</CardTitle>
</CardHeader>
<CardBody>
<div class="flex justify-around align-middle h-full">
<div class="text-center flex flex-col justify-between align-middle">
{#if typeof date === 'function'}
{@render date()}
{:else}
<Text class="text-4xl">{date.toRelative()}</Text>
<Text color="muted">{date.toLocaleString(DateTime.DATE_FULL)}</Text>
{/if}
</div>
</div>
</CardBody>
</Card>
@@ -64,7 +64,7 @@
id,
padding: [8, 8, 8, 8],
cursor: cursorOpts,
width: 500,
width: 200,
height: 300,
scales: {},
legend: {
@@ -0,0 +1,26 @@
<script lang="ts">
import { Card, CardBody, CardHeader, CardTitle, HStack, Icon, type IconLike } from '@immich/ui';
import type { Snippet } from 'svelte';
type Props = {
title: string;
icon: IconLike;
children: Snippet;
};
const { title, icon, children }: Props = $props();
</script>
<Card color="secondary">
<CardHeader>
<CardTitle>
<HStack>
<Icon {icon} />
<span>{title}</span>
</HStack>
</CardTitle>
</CardHeader>
<CardBody>
{@render children()}
</CardBody>
</Card>
@@ -0,0 +1,11 @@
<script lang="ts">
import type { Snippet } from 'svelte';
type Props = {
children: Snippet;
};
const { children }: Props = $props();
</script>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">{@render children()}</div>
@@ -0,0 +1,18 @@
<script lang="ts">
import { Heading, Text, Link } from '@immich/ui';
type Props = {
title: string;
link: {
title: string;
href: string;
};
};
const { title, link }: Props = $props();
</script>
<Heading size="large" tag="h1">{title}</Heading>
<Text color="muted" class="mb-4">
<Link href={link.href} underline={false}>{link.title}</Link>
</Text>
+8 -1
View File
@@ -1 +1,8 @@
// place files you want to import through the `$lib` alias in this folder.
import type { Metadata } from '@immich/ui';
export const siteMetadata: Metadata = {
title: 'Immich Data',
description:
'Interesting data about the growth of Immich, the open-source self-hosted photo and video management solution.',
imageUrl: '/img/social-preview.png',
};
+9 -3
View File
@@ -14,7 +14,7 @@ export const githubPullRequests = $state<{ value?: DataRecord[] }>({});
export const githubDiscussions = $state<{ value?: DataRecord[] }>({});
export const githubMergedPullRequests = $state<{ value?: DataRecord[] }>({});
export const loadGithubData = async () => {
const loadGithubData = async () => {
const response = await fetch('/api/github');
if (response.ok) {
const { stars, issues, pullRequests, discussions, mergedPullRequests } =
@@ -45,7 +45,7 @@ export const discordOnline = $state<{ value?: DataRecord[] }>({});
export const discordNitro = $state<{ value?: DataRecord[] }>({});
export const discordServerTier = $state<{ value?: DataRecord[] }>({});
export const loadRedditData = async () => {
const loadRedditData = async () => {
const response = await fetch('/api/reddit');
if (response.ok) {
const { subscribers } = (await response.json()) as RedditDataResponse;
@@ -53,7 +53,7 @@ export const loadRedditData = async () => {
}
};
export const loadDiscordData = async () => {
const loadDiscordData = async () => {
const response = await fetch('/api/discord');
if (response.ok) {
const { members, online, nitro, serverTier } = (await response.json()) as DiscordDataResponse;
@@ -63,3 +63,9 @@ export const loadDiscordData = async () => {
discordServerTier.value = serverTier;
}
};
export const load = () => {
void loadGithubData();
void loadRedditData();
void loadDiscordData();
};
+1 -1
View File
@@ -13,7 +13,7 @@
let { children }: Props = $props();
initializeTheme();
initializeTheme({ selector: 'html', lightClass: 'light', darkClass: 'dark' });
console.log(`Backend URL: ${backendUrl}`);
</script>
+1 -1
View File
@@ -1 +1 @@
export const ssr = false;
export const prerender = true;
+97 -214
View File
@@ -1,33 +1,25 @@
<script lang="ts">
import { siteMetadata } from '$lib';
import '$lib/app.css';
import GithubGraph from '$lib/components/graphs/github-graph.svelte';
import DateCard from '$lib/components/DateCard.svelte';
import GraphCard from '$lib/components/GraphCard.svelte';
import Graph from '$lib/components/Graph.svelte';
import SectionGrid from '$lib/components/SectionGrid.svelte';
import SectionTitle from '$lib/components/SectionTitle.svelte';
import {
discordMembers,
githubDiscussions,
githubIssues,
githubMergedPullRequests,
githubPullRequests,
githubStars,
githubMergedPullRequests,
loadGithubData,
loadRedditData,
load,
redditMembers,
loadDiscordData,
discordMembers,
} from '$lib/services/api.svelte';
import {
Card,
CardBody,
CardHeader,
CardTitle,
Container,
Heading,
HStack,
Icon,
Link,
Stack,
Text,
} from '@immich/ui';
import { Constants, Container, Heading, SiteMetadata, Text } from '@immich/ui';
import { mdiAccountGroup, mdiBugOutline, mdiMessageOutline, mdiSourceBranch, mdiStarOutline } from '@mdi/js';
import { DateTime } from 'luxon';
import { onMount } from 'svelte';
import uPlot from 'uplot';
import 'uplot/dist/uPlot.min.css';
@@ -42,206 +34,97 @@
},
};
const commit = DateTime.fromISO('2022-02-03T15:56:28.000Z', { zone: 'America/Chicago' });
const futo = DateTime.fromObject({ year: 2024, month: 5, day: 1 }, { zone: 'America/Chicago' });
onMount(() => {
load();
});
void loadGithubData();
void loadRedditData();
void loadDiscordData();
const milestones = [
{
title: '1st Commit',
href: 'https://github.com/immich-app/immich/commit/af2efbdbbddc27cd06142f22253ccbbbbeec1f55',
date: DateTime.fromISO('2022-02-03T15:56:28.000Z', { zone: 'America/Chicago' }),
},
{
title: 'Joined FUTO',
href: 'https://immich.app/blog/2024/immich-core-team-goes-fulltime',
date: DateTime.fromObject({ year: 2024, month: 5, day: 1 }, { zone: 'America/Chicago' }),
},
{
title: 'Stable Release',
href: 'https://immich.app/blog/stable-release',
date: DateTime.fromObject({ year: 2025, month: 10, day: 1 }, { zone: 'America/Chicago' }),
},
];
</script>
<SiteMetadata site={siteMetadata} />
<Container size="giant" center>
<Stack gap={8}>
<Heading size="title">Immich Data</Heading>
<Heading size="giant" class="mb-1">{siteMetadata.title}</Heading>
<Text color="muted" class="mb-8">{siteMetadata.description}</Text>
<section class="flex flex-col gap-4">
<Heading size="large" tag="h1">Statistics</Heading>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-3">
<Card color="secondary">
<CardHeader class="text-center">
<CardTitle>
<Link href="https://github.com/immich-app/immich/commit/af2efbdbbddc27cd06142f22253ccbbbbeec1f55"
>1st Commit</Link
>
</CardTitle>
</CardHeader>
<CardBody>
<div class="flex justify-around align-middle h-full">
<div class="text-center flex flex-col justify-between align-middle">
<Text class="text-4xl">{commit.toRelative()}</Text>
<Text color="muted">{commit.toLocaleString(DateTime.DATETIME_MED)}</Text>
</div>
</div>
</CardBody>
</Card>
<Card color="secondary">
<CardHeader class="text-center">
<CardTitle>
<Link href="https://immich.app/blog/2024/immich-core-team-goes-fulltime">Joined FUTO</Link>
</CardTitle>
</CardHeader>
<CardBody>
<div class="flex justify-around align-middle h-full">
<div class="text-center flex flex-col justify-between align-middle">
<Text class="text-4xl">{futo.toRelative()}</Text>
<Text color="muted">{futo.toLocaleString(DateTime.DATE_FULL)}</Text>
</div>
</div>
</CardBody>
</Card>
<Card color="secondary">
<CardHeader class="text-center">
<CardTitle>Last timezone issue</CardTitle>
</CardHeader>
<CardBody>
<div class="flex justify-around align-middle h-full">
<div class="text-center flex flex-col justify-between align-middle">
<Text class="text-6xl">-1</Text>
<Text color="muted">days ago</Text>
</div>
</div>
</CardBody>
</Card>
</div>
</section>
<SectionTitle title="Milestones" link={{ title: 'Important dates for Immich', href: Constants.Pages.Roadmap }} />
<div class="grid grid-cols-1 md:gric-cols-2 lg:grid-cols-4 gap-4 mb-4">
{#each milestones as milestone (milestone.title)}
<DateCard title={milestone.title} href={milestone.href} date={milestone.date} />
{/each}
<DateCard title="Last timezone issue" href={Constants.Pages.CursedKnowledge}>
{#snippet date()}
<Text class="text-4xl">-1</Text>
<Text color="muted">days ago</Text>
{/snippet}
</DateCard>
</div>
<section class="flex flex-col gap-4">
<div>
<Heading size="large" tag="h1">GitHub</Heading>
<Text color="muted">immich-app/immich</Text>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card color="secondary">
<CardHeader>
<CardTitle>
<HStack>
<Icon icon={mdiStarOutline} />
Stars
</HStack>
</CardTitle>
</CardHeader>
<CardBody>
<GithubGraph color="yellow" id="stars-chart" data={githubStars.value} {cursorOpts} label="Stars" />
</CardBody>
</Card>
<Card color="secondary">
<CardHeader>
<CardTitle>
<HStack>
<Icon icon={mdiBugOutline} />
<span>Issues</span>
</HStack>
</CardTitle>
</CardHeader>
<CardBody>
<GithubGraph color="blue" id="issues-chart" data={githubIssues.value} {cursorOpts} label="Issues" />
</CardBody>
</Card>
<Card color="secondary">
<CardHeader>
<CardTitle>
<HStack>
<Icon icon={mdiSourceBranch} />
<span>Open Pull Requests</span>
</HStack>
</CardTitle>
</CardHeader>
<CardBody>
<GithubGraph
color="green"
id="pull-requests-chart"
data={githubPullRequests.value}
{cursorOpts}
label="PR"
/>
</CardBody>
</Card>
<Card color="secondary">
<CardHeader>
<CardTitle>
<HStack>
<Icon icon={mdiMessageOutline} />
<span>Total Discussions</span>
</HStack>
</CardTitle>
</CardHeader>
<CardBody>
<GithubGraph
color="purple"
id="discussions-chart"
data={githubDiscussions.value}
{cursorOpts}
label="Topics"
/>
</CardBody>
</Card>
<Card color="secondary">
<CardHeader>
<CardTitle>
<HStack>
<Icon icon={mdiSourceBranch} />
<span>Total PRs Merged (Cumulative)</span>
</HStack>
</CardTitle>
</CardHeader>
<CardBody>
<GithubGraph
color="green"
id="merged-prs-chart"
data={githubMergedPullRequests.value}
{cursorOpts}
label="Merged PRs"
/>
</CardBody>
</Card>
</div>
</section>
<SectionTitle title="GitHub" link={{ title: 'immich-app/immich', href: Constants.Socials.Github }} />
<SectionGrid>
<GraphCard title="Stars" icon={mdiStarOutline}>
<Graph color="yellow" id="stars-chart" data={githubStars.value} {cursorOpts} label="Stars" />
</GraphCard>
<GraphCard title="Issues" icon={mdiBugOutline}>
<Graph color="blue" id="issues-chart" data={githubIssues.value} {cursorOpts} label="Issues" />
</GraphCard>
<GraphCard title="Open Pull Requests" icon={mdiSourceBranch}>
<Graph
color="green"
id="pull-requests-chart"
data={githubPullRequests.value}
{cursorOpts}
label="Open pull requests"
/>
</GraphCard>
<GraphCard title="Merged Pull Requests" icon={mdiSourceBranch}>
<Graph
color="green"
id="merged-prs-chart"
data={githubMergedPullRequests.value}
{cursorOpts}
label="Merged pull requests"
/>
</GraphCard>
<GraphCard title="Discussions" icon={mdiMessageOutline}>
<Graph
color="purple"
id="discussions-chart"
data={githubDiscussions.value}
{cursorOpts}
label="Total discussions"
/>
</GraphCard>
</SectionGrid>
<section class="flex flex-col gap-4">
<div>
<Heading size="large" tag="h1">Community</Heading>
<Text color="muted">r/immich • discord.gg/immich</Text>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card color="secondary">
<CardHeader>
<CardTitle>
<HStack>
<Icon icon={mdiAccountGroup} />
<span>Reddit Members</span>
</HStack>
</CardTitle>
</CardHeader>
<CardBody>
<GithubGraph
color="orange"
id="reddit-members"
data={redditMembers.value}
{cursorOpts}
label="Reddit members"
/>
</CardBody>
</Card>
<Card color="secondary">
<CardHeader>
<CardTitle>
<HStack>
<Icon icon={mdiAccountGroup} />
<span>Discord Members</span>
</HStack>
</CardTitle>
</CardHeader>
<CardBody>
<GithubGraph
color="purple"
id="discord-members"
data={discordMembers.value}
{cursorOpts}
label="Discord members"
/>
</CardBody>
</Card>
</div>
</section>
</Stack>
<SectionGrid>
<div>
<SectionTitle title="Reddit" link={{ title: 'r/immich', href: Constants.Socials.Reddit }} />
<GraphCard title="Reddit Members" icon={mdiAccountGroup}>
<Graph color="orange" id="reddit-members" data={redditMembers.value} {cursorOpts} label="Reddit members" />
</GraphCard>
</div>
<div>
<SectionTitle title="Discord" link={{ title: 'discord.immich.app', href: Constants.Socials.Discord }} />
<GraphCard title="Discord Members" icon={mdiAccountGroup}>
<Graph color="purple" id="discord-members" data={discordMembers.value} {cursorOpts} label="Discord members" />
</GraphCard>
</div>
</SectionGrid>
</Container>
-3
View File
@@ -5,13 +5,10 @@ process.env.PUBLIC_IMMICH_BACKEND_URL = process.env.PUBLIC_IMMICH_BACKEND_URL ||
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
fallback: 'index.html',
pages: '../dist/frontend',
}),
},