mirror of
https://github.com/NickKaramoff/toot
synced 2025-06-05 21:59:33 +02:00
WIP
This commit is contained in:
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# Fresh build directory
|
||||||
|
_fresh/
|
||||||
|
# npm dependencies
|
||||||
|
node_modules/
|
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"denoland.vscode-deno"
|
||||||
|
]
|
||||||
|
}
|
17
.vscode/settings.json
vendored
Normal file
17
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"deno.enable": true,
|
||||||
|
"deno.lint": true,
|
||||||
|
"editor.defaultFormatter": "denoland.vscode-deno",
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "denoland.vscode-deno"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "denoland.vscode-deno"
|
||||||
|
},
|
||||||
|
"[javascriptreact]": {
|
||||||
|
"editor.defaultFormatter": "denoland.vscode-deno"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "denoland.vscode-deno"
|
||||||
|
}
|
||||||
|
}
|
16
README.md
Normal file
16
README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Fresh project
|
||||||
|
|
||||||
|
Your new Fresh project is ready to go. You can follow the Fresh "Getting
|
||||||
|
Started" guide here: https://fresh.deno.dev/docs/getting-started
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
Make sure to install Deno: https://deno.land/manual/getting_started/installation
|
||||||
|
|
||||||
|
Then start the project:
|
||||||
|
|
||||||
|
```
|
||||||
|
deno task start
|
||||||
|
```
|
||||||
|
|
||||||
|
This will watch the project directory and restart as necessary.
|
12
components/Button.tsx
Normal file
12
components/Button.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { JSX } from "preact";
|
||||||
|
import { IS_BROWSER } from "$fresh/runtime.ts";
|
||||||
|
|
||||||
|
export function Button(props: JSX.HTMLAttributes<HTMLButtonElement>) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
disabled={!IS_BROWSER || props.disabled}
|
||||||
|
class="px-2 py-1 border-gray-500 border-2 rounded bg-white hover:bg-gray-200 transition-colors"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
27
deno.json
Normal file
27
deno.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"lock": false,
|
||||||
|
"tasks": {
|
||||||
|
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
|
||||||
|
"cli": "echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -",
|
||||||
|
"manifest": "deno task cli manifest $(pwd)",
|
||||||
|
"start": "deno run -A --watch=static/,routes/ dev.ts",
|
||||||
|
"build": "deno run -A dev.ts build",
|
||||||
|
"preview": "deno run -A main.ts",
|
||||||
|
"update": "deno run -A -r https://fresh.deno.dev/update ."
|
||||||
|
},
|
||||||
|
"lint": { "rules": { "tags": ["fresh", "recommended"] } },
|
||||||
|
"exclude": ["**/_fresh/*"],
|
||||||
|
"imports": {
|
||||||
|
"$fresh/": "https://deno.land/x/fresh@1.7.1/",
|
||||||
|
"$lib/": "./lib/",
|
||||||
|
"$i18n/": "./i18n/",
|
||||||
|
"@http/response": "jsr:@http/response@^0.21.0",
|
||||||
|
"@preact/signals": "https://esm.sh/*@preact/signals@1.3.0",
|
||||||
|
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.8.0",
|
||||||
|
"@std/dotenv": "jsr:@std/dotenv@^0.225.1",
|
||||||
|
"preact": "https://esm.sh/preact@10.23.2",
|
||||||
|
"preact/": "https://esm.sh/preact@10.23.2/",
|
||||||
|
"zod": "https://deno.land/x/zod@v3.23.8/mod.ts"
|
||||||
|
},
|
||||||
|
"compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" }
|
||||||
|
}
|
8
dev.ts
Executable file
8
dev.ts
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env -S deno run -A --watch=static/,routes/
|
||||||
|
|
||||||
|
import dev from "$fresh/dev.ts";
|
||||||
|
import config from "./fresh.config.ts";
|
||||||
|
|
||||||
|
import "@std/dotenv/load";
|
||||||
|
|
||||||
|
await dev(import.meta.url, "./main.ts", config);
|
7
fresh.config.ts
Normal file
7
fresh.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "$fresh/server.ts";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
port: 9979,
|
||||||
|
},
|
||||||
|
});
|
31
fresh.gen.ts
Normal file
31
fresh.gen.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// DO NOT EDIT. This file is generated by Fresh.
|
||||||
|
// This file SHOULD be checked into source version control.
|
||||||
|
// This file is automatically updated during development when running `dev.ts`.
|
||||||
|
|
||||||
|
import * as $_404 from "./routes/_404.tsx";
|
||||||
|
import * as $_app from "./routes/_app.tsx";
|
||||||
|
import * as $_middleware from "./routes/_middleware.ts";
|
||||||
|
import * as $api_detect_domain_ from "./routes/api/detect/[domain].ts";
|
||||||
|
import * as $api_instances from "./routes/api/instances.ts";
|
||||||
|
import * as $greet_name_ from "./routes/greet/[name].tsx";
|
||||||
|
import * as $index from "./routes/index.tsx";
|
||||||
|
import * as $Counter from "./islands/Counter.tsx";
|
||||||
|
import type { Manifest } from "$fresh/server.ts";
|
||||||
|
|
||||||
|
const manifest = {
|
||||||
|
routes: {
|
||||||
|
"./routes/_404.tsx": $_404,
|
||||||
|
"./routes/_app.tsx": $_app,
|
||||||
|
"./routes/_middleware.ts": $_middleware,
|
||||||
|
"./routes/api/detect/[domain].ts": $api_detect_domain_,
|
||||||
|
"./routes/api/instances.ts": $api_instances,
|
||||||
|
"./routes/greet/[name].tsx": $greet_name_,
|
||||||
|
"./routes/index.tsx": $index,
|
||||||
|
},
|
||||||
|
islands: {
|
||||||
|
"./islands/Counter.tsx": $Counter,
|
||||||
|
},
|
||||||
|
baseUrl: import.meta.url,
|
||||||
|
} satisfies Manifest;
|
||||||
|
|
||||||
|
export default manifest;
|
78
i18n/engine.ts
Normal file
78
i18n/engine.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/*!
|
||||||
|
* This file is part of Share₂Fedi
|
||||||
|
* https://github.com/kytta/share2fedi
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: © 2023 Nikita Karamov <me@kytta.dev>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defaultLanguage, languages, strings } from "./translations";
|
||||||
|
|
||||||
|
export function useTranslations(language: string) {
|
||||||
|
return function t(
|
||||||
|
key: keyof (typeof strings)[typeof defaultLanguage],
|
||||||
|
): string {
|
||||||
|
return (
|
||||||
|
strings[language as keyof typeof strings][key] ||
|
||||||
|
strings[defaultLanguage][key] ||
|
||||||
|
""
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findBestLanguage(): string {
|
||||||
|
const urlLanguage = new URLSearchParams(window.location.search).get("lang");
|
||||||
|
if (urlLanguage && urlLanguage in languages) {
|
||||||
|
return urlLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
let browserLanguages = navigator.languages;
|
||||||
|
if (!navigator.languages) browserLanguages = [navigator.language];
|
||||||
|
for (const language of browserLanguages) {
|
||||||
|
const locale = new Intl.Locale(language);
|
||||||
|
const minimized = locale.minimize();
|
||||||
|
|
||||||
|
for (const candidate of [locale.baseName, minimized.baseName]) {
|
||||||
|
if (candidate in languages) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyTranslations(language: string) {
|
||||||
|
if (!(language in strings)) {
|
||||||
|
language = defaultLanguage;
|
||||||
|
}
|
||||||
|
const t = useTranslations(language);
|
||||||
|
|
||||||
|
for (const node of document.querySelectorAll("[data-translate]")) {
|
||||||
|
const dataset = (node as HTMLElement).dataset;
|
||||||
|
|
||||||
|
if (dataset.translateAttribute) {
|
||||||
|
node.setAttribute(dataset.translateAttribute, t(dataset.translate!));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let splitTranslated = t(dataset.translate!).split("{}");
|
||||||
|
if (splitTranslated.length === 1) {
|
||||||
|
node.innerHTML = t(dataset.translate!);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// XXX: this is needed for the strings where the placholder sits at the very
|
||||||
|
// beginning, which introduces phantom empty strings.
|
||||||
|
splitTranslated = splitTranslated.filter((string) => string !== "");
|
||||||
|
|
||||||
|
for (const child of node.childNodes) {
|
||||||
|
if (child.nodeType === Node.TEXT_NODE) {
|
||||||
|
child.textContent = splitTranslated.shift() || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.documentElement.lang = language;
|
||||||
|
document.documentElement.dir =
|
||||||
|
languages[language as keyof typeof languages].dir;
|
||||||
|
}
|
34
i18n/translations.ts
Normal file
34
i18n/translations.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*!
|
||||||
|
* This file is part of Share₂Fedi
|
||||||
|
* https://github.com/kytta/share2fedi
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: © 2023 Nikita Karamov <me@kytta.dev>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import de from "./translations/de.json" with {type: "json"};
|
||||||
|
import en from "./translations/en.json" with {type: "json"};
|
||||||
|
import es from "./translations/es.json" with {type: "json"};
|
||||||
|
import fr from "./translations/fr.json" with {type: "json"};
|
||||||
|
import nl from "./translations/nl.json" with {type: "json"};
|
||||||
|
import ru from "./translations/ru.json" with {type: "json"};
|
||||||
|
|
||||||
|
export const languages = {
|
||||||
|
en: { autonym: "English", dir: "ltr" },
|
||||||
|
de: { autonym: "Deutsch", dir: "ltr" },
|
||||||
|
es: { autonym: "Español", dir: "ltr" },
|
||||||
|
fr: { autonym: "Français", dir: "ltr" },
|
||||||
|
nl: { autonym: "Nederlands", dir: "ltr" },
|
||||||
|
ru: { autonym: "Русский", dir: "ltr" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const strings: Record<keyof typeof languages, Record<string, string>> = {
|
||||||
|
en,
|
||||||
|
de,
|
||||||
|
es,
|
||||||
|
fr,
|
||||||
|
nl,
|
||||||
|
ru,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const defaultLanguage: keyof typeof strings = "en";
|
23
i18n/translations/ar.json
Normal file
23
i18n/translations/ar.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"privacyNotice": "إشعار الخصوصية",
|
||||||
|
"postText": "نص المنشور{}",
|
||||||
|
"fediverse": "الفديفرس",
|
||||||
|
"vercelPP": "سياسة الخصوصية لـ Vercel",
|
||||||
|
"description": "شير تو فيدي Share₂Fedi هو موقع مشاركة لـ {}. يمكنك من خلاله نشر محتوى على منصات متعددة تتبع نموذج الفديرالية من صفحة واحدة.",
|
||||||
|
"incl": "بما فيه",
|
||||||
|
"postTextPlaceholder": "فيمَ تفكّر؟",
|
||||||
|
"rememberInstance": "{} تذكر مثيل الخادم على هذا الجهاز",
|
||||||
|
"instance": "مثيل خادم الفديفرس{}",
|
||||||
|
"licence1": "شير تو فيدي Share₂Fedi هو برنامج حر: يمكنك إعادة توزيعه و/أو تعديله وفقًا لشروط الرخصة العامة GNU Affero، الإصدار 3، كما نَشرَتها مؤسسة البرمجيات الحرة.",
|
||||||
|
"previouslyUsed": "تم استخدامه مسبقًا: {}",
|
||||||
|
"metaDescription": "شير تو فيدي Share₂Fedi هي صفحة مشاركة لـ Mastodon و Misskey و Friendica وغيرها. اكتب نص منشورك وعنوان الرابط التشعبي لمثيل الخادم، ثم انقر على ”أنشر“!",
|
||||||
|
"nikita": "نيكيتا كاراموف",
|
||||||
|
"statusPage": "صفحة الحالة",
|
||||||
|
"publish": "أنشر",
|
||||||
|
"licence": "الرخصة",
|
||||||
|
"language": "اللغة: {}",
|
||||||
|
"supportedProjects": "المشاريع المدعومة:",
|
||||||
|
"privacy2": "عندما تنقر على زر ”أنشر“، سيتم توجيهك إلى مثيل خادم الفديفرس الذي حددته. قد يقوم بمعالجة و/أو تخزين بياناتك. يرجى الرجوع إلى سياسة الخصوصية للخادم المعني.",
|
||||||
|
"onGitHub": "على جت هب",
|
||||||
|
"credits": "تم تطوير Share₂Fedi وصيانته من طرف {}. الشيفرة المصدرية متوفرة على {}. مستضافة على {}. {}."
|
||||||
|
}
|
25
i18n/translations/de.json
Normal file
25
i18n/translations/de.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"metaDescription": "Share₂Fedi ist eine Share-Seite für Mastodon, Misskey, Friendica und andere. Geben Sie Ihren Beitragstext und die Instanz-URL ein und klicken Sie auf „Veröffentlichen“!",
|
||||||
|
"language": "Sprache: {}",
|
||||||
|
"description": "Share₂Fedi ist eine instanzunabhängige Share-Seite für {}. Mit ihr können Sie von einer einzigen Seite aus auf verschiedenen föderierten Plattformen posten.",
|
||||||
|
"fediverse": "das Fediverse",
|
||||||
|
"supportedProjects": "Unterstützte Projekte:",
|
||||||
|
"incl": "inkl.",
|
||||||
|
"credits": "Share₂Fedi wird von {} entwickelt und gepflegt. Der Quellcode ist {}. Gehostet mit {}. {}.",
|
||||||
|
"nikita": "Nikita Karamov",
|
||||||
|
"onGitHub": "auf GitHub",
|
||||||
|
"statusPage": "Statusseite",
|
||||||
|
"licence": "Lizenz",
|
||||||
|
"licence1": "Share₂Fedi ist freie Software: Sie können es unter den Bedingungen der GNU Affero General Public License, Version 3, wie von der Free Software Foundation veröffentlicht, weitergeben und/oder modifizieren.",
|
||||||
|
"licence2": "Die Veröffentlichung von Share₂Fedi erfolgt in der Hoffnung, dass es Ihnen von Nutzen sein wird, aber OHNE IRGENDEINE GARANTIE, sogar ohne die implizite Garantie der MARKTREIFE oder der VERWENDBARKEIT FÜR EINEN BESTIMMTEN ZWECK. Details finden Sie in der GNU Affero General Public License.",
|
||||||
|
"privacyNotice": "Datenschutzhinweis",
|
||||||
|
"privacy1": "s2f.kytta.dev wird auf Vercel gehostet. Vercel verarbeitet IP-Adressen, Systemkonfigurationsinformationen und andere Informationen über den Verkehr von und zu s2f.kytta.dev. Vercel speichert diese Informationen nicht und gibt sie auch nicht an Dritte weiter. Siehe {} für weitere Informationen.",
|
||||||
|
"privacy2": "Wenn Sie auf die „Veröffentlichen“ klicken, werden Sie zu einer Fediverse-Instanz weitergeleitet, die Sie angegeben haben. Diese kann Ihre Daten verarbeiten und/oder speichern. Bitte beachten Sie die Datenschutzrichtlinien der jeweiligen Instanz.",
|
||||||
|
"vercelPP": "Vercels Datenschutzpolitik",
|
||||||
|
"postText": "Beitragstext{}",
|
||||||
|
"postTextPlaceholder": "Was gibt’s Neues?",
|
||||||
|
"instance": "Fediverse-Instanz{}",
|
||||||
|
"previouslyUsed": "Bisher verwendet: {}",
|
||||||
|
"rememberInstance": "{} Instanz auf diesem Gerät merken",
|
||||||
|
"publish": "Veröffentlichen"
|
||||||
|
}
|
25
i18n/translations/en.json
Normal file
25
i18n/translations/en.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"metaDescription": "Share₂Fedi is a share page for Mastodon, Misskey, Friendica, and others. Type in your post text and the instance URL and click ‘Publish’!",
|
||||||
|
"language": "Language: {}",
|
||||||
|
"description": "Share₂Fedi is an instance-agnostic share page for {}. With it, you can post to various federated platforms from a single page.",
|
||||||
|
"fediverse": "the Fediverse",
|
||||||
|
"supportedProjects": "Supported projects:",
|
||||||
|
"incl": "incl.",
|
||||||
|
"credits": "Share₂Fedi is developed and maintained by {}. Source code is {}. Hosted with {}. {}.",
|
||||||
|
"nikita": "Nikita Karamov",
|
||||||
|
"onGitHub": "on GitHub",
|
||||||
|
"statusPage": "Status page",
|
||||||
|
"licence": "Licence",
|
||||||
|
"licence1": "Share₂Fedi is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation.",
|
||||||
|
"licence2": "Share₂Fedi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.",
|
||||||
|
"privacyNotice": "Privacy Notice",
|
||||||
|
"privacy1": "s2f.kytta.dev is hosted on Vercel. Vercel processes IP addresses, system configuration information, and other information about traffic to and from s2f.kytta.dev. Vercel does not store this information nor does it get shared with third parties. See {} for more information.",
|
||||||
|
"privacy2": "When you click the ‘Publish’ button, you’ll get redirected to a Fediverse instance you’ve specified. It may process and/or store your data. Please refer to the privacy policy of the respective instance.",
|
||||||
|
"vercelPP": "Vercel’s privacy policy",
|
||||||
|
"postText": "Post text{}",
|
||||||
|
"postTextPlaceholder": "What’s on your mind?",
|
||||||
|
"instance": "Fediverse instance{}",
|
||||||
|
"previouslyUsed": "Previously used: {}",
|
||||||
|
"rememberInstance": "{} Remember instance on this device",
|
||||||
|
"publish": "Publish"
|
||||||
|
}
|
25
i18n/translations/es.json
Normal file
25
i18n/translations/es.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"metaDescription": "Share₂Fedi es una página de compartir para Mastodon, Misskey, Friendica y otros. ¡Escriba su texto de publicación y la URL de la instancia y haga clic en «Publicar»!",
|
||||||
|
"language": "Idioma: {}",
|
||||||
|
"description": "Share₂Fedi es una página de compartir independiente de la instancia para {}. Con ella, puede publicar en varias plataformas federadas desde una sola página.",
|
||||||
|
"fediverse": "el Fediverso",
|
||||||
|
"supportedProjects": "Proyectos compatibles:",
|
||||||
|
"incl": "incl.",
|
||||||
|
"credits": "Share₂Fedi es desarrollado y mantenido por {}. El código fuente es {}. Alojado con {}. {}.",
|
||||||
|
"nikita": "Nikita Karamov",
|
||||||
|
"onGitHub": "en GitHub",
|
||||||
|
"statusPage": "Página de estado",
|
||||||
|
"licence": "Licencia",
|
||||||
|
"licence1": "Share₂Fedi es un software libre: puedes redistribuirlo y/o modificarlo bajo los términos de la Licencia Pública General GNU Affero, versión 3, publicada por la Free Software Foundation.",
|
||||||
|
"licence2": "Share₂Fedi se distribuye con la esperanza de que sea útil, pero SIN NINGUNA GARANTÍA; sin siquiera la garantía implícita de COMERCIABILIDAD o IDONEIDAD PARA UN PROPÓSITO PARTICULAR. Consulte la Licencia Pública General Affero de GNU para obtener más detalles.",
|
||||||
|
"privacyNotice": "Aviso de privacidad",
|
||||||
|
"privacy1": "s2f.kytta.dev está alojado en Vercel. Vercel procesa las direcciones IP, la información de configuración del sistema y otra información sobre el tráfico hacia y desde s2f.kytta.dev. Vercel no almacena esta información ni la comparte con terceros. Consulte {} para obtener más información.",
|
||||||
|
"privacy2": "Cuando hace clic en el botón «Publicar», se le redirige a una instancia de Fediverso que ha especificado. Puede procesar y/o almacenar sus datos. Consulte la política de privacidad de la instancia correspondiente.",
|
||||||
|
"vercelPP": "Política de privacidad de Vercel",
|
||||||
|
"postText": "Texto de la publicación{}",
|
||||||
|
"postTextPlaceholder": "¿Qué hay de nuevo?",
|
||||||
|
"instance": "Instancia del Fediverso{}",
|
||||||
|
"previouslyUsed": "Usado anteriormente: {}",
|
||||||
|
"rememberInstance": "{} Recordar instancia en este dispositivo",
|
||||||
|
"publish": "Publicar"
|
||||||
|
}
|
25
i18n/translations/fr.json
Normal file
25
i18n/translations/fr.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"metaDescription": "Share₂Fedi est une page de partage pour Mastodon, Misskey, Friendica et autres. Tapez votre texte de publication et l’URL de l’instance, puis cliquez sur « Publier » !",
|
||||||
|
"language": "Langue : {}",
|
||||||
|
"description": "Share₂Fedi est une page de partage indépendante pour {}. Avec elle, vous pouvez publier sur diverses plateformes fédérées depuis une seule page.",
|
||||||
|
"fediverse": "le Fediverse",
|
||||||
|
"supportedProjects": "Projets pris en charge :",
|
||||||
|
"incl": "dont",
|
||||||
|
"credits": "Share₂Fedi est développé et maintenu par {}. Le code source est {}. Hébergé avec {}. {}.",
|
||||||
|
"nikita": "Nikita Karamov",
|
||||||
|
"onGitHub": "sur GitHub",
|
||||||
|
"statusPage": "Page de statut",
|
||||||
|
"licence": "Licence",
|
||||||
|
"licence1": "Share₂Fedi est un logiciel libre : vous pouvez le redistribuer et/ou le modifier selon les termes de la licence publique générale Affero, version 3, telle que publiée par la Free Software Foundation.",
|
||||||
|
"licence2": "Share₂Fedi est distribué dans l’espoir qu’il sera utile, mais SANS AUCUNE GARANTIE ; sans même la garantie implicite de QUALITÉ MARCHANDE ou D’ADÉQUATION À UN USAGE PARTICULIER. Consultez la licence publique générale Affero de GNU pour plus de détails.",
|
||||||
|
"privacyNotice": "Avis de confidentialité",
|
||||||
|
"privacy1": "s2f.kytta.dev est hébergé sur Vercel. Vercel traite les adresses IP, les informations de configuration système et d’autres informations sur le trafic vers et depuis s2f.kytta.dev. Vercel ne stocke pas ces informations et ne les partage pas avec des tiers. Voir {} pour plus d’informations.",
|
||||||
|
"privacy2": "Lorsque vous cliquez sur le bouton « Publier », vous êtes redirigé vers une instance du Fediverse que vous avez spécifiée. Elle peut traiter et/ou stocker vos données. Veuillez vous référer à la politique de confidentialité de l’instance respective.",
|
||||||
|
"vercelPP": "Politique de confidentialité de Vercel",
|
||||||
|
"postText": "Texte de la publication{}",
|
||||||
|
"postTextPlaceholder": "Quoi de neuf ?",
|
||||||
|
"instance": "Instance du Fediverse{}",
|
||||||
|
"previouslyUsed": "Utilisé précédemment : {}",
|
||||||
|
"rememberInstance": "{} Se souvenir de l’instance sur cet appareil",
|
||||||
|
"publish": "Publier"
|
||||||
|
}
|
25
i18n/translations/nl.json
Normal file
25
i18n/translations/nl.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"metaDescription": "Share₂Fedi is een deelpagina voor Mastodon, Misskey, Friendica en andere fediversenetwerken. Voer een bericht in, plak de instantie-url en klik op ‘Publiceren’!",
|
||||||
|
"language": "Taal: {}",
|
||||||
|
"description": "Share₂Fedi is een instantie-onafhankelijke deelpagina voor {}. Zo kun je berichten plaatsen op meerdere fediversenetwerken tegelijk.",
|
||||||
|
"fediverse": "het Fediverse",
|
||||||
|
"supportedProjects": "Ondersteunde netwerken:",
|
||||||
|
"incl": "incl.",
|
||||||
|
"nikita": "Nikita Karamov",
|
||||||
|
"onGitHub": "op GitHub",
|
||||||
|
"statusPage": "Statuspagina",
|
||||||
|
"licence": "Licentie",
|
||||||
|
"privacyNotice": "Privacybeleid",
|
||||||
|
"privacy2": "Als je op publiceren klikt, wordt je doorgestuurd naar de opgegeven fediverse-instantie. Hierbij worden je gegevens mogelijk verwerkt en/of bewaard. Bekijk voor meer informatie het privacybeleid van je instantie.",
|
||||||
|
"vercelPP": "Vercels privacybeleid",
|
||||||
|
"postTextPlaceholder": "Wat gaat er door je hoofd?",
|
||||||
|
"instance": "Fediverse-instantie{}",
|
||||||
|
"postText": "Berichtinhoud{}",
|
||||||
|
"previouslyUsed": "Onlangs gebruikt: {}",
|
||||||
|
"rememberInstance": "{} Instantie onthouden op dit apparaat",
|
||||||
|
"publish": "Publiceren",
|
||||||
|
"credits": "Share₂Fedi wordt ontwikkeld en onderhouden door {}. Broncode: {} - Hosting: {}. {}.",
|
||||||
|
"licence1": "Share₂Fedi is vrije software: je mag de software opnieuw uitgeven en/of aanpassen onder de voorwaarden van de GNU Affero General Public License, versie 3, zoals omschreven door de Free Software Foundation.",
|
||||||
|
"licence2": "Share₂Fedi wordt verspreid in de hoop nuttig te zijn, maar wordt geleverd ZONDER ENIGE VORM VAN GARANTIE; zelfs zonder de garantie van VERKOOPBAARHEID of GESCHIKTHEID VOOR EEN BEPAALD DOEL. Bekijk voor meer informatie de GNU Affero General Public License.",
|
||||||
|
"privacy1": "s2f.kytta.dev wordt gehost door Vercel. Vercel verwerkt ip-adressen, systeemconfiguraties en andere informatie omtrent verkeer van en naar s2f.kytta.dev. Vercel bewaart deze informatie echter niet, noch deelt het de informatie met externe partijen. Bekijk voor meer informatie {}."
|
||||||
|
}
|
25
i18n/translations/ru.json
Normal file
25
i18n/translations/ru.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"metaDescription": "Share₂Fedi — это share-страница для Mastodon, Misskey, Friendica и других сервисов. Введите текст поста и URL-адрес инстанса и нажмите «Опубликовать»!",
|
||||||
|
"language": "Язык: {}",
|
||||||
|
"description": "Share₂Fedi — это инстанс-независимая share-страница для {}. С её помощью вы можете делиться информацией на различных федеративных платформах с одной страницы.",
|
||||||
|
"fediverse": "Федивёрса",
|
||||||
|
"supportedProjects": "Поддерживаемые проекты:",
|
||||||
|
"incl": "вкл.",
|
||||||
|
"credits": "Share₂Fedi разрабатывается и поддерживается {}. Исходный код {}. Хостится на {}. {}.",
|
||||||
|
"nikita": "Никитой Карамовым",
|
||||||
|
"onGitHub": "на GitHub",
|
||||||
|
"statusPage": "Статус",
|
||||||
|
"licence": "Лицензия",
|
||||||
|
"licence1": "Share₂Fedi является свободным программным обеспечением: вы можете распространять её и (или) изменять, соблюдая условия GNU Affero General Public License редакции 3, опубликованной Free Software Foundation.",
|
||||||
|
"licence2": "Share₂Fedi распространяется в расчёте на то, что она окажется полезной, но БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, включая подразумеваемую гарантию КАЧЕСТВА либо ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЁННЫХ ЦЕЛЕЙ. Ознакомьтесь с GNU Affero General Public License для получения более подробной информации.",
|
||||||
|
"privacyNotice": "Политика конфиденциальности",
|
||||||
|
"privacy1": "Сайт s2f.kytta.dev размещён на серверах Vercel. Vercel обрабатывает IP-адреса, информацию о конфигурации системы и другую информацию о трафике, идущем к s2f.kytta.dev и от него. Vercel не хранит эту информацию и не передает её третьим лицам. Более подробную информацию см. в {}.",
|
||||||
|
"privacy2": "Когда вы нажимаете кнопку «Опубликовать», вы попадаете на указанный вами инстанс Федивёрса. Он может обрабатывать и/или хранить ваши данные. Пожалуйста, ознакомьтесь с политикой конфиденциальности соответствующего инстанса.",
|
||||||
|
"vercelPP": "Политике конфиденциальности Vercel",
|
||||||
|
"postText": "Текст поста{}",
|
||||||
|
"postTextPlaceholder": "О чём думаете?",
|
||||||
|
"instance": "Инстанс Федивёрса{}",
|
||||||
|
"previouslyUsed": "Ранее использовались: {}",
|
||||||
|
"rememberInstance": "{} Запомнить инстанс на этом устройстве",
|
||||||
|
"publish": "Опубликовать"
|
||||||
|
}
|
13
i18n/translations/uk.json
Normal file
13
i18n/translations/uk.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"privacyNotice": "Політика конфіденційності",
|
||||||
|
"incl": "вкл.",
|
||||||
|
"postTextPlaceholder": "Що у вас на думці?",
|
||||||
|
"rememberInstance": "{} Запам'ятати інстанс на цьому пристрої",
|
||||||
|
"metaDescription": "Share₂Fedi — це share-сторінка для Mastodon, Misskey, Friendica та інших сервісів. Введіть текст посту та URL-адресу інстансу та натисніть «Опублікувати»!",
|
||||||
|
"statusPage": "Статус",
|
||||||
|
"publish": "Опублікувати",
|
||||||
|
"licence": "Ліцензія",
|
||||||
|
"language": "Мова: {}",
|
||||||
|
"supportedProjects": "Підтримувані проєкти:",
|
||||||
|
"onGitHub": "на GitHub"
|
||||||
|
}
|
16
islands/Counter.tsx
Normal file
16
islands/Counter.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { Signal } from "@preact/signals";
|
||||||
|
import { Button } from "../components/Button.tsx";
|
||||||
|
|
||||||
|
interface CounterProps {
|
||||||
|
count: Signal<number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Counter(props: CounterProps) {
|
||||||
|
return (
|
||||||
|
<div class="flex gap-8 py-6">
|
||||||
|
<Button onClick={() => props.count.value -= 1}>-1</Button>
|
||||||
|
<p class="text-3xl tabular-nums">{props.count}</p>
|
||||||
|
<Button onClick={() => props.count.value += 1}>+1</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
60
lib/instance.ts
Normal file
60
lib/instance.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/*!
|
||||||
|
* This file is part of Share₂Fedi
|
||||||
|
* https://github.com/kytta/share2fedi
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: © 2023 Nikita Karamov <me@kytta.dev>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { supportedProjects } from "$lib/project.ts";
|
||||||
|
|
||||||
|
interface Instance {
|
||||||
|
domain: string;
|
||||||
|
score: number;
|
||||||
|
active_users_monthly: number;
|
||||||
|
total_users: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInstancesForProject = async (
|
||||||
|
project: keyof typeof supportedProjects,
|
||||||
|
): Promise<Instance[]> => {
|
||||||
|
let instances: Instance[];
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://api.fediverse.observer/", {
|
||||||
|
headers: {
|
||||||
|
Accept: "*/*",
|
||||||
|
"Accept-Language": "en;q=1.0",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query:
|
||||||
|
`{nodes(status:"UP",softwarename:"${project}"){domain score active_users_monthly total_users}}`,
|
||||||
|
}),
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
const json = await response.json();
|
||||||
|
instances = json.data.nodes;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Could not fetch instances for "${project}"`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return instances.filter(
|
||||||
|
(instance) =>
|
||||||
|
instance.score > 90 &&
|
||||||
|
// sanity check for some spammy-looking instances
|
||||||
|
instance.total_users >= instance.active_users_monthly,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPopularInstanceDomains = async (): Promise<string[]> => {
|
||||||
|
const instancesPerProject = await Promise.all(
|
||||||
|
Object.keys(supportedProjects).map((project) =>
|
||||||
|
getInstancesForProject(project)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const instances = instancesPerProject.flat();
|
||||||
|
instances.sort((a, b) => b.active_users_monthly - a.active_users_monthly);
|
||||||
|
|
||||||
|
return instances.slice(0, 200).map((instance) => instance.domain);
|
||||||
|
};
|
61
lib/nodeinfo.ts
Normal file
61
lib/nodeinfo.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/*!
|
||||||
|
* This file is part of Share₂Fedi
|
||||||
|
* https://github.com/kytta/share2fedi
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: © 2023 Nikita Karamov <me@kytta.dev>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { normalizeURL } from "$lib/url.ts";
|
||||||
|
|
||||||
|
interface NodeInfoList {
|
||||||
|
links: {
|
||||||
|
rel: string;
|
||||||
|
href: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NodeInfo {
|
||||||
|
software: {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSoftwareName = async (
|
||||||
|
domain: string,
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
const nodeInfoListUrl = new URL(
|
||||||
|
"/.well-known/nodeinfo",
|
||||||
|
normalizeURL(domain),
|
||||||
|
);
|
||||||
|
|
||||||
|
let nodeInfoList: NodeInfoList;
|
||||||
|
try {
|
||||||
|
const nodeInfoListResponse = await fetch(nodeInfoListUrl);
|
||||||
|
nodeInfoList = await nodeInfoListResponse.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Could not fetch '.well-known/nodeinfo':", error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const link of nodeInfoList.links) {
|
||||||
|
if (
|
||||||
|
/^http:\/\/nodeinfo\.diaspora\.software\/ns\/schema\/(1\.0|1\.1|2\.0|2\.1)/
|
||||||
|
.test(
|
||||||
|
link.rel,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const nodeInfoResponse = await fetch(link.href);
|
||||||
|
const nodeInfo = (await nodeInfoResponse.json()) as NodeInfo;
|
||||||
|
|
||||||
|
return nodeInfo.software.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// not found
|
||||||
|
console.warn("No NodeInfo found for domain:", domain);
|
||||||
|
return undefined;
|
||||||
|
};
|
64
lib/project.ts
Normal file
64
lib/project.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/*!
|
||||||
|
* This file is part of Share₂Fedi
|
||||||
|
* https://github.com/kytta/share2fedi
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: © 2023 Nikita Karamov <me@kytta.dev>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ProjectPublishConfig {
|
||||||
|
endpoint: string;
|
||||||
|
params: {
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mastodonConfig: ProjectPublishConfig = {
|
||||||
|
endpoint: "share",
|
||||||
|
params: {
|
||||||
|
text: "text",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const misskeyConfig: ProjectPublishConfig = {
|
||||||
|
endpoint: "share",
|
||||||
|
params: {
|
||||||
|
text: "text",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of the supported fediverse projects.
|
||||||
|
*
|
||||||
|
* The keys of this mapping can be used as keys for the fediverse.observer API,
|
||||||
|
* icon names, etc.
|
||||||
|
*/
|
||||||
|
export const supportedProjects: Record<string, ProjectPublishConfig> = {
|
||||||
|
calckey: misskeyConfig,
|
||||||
|
fedibird: mastodonConfig,
|
||||||
|
firefish: misskeyConfig,
|
||||||
|
foundkey: misskeyConfig,
|
||||||
|
friendica: {
|
||||||
|
endpoint: "compose",
|
||||||
|
params: {
|
||||||
|
text: "body",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
glitchcafe: mastodonConfig,
|
||||||
|
gnusocial: {
|
||||||
|
endpoint: "notice/new",
|
||||||
|
params: {
|
||||||
|
text: "status_textarea",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hometown: mastodonConfig,
|
||||||
|
hubzilla: {
|
||||||
|
endpoint: "rpost",
|
||||||
|
params: {
|
||||||
|
text: "body",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mastodon: mastodonConfig,
|
||||||
|
meisskey: misskeyConfig,
|
||||||
|
misskey: misskeyConfig,
|
||||||
|
};
|
30
lib/response.ts
Normal file
30
lib/response.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/*!
|
||||||
|
* This file is part of Share₂Fedi
|
||||||
|
* https://github.com/kytta/share2fedi
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: © 2023 Nikita Karamov <me@kytta.dev>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const json = (
|
||||||
|
body: unknown,
|
||||||
|
status: number = 200,
|
||||||
|
headers: Record<string, string> = {},
|
||||||
|
) => {
|
||||||
|
return new Response(JSON.stringify(body), {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const error = (message: string, status: number = 400) => {
|
||||||
|
return json(
|
||||||
|
{
|
||||||
|
error: message,
|
||||||
|
},
|
||||||
|
status,
|
||||||
|
);
|
||||||
|
};
|
35
lib/url.ts
Normal file
35
lib/url.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*!
|
||||||
|
* This file is part of Share₂Fedi
|
||||||
|
* https://github.com/kytta/share2fedi
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: © 2023 Nikita Karamov <me@kytta.dev>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds missing "https://" and ending slash to the URL
|
||||||
|
*
|
||||||
|
* @param url URL to normalize
|
||||||
|
* @return normalized URL
|
||||||
|
*/
|
||||||
|
export const normalizeURL = (url: string): string => {
|
||||||
|
if (!(url.startsWith("https://") || url.startsWith("http://"))) {
|
||||||
|
url = "https://" + url;
|
||||||
|
}
|
||||||
|
if (!url.endsWith("/")) {
|
||||||
|
url += "/";
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUrlDomain = (url: string | URL): string => {
|
||||||
|
if (typeof url === "string") {
|
||||||
|
url = url.trim();
|
||||||
|
|
||||||
|
if (!/^https?:\/\//.test(url)) {
|
||||||
|
url = `https://${url}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URL(url).host;
|
||||||
|
};
|
13
main.ts
Normal file
13
main.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/// <reference no-default-lib="true" />
|
||||||
|
/// <reference lib="dom" />
|
||||||
|
/// <reference lib="dom.iterable" />
|
||||||
|
/// <reference lib="dom.asynciterable" />
|
||||||
|
/// <reference lib="deno.ns" />
|
||||||
|
|
||||||
|
import "@std/dotenv/load";
|
||||||
|
|
||||||
|
import { start } from "$fresh/server.ts";
|
||||||
|
import manifest from "./fresh.gen.ts";
|
||||||
|
import config from "./fresh.config.ts";
|
||||||
|
|
||||||
|
await start(manifest, config);
|
27
routes/_404.tsx
Normal file
27
routes/_404.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Head } from "$fresh/runtime.ts";
|
||||||
|
|
||||||
|
export default function Error404() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>404 - Page not found</title>
|
||||||
|
</Head>
|
||||||
|
<div class="px-4 py-8 mx-auto bg-[#86efac]">
|
||||||
|
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
|
||||||
|
<img
|
||||||
|
class="my-6"
|
||||||
|
src="/logo.svg"
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
alt="the Fresh logo: a sliced lemon dripping with juice"
|
||||||
|
/>
|
||||||
|
<h1 class="text-4xl font-bold">404 - Page not found</h1>
|
||||||
|
<p class="my-4">
|
||||||
|
The page you were looking for doesn't exist.
|
||||||
|
</p>
|
||||||
|
<a href="/" class="underline">Go back home</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
22
routes/_app.tsx
Normal file
22
routes/_app.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { type PageProps } from "$fresh/server.ts";
|
||||||
|
|
||||||
|
export default function App({ Component, state }: PageProps) {
|
||||||
|
return (
|
||||||
|
<html lang={state.prerenderLanguage as string}>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Share₂Fedi</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Share₂Fedi is a share page for Mastodon, Misskey, Friendica, and others. Type in your post text and the instance URL and click ‘Publish!’"
|
||||||
|
data-translate="metaDescription"
|
||||||
|
data-translate-attribute="content"
|
||||||
|
/>
|
||||||
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Component />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
35
routes/_middleware.ts
Normal file
35
routes/_middleware.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { FreshContext } from "$fresh/server.ts";
|
||||||
|
import { defaultLanguage, languages } from "$i18n/translations.ts";
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
languages: string[];
|
||||||
|
prerenderLanguage: string;
|
||||||
|
}
|
||||||
|
const chooseLanguage = (localeTags: string[]): string => {
|
||||||
|
for (const tag of localeTags) {
|
||||||
|
const locale = new Intl.Locale(tag);
|
||||||
|
const minimized = locale.minimize();
|
||||||
|
|
||||||
|
for (const candidate of [locale.baseName, minimized.baseName]) {
|
||||||
|
if (candidate in languages) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultLanguage;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function handler(
|
||||||
|
req: Request,
|
||||||
|
ctx: FreshContext<State>,
|
||||||
|
) {
|
||||||
|
const acceptLanguages = req.headers.get("Accept-Language")
|
||||||
|
?.split(",")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((tag) => tag.split(";")[0].trim())
|
||||||
|
.filter((tag) => tag !== "*") ?? [];
|
||||||
|
ctx.state.prerenderLanguage = chooseLanguage(acceptLanguages);
|
||||||
|
|
||||||
|
return ctx.next();
|
||||||
|
}
|
37
routes/api/detect/[domain].ts
Normal file
37
routes/api/detect/[domain].ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { FreshContext, Handlers } from "$fresh/server.ts";
|
||||||
|
import { getSoftwareName } from "$lib/nodeinfo.ts";
|
||||||
|
import { type ProjectPublishConfig, supportedProjects } from "$lib/project.ts";
|
||||||
|
import { error, json } from "$lib/response.ts";
|
||||||
|
|
||||||
|
type Detection = {
|
||||||
|
domain: string;
|
||||||
|
project: keyof typeof supportedProjects;
|
||||||
|
} & ProjectPublishConfig;
|
||||||
|
|
||||||
|
export const handler: Handlers<Detection> = {
|
||||||
|
async GET(_req: Request, ctx: FreshContext): Promise<Response> {
|
||||||
|
const domain = ctx.params.domain;
|
||||||
|
|
||||||
|
const softwareName = await getSoftwareName(domain);
|
||||||
|
if (softwareName === undefined) {
|
||||||
|
return error("Could not detect Fediverse project.");
|
||||||
|
}
|
||||||
|
if (!(softwareName in supportedProjects)) {
|
||||||
|
return error(`Fediverse project "${softwareName}" is not supported yet.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const publishConfig =
|
||||||
|
supportedProjects[softwareName] as ProjectPublishConfig;
|
||||||
|
return json(
|
||||||
|
{
|
||||||
|
domain,
|
||||||
|
project: softwareName,
|
||||||
|
...publishConfig,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"Cache-Control": "public, s-maxage=86400, max-age=604800",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
15
routes/api/instances.ts
Normal file
15
routes/api/instances.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { FreshContext, Handlers } from "$fresh/server.ts";
|
||||||
|
import { getPopularInstanceDomains } from "$lib/instance.ts";
|
||||||
|
import { json } from "$lib/response.ts";
|
||||||
|
|
||||||
|
export const handler: Handlers<string[]> = {
|
||||||
|
async GET(_req: Request, _ctx: FreshContext): Promise<Response> {
|
||||||
|
const popularInstanceDomains = await getPopularInstanceDomains();
|
||||||
|
|
||||||
|
return json(popularInstanceDomains, 200, {
|
||||||
|
"Cache-Control": popularInstanceDomains.length > 0
|
||||||
|
? "public, s-maxage=86400, max-age=604800"
|
||||||
|
: "public, s-maxage=60, max-age=3600",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
5
routes/greet/[name].tsx
Normal file
5
routes/greet/[name].tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { PageProps } from "$fresh/server.ts";
|
||||||
|
|
||||||
|
export default function Greet(props: PageProps) {
|
||||||
|
return <div>Hello {props.params.name}</div>;
|
||||||
|
}
|
71
routes/index.tsx
Normal file
71
routes/index.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import type { FreshContext, Handlers } from "$fresh/server.ts";
|
||||||
|
import { useSignal } from "@preact/signals";
|
||||||
|
import Counter from "../islands/Counter.tsx";
|
||||||
|
import { type ProjectPublishConfig, supportedProjects } from "$lib/project.ts";
|
||||||
|
import { getUrlDomain } from "$lib/url.ts";
|
||||||
|
import { seeOther } from "@http/response/see-other";
|
||||||
|
|
||||||
|
type Detection = {
|
||||||
|
domain: string;
|
||||||
|
project: keyof typeof supportedProjects;
|
||||||
|
} & ProjectPublishConfig;
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const postSchema = z.object({
|
||||||
|
text: z.string({
|
||||||
|
required_error: "Please enter post text.",
|
||||||
|
}),
|
||||||
|
instance: z.string({
|
||||||
|
required_error: "Please enter instance domain.",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type POST = z.infer<typeof postSchema>;
|
||||||
|
|
||||||
|
export const handler: Handlers = {
|
||||||
|
async POST(req: Request, ctx: FreshContext): Promise<Response> {
|
||||||
|
const formData = await req.formData();
|
||||||
|
const form = postSchema.safeParse(formData);
|
||||||
|
|
||||||
|
if (!form.success) {
|
||||||
|
const errors = form.error?.format();
|
||||||
|
return await ctx.render({ errors });
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = getUrlDomain(form.data.instance);
|
||||||
|
const detectResponse = await fetch(
|
||||||
|
new URL(`/api/detect/${instance}`, ctx.url),
|
||||||
|
);
|
||||||
|
const { domain, endpoint, params } = await detectResponse.json();
|
||||||
|
const publishUrl = new URL(endpoint, `https://${domain}/`);
|
||||||
|
publishUrl.search = new URLSearchParams([
|
||||||
|
[params.text, form.data.text],
|
||||||
|
]).toString();
|
||||||
|
|
||||||
|
return seeOther(publishUrl);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const count = useSignal(3);
|
||||||
|
return (
|
||||||
|
<div class="px-4 py-8 mx-auto bg-[#86efac]">
|
||||||
|
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
|
||||||
|
<img
|
||||||
|
class="my-6"
|
||||||
|
src="/logo.svg"
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
alt="the Fresh logo: a sliced lemon dripping with juice"
|
||||||
|
/>
|
||||||
|
<h1 class="text-4xl font-bold">Welcome to Fresh</h1>
|
||||||
|
<p class="my-4">
|
||||||
|
Try updating this message in the
|
||||||
|
<code class="mx-2">./routes/index.tsx</code> file, and refresh.
|
||||||
|
</p>
|
||||||
|
<Counter count={count} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
6
static/logo.svg
Normal file
6
static/logo.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="40" height="40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M34.092 8.845C38.929 20.652 34.092 27 30 30.5c1 3.5-2.986 4.222-4.5 2.5-4.457 1.537-13.512 1.487-20-5C2 24.5 4.73 16.714 14 11.5c8-4.5 16-7 20.092-2.655Z" fill="#FFDB1E"/>
|
||||||
|
<path d="M14 11.5c6.848-4.497 15.025-6.38 18.368-3.47C37.5 12.5 21.5 22.612 15.5 25c-6.5 2.587-3 8.5-6.5 8.5-3 0-2.5-4-5.183-7.75C2.232 23.535 6.16 16.648 14 11.5Z" fill="#fff" stroke="#FFDB1E"/>
|
||||||
|
<path d="M28.535 8.772c4.645 1.25-.365 5.695-4.303 8.536-3.732 2.692-6.606 4.21-7.923 4.83-.366.173-1.617-2.252-1.617-1 0 .417-.7 2.238-.934 2.326-1.365.512-4.223 1.29-5.835 1.29-3.491 0-1.923-4.754 3.014-9.122.892-.789 1.478-.645 2.283-.645-.537-.773-.534-.917.403-1.546C17.79 10.64 23 8.77 25.212 8.42c.366.014.82.35.82.629.41-.14 2.095-.388 2.503-.278Z" fill="#FFE600"/>
|
||||||
|
<path d="M14.297 16.49c.985-.747 1.644-1.01 2.099-2.526.566.121.841-.08 1.29-.701.324.466 1.657.608 2.453.701-.715.451-1.057.852-1.452 2.106-1.464-.611-3.167-.302-4.39.42Z" fill="#fff"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
129
static/styles.css
Normal file
129
static/styles.css
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
button, [role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||||
|
"Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
img,
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||||
|
"Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
|
||||||
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
}
|
||||||
|
.transition-colors {
|
||||||
|
transition-property: background-color, border-color, color, fill, stroke;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
.my-6 {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
.text-4xl {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
line-height: 2.5rem;
|
||||||
|
}
|
||||||
|
.mx-2 {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
.my-4 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.mx-auto {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.px-4 {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
.py-8 {
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
padding-top: 2rem;
|
||||||
|
}
|
||||||
|
.bg-\[\#86efac\] {
|
||||||
|
background-color: #86efac;
|
||||||
|
}
|
||||||
|
.text-3xl {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
line-height: 2.25rem;
|
||||||
|
}
|
||||||
|
.py-6 {
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
}
|
||||||
|
.px-2 {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
.py-1 {
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
}
|
||||||
|
.border-gray-500 {
|
||||||
|
border-color: #6b7280;
|
||||||
|
}
|
||||||
|
.bg-white {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.gap-8 {
|
||||||
|
grid-gap: 2rem;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
.font-bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.max-w-screen-md {
|
||||||
|
max-width: 768px;
|
||||||
|
}
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.border-2 {
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
.rounded {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
.hover\:bg-gray-200:hover {
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
.tabular-nums {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
Reference in New Issue
Block a user