mirror of
https://github.com/MbinOrg/mbin-website.git
synced 2025-06-29 14:48:57 +00:00
Merge pull request #1 from MbinOrg/new-pages
Add an Apps and Releases page and numerous enhancements to the Servers page
This commit is contained in:
commit
c5d404f57a
10 changed files with 511 additions and 101 deletions
|
@ -9,6 +9,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/material-symbols": "^1.1.82",
|
||||
"@iconify-json/simple-icons": "^1.1.109",
|
||||
"@kobalte/core": "^0.13.3",
|
||||
"@solidjs/router": "^0.13.6",
|
||||
"@solidjs/start": "^1.0.2",
|
||||
|
|
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
@ -11,6 +11,9 @@ importers:
|
|||
'@iconify-json/material-symbols':
|
||||
specifier: ^1.1.82
|
||||
version: 1.1.82
|
||||
'@iconify-json/simple-icons':
|
||||
specifier: ^1.1.109
|
||||
version: 1.1.109
|
||||
'@kobalte/core':
|
||||
specifier: ^0.13.3
|
||||
version: 0.13.3(solid-js@1.8.18)
|
||||
|
@ -500,6 +503,9 @@ packages:
|
|||
'@iconify-json/material-symbols@1.1.82':
|
||||
resolution: {integrity: sha512-E67LgMFiAbEVF7rE38ulZU6NeXcPvayFF4hUUqt3g33tWrLsDNqEFTSsPt03l34rH5uGGtHIakTqtBlZ+/hRkw==}
|
||||
|
||||
'@iconify-json/simple-icons@1.1.109':
|
||||
resolution: {integrity: sha512-vIhIJQDdbS5R6kSyIHVBRCaR2jiFjVlbVtB4PAoLjQL45vJRHMTwkrFa536XcX7yW69HbQkoanydcyDjknI6pw==}
|
||||
|
||||
'@iconify/types@2.0.0':
|
||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||
|
||||
|
@ -3634,6 +3640,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify-json/simple-icons@1.1.109':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify/types@2.0.0': {}
|
||||
|
||||
'@iconify/utils@2.1.25':
|
||||
|
|
|
@ -4,17 +4,30 @@ import { Dynamic } from 'solid-js/web';
|
|||
const Chip: ParentComponent<{
|
||||
icon?: (props: ComponentProps<'svg'>) => JSX.Element;
|
||||
title?: string;
|
||||
class?: string;
|
||||
classList?: {
|
||||
[k: string]: boolean | undefined;
|
||||
};
|
||||
href?: string;
|
||||
}> = (props) => {
|
||||
return (
|
||||
<span
|
||||
class="px-2 py-1 border rounded-lg inline-flex items-center"
|
||||
<Dynamic
|
||||
component={props.href ? 'a' : 'span'}
|
||||
class={
|
||||
'px-2 py-1 border rounded-lg inline-flex items-center ' + props.class
|
||||
}
|
||||
classList={{
|
||||
'hover:text-blue-500': !!props.href,
|
||||
...props.classList,
|
||||
}}
|
||||
title={props.title}
|
||||
href={props.href}
|
||||
>
|
||||
<Show when={props.icon}>
|
||||
<Dynamic component={props.icon} class="pr-1" />
|
||||
</Show>
|
||||
{props.children}
|
||||
</span>
|
||||
</Dynamic>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -6,23 +6,34 @@ export default function Nav() {
|
|||
path == location.pathname
|
||||
? 'border-sky-600'
|
||||
: 'border-transparent hover:border-sky-600';
|
||||
|
||||
const navItemClass = 'border-b-2 mx-1.5 sm:mx-6 ';
|
||||
|
||||
return (
|
||||
<nav class="flex px-4 py-2 items-center bg-[#29144a]">
|
||||
<a href="/">
|
||||
<img src="/logo.svg" class="size-8" />
|
||||
</a>
|
||||
<ul class="container flex items-center p-3 text-gray-200">
|
||||
<li class={`border-b-2 ${active('/')} mx-1.5 sm:mx-6`}>
|
||||
<li class={navItemClass + active('/')}>
|
||||
<a href="/">Home</a>
|
||||
</li>
|
||||
<li class={`border-b-2 ${active('/servers')} mx-1.5 sm:mx-6`}>
|
||||
<li class={navItemClass + active('/servers')}>
|
||||
<a href="/servers">Servers</a>
|
||||
</li>
|
||||
<li class={navItemClass + active('/apps')}>
|
||||
<a href="/apps">Apps</a>
|
||||
</li>
|
||||
<li class={navItemClass + active('/releases')}>
|
||||
<a href="/releases">Releases</a>
|
||||
</li>
|
||||
|
||||
<span class="grow"></span>
|
||||
<li class={`border-b-2 mx-1.5 sm:mx-6`}>
|
||||
|
||||
<li class={navItemClass}>
|
||||
<a href="https://docs.joinmbin.org/">Docs</a>
|
||||
</li>
|
||||
<li class={`border-b-2 mx-1.5 sm:mx-6`}>
|
||||
<li class={navItemClass}>
|
||||
<a href="https://github.com/mbinOrg/mbin">GitHub</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
34
src/components/ui/tooltip.tsx
Normal file
34
src/components/ui/tooltip.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { splitProps, ValidComponent, type Component } from "solid-js"
|
||||
|
||||
import { PolymorphicProps } from "@kobalte/core/polymorphic"
|
||||
import * as TooltipPrimitive from "@kobalte/core/tooltip"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const Tooltip: Component<TooltipPrimitive.TooltipRootProps> = (props) => {
|
||||
return <TooltipPrimitive.Root gutter={4} {...props} />
|
||||
}
|
||||
|
||||
type TooltipContentProps<T extends ValidComponent = "div"> =
|
||||
TooltipPrimitive.TooltipContentProps<T> & { class?: string | undefined }
|
||||
|
||||
const TooltipContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, TooltipContentProps<T>>
|
||||
) => {
|
||||
const [local, others] = splitProps(props as TooltipContentProps, ["class"])
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
class={cn(
|
||||
"z-50 origin-[var(--kb-popover-content-transform-origin)] overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95",
|
||||
local.class
|
||||
)}
|
||||
{...others}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent }
|
31
src/data/apps.ts
Normal file
31
src/data/apps.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
export interface App {
|
||||
name: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
links: {
|
||||
appleStore?: string;
|
||||
fdroid?: string;
|
||||
flathub?: string;
|
||||
github?: string;
|
||||
googlePlay?: string;
|
||||
microsoftStore?: string;
|
||||
matrix?: string;
|
||||
snapcraft?: string;
|
||||
web?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const apps: App[] = [
|
||||
{
|
||||
name: 'Interstellar',
|
||||
icon: 'https://raw.githubusercontent.com/jwr1/interstellar/6d8fce0972febccec91fa056746fdb9f4f960217/assets/icons/logo.png',
|
||||
description: 'An app for Mbin and Lemmy, connecting you to the fediverse.',
|
||||
links: {
|
||||
flathub: 'https://flathub.org/apps/one.jwr.interstellar',
|
||||
github: 'https://github.com/jwr1/interstellar',
|
||||
googlePlay:
|
||||
'https://play.google.com/store/apps/details?id=one.jwr.interstellar',
|
||||
matrix: 'https://matrix.to/#/#interstellar-space:matrix.org',
|
||||
},
|
||||
},
|
||||
];
|
|
@ -3,6 +3,32 @@ import fs from 'node:fs/promises';
|
|||
await fs.rm('./.output/data', { recursive: true, force: true });
|
||||
await fs.mkdir('./.output/data', { recursive: true });
|
||||
|
||||
/** @returns {Promise<Array<import('./routes/releases').Release>>} */
|
||||
const fetchReleases = async () => {
|
||||
const releasesJson = await (
|
||||
await fetch(
|
||||
'https://api.github.com/repos/mbinOrg/mbin/releases?per_page=100',
|
||||
)
|
||||
).json();
|
||||
|
||||
/** @type {Array<import('./routes/releases').Release>} */
|
||||
const output = releasesJson.map((v, i, a) => ({
|
||||
version: v.tag_name.substring(1),
|
||||
// A server is considered outdated if a newer version has been available for more than 30 days.
|
||||
outdated:
|
||||
i > 0 &&
|
||||
Date.now() - Date.parse(a[i - 1].published_at) > 1000 * 60 * 60 * 24 * 30,
|
||||
publishedAt: v.published_at,
|
||||
githubUrl: v.html_url,
|
||||
body: v.body,
|
||||
}));
|
||||
|
||||
return output.sort((a, b) => a.publishedAt - b.publishedAt);
|
||||
};
|
||||
|
||||
const releases = await fetchReleases();
|
||||
fs.writeFile('./.output/data/releases.json', JSON.stringify(releases), 'utf8');
|
||||
|
||||
/**
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
|
@ -73,17 +99,29 @@ const fetchServerList = async () => {
|
|||
const fetchServerInfo = async (domain) => {
|
||||
console.log('START:', domain);
|
||||
|
||||
const jsonNodeInfo = await (
|
||||
let jsonNodeInfo;
|
||||
try {
|
||||
jsonNodeInfo = await (
|
||||
await fetch(`https://${domain}/nodeinfo/2.1.json`)
|
||||
).json();
|
||||
|
||||
if (jsonNodeInfo.software.name != 'mbin') {
|
||||
throw new Error(`${domain} software does not match mbin (skipped)`);
|
||||
throw new Error(`software check failed`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`${domain}: invalid nodeinfo response (skip)`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
const jsonApiInfo = await (await fetch(`https://${domain}/api/info`)).json();
|
||||
/** @type {import('./routes/servers').Server['api']} */
|
||||
let apiOutput;
|
||||
try {
|
||||
const jsonApiInfo = await (
|
||||
await fetch(`https://${domain}/api/info`)
|
||||
).json();
|
||||
if (jsonApiInfo.websiteDomain != domain) {
|
||||
throw new Error(`${domain} api not setup correctly (skipped)`);
|
||||
throw new Error(`domain check failed`);
|
||||
}
|
||||
const jsonApiInstance = await (
|
||||
await fetch(`https://${domain}/api/instance`)
|
||||
|
@ -92,43 +130,60 @@ const fetchServerInfo = async (domain) => {
|
|||
await fetch(`https://${domain}/api/defederated`)
|
||||
).json();
|
||||
|
||||
console.log('FINISH:', domain);
|
||||
apiOutput = {
|
||||
defaultLang: jsonApiInfo.websiteDefaultLang ?? 'en',
|
||||
federationEnabled: jsonApiInfo.websiteFederationEnabled,
|
||||
contactEmail: jsonApiInfo.websiteContactEmail,
|
||||
pages: jsonApiInstance,
|
||||
defederated: jsonApiDefederated.instances ?? [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(
|
||||
new Error(`${domain}: invalid api response (continue)`, {
|
||||
cause: error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
/** @type {import('./routes/servers').Server} */
|
||||
const output = {
|
||||
domain: domain,
|
||||
version: jsonNodeInfo.software.version,
|
||||
versionOutdated: releases.find(
|
||||
(v) => v.version === jsonNodeInfo.software.version,
|
||||
).outdated,
|
||||
name: jsonNodeInfo.metadata.nodeName,
|
||||
description: jsonNodeInfo.metadata.nodeDescription,
|
||||
openRegistrations: jsonNodeInfo.openRegistrations,
|
||||
federationEnabled: jsonApiInfo.websiteFederationEnabled,
|
||||
language: jsonApiInfo.websiteDefaultLang ?? 'en',
|
||||
contactEmail: jsonApiInfo.websiteContactEmail,
|
||||
totalUsers: jsonNodeInfo.usage.users.total,
|
||||
activeHalfyearUsers: jsonNodeInfo.usage.users.activeHalfyear,
|
||||
activeMonthUsers: jsonNodeInfo.usage.users.activeMonth,
|
||||
localPosts: jsonNodeInfo.usage.localPosts,
|
||||
localComments: jsonNodeInfo.usage.localComments,
|
||||
pages: jsonApiInstance,
|
||||
defederated: jsonApiDefederated.instances ?? [],
|
||||
api: apiOutput,
|
||||
};
|
||||
|
||||
console.log('FINISH:', domain);
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
const initServerData = async () => {
|
||||
const servers = await fetchServerList();
|
||||
const serverDomains = await fetchServerList();
|
||||
|
||||
const serversJson = (await Promise.allSettled(servers.map(fetchServerInfo)))
|
||||
const serversJson = (
|
||||
await Promise.allSettled(serverDomains.map(fetchServerInfo))
|
||||
)
|
||||
.filter((v) => {
|
||||
const isOk = v.status == 'fulfilled';
|
||||
|
||||
if (!isOk) {
|
||||
console.error(v.reason);
|
||||
}
|
||||
if (!isOk) console.error(v.reason);
|
||||
|
||||
return isOk;
|
||||
})
|
||||
.map((v) => v.value);
|
||||
|
||||
console.log('Successful Mbin servers found:', serversJson.length);
|
||||
console.log('Mbin servers found:', serversJson.length);
|
||||
|
||||
fs.writeFile(
|
||||
'./.output/data/servers.json',
|
||||
|
|
116
src/routes/apps.tsx
Normal file
116
src/routes/apps.tsx
Normal file
|
@ -0,0 +1,116 @@
|
|||
import { ComponentProps, For, JSX, Show } from 'solid-js';
|
||||
import Chip from '~/components/Chip';
|
||||
|
||||
import SimpleIconsAppstore from '~icons/simple-icons/appstore';
|
||||
import SimpleIconsFdroid from '~icons/simple-icons/fdroid';
|
||||
import SimpleIconsFlathub from '~icons/simple-icons/flathub';
|
||||
import SimpleIconsGithub from '~icons/simple-icons/github';
|
||||
import SimpleIconsGoogleplay from '~icons/simple-icons/googleplay';
|
||||
import SimpleIconsMatrix from '~icons/simple-icons/matrix';
|
||||
import SimpleIconsSnapcraft from '~icons/simple-icons/snapcraft';
|
||||
import MaterialSymbolsWeb from '~icons/material-symbols/web';
|
||||
|
||||
import { apps, App } from '~/data/apps';
|
||||
|
||||
const linkMap: Record<
|
||||
keyof App['links'],
|
||||
{ name: string; icon: (props: ComponentProps<'svg'>) => JSX.Element }
|
||||
> = {
|
||||
github: {
|
||||
name: 'GitHub',
|
||||
icon: SimpleIconsGithub,
|
||||
},
|
||||
matrix: {
|
||||
name: 'Matrix',
|
||||
icon: SimpleIconsMatrix,
|
||||
},
|
||||
appleStore: {
|
||||
name: 'Apple App Store',
|
||||
icon: SimpleIconsAppstore,
|
||||
},
|
||||
fdroid: {
|
||||
name: 'F-Droid',
|
||||
icon: SimpleIconsFdroid,
|
||||
},
|
||||
flathub: {
|
||||
name: 'Flathub',
|
||||
icon: SimpleIconsFlathub,
|
||||
},
|
||||
googlePlay: {
|
||||
name: 'Google Play',
|
||||
icon: SimpleIconsGoogleplay,
|
||||
},
|
||||
microsoftStore: {
|
||||
name: 'Microsoft Store',
|
||||
icon: () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M8 3.75V6H2.75a.75.75 0 0 0-.75.75v11.5A2.75 2.75 0 0 0 4.75 21h14.5A2.75 2.75 0 0 0 22 18.25V6.75a.75.75 0 0 0-.75-.75H16V3.75A1.75 1.75 0 0 0 14.25 2h-4.5A1.75 1.75 0 0 0 8 3.75m1.75-.25h4.5a.25.25 0 0 1 .25.25V6h-5V3.75a.25.25 0 0 1 .25-.25M8 13V9.5h3.5V13zm0 4.5V14h3.5v3.5zm8-4.5h-3.5V9.5H16zm-3.5 4.5V14H16v3.5z"
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
snapcraft: {
|
||||
name: 'Snapcraft',
|
||||
icon: SimpleIconsSnapcraft,
|
||||
},
|
||||
web: {
|
||||
name: 'Web',
|
||||
icon: MaterialSymbolsWeb,
|
||||
},
|
||||
};
|
||||
|
||||
export default function ServersPage() {
|
||||
return (
|
||||
<main class="mx-auto p-4 max-w-screen-xl">
|
||||
<h1 class="max-6-xs text-6xl text-sky-600 font-extralight uppercase my-16">
|
||||
Mbin Apps
|
||||
</h1>
|
||||
|
||||
<div class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
||||
<For each={apps}>
|
||||
{(app) => {
|
||||
return (
|
||||
<div class="border rounded-lg p-4 block relative text-center">
|
||||
<img src={app.icon} class="inline-block size-16 rounded" />
|
||||
<br />
|
||||
<div class="text-3xl text-sky-600">{app.name}</div>
|
||||
|
||||
<p class="text-center">{app.description}</p>
|
||||
|
||||
<div class="my-1 flex flex-wrap gap-1">
|
||||
<For each={Object.entries(linkMap)}>
|
||||
{([key, value]) => (
|
||||
<Show when={app.links[key]}>
|
||||
<Chip href={app.links[key]} icon={value.icon}>
|
||||
{value.name}
|
||||
</Chip>
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div class="my-8 text-center font-light">
|
||||
Don't see your app here? Contribute to the list by making a pull request{' '}
|
||||
<a
|
||||
href="https://github.com/MbinOrg/mbin-website/blob/main/src/data/apps.ts"
|
||||
class="text-sky-600"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
67
src/routes/releases.tsx
Normal file
67
src/routes/releases.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { For } from 'solid-js';
|
||||
import releasesJson from '../../.output/data/releases.json';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '~/components/ui/accordion';
|
||||
import Markdown from '~/components/Markdown';
|
||||
import SimpleIconsGithub from '~icons/simple-icons/github';
|
||||
import { useSearchParams } from '@solidjs/router';
|
||||
|
||||
const releases = releasesJson as Release[];
|
||||
|
||||
export interface Release {
|
||||
version: string;
|
||||
outdated: boolean;
|
||||
publishedAt: string;
|
||||
githubUrl: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export default function ReleasesPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
return (
|
||||
<main class="mx-auto p-4 max-w-screen-xl">
|
||||
<h1 class="max-6-xs text-6xl text-sky-600 font-extralight uppercase my-16">
|
||||
Mbin Releases
|
||||
</h1>
|
||||
|
||||
<div class="mb-12 font-light">
|
||||
Also view releases on{' '}
|
||||
<a href="https://github.com/MbinOrg/mbin/releases" class="text-sky-600">
|
||||
GitHub
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
|
||||
<Accordion
|
||||
multiple
|
||||
defaultValue={[searchParams.version ?? releases[0].version]}
|
||||
>
|
||||
<For each={releases}>
|
||||
{(release) => (
|
||||
<AccordionItem value={release.version}>
|
||||
<AccordionTrigger class="text-lg">
|
||||
<span class="inline-flex items-center gap-3">
|
||||
{release.version}
|
||||
<span class="text-base font-light">
|
||||
{new Date(release.publishedAt).toLocaleDateString()}
|
||||
</span>
|
||||
<a href={release.githubUrl} class="hover:text-blue-500">
|
||||
<SimpleIconsGithub />
|
||||
</a>
|
||||
</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Markdown>{release.body}</Markdown>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
)}
|
||||
</For>
|
||||
</Accordion>
|
||||
</main>
|
||||
);
|
||||
}
|
|
@ -23,6 +23,7 @@ import MaterialSymbolsPerson from '~icons/material-symbols/person';
|
|||
import MaterialSymbolsPersonCheck from '~icons/material-symbols/person-check';
|
||||
import MaterialSymbolsNews from '~icons/material-symbols/news';
|
||||
import MaterialSymbolsComment from '~icons/material-symbols/comment';
|
||||
import MaterialSymbolsWarning from '~icons/material-symbols/warning';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
@ -32,23 +33,30 @@ import {
|
|||
} from '~/components/ui/select';
|
||||
import { Checkbox } from '~/components/ui/checkbox';
|
||||
import { Label } from '~/components/ui/label';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '~/components/ui/tooltip';
|
||||
|
||||
const servers = serversJson as Server[];
|
||||
|
||||
export interface Server {
|
||||
domain: string;
|
||||
version: string;
|
||||
versionOutdated: boolean;
|
||||
name: string;
|
||||
description: string;
|
||||
openRegistrations: boolean;
|
||||
federationEnabled: boolean;
|
||||
language: string;
|
||||
contactEmail: string;
|
||||
totalUsers: number;
|
||||
activeHalfyearUsers: number;
|
||||
activeMonthUsers: number;
|
||||
localPosts: number;
|
||||
localComments: number;
|
||||
api?: {
|
||||
contactEmail: string;
|
||||
federationEnabled: boolean;
|
||||
defaultLang: string;
|
||||
pages: {
|
||||
about?: string;
|
||||
contact?: string;
|
||||
|
@ -57,9 +65,10 @@ export interface Server {
|
|||
terms?: string;
|
||||
};
|
||||
defederated: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const pageNames: Required<Server['pages']> = {
|
||||
const pageNames: Required<NonNullable<Server['api']>['pages']> = {
|
||||
about: 'About',
|
||||
contact: 'Contact',
|
||||
faq: 'Frequently Asked Questions',
|
||||
|
@ -88,7 +97,7 @@ export default function ServersPage() {
|
|||
.filter(
|
||||
(server) =>
|
||||
(!filterRegistration() || server.openRegistrations) &&
|
||||
(!langFilter() || server.language == langFilter()),
|
||||
(!langFilter() || server.api?.defaultLang == langFilter()),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
switch (sort()) {
|
||||
|
@ -109,6 +118,17 @@ export default function ServersPage() {
|
|||
<h1 class="max-6-xs text-6xl text-sky-600 font-extralight uppercase my-16">
|
||||
Mbin Servers
|
||||
</h1>
|
||||
<div class="mb-12 font-light">
|
||||
Also view servers on{' '}
|
||||
<a href="https://fedidb.org/software/mbin" class="text-sky-600">
|
||||
FediDB
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
<a href="https://mbin.fediverse.observer/list" class="text-sky-600">
|
||||
Fediverse Observer
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
<div class="flex">
|
||||
<Checkbox
|
||||
id="open-registration"
|
||||
|
@ -123,7 +143,11 @@ export default function ServersPage() {
|
|||
<Select
|
||||
value={langFilter()}
|
||||
onChange={setLangFilter}
|
||||
options={[...new Set(servers.map((v) => v.language))]}
|
||||
options={[
|
||||
...new Set(
|
||||
servers.filter((v) => v.api).map((v) => v.api?.defaultLang),
|
||||
),
|
||||
]}
|
||||
placeholder="All Languages"
|
||||
itemComponent={(props) => (
|
||||
<SelectItem item={props.item}>
|
||||
|
@ -131,7 +155,7 @@ export default function ServersPage() {
|
|||
</SelectItem>
|
||||
)}
|
||||
>
|
||||
<SelectTrigger aria-label="Fruit" class="w-[180px]">
|
||||
<SelectTrigger aria-label="Language" class="w-[180px]">
|
||||
<SelectValue<string>>
|
||||
{(state) => languageNames.of(state.selectedOption())}
|
||||
</SelectValue>
|
||||
|
@ -151,7 +175,7 @@ export default function ServersPage() {
|
|||
)}
|
||||
>
|
||||
<SelectTrigger aria-label="Fruit" class="w-[180px]">
|
||||
<SelectValue<string>>
|
||||
<SelectValue<keyof typeof sortNameMap>>
|
||||
{(state) => sortNameMap[state.selectedOption()]}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
|
@ -167,20 +191,50 @@ export default function ServersPage() {
|
|||
users
|
||||
</Chip>
|
||||
</div>
|
||||
<div
|
||||
class="grid gap-4"
|
||||
style={{
|
||||
'grid-template-columns': 'repeat(auto-fit, minmax(350px, 1fr))',
|
||||
}}
|
||||
>
|
||||
<div class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
||||
<For each={resultServers()}>
|
||||
{(server) => {
|
||||
const StatChips = () => (
|
||||
<>
|
||||
<Chip title="Mbin Version">Mbin {server.version}</Chip>
|
||||
<Chip title="Language" icon={MaterialSymbolsLanguage}>
|
||||
{languageNames.of(server.language)}
|
||||
<Chip
|
||||
classList={{
|
||||
'bg-red-900 bg-opacity-40': server.versionOutdated,
|
||||
}}
|
||||
href={`/releases?version=${server.version}`}
|
||||
>
|
||||
Mbin {server.version}
|
||||
<Show when={server.versionOutdated}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<MaterialSymbolsWarning class="ml-1 text-red-500" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
This server is using an outdated version. Please ask the
|
||||
server admin to upgrade or use a different server.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</Chip>
|
||||
<Show when={!server.api}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Chip class="bg-red-900 bg-opacity-40">
|
||||
No API
|
||||
<MaterialSymbolsWarning class="ml-1 text-red-500" />
|
||||
</Chip>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
This server's api is inaccessible and will not work with
|
||||
apps. Please ask the server admin to fix the issue or use
|
||||
a different server.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<Show when={server.api}>
|
||||
<Chip title="Language" icon={MaterialSymbolsLanguage}>
|
||||
{languageNames.of(server.api!.defaultLang)}
|
||||
</Chip>
|
||||
</Show>
|
||||
<Chip title="Total users" icon={MaterialSymbolsPerson}>
|
||||
{server.totalUsers}
|
||||
</Chip>
|
||||
|
@ -199,7 +253,7 @@ export default function ServersPage() {
|
|||
>
|
||||
<img
|
||||
src={`https://${server.domain}/favicon.ico`}
|
||||
class="inline-block size-16"
|
||||
class="inline-block size-16 rounded"
|
||||
/>
|
||||
<br />
|
||||
<div class="text-3xl text-sky-600">{server.domain}</div>
|
||||
|
@ -254,25 +308,41 @@ export default function ServersPage() {
|
|||
</Chip>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={server.api}
|
||||
fallback={
|
||||
<div>
|
||||
No information available due to inaccessible api.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Accordion
|
||||
multiple={false}
|
||||
collapsible
|
||||
defaultValue={['about']}
|
||||
>
|
||||
<For each={Object.entries(server.pages)}>
|
||||
<For each={Object.entries(server.api!.pages)}>
|
||||
{([page, pageContent]) => (
|
||||
<Show when={pageContent || page === 'contact'}>
|
||||
<AccordionItem value={page}>
|
||||
<AccordionTrigger>
|
||||
{pageNames[page as keyof Server['pages']]}
|
||||
{
|
||||
pageNames[
|
||||
page as keyof NonNullable<
|
||||
Server['api']
|
||||
>['pages']
|
||||
]
|
||||
}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Show when={page == 'contact'}>
|
||||
<Button
|
||||
href={`mailto:${server.contactEmail}`}
|
||||
href={`mailto:${
|
||||
server.api!.contactEmail
|
||||
}`}
|
||||
as="a"
|
||||
>
|
||||
Email admin: {server.contactEmail}
|
||||
Email admin: {server.api!.contactEmail}
|
||||
</Button>
|
||||
</Show>
|
||||
<Markdown>{pageContent}</Markdown>
|
||||
|
@ -281,14 +351,15 @@ export default function ServersPage() {
|
|||
</Show>
|
||||
)}
|
||||
</For>
|
||||
<Show when={server.defederated.length}>
|
||||
<Show when={server.api!.defederated.length}>
|
||||
<AccordionItem value="defederated">
|
||||
<AccordionTrigger>
|
||||
Defederated Servers ({server.defederated.length})
|
||||
Defederated Servers (
|
||||
{server.api!.defederated.length})
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<For each={server.defederated}>
|
||||
<For each={server.api!.defederated}>
|
||||
{(server) => <Chip>{server}</Chip>}
|
||||
</For>
|
||||
</div>
|
||||
|
@ -296,6 +367,7 @@ export default function ServersPage() {
|
|||
</AccordionItem>
|
||||
</Show>
|
||||
</Accordion>
|
||||
</Show>
|
||||
|
||||
<Show when={server.openRegistrations}>
|
||||
<DialogFooter>
|
||||
|
|
Loading…
Add table
Reference in a new issue