Migrate to Astro (#30)
|
@ -1,15 +1,28 @@
|
|||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:unicorn/recommended",
|
||||
"plugin:astro/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:unicorn/recommended", "prettier"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["api/*.js"],
|
||||
"files": ["*.astro"],
|
||||
"parser": "astro-eslint-parser",
|
||||
"parserOptions": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extraFileExtensions": [".astro"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["api/*.js", "astro.config.ts"],
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": false
|
||||
|
@ -17,6 +30,12 @@
|
|||
"rules": {
|
||||
"unicorn/prefer-node-protocol": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["src/env.d.ts"],
|
||||
"rules": {
|
||||
"unicorn/prevent-abbreviations": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -16,17 +16,27 @@ repos:
|
|||
- id: prettier
|
||||
additional_dependencies:
|
||||
- prettier@2
|
||||
- prettier-plugin-astro
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: "v8.43.0"
|
||||
hooks:
|
||||
- id: eslint
|
||||
files: \.([jt]s|astro)$ # *.js, *.ts and *.astro
|
||||
types: [file]
|
||||
additional_dependencies:
|
||||
- "@typescript-eslint/eslint-plugin"
|
||||
- "@typescript-eslint/parser"
|
||||
- astro-eslint-parser
|
||||
- eslint
|
||||
- eslint-config-prettier
|
||||
- eslint-plugin-astro
|
||||
- eslint-plugin-unicorn
|
||||
- prettier@2
|
||||
- prettier-plugin-astro
|
||||
- repo: https://github.com/awebdeveloper/pre-commit-stylelint
|
||||
rev: "0.0.2"
|
||||
hooks:
|
||||
- id: stylelint
|
||||
additional_dependencies: ["stylelint", "stylelint-config-standard-scss"]
|
||||
additional_dependencies:
|
||||
- stylelint
|
||||
- stylelint-config-standard-scss
|
||||
|
|
26
CHANGELOG.md
|
@ -13,15 +13,27 @@ as to indicate that sharing to other federated networks is now possible.
|
|||
|
||||
### ⚠️ BREAKING CHANGES
|
||||
|
||||
- **new API endpoint path**: ~~`/api/toot`~~ → `/api/share`
|
||||
- **new API endpoint port**: ~~`:8000`~~ → `:8080`
|
||||
- API endpoint **is now ESM-based** instead of CommonJS
|
||||
- **new static path**: ~~`./public`~~ → `./dist`
|
||||
Share₂Fedi is now an [Astro](https://astro.build/) site. The migration allowed
|
||||
us to have a performant service that is easily hostable on both serverless
|
||||
platforms, like Vercel or Netlify, as well as locally. Setting the project up
|
||||
now takes seconds! This comes with changes, though:
|
||||
|
||||
- **static files aren't built any more**, but generated server-side
|
||||
- **new output directory**: ~~`public`~~ → `dist`
|
||||
- this also means that `public` **is not ignored any more**
|
||||
|
||||
Some changes came with the name change:
|
||||
|
||||
- **changed API endpoint path**: ~~`/api/toot`~~ → `/api/share`
|
||||
|
||||
### Added
|
||||
|
||||
- support for Pleroma
|
||||
- support for GNU Social
|
||||
- remembering of multiple Fediverse instances
|
||||
- new API endpoints
|
||||
- `/api/instances` will return the list of popular instances
|
||||
- `/api/detect/[host]` will detect the Fediverse project used by a host
|
||||
- when developing, the API endpoint can now be tested locally thanks to
|
||||
[`vite-plugin-node`](https://github.com/axe-me/vite-plugin-node)
|
||||
- a privacy policy describing what data is being processed and stored
|
||||
|
@ -34,9 +46,9 @@ as to indicate that sharing to other federated networks is now possible.
|
|||
- new logo
|
||||
- new design
|
||||
- repository moved back to GitHub
|
||||
- s2f is now being built with Vite
|
||||
- `@vitejs/plugin-legacy` is used, which allows JS work on old browsers, which
|
||||
comes, with big bundle sizes. Modern browsers still get a small bundle.
|
||||
- s2f is now being built with Astro
|
||||
- Share₂Fedi is now 100% server-side rendered. You don't have to host any
|
||||
static files, all you need is to run the Node server.
|
||||
|
||||
## [2.4.5] - 2023-06-17
|
||||
|
||||
|
|
57
README.md
|
@ -24,12 +24,18 @@ appended if used later—handy!
|
|||
|
||||
## Hosting
|
||||
|
||||
### One-click Vercel deploy
|
||||
### Vercel, Netlify, Cloudflare Pages
|
||||
|
||||
**Share₂Fedi** is designed to run on [Vercel](https://vercel.com/). To deploy it
|
||||
yourself (it's free!), you can use the following button:
|
||||
**Share₂Fedi** was designed to run on [Vercel](https://vercel.com/), but you can
|
||||
also run it on [Netlify](https://www.netlify.com/) or
|
||||
[Cloudflare Pages](https://pages.cloudflare.com/). To deploy it yourself (it's
|
||||
free!), you can use the following buttons:
|
||||
|
||||
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fkytta%2Fshare2fedi)
|
||||
[![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fkytta%2Fshare2fedi)
|
||||
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/kytta/share2fedi)
|
||||
|
||||
To deploy to Cloudflare Pages, fork the repo and
|
||||
[follow the instructions](https://docs.astro.build/en/guides/deploy/cloudflare/#how-to-deploy-a-site-with-git).
|
||||
|
||||
### Host it yourself
|
||||
|
||||
|
@ -37,67 +43,64 @@ Self-hosting **Share₂Fedi** outside of Vercel requires some extra setup:
|
|||
|
||||
**Prerequisites:** modern Node.js (v16 or later), `pnpm`.
|
||||
|
||||
1. Install the dependencies:
|
||||
1. Install dependencies.
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. Build the static files:
|
||||
2. Build.
|
||||
|
||||
```sh
|
||||
pnpm build
|
||||
```
|
||||
|
||||
3. Run the backend server for the form:
|
||||
3. Run server.
|
||||
|
||||
> By default, this will only listen on localhost port 3000. To enable
|
||||
> listening on a ceratin hostand/or port, set the `HOST` and `PORT`
|
||||
> environment variables, respectively.
|
||||
|
||||
```sh
|
||||
node api/share.js
|
||||
node dist/server/entry.mjs
|
||||
```
|
||||
|
||||
alternatively, if you want to run the process in the background:
|
||||
|
||||
```sh
|
||||
pm2 start api/share.js --watch --ignore-watch="node_modules"
|
||||
pm2 start dist/server/entry.mjs --watch --ignore-watch="node_modules"
|
||||
```
|
||||
|
||||
> You can find a summary for pm2 at:
|
||||
> https://pm2.keymetrics.io/docs/usage/quick-start/
|
||||
|
||||
4. Set up a web server
|
||||
> More information about self-hosting an Astro website with Node:
|
||||
> https://docs.astro.build/en/guides/integrations-guide/node/#standalone
|
||||
|
||||
Basically, you need to run a server that would proxy the requests to
|
||||
`/api/share`. to the Node.js server you started. Here's how to achieve this
|
||||
in various HTTP servers:
|
||||
4. Set up a reverse proxy.
|
||||
|
||||
Basically, you need to run a reverse proxy that would redirect all incoming
|
||||
requests to `localhost:3000`. Here's how to achieve this in various HTTP
|
||||
servers:
|
||||
|
||||
1. Apache
|
||||
|
||||
```apacheconf
|
||||
DocumentRoot "<PATH_TO_SHARE2FEDI>/dist"
|
||||
|
||||
ProxyPass "/api/share" "http://localhost:8080/"
|
||||
ProxyPass "/" "http://localhost:3000/"
|
||||
```
|
||||
|
||||
2. Nginx
|
||||
|
||||
```nginxconf
|
||||
root <PATH_TO_SHARE2FEDI>/dist;
|
||||
index.html;
|
||||
|
||||
location /api/share {
|
||||
proxy_pass http://localhost:8080/;
|
||||
location / {
|
||||
proxy_pass http://localhost:3000/;
|
||||
}
|
||||
```
|
||||
|
||||
3. Caddy
|
||||
|
||||
```caddy
|
||||
root * <PATH_TO_SHARE2FEDI>/dist;
|
||||
try_files index.html
|
||||
|
||||
handle_path /api/share {
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
reverse_proxy :3000
|
||||
```
|
||||
|
||||
## See also
|
||||
|
|
102
api/share.js
|
@ -1,102 +0,0 @@
|
|||
/*!
|
||||
share2fedi - Instance-agnostic share page for the Fediverse.
|
||||
Copyright (C) 2020-2023 Nikita Karamov <me@kytta.dev>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program 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.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import http from "http";
|
||||
import https from "https";
|
||||
|
||||
const pathsMap = {
|
||||
mastodon: {
|
||||
checkUrl: "/api/v1/instance/rules",
|
||||
postUrl: "share",
|
||||
textParam: "text",
|
||||
},
|
||||
gnuSocial: {
|
||||
checkUrl: "/api/statusnet/version.xml",
|
||||
postUrl: "/notice/new",
|
||||
textParam: "status_textarea",
|
||||
},
|
||||
pleroma: {
|
||||
checkUrl: "/api/v1/pleroma/federation_status",
|
||||
postUrl: "share",
|
||||
textParam: "message",
|
||||
},
|
||||
};
|
||||
|
||||
const queryUrl = (url, service) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const get = url.protocol === "https:" ? https.get : http.get;
|
||||
get(url, ({ statusCode }) => {
|
||||
if (statusCode === 200) {
|
||||
console.debug(url.href, "is", service);
|
||||
resolve(service);
|
||||
} else {
|
||||
reject(url);
|
||||
}
|
||||
}).on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const detectService = async (instanceURL) => {
|
||||
const checkPromises = Object.entries(pathsMap).map(
|
||||
([service, { checkUrl }]) =>
|
||||
queryUrl(new URL(checkUrl, instanceURL), service),
|
||||
);
|
||||
|
||||
return await Promise.any(checkPromises);
|
||||
};
|
||||
|
||||
const requestListener = async (request, response) => {
|
||||
if (request.method !== "POST") {
|
||||
response.writeHead(405).end();
|
||||
return;
|
||||
}
|
||||
|
||||
let data = "";
|
||||
request.on("data", (chunk) => {
|
||||
data += chunk.toString();
|
||||
});
|
||||
|
||||
request.on("end", () => {
|
||||
const requestBody = new URLSearchParams(data);
|
||||
const postText = requestBody.get("text") || "";
|
||||
const instanceURL =
|
||||
requestBody.get("instance") || "https://mastodon.social/";
|
||||
|
||||
detectService(instanceURL)
|
||||
.then((service) => {
|
||||
const publishUrl = new URL(pathsMap[service].postUrl, instanceURL);
|
||||
publishUrl.search = new URLSearchParams([
|
||||
[pathsMap[service].textParam, postText],
|
||||
]);
|
||||
response.writeHead(303, { Location: publishUrl.toString() }).end();
|
||||
})
|
||||
.catch((error) => {
|
||||
response.writeHead(400).end(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (!import.meta.env || import.meta.env.PROD) {
|
||||
http.createServer(requestListener).listen(8080);
|
||||
}
|
||||
|
||||
export const viteNodeApp = requestListener;
|
|
@ -1,3 +1,4 @@
|
|||
<!-- © 2022 Nikita Karamov. Licensed under CC-BY 4.0 -->
|
||||
<svg viewBox="0 0 64 64" width="512" height="512" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
|
||||
<defs>
|
||||
|
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
|
@ -1,3 +1,4 @@
|
|||
<!-- © 2022 Nikita Karamov. Licensed under CC-BY 4.0 -->
|
||||
<svg viewBox="0 0 64 64" width="512" height="512" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
|
||||
<defs>
|
||||
|
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.9 KiB |
|
@ -1,3 +1,4 @@
|
|||
<!-- © 2022 Nikita Karamov. Licensed under CC-BY 4.0 -->
|
||||
<svg viewBox="0 0 260 80" width="520" height="160" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
|
||||
<defs>
|
||||
|
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4.0 KiB |
|
@ -0,0 +1,29 @@
|
|||
import { defineConfig } from "astro/config";
|
||||
|
||||
import cloudflare from "@astrojs/cloudflare";
|
||||
import netlify from "@astrojs/netlify/functions";
|
||||
import node from "@astrojs/node";
|
||||
import vercel from "@astrojs/vercel/serverless";
|
||||
|
||||
let astroAdapter;
|
||||
if (process.env.CF_PAGES) {
|
||||
console.debug("Using Cloudflare adapter");
|
||||
astroAdapter = cloudflare();
|
||||
} else if (process.env.VERCEL) {
|
||||
console.debug("Using Vercel adapter");
|
||||
astroAdapter = vercel();
|
||||
} else if (process.env.NETLIFY) {
|
||||
console.debug("Using Netlify adapter");
|
||||
astroAdapter = netlify();
|
||||
} else {
|
||||
console.debug("Using Node.js adapter");
|
||||
astroAdapter = node({
|
||||
mode: "standalone",
|
||||
});
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
site: "https://s2f.kytta.dev",
|
||||
adapter: astroAdapter,
|
||||
output: "server",
|
||||
});
|
59
lib/count.js
|
@ -1,59 +0,0 @@
|
|||
/*!
|
||||
* @source: https://github.com/kytta/share2fedi/blob/main/lib/count.js
|
||||
*
|
||||
* @licstart The following is the entire license notice for the
|
||||
* JavaScript code in this page.
|
||||
*
|
||||
* share2fedi - Instance-agnostic share page for the Fediverse.
|
||||
* Copyright (C) 2023 Nikita Karamov <me@kytta.dev>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @licend The above is the entire license notice
|
||||
* for the JavaScript code in this page.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
// This is the analytics code for Share₂Fedi. It just sends a beacon
|
||||
// to GoatCounter with hardcoded path. This is way more lightweight, performant
|
||||
// and privacy-friendly than the default GC script.
|
||||
|
||||
if (
|
||||
window.location.host === "s2f.kytta.dev" ||
|
||||
window.location.host === "share2fedi.kytta.dev"
|
||||
) {
|
||||
// eslint-disable-next-line unicorn/prefer-top-level-await
|
||||
fetch("//gc.zgo.at/", { method: "HEAD" }).then((result) => {
|
||||
// Check if the default GC URL resolves
|
||||
// This allows us to not track people with ad blockers
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const screen = encodeURIComponent(
|
||||
[
|
||||
window.screen.width,
|
||||
window.screen.height,
|
||||
window.devicePixelRatio || 1,
|
||||
].join(","),
|
||||
);
|
||||
|
||||
const random = encodeURIComponent(Math.random().toString(36).slice(2));
|
||||
|
||||
navigator.sendBeacon(
|
||||
`https://share2fedi.goatcounter.com/count?p=%2F&s=${screen}&b=0&rnd=${random}`,
|
||||
);
|
||||
});
|
||||
}
|
142
lib/main.js
|
@ -1,142 +0,0 @@
|
|||
/*!
|
||||
* @source: https://github.com/kytta/share2fedi/blob/main/lib/main.js
|
||||
*
|
||||
* @licstart The following is the entire license notice for the
|
||||
* JavaScript code in this page.
|
||||
*
|
||||
* share2fedi - Instance-agnostic share page for the Fediverse.
|
||||
* Copyright (C) 2020-2023 Nikita Karamov <me@kytta.dev>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @licend The above is the entire license notice
|
||||
* for the JavaScript code in this page.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./scss/style.scss";
|
||||
|
||||
const INSTANCE_LIST_URL = "https://api.joinmastodon.org/servers";
|
||||
const LOCAL_STORAGE_KEY = "recentInstances";
|
||||
const RECENT_INSTANCES_SIZE = 5;
|
||||
|
||||
const $form = document.querySelector("#js-s2f-form");
|
||||
const $instance = document.querySelector("#instance");
|
||||
const $instanceDatalist = document.querySelector("#instanceDatalist");
|
||||
|
||||
/**
|
||||
* Adds missing "https://" and ending slash to the URL
|
||||
*
|
||||
* @param {string} url URL to normalize
|
||||
* @return {string} normalized URL
|
||||
*/
|
||||
function normalizeUrl(url) {
|
||||
if (!url.includes("http://") && !url.includes("https://")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
if (url.at(-1) !== "/") {
|
||||
url = url + "/";
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function onLoadInstancesError() {
|
||||
console.error("Couldn't load instance list");
|
||||
}
|
||||
|
||||
function onLoadInstancesSuccess() {
|
||||
if (this.status >= 400) {
|
||||
return onLoadInstancesError();
|
||||
}
|
||||
|
||||
const currentInstance = $instance.value;
|
||||
const instanceDomains = JSON.parse(this.responseText).map(
|
||||
(index) => index.domain,
|
||||
);
|
||||
if (currentInstance && !instanceDomains.includes(currentInstance)) {
|
||||
instanceDomains.push(currentInstance);
|
||||
}
|
||||
instanceDomains.sort();
|
||||
|
||||
for (const instanceDomain of instanceDomains) {
|
||||
const $option = document.createElement("option");
|
||||
$option.value = normalizeUrl(instanceDomain);
|
||||
$instanceDatalist.append($option);
|
||||
}
|
||||
}
|
||||
|
||||
function loadInstances() {
|
||||
if ($instanceDatalist.children.length === 0) {
|
||||
const request = new XMLHttpRequest();
|
||||
|
||||
request.addEventListener("load", onLoadInstancesSuccess);
|
||||
request.addEventListener("error", onLoadInstancesError);
|
||||
|
||||
request.open("GET", INSTANCE_LIST_URL);
|
||||
request.send();
|
||||
}
|
||||
}
|
||||
|
||||
function getRecentInstances() {
|
||||
const storedValue = window.localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
if (!storedValue) return [];
|
||||
return JSON.parse(storedValue);
|
||||
}
|
||||
|
||||
function rememberInstance(instance) {
|
||||
const recentInstances = getRecentInstances();
|
||||
|
||||
const index = recentInstances.indexOf(instance);
|
||||
if (index >= 0) {
|
||||
recentInstances.splice(index, 1);
|
||||
}
|
||||
recentInstances.unshift(instance);
|
||||
recentInstances.length = RECENT_INSTANCES_SIZE;
|
||||
|
||||
window.localStorage.setItem(
|
||||
LOCAL_STORAGE_KEY,
|
||||
JSON.stringify(recentInstances),
|
||||
);
|
||||
}
|
||||
|
||||
function onFormSubmit(event) {
|
||||
const formData = new FormData(event.target);
|
||||
|
||||
if (formData.get("remember")) {
|
||||
rememberInstance(formData.get("instance"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
let prefillInstance = getRecentInstances()[0];
|
||||
|
||||
const URLParameters = window.location.search.slice(1).split("&");
|
||||
for (const URLParameter of URLParameters) {
|
||||
const URLParameterPair = URLParameter.split("=");
|
||||
if (URLParameterPair[0] === "text") {
|
||||
document.querySelector("#text").value = decodeURIComponent(
|
||||
URLParameterPair[1],
|
||||
);
|
||||
} else if (URLParameterPair[0] === "instance") {
|
||||
prefillInstance = decodeURIComponent(URLParameterPair[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (prefillInstance != undefined) {
|
||||
$instance.value = normalizeUrl(prefillInstance);
|
||||
}
|
||||
|
||||
$instance.addEventListener("focus", loadInstances);
|
||||
$form.addEventListener("submit", onFormSubmit);
|
26
package.json
|
@ -12,33 +12,41 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite",
|
||||
"fmt": "prettier --write .",
|
||||
"build": "astro build",
|
||||
"dev": "astro dev",
|
||||
"fmt": "prettier --write --plugin-search-dir=. .",
|
||||
"lint": "prettier --check . && eslint . && stylelint '**/*.scss'",
|
||||
"preview": "vite preview",
|
||||
"preview": "astro preview",
|
||||
"start": "astro dev",
|
||||
"test": "pnpm run lint"
|
||||
},
|
||||
"browserslist": "cover 95%, last 2 versions, Firefox ESR, not dead",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "^6.2.1",
|
||||
"@astrojs/netlify": "^2.2.0",
|
||||
"@astrojs/node": "^5.1.0",
|
||||
"@astrojs/vercel": "^3.2.1",
|
||||
"astro": "^2.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-legacy": "^4.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||
"@typescript-eslint/parser": "^5.55.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"browserslist": "^4.21.5",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint-config-prettier": "^8.7.0",
|
||||
"eslint-plugin-astro": "^0.25.0",
|
||||
"eslint-plugin-unicorn": "^46.0.0",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-csso": "^6.0.1",
|
||||
"prettier": "^2.8.4",
|
||||
"prettier-plugin-astro": "^0.8.0",
|
||||
"sass": "^1.59.3",
|
||||
"sharp": "^0.31.3",
|
||||
"stylelint": "^15.2.0",
|
||||
"stylelint-config-standard-scss": "^7.0.1",
|
||||
"svgo": "^3.0.2",
|
||||
"terser": "^5.16.6",
|
||||
"vite": "^4.2.0",
|
||||
"vite-plugin-image-optimizer": "^1.1.2",
|
||||
"vite-plugin-node": "^3.0.2"
|
||||
"typescript": "^5.0.2"
|
||||
},
|
||||
"postcss": {
|
||||
"map": true,
|
||||
|
|
4087
pnpm-lock.yaml
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 149 KiB |
|
@ -1,25 +1 @@
|
|||
<svg viewBox="0 0 64 64" width="512" height="512" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
|
||||
<defs>
|
||||
<filter id="blur">
|
||||
<feGaussianBlur stdDeviation="15"></feGaussianBlur>
|
||||
</filter>
|
||||
<clipPath id="pentagon">
|
||||
<path d="m38.802 1.632 23.656 29.212-20.473 31.524-36.307-9.729-1.968-37.537z"></path>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#pentagon)">
|
||||
<g filter="url(#blur)">
|
||||
<circle cx="0" cy="10" fill="#F44336" r="39"></circle>
|
||||
<circle cx="45" cy="-5" fill="#FFEB3B" r="39"></circle>
|
||||
<circle cx="70" cy="25" fill="#4CAF50" r="39"></circle>
|
||||
<circle cx="50" cy="65" fill="#03A9F4" r="39"></circle>
|
||||
<circle cx="5" cy="70" fill="#673AB7" r="39"></circle>
|
||||
<circle cx="50" cy="31.5" fill="#4CAF50" r="12"></circle>
|
||||
<circle cx="37.5" cy="50.5" fill="#03A9F4" r="12"></circle>
|
||||
<circle cx="15.5" cy="44.5" fill="#673AB7" r="12"></circle>
|
||||
<circle cx="14" cy="22" fill="#F44336" r="12"></circle>
|
||||
<circle cx="36" cy="14" fill="#FFEB3B" r="12"></circle>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 64 64"><defs><clipPath id="a"><path d="m38.802 1.632 23.656 29.212-20.473 31.524-36.307-9.729L3.71 15.102z"/></clipPath><filter id="b"><feGaussianBlur stdDeviation="15"/></filter></defs><g clip-path="url(#a)" filter="url(#b)"><circle cy="10" r="39" fill="#F44336"/><circle cx="45" cy="-5" r="39" fill="#FFEB3B"/><circle cx="70" cy="25" r="39" fill="#4CAF50"/><circle cx="50" cy="65" r="39" fill="#03A9F4"/><circle cx="5" cy="70" r="39" fill="#673AB7"/><circle cx="50" cy="31.5" r="12" fill="#4CAF50"/><circle cx="37.5" cy="50.5" r="12" fill="#03A9F4"/><circle cx="15.5" cy="44.5" r="12" fill="#673AB7"/><circle cx="14" cy="22" r="12" fill="#F44336"/><circle cx="36" cy="14" r="12" fill="#FFEB3B"/></g></svg>
|
||||
|
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 786 B |
|
@ -1,6 +1,9 @@
|
|||
#!/bin/bash
|
||||
# This script converts raw SVG icons to favicons according to the article:
|
||||
# https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs
|
||||
#
|
||||
# © 2023 Nikita Karamov
|
||||
# Licensed under AGPL v3 or later
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
@ -9,17 +12,9 @@ if ! type "magick"; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "[1/4] Making 'favicon.ico' for legacy browsers..."
|
||||
magick convert -background none assets/pentagon.svg -alpha Set -define icon:auto-resize="32,16" -channel RGBA -depth 8 public/favicon.ico
|
||||
node script/icons.js
|
||||
|
||||
echo "[2/4] Making 'icon.svg' for modern browsers..."
|
||||
cp -f assets/pentagon.svg public/icon.svg
|
||||
|
||||
echo "[3/4] Making 'apple-touch-icon.png'..."
|
||||
magick convert assets/s2f.svg -resize 140x140 -background white -gravity center -extent 180x180 public/apple-touch-icon.png
|
||||
|
||||
echo "[4/4] Making 'icon-*.png' icons for PWAs..."
|
||||
magick convert -background none assets/s2f.svg -alpha Set -resize 192x192 public/icon-192.png
|
||||
magick convert -background none assets/s2f.svg -alpha Set -resize 512x512 public/icon-512.png
|
||||
magick convert public/favicon-32.png public/favicon-16.png public/favicon.ico
|
||||
rm public/favicon-32.png public/favicon-16.png
|
||||
|
||||
echo "Done."
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*!
|
||||
* © 2023 Nikita Karamov
|
||||
* Licensed under AGPL v3 or later
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import sharp from "sharp";
|
||||
import { optimize } from "svgo";
|
||||
|
||||
const pentagon = join(".", "assets", "pentagon.svg");
|
||||
const smallLogo = join(".", "assets", "s2f.svg");
|
||||
|
||||
const outputDirectory = join(".", "public");
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
sharp(pentagon).resize(32).toFile(join(outputDirectory, "favicon-32.png")),
|
||||
sharp(pentagon).resize(16).toFile(join(outputDirectory, "favicon-16.png")),
|
||||
sharp(smallLogo)
|
||||
.resize(140)
|
||||
.extend({
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
background: "#000",
|
||||
})
|
||||
.flatten({ background: "#000" })
|
||||
.toFile(join(outputDirectory, "apple-touch-icon.png")),
|
||||
sharp(smallLogo).resize(192).toFile(join(outputDirectory, "icon-192.png")),
|
||||
sharp(smallLogo).resize(512).toFile(join(outputDirectory, "icon-512.png")),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
join(outputDirectory, "icon.svg"),
|
||||
optimize(readFileSync(pentagon), {
|
||||
path: pentagon,
|
||||
multipass: true,
|
||||
}).data,
|
||||
);
|
|
@ -0,0 +1,144 @@
|
|||
---
|
||||
/*!
|
||||
* © 2023 Nikita Karamov
|
||||
* Licensed under AGPL v3 or later
|
||||
*/
|
||||
|
||||
let instances;
|
||||
try {
|
||||
const response = await fetch(new URL("/api/instances", Astro.url));
|
||||
instances = await response.json();
|
||||
} catch (error) {
|
||||
console.error("Couln't fetch instances:", error);
|
||||
instances = [];
|
||||
}
|
||||
|
||||
const { prefilledInstance } = Astro.props;
|
||||
---
|
||||
|
||||
<datalist id="instanceDatalist">
|
||||
{instances.map((instance) => <option value={instance} />)}
|
||||
</datalist>
|
||||
<label id="s2f-instanceContainer">
|
||||
Mastodon, Pleroma, or GNU Social instance
|
||||
<div class="instance-input">
|
||||
<span id="https-label">https://</span>
|
||||
<input
|
||||
type="text"
|
||||
name="instance"
|
||||
id="instance"
|
||||
placeholder="mastodon.social"
|
||||
list="instanceDatalist"
|
||||
required
|
||||
aria-describedby="https-label"
|
||||
value={prefilledInstance}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label for="remember">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="remember"
|
||||
name="remember"
|
||||
/>
|
||||
Remember my instance on this device
|
||||
</label>
|
||||
|
||||
<style lang="scss">
|
||||
:global(.previously-used) {
|
||||
color: var(--s2f-accent-color-contrast);
|
||||
cursor: pointer;
|
||||
text-decoration: 1px solid underline currentColor;
|
||||
}
|
||||
|
||||
.instance-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
width: 1%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { extractHost, normalizeURL } from "../util";
|
||||
|
||||
const LOCAL_STORAGE_KEY = "recentInstances";
|
||||
const RECENT_INSTANCES_SIZE = 5;
|
||||
|
||||
const $form = document.querySelector("#js-s2f-form");
|
||||
const $instanceContainer = document.querySelector("#s2f-instanceContainer");
|
||||
const $instance: HTMLInputElement = document.querySelector("#instance");
|
||||
|
||||
const getSavedInstances = (): Array<string> => {
|
||||
const storageValue = window.localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
if (!storageValue) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return JSON.parse(storageValue);
|
||||
};
|
||||
|
||||
const savedInstances = getSavedInstances();
|
||||
|
||||
if (savedInstances.length > 0) {
|
||||
$instanceContainer.append(
|
||||
"Previously used: ",
|
||||
...savedInstances
|
||||
.flatMap((instance, index) => {
|
||||
if (!instance) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const host = extractHost(instance);
|
||||
if (index == 0 && !$instance.value) {
|
||||
$instance.value = host;
|
||||
}
|
||||
const element = document.createElement("span");
|
||||
element.textContent = host;
|
||||
element.classList.add("previously-used");
|
||||
element.addEventListener("click", () => {
|
||||
$instance.value = host;
|
||||
});
|
||||
return [element, ", "];
|
||||
})
|
||||
.slice(0, -1),
|
||||
);
|
||||
}
|
||||
|
||||
$form.addEventListener("submit", (event) => {
|
||||
const formData = new FormData(event.target as HTMLFormElement);
|
||||
|
||||
if (formData.get("remember")) {
|
||||
const instance = normalizeURL(formData.get("instance") as string);
|
||||
const index = savedInstances.indexOf(instance);
|
||||
if (index >= 0) {
|
||||
savedInstances.splice(index, 1);
|
||||
}
|
||||
savedInstances.unshift(instance);
|
||||
savedInstances.length = RECENT_INSTANCES_SIZE;
|
||||
|
||||
window.localStorage.setItem(
|
||||
LOCAL_STORAGE_KEY,
|
||||
JSON.stringify(savedInstances),
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,35 @@
|
|||
/*!
|
||||
* © 2022 Nikita Karamov
|
||||
* Licensed under AGPL v3 or later
|
||||
*/
|
||||
|
||||
// This is the analytics code for Share₂Fedi. It just sends a beacon
|
||||
// to GoatCounter with hardcoded path. This is way more lightweight, performant
|
||||
// and privacy-friendly than the default GC script.
|
||||
if (
|
||||
window.location.host === "s2f.kytta.dev" ||
|
||||
window.location.host === "share2fedi.kytta.dev"
|
||||
) {
|
||||
// eslint-disable-next-line unicorn/prefer-top-level-await
|
||||
fetch("//gc.zgo.at/", { method: "HEAD" }).then((result) => {
|
||||
// Check if the default GC URL resolves
|
||||
// This allows us to not track people with ad blockers
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const screen = encodeURIComponent(
|
||||
[
|
||||
window.screen.width,
|
||||
window.screen.height,
|
||||
window.devicePixelRatio || 1,
|
||||
].join(","),
|
||||
);
|
||||
|
||||
const random = encodeURIComponent(Math.random().toString(36).slice(2));
|
||||
|
||||
navigator.sendBeacon(
|
||||
`https://share2fedi.goatcounter.com/count?p=%2F&s=${screen}&b=0&rnd=${random}`,
|
||||
);
|
||||
});
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="astro/client" />
|
|
@ -0,0 +1,83 @@
|
|||
/*!
|
||||
* © 2023 Nikita Karamov
|
||||
* Licensed under AGPL v3 or later
|
||||
*/
|
||||
|
||||
import { APIRoute } from "astro";
|
||||
import { normalizeURL } from "../../../util";
|
||||
|
||||
const PROJECTS = {
|
||||
mastodon: {
|
||||
checkUrl: "/api/v1/instance/rules",
|
||||
publishEndpoint: "share",
|
||||
params: {
|
||||
text: "text",
|
||||
},
|
||||
},
|
||||
gnuSocial: {
|
||||
checkUrl: "/api/statusnet/version.xml",
|
||||
publishEndpoint: "/notice/new",
|
||||
params: {
|
||||
text: "status_textarea",
|
||||
},
|
||||
},
|
||||
pleroma: {
|
||||
checkUrl: "/api/v1/pleroma/federation_status",
|
||||
publishEndpoint: "share",
|
||||
params: {
|
||||
text: "message",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const checkProjectUrl = (
|
||||
urlToCheck: URL,
|
||||
projectId: string,
|
||||
): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(urlToCheck)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
resolve(projectId);
|
||||
} else {
|
||||
reject(urlToCheck);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error.toString());
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const get: APIRoute = async ({ params }) => {
|
||||
const host = params.host;
|
||||
|
||||
const promises = Object.entries(PROJECTS).map(([service, { checkUrl }]) =>
|
||||
checkProjectUrl(new URL(checkUrl, normalizeURL(host)), service),
|
||||
);
|
||||
try {
|
||||
const project = await Promise.any(promises);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
host,
|
||||
project,
|
||||
publishEndpoint: PROJECTS[project].publishEndpoint,
|
||||
params: PROJECTS[project].params,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"Cache-Control": "s-maxage=86400, max-age=86400, public",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ error: "Couldn't detect instance" }), {
|
||||
status: 400,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
/*!
|
||||
* © 2023 Nikita Karamov
|
||||
* Licensed under AGPL v3 or later
|
||||
*/
|
||||
|
||||
import { APIRoute } from "astro";
|
||||
|
||||
export const get: APIRoute = async () => {
|
||||
try {
|
||||
const response = await fetch("https://api.joinmastodon.org/servers");
|
||||
const instances = await response.json();
|
||||
|
||||
return new Response(
|
||||
JSON.stringify(instances.map((instance) => instance.domain)),
|
||||
{
|
||||
headers: {
|
||||
"Cache-Control": "s-maxage=86400, max-age=86400, public",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Could not fetch instances:", error);
|
||||
return new Response(JSON.stringify([]), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/*!
|
||||
* © 2023 Nikita Karamov
|
||||
* Licensed under AGPL v3 or later
|
||||
*/
|
||||
|
||||
import { APIRoute } from "astro";
|
||||
|
||||
export const post: APIRoute = async ({ redirect, request, url }) => {
|
||||
const formData = await request.formData();
|
||||
|
||||
const text = (formData.get("text") as string) || "";
|
||||
const instanceHost =
|
||||
(formData.get("instance") as string) || "mastodon.social";
|
||||
|
||||
try {
|
||||
const response = await fetch(new URL(`/api/detect/${instanceHost}`, url));
|
||||
const { host, publishEndpoint, params } = await response.json();
|
||||
const publishUrl = new URL(publishEndpoint, `https://${host}/`);
|
||||
publishUrl.search = new URLSearchParams([[params.text, text]]).toString();
|
||||
return redirect(publishUrl.toString(), 303);
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ error: "Couldn't detect instance" }), {
|
||||
status: 400,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
|
@ -1,29 +1,20 @@
|
|||
<!--
|
||||
@source: https://github.com/kytta/share2fedi/blob/main/index.html
|
||||
---
|
||||
/*!
|
||||
* © 2023 Nikita Karamov
|
||||
* Licensed under AGPL v3 or later
|
||||
*/
|
||||
import InstanceSelect from "../components/instance-select.astro";
|
||||
import "../styles/main.scss";
|
||||
|
||||
share2fedi - Instance-agnostic share page for the Fediverse.
|
||||
Copyright (C) 2020-2023 Nikita Karamov <me@kytta.dev>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program 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.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
const searchParameters = new URL(Astro.request.url).searchParams;
|
||||
const prefilledText = searchParameters.get("text");
|
||||
const prefilledInstance = searchParameters.get("instance");
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
|
@ -33,26 +24,14 @@
|
|||
</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Share₂Fedi is a share page for Mastodon, Pleroma, and GNU Social. Type in your post text and the instance URL and click ‘Publish!’"
|
||||
content="Share₂Fedi is a share page for Mastodon, Pleroma, and GNU Social. Type in your post text and the instance URL and click ‘Publish!’"
|
||||
/>
|
||||
<link
|
||||
rel="canonical"
|
||||
href="https://s2f.kytta.dev/"
|
||||
/>
|
||||
|
||||
<script
|
||||
type="module"
|
||||
src="/lib/main.js"
|
||||
async
|
||||
defer
|
||||
></script>
|
||||
<script
|
||||
type="module"
|
||||
src="/lib/count.js"
|
||||
async
|
||||
defer
|
||||
></script>
|
||||
|
||||
<script src="../count.js"></script>
|
||||
<link
|
||||
rel="icon"
|
||||
href="/favicon.ico"
|
||||
|
@ -71,6 +50,11 @@
|
|||
rel="manifest"
|
||||
href="/manifest.webmanifest"
|
||||
/>
|
||||
|
||||
<meta
|
||||
name="generator"
|
||||
content="{Astro.generator}"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
|
@ -95,30 +79,11 @@
|
|||
rows="7"
|
||||
placeholder="What's on your mind?"
|
||||
required
|
||||
></textarea>
|
||||
>{prefilledText}</textarea
|
||||
>
|
||||
</label>
|
||||
|
||||
<datalist id="instanceDatalist"></datalist>
|
||||
<label>
|
||||
Choose your Mastodon, Pleroma, or GNU Social instance
|
||||
<input
|
||||
type="url"
|
||||
name="instance"
|
||||
id="instance"
|
||||
placeholder="https://"
|
||||
list="instanceDatalist"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label for="remember">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="remember"
|
||||
name="remember"
|
||||
/>
|
||||
Remember my instance on this device
|
||||
</label>
|
||||
<InstanceSelect prefilledInstance={prefilledInstance} />
|
||||
|
||||
<input
|
||||
type="submit"
|
|
@ -1,25 +1,10 @@
|
|||
/*!
|
||||
* @source: https://github.com/kytta/share2fedi/blob/main/lib/scss/style.scss
|
||||
*
|
||||
* share2fedi - Instance-agnostic share page for the Fediverse.
|
||||
* Copyright (C) 2020-2023 Nikita Karamov <me@kytta.dev>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* © 2023 Nikita Karamov
|
||||
* Licensed under AGPL v3 or later
|
||||
*/
|
||||
|
||||
@use "sass:math";
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
|
@ -31,6 +16,7 @@
|
|||
--s2f-accent-color-light: #5d8379;
|
||||
--s2f-accent-color-contrast: #005e4e;
|
||||
--s2f-border-color: #ccc;
|
||||
--s2f-input-group-bg-color: #eee;
|
||||
--s2f-input-bg-color: #fff;
|
||||
--s2f-input-text-color: #000;
|
||||
--s2f-button-text-color: #fff;
|
||||
|
@ -103,7 +89,7 @@ textarea {
|
|||
resize: vertical;
|
||||
}
|
||||
|
||||
input[type="url"],
|
||||
input[type="text"],
|
||||
textarea {
|
||||
width: 100%;
|
||||
color: var(--s2f-input-text-color);
|
||||
|
@ -137,6 +123,7 @@ input[type="submit"] {
|
|||
--s2f-accent-color-light: #619587;
|
||||
--s2f-accent-color-contrast: #a8f7e2;
|
||||
--s2f-border-color: #333;
|
||||
--s2f-input-group-bg-color: #111;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -147,10 +134,10 @@ input[type="submit"] {
|
|||
}
|
||||
|
||||
main {
|
||||
width: (200% / 3);
|
||||
width: math.div(200%, 3);
|
||||
}
|
||||
|
||||
aside {
|
||||
width: (100% / 3);
|
||||
width: math.div(100%, 3);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*!
|
||||
* © 2023 Nikita Karamov
|
||||
* Licensed under AGPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 extractHost = (url: string): string => {
|
||||
if (!(url.startsWith("https://") || url.startsWith("http://"))) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
return new URL(url).host;
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/base"
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import legacy from "@vitejs/plugin-legacy";
|
||||
import { ViteImageOptimizer } from "vite-plugin-image-optimizer";
|
||||
import { VitePluginNode } from "vite-plugin-node";
|
||||
|
||||
const vitePluginNode = VitePluginNode({
|
||||
// Workaround from: https://github.com/axe-me/vite-plugin-node/issues/47
|
||||
adapter({ app, req, res, next }) {
|
||||
if (req.url.startsWith("/api/")) {
|
||||
app(req, res);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
appPath: "./api/share.js",
|
||||
});
|
||||
vitePluginNode[0].apply = "serve";
|
||||
|
||||
export default {
|
||||
build: {
|
||||
minify: "terser",
|
||||
terserOptions: { ecma: 5 },
|
||||
sourcemap: "true",
|
||||
},
|
||||
plugins: [legacy(), ...vitePluginNode, ViteImageOptimizer({})],
|
||||
};
|