New tailwind-based joinlemmy-site (#243)

* Starting on new tailwind based joinlemmy-site.

* Formatting fix.

* Adding follow communities block.

* Adding a few more blocks.

* Finishing up blocks.

* Adding a few more pages.

* Adding apps page.

* Almost done with donation page.

* Adding most of instances page.

* Trying to fix CI 1.

* Adding navbar and footer.

* Adding bottom spacer.

* Finishing up more info modal.

* Adding icons to main page.

* Eruda only in development mode.

* Finishing up main page, starting to work on recs.

* Adding main images.

* Adding images 2.

* Starting to add filters.

* Finishing up helper modal.

* Adding topic icons.

* Adding more instances.

* Fixing recommended.

* Forgot to add instance picker.

* Adding world background image.

* Adding alexandrite.

* Adding funding goal block.

* Fix dockerfile.

* Upgrading deps.

* Fixing package json.

* Updating coders, sponsors.

* Fixing mobile margins.

* Fixing navbar auto-close when clicked.

* Removing todo.

* Removing some useless instance helper links.

* Fixing news titling.

* Addressing PR comments.

* Updating instance stats.

* Fixing class -> className

* Fixing sm:max directives.

* Make instance images links to their sites.

* Use ubuntu font.

* Addressing PR comments.

* Adding a few more android apps.

* Adding thunder and combustible apps.

* Fixing z index.

* Add a warning alert for closed source apps.

* Adding MLMYM app. Fixes #213

* Fixing i18n key.

* Adding QR codes for cryptos. Fixes #219

* Addressing PR comments.

* Fixing news preview.

* Adding registration mode to details modal. Fixes #153

* Filter out bot instances.

* Using glide carousel.

* Adding glide min css.

* Adding donation platform fetching. Fixes #248

* Prettying glide css.

* Change dev goal to 3

* Adding sign up button.

* Minifying docker image.

* Removing sortpack.
This commit is contained in:
Dessalines 2023-10-31 09:31:03 -04:00 committed by GitHub
parent d47e848b25
commit d17c4d7d2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 10615 additions and 9349 deletions

View File

@ -1,3 +1,4 @@
.git
node_modules
dist
lemmy-stats-crawler

View File

@ -1,3 +1,4 @@
generate_translations.js
webpack.config.js
tailwind.config.js
src/api_tests

3
.gitmodules vendored
View File

@ -5,7 +5,7 @@
[submodule "joinlemmy-translations"]
path = joinlemmy-translations
url = https://github.com/lemmynet/joinlemmy-translations
branch = main
branch = tailwind_rework
[submodule "lemmy-translations"]
path = lemmy-translations
url = https://github.com/lemmynet/lemmy-translations
@ -17,3 +17,4 @@
[submodule "lemmy-stats-crawler"]
path = lemmy-stats-crawler
url = https://github.com/LemmyNet/lemmy-stats-crawler.git
branch = main

View File

@ -16,25 +16,25 @@ pipeline:
prettier_markdown_check:
image: tmknom/prettier:3.0.0
commands:
- prettier -c . "!dist" "!lemmy-docs" "!lemmy-translations" "!joinlemmy-translations" "!lemmy-js-client" "!lemmy-stats-crawler" "!src/shared/instance_stats.ts"
- prettier -c . "!dist" "!lemmy-docs" "!lemmy-translations" "!joinlemmy-translations" "!lemmy-js-client" "!lemmy-stats-crawler" "!src/shared/instance_stats.ts" "!src/shared/donation_stats.ts"
yarn:
image: node:14-alpine
image: node:alpine
commands:
- yarn
yarn_lint:
image: node:14-alpine
image: node:alpine
commands:
- yarn lint
yarn_build_dev:
image: node:14-alpine
image: node:alpine
commands:
- yarn build:dev
nightly_instance_crawl:
image: node:14-alpine
image: node:alpine
commands:
# libpq and openssl can probably be removed after lemmy dep is upgraded to 0.16.4+
- apk add cargo pkgconfig openssl openssl-dev libpq libpq-dev
@ -58,7 +58,7 @@ pipeline:
- cron
release_instance_crawl:
image: node:14-alpine
image: node:alpine
commands:
# libpq and openssl can probably be removed after lemmy dep is upgraded to 0.16.4+
- apk add cargo pkgconfig openssl openssl-dev libpq libpq-dev

View File

@ -24,19 +24,21 @@ RUN yarn docs
# Build the isomorphic app
FROM node:alpine as builder
RUN apk update && apk add yarn python3 build-base gcc wget git --no-cache
RUN apk update && apk add yarn python3 build-base gcc wget git curl --no-cache
RUN curl -sf https://gobinaries.com/tj/node-prune | sh
WORKDIR /app
# Cache deps
COPY package.json yarn.lock ./
RUN yarn install --pure-lockfile
RUN yarn --production --prefer-offline --pure-lockfile
# Build
COPY tsconfig.json \
webpack.config.js \
.babelrc \
generate_translations.mjs \
tailwind.config.js \
./
COPY joinlemmy-translations joinlemmy-translations
@ -47,9 +49,18 @@ COPY src src
COPY --from=docs /app/docs ./src/assets/docs
COPY --from=api /app/lemmy-js-client/docs ./src/assets/api
RUN yarn install --pure-lockfile
RUN yarn --production --prefer-offline
RUN yarn build:prod
# Prune the image
RUN node-prune ./node_modules
RUN rm -rf ./node_modules/import-sort-parser-typescript
RUN rm -rf ./node_modules/typescript
RUN rm -rf ./node_modules/npm
RUN du -sh ./node_modules/* | sort -nr | grep '\dM.*'
FROM node:alpine as runner
COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/node_modules /app/node_modules

View File

@ -52,7 +52,7 @@ try {
process.stdout.write(strData);
});
run.on("close", exitCode => {
run.on("close", _exitCode => {
var stats = JSON.parse(savedOutput);
// Crawl results from all instances include tons of data which needs to be compiled.
// If it is too much data it breaks the build, so we need to exclude as much as possible.

View File

@ -4,8 +4,10 @@
./update_submodules.sh
yarn crawl
yarn update-donations
git add "src/shared/instance_stats.ts"
git commit -m "Crawl instance statistics"
git add "src/shared/donation_stats.ts"
git commit -m "Crawl and donation instance statistics"
# look for unused translations
for langfile in joinlemmy-translations/translations/*.json; do

@ -1 +1 @@
Subproject commit 1bc69869fda7ee144ddfbc4d9fb29af3a0d4619e
Subproject commit 4e8db96053291ec8d147e41b4e6b4640dc16712f

@ -1 +1 @@
Subproject commit 0dc13b4ea11daaa07d964c17f7a622e3a222367a
Subproject commit a6e2572e4c81f70ef9ae48e16c39265d4714f92c

@ -1 +1 @@
Subproject commit 4553c749cb0fb74d62fd156ebd119dc479693dfd
Subproject commit 406d1c187bd4bd13203cfd41ea1c8872e69aae3f

@ -1 +1 @@
Subproject commit 788a3dd6e02fbe153e6d7c6315601ade15637f8c
Subproject commit ff8a2db505771c9f142ab31626b49de54a756192

@ -1 +1 @@
Subproject commit 5a9d44656e2658ab7cb2dbec3fd1bfaf57654533
Subproject commit 0bc4ee5944d2b5d1f2a83ec558788e7bc5d7a445

View File

@ -8,64 +8,71 @@
"build:prod": "webpack --mode=production",
"clean": "yarn run rimraf dist",
"crawl": "node crawl.mjs",
"update-donations": "node update_donations.mjs",
"lint": "node generate_translations.mjs && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src && prettier --check \"src/**/*.{ts,tsx,js,css,scss}\"",
"prebuild:dev": "yarn clean && node generate_translations.mjs",
"prebuild:prod": "yarn clean && node generate_translations.mjs",
"prebuild:dev": "yarn clean && node generate_translations.mjs && yarn tailwind",
"prebuild:prod": "yarn clean && node generate_translations.mjs && yarn tailwind",
"tailwind": "tailwindcss -i ./src/style.css -o ./dist/styles/styles.css --minify",
"prepare": "husky install",
"start": "yarn build:dev --watch"
"start": "yarn build:dev --watch & yarn tailwind --watch"
},
"repository": "https://github.com/LemmyNet/joinlemmy-site",
"dependencies": {
"@typescript-eslint/parser": "^5.60.1",
"chota": "^0.9.2",
"@glidejs/glide": "3.5.2",
"classnames": "^2.3.2",
"express": "~4.18.2",
"i18next": "^23.2.6",
"inferno": "^8.2.1",
"inferno-create-element": "^8.2.1",
"i18next": "^23.5.1",
"inferno": "^8.2.2",
"inferno-create-element": "^8.2.2",
"inferno-helmet": "^5.2.1",
"inferno-hydrate": "^8.2.1",
"inferno-hydrate": "^8.2.2",
"inferno-i18next": "github:nimbusec-oss/inferno-i18next#semver:^7.4.2",
"inferno-router": "^8.2.1",
"inferno-server": "^8.2.1",
"markdown-it": "^13.0.1",
"node-fetch": "^3.3.1"
},
"devDependencies": {
"@babel/core": "^7.22.5",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-transform-runtime": "^7.22.5",
"@babel/plugin-transform-typescript": "^7.22.5",
"@babel/preset-env": "7.22.5",
"@babel/preset-typescript": "^7.22.5",
"@babel/runtime": "^7.22.5",
"@types/express": "^4.17.17",
"@types/node": "^20.3.2",
"@types/node-fetch": "^2.6.4",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"babel-loader": "^9.1.2",
"babel-plugin-inferno": "^6.6.0",
"inferno-router": "^8.2.2",
"tailwindcss": "^3.3.3",
"inferno-server": "^8.2.2",
"webpack": "5.88.2",
"daisyui": "^3.9.2",
"@babel/runtime": "^7.23.2",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^11.0.0",
"countries-list": "^2.6.1",
"css-loader": "^6.8.1",
"eslint": "^8.43.0",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.3",
"lint-staged": "^13.2.3",
"mini-css-extract-plugin": "^2.7.6",
"prettier": "^3.0.0",
"rimraf": "^5.0.1",
"run-node-webpack-plugin": "^1.3.0",
"sass": "^1.63.6",
"sass-loader": "^13.3.2",
"sortpack": "^2.3.4",
"style-loader": "^3.3.3",
"terser": "^5.18.2",
"typescript": "^5.1.6",
"webpack": "5.88.1",
"@tailwindcss/typography": "^0.5.10",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-transform-runtime": "^7.23.2",
"@babel/plugin-transform-typescript": "^7.22.15",
"markdown-it": "^13.0.2",
"babel-loader": "^9.1.3",
"@babel/preset-env": "7.23.2",
"@babel/preset-typescript": "^7.23.2",
"babel-plugin-inferno": "^6.7.0",
"@babel/core": "^7.23.2",
"webpack-node-externals": "^3.0.0",
"countries-list": "^2.6.1",
"node-fetch": "^3.3.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "4.15.1",
"webpack-node-externals": "^3.0.0"
"qrcode": "^1.5.3"
},
"devDependencies": {
"@typescript-eslint/parser": "^6.7.5",
"@types/express": "^4.17.19",
"@types/glidejs__glide": "^3.6.3",
"@types/node": "^20.8.4",
"@types/node-fetch": "^2.6.6",
"@types/qrcode": "^1.5.4",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"css-loader": "^6.8.1",
"eslint": "^8.51.0",
"eslint-plugin-prettier": "^5.0.1",
"husky": "^8.0.3",
"lint-staged": "^14.0.1",
"prettier": "^3.0.3",
"rimraf": "^5.0.5",
"sass": "^1.69.3",
"sass-loader": "^13.3.2",
"style-loader": "^3.3.3",
"terser": "^5.21.0",
"typescript": "^5.2.2",
"webpack-dev-server": "4.15.1"
},
"engines": {
"node": ">=8.9.0"
@ -75,9 +82,6 @@
"*.{ts,tsx,js}": [
"prettier --write",
"eslint --fix"
],
"package.json": [
"sortpack"
]
}
}

View File

@ -1,24 +1,19 @@
{
"en": [
"sopuli.xyz",
"sh.itjust.works",
"lemmy.fmhy.ml",
"discuss.tchncs.de",
"start_scan_instances": [
"lemmy.ml",
"lemm.ee",
"lemmy.world",
"feddit.it",
"lemmygrad.ml",
"reddthat.com",
"lemmy.sdf.org",
"feddit.uk",
"hexbear.net",
"lemmy.fmhy.ml",
"discuss.online",
"feddit.de",
"lemmings.world"
],
"fr": ["sh.itjust.works"],
"da": ["feddit.dk"],
"de": ["feddit.de", "discuss.tchncs.de"],
"nl": ["feddit.nl"],
"pt": ["lemmy.pt"],
"pt-PT": ["lemmy.pt"],
"pt-BR": ["lemmy.pt"],
"eu": ["lemmy.eus"],
"ja": ["tabinezumi.net", "lm.korako.me"],
"it": ["feddit.it"],
"exclude": [
"lemmy.glasgow.social",
"ds9.lemmy.ml",
@ -29,6 +24,7 @@
"lemmy.burger.rodeo",
"bakchodi.org",
"lemmy.comfysnug.space",
"ani.social",
"rqd2.net"
]
}

View File

@ -1,67 +0,0 @@
:root {
--font-family-sans: Inter, Helvetica, Roboto, sans-serif;
--grid-maxWidth: 108rem;
--grid-gutter: 4rem;
--color-success: #fafafa;
--bg-color: #222222;
--bg-secondary-color: #131316;
--font-color: #f5f5f5;
--color-grey: #ccc;
--color-darkGrey: #777;
--color-success: #222222;
}
.card {
webkit-box-shadow: unset;
box-shadow: unset;
border: 1px solid var(--color-darkGrey) !important;
}
.stylized {
font-family: "CaviarDreams", Fallback, sans-serif;
font-size: 4em;
font-weight: bold;
margin: 0;
}
.icon {
display: inline-block;
max-width: 1.5rem;
max-height: 1.5rem;
stroke-width: 0;
stroke: currentColor;
fill: currentColor;
}
p {
font: 1.2em/1.62 sans-serif;
}
.join-banner {
width: 100%;
height: 100px;
object-fit: scale-down;
}
.app-banner {
width: 100%;
height: 300px;
object-fit: scale-down;
}
.app-icon {
display: inline-block;
height: 30px;
width: 30px;
margin-right: 10px;
}
img {
max-width: unset;
}
.footer-name {
display: inline-block !important;
padding: 0px 6px 0px 6px !important;
}
.gold {
border-color: #ffd700 !important;
color: #ffd700;
}
.language-selector {
margin-top: 7px;
background-color: #333;
}

55
src/assets/glide.core.min.css vendored Normal file
View File

@ -0,0 +1,55 @@
.glide {
position: relative;
width: 100%;
box-sizing: border-box;
}
.glide * {
box-sizing: inherit;
}
.glide__track {
overflow: hidden;
}
.glide__slides {
position: relative;
width: 100%;
list-style: none;
backface-visibility: hidden;
transform-style: preserve-3d;
touch-action: pan-Y;
overflow: hidden;
margin: 0;
padding: 0;
white-space: nowrap;
display: flex;
flex-wrap: nowrap;
will-change: transform;
}
.glide__slides--dragging {
user-select: none;
}
.glide__slide {
width: 100%;
height: 100%;
flex-shrink: 0;
white-space: normal;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}
.glide__slide a {
user-select: none;
-webkit-user-drag: none;
-moz-user-select: none;
-ms-user-select: none;
}
.glide__arrows {
-webkit-touch-callout: none;
user-select: none;
}
.glide__bullets {
-webkit-touch-callout: none;
user-select: none;
}
.glide--rtl {
direction: rtl;
} /*# sourceMappingURL=glide.core.min.css.map */

View File

@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100"
height="100"
viewBox="0 0 26.458333 26.458333"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="logo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="11.313708"
inkscape:cx="41.233164"
inkscape:cy="48.436815"
inkscape:window-width="2560"
inkscape:window-height="1367"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="false">
<inkscape:grid
type="xygrid"
id="grid1971" />
</sodipodi:namedview>
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient2194">
<stop
style="stop-color:#69214d;stop-opacity:1;"
offset="0"
id="stop2190" />
<stop
style="stop-color:#00b6da;stop-opacity:1;"
offset="1"
id="stop2192" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient2186">
<stop
style="stop-color:#702348;stop-opacity:1;"
offset="0"
id="stop2182" />
<stop
style="stop-color:#00b6da;stop-opacity:1;"
offset="1"
id="stop2184" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient2178">
<stop
style="stop-color:#96275c;stop-opacity:1;"
offset="0.30952382"
id="stop2174" />
<stop
style="stop-color:#00b6da;stop-opacity:1;"
offset="1"
id="stop2176" />
</linearGradient>
<inkscape:path-effect
effect="mirror_symmetry"
start_point="3.5167243,10.263454"
end_point="50,50"
center_point="25,25"
id="path-effect1226"
is_visible="true"
lpeversion="1.2"
lpesatellites=""
mode="vertical"
discard_orig_path="false"
fuse_paths="false"
oposite_fuse="false"
split_items="false"
split_open="false"
link_styles="false" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2178"
id="linearGradient2180"
x1="8.4303036"
y1="14.992496"
x2="19.706308"
y2="7.0842395"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.97582708,0,0,0.91201314,0.31978739,1.3079239)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2186"
id="linearGradient2188"
x1="18.118645"
y1="17.21011"
x2="20.332994"
y2="7.4455729"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.98836857,0,0,0.92373448,0.64928223,1.3577141)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2194"
id="linearGradient2196"
x1="10.871438"
y1="6.762928"
x2="20.164021"
y2="2.1349447"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.98836857,0,0,0.92373448,0.15387403,0.56481077)" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
id="path1853"
style="fill:#d3f0f7;fill-opacity:0.996078;stroke-width:0.332563"
d="m 23.600833,16.98625 -7.540624,6.630448 H 10.398125 L 2.8575,16.98625 5.476875,6.8510476 13.229167,2.8945519 20.981458,6.8510477 Z"
sodipodi:nodetypes="cccccccc" />
<path
id="path1907"
style="fill:url(#linearGradient2180);fill-opacity:1;stroke:none;stroke-width:0.277336"
inkscape:transform-center-y="1.5295961"
d="M 13.229167,18.199166 7.0326646,7.8231178 c 4.1828054,-0.014194 3.0966154,0.014192 12.3930044,0 z"
sodipodi:nodetypes="cccc" />
<path
id="path1915"
style="fill:url(#linearGradient2196);fill-opacity:1;stroke:none;stroke-width:0.152598"
inkscape:transform-center-y="-0.50800231"
d="M 13.229167,3.791931 6.9530261,6.9193343 H 19.505307 Z"
sodipodi:nodetypes="cccc" />
<path
id="path1927"
style="fill:url(#linearGradient2188);fill-opacity:1;stroke:none;stroke-width:0.208"
inkscape:transform-center-x="0.75322077"
inkscape:transform-center-y="-0.96019183"
d="M 22.354268,16.021999 20.262221,8.0299637 14.012231,18.539368 Z"
sodipodi:nodetypes="cccc" />
<path
id="path1929"
style="fill:#82053f;fill-opacity:0.996078;stroke:none;stroke-width:0.208"
inkscape:transform-center-x="-0.75322083"
inkscape:transform-center-y="-0.96019183"
d="M 4.1040652,16.021999 6.1961119,8.0299637 12.446102,18.539368 Z"
sodipodi:nodetypes="cccc" />
<path
id="path1931"
style="fill:#59001f;fill-opacity:0.996078;stroke:none;stroke-width:0.183074"
inkscape:transform-center-x="-0.48119251"
inkscape:transform-center-y="-0.015901639"
d="M 10.746314,22.448487 4.0779145,16.802736 12.472252,19.442307 Z"
sodipodi:nodetypes="cccc" />
<path
id="path1934"
style="fill:#730036;fill-opacity:0.996078;stroke:none;stroke-width:0.183074"
inkscape:transform-center-x="0.48119254"
inkscape:transform-center-y="-0.015901676"
d="m 15.712019,22.448487 6.6684,-5.645751 -8.394338,2.639571 z"
sodipodi:nodetypes="cccc" />
<path
id="path1936"
style="fill:#540028;fill-opacity:0.996078;stroke:none;stroke-width:0.0735944"
inkscape:transform-center-y="-0.44659471"
d="m 13.229167,19.902129 -1.689503,2.752004 h 3.379005 z"
sodipodi:nodetypes="cccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
height="1000"
width="1000"
version="1.1"
id="Layer_1"
viewBox="0 0 640 640"
xml:space="preserve"
sodipodi:docname="rocket-svgrepo-com.svg"
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs23" /><sodipodi:namedview
id="namedview21"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="1"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="0.295"
inkscape:cx="400"
inkscape:cy="415.25424"
inkscape:window-width="1269"
inkscape:window-height="605"
inkscape:window-x="171"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="Layer_1" />
<path
style="fill:#507c5c"
d="M 408.318,442.104 H 214.142 c -7.579,0 -13.727,-6.135 -13.743,-13.713 -0.004,-1.861 -0.41,-186.327 -0.379,-217.421 0.058,-61.913 63.73,-121.176 83.243,-137.865 16.24,-13.889 39.78,-13.841 55.973,0.115 22.198,19.129 52.373,52.623 70.487,89.258 3.364,6.804 0.576,15.047 -6.227,18.41 -6.807,3.363 -15.047,0.576 -18.41,-6.227 -16.294,-32.951 -43.637,-63.25 -63.795,-80.62 -5.833,-5.026 -14.312,-5.051 -20.164,-0.048 -35.566,30.419 -73.584,77.13 -73.621,117.004 -0.025,26.047 0.257,160.023 0.35,203.621 H 408.32 c 7.589,0 13.743,6.154 13.743,13.743 0,7.589 -6.154,13.743 -13.745,13.743 z"
id="path2" />
<path
style="fill:#cff09e"
d="M 481.45,470.185 408.874,428.951 V 252.436 l 54.107,51.233 c 11.79,11.165 18.469,26.688 18.469,42.926 z"
id="path4" />
<path
style="fill:#507c5c"
d="m 481.45,483.93 c -2.342,0 -4.682,-0.598 -6.788,-1.795 l -72.576,-41.234 c -4.299,-2.441 -6.955,-7.005 -6.955,-11.948 V 252.436 c 0,-5.49 3.268,-10.453 8.312,-12.624 5.044,-2.169 10.894,-1.13 14.881,2.644 l 54.107,51.233 c 14.465,13.699 22.763,32.982 22.763,52.904 v 123.591 c 0,4.894 -2.602,9.417 -6.83,11.878 -2.135,1.244 -4.525,1.868 -6.914,1.868 z m -58.832,-62.977 45.09,25.619 v -99.976 c 0,-12.406 -5.167,-24.415 -14.175,-32.947 l -30.914,-29.273 v 136.577 z"
id="path6" />
<path
style="fill:#cff09e"
d="m 140.63,470.185 72.576,-41.234 V 252.436 l -54.107,51.233 c -11.79,11.165 -18.469,26.688 -18.469,42.926 0,0 0,123.59 0,123.59 z"
id="path8" />
<path
style="fill:#507c5c"
d="m 140.63,483.93 c -2.389,0 -4.777,-0.623 -6.913,-1.865 -4.229,-2.461 -6.83,-6.984 -6.83,-11.878 V 346.595 c 0,-19.922 8.297,-39.205 22.763,-52.903 l 54.107,-51.235 c 3.987,-3.772 9.843,-4.81 14.881,-2.644 5.044,2.17 8.312,7.133 8.312,12.625 v 176.515 c 0,4.945 -2.657,9.508 -6.955,11.948 l -72.576,41.234 c -2.105,1.197 -4.449,1.795 -6.789,1.795 z m 58.833,-199.554 -30.914,29.274 c -9.009,8.53 -14.175,20.538 -14.175,32.945 v 99.976 l 45.09,-25.619 V 284.376 Z"
id="path10" />
<circle
style="fill:#cff09e"
cx="311.03397"
cy="248.30794"
r="46.891998"
id="circle12" />
<path
style="fill:#507c5c"
d="m 311.04,308.947 c -33.434,0 -60.635,-27.201 -60.635,-60.634 0,-33.434 27.201,-60.635 60.635,-60.635 33.434,0 60.635,27.201 60.635,60.635 0,33.434 -27.201,60.634 -60.635,60.634 z m 0,-93.782 c -18.278,0 -33.149,14.87 -33.149,33.149 0,18.279 14.87,33.147 33.149,33.147 18.279,0 33.149,-14.869 33.149,-33.147 0,-18.278 -14.871,-33.149 -33.149,-33.149 z"
id="path14" />
<path
style="fill:#cff09e"
d="m 260.066,483.395 c -0.216,1.909 -0.338,3.844 -0.338,5.809 0,50.74 51.31,71.772 51.31,71.772 0,0 51.31,-19.892 51.31,-71.772 0,-1.965 -0.122,-3.9 -0.338,-5.809 z"
id="path16" />
<path
style="fill:#507c5c"
d="m 311.04,574.72 c -1.771,0 -3.543,-0.342 -5.213,-1.027 -2.442,-1.002 -59.841,-25.211 -59.841,-84.489 0,-2.379 0.143,-4.851 0.423,-7.347 0.782,-6.953 6.663,-12.207 13.658,-12.207 h 101.947 c 6.995,0 12.875,5.254 13.658,12.207 0.28,2.494 0.423,4.967 0.423,7.347 0,60.595 -57.632,83.634 -60.085,84.587 -1.602,0.618 -3.285,0.929 -4.97,0.929 z m -37.023,-77.582 c 3.823,27.415 26.791,42.919 37.163,48.603 10.438,-5.497 33.208,-20.545 36.913,-48.603 z"
id="path18" />
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 169 KiB

View File

@ -19,4 +19,8 @@ const wrapper = (
</BrowserRouter>
);
hydrate(wrapper, document.getElementById("root"));
const root = document.getElementById("root");
if (root) {
hydrate(wrapper, root);
}

View File

@ -1,4 +1,4 @@
import express, { RequestHandler } from "express";
import express, { RequestHandler, Request, Response } from "express";
import { StaticRouter } from "inferno-router";
import { renderToString } from "inferno-server";
// import { matchPath } from "inferno-router";
@ -18,13 +18,21 @@ server.use("/static", express.static(path.resolve("./dist")));
server.use("/docs", express.static(path.resolve("./dist/assets/docs")));
server.use("/api", express.static(path.resolve("./dist/assets/api")));
server.get("/*", async (req, res) => {
// const activeRoute = routes.find(route => matchPath(req.path, route)) || {};
const context = {} as any;
function erudaInit(): string {
if (process.env["NODE_ENV"] == "development") {
return `
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
<script>eruda.init();</script>
`;
} else {
return "";
}
}
function setLanguage(req: Request, res: Response): string {
// Setting the language for non-js browsers
const cookieLang = getLanguageFromCookie(req.headers.cookie);
var language: string;
let language: string;
if (req.query["lang"] != null) {
language = req.query["lang"].toString();
res.cookie("lang", language, {
@ -37,6 +45,14 @@ server.get("/*", async (req, res) => {
? req.headers["accept-language"].split(",")[0]
: "en";
}
return language;
}
server.get("/*", async (req, res) => {
// const activeRoute = routes.find(route => matchPath(req.path, route)) || {};
const context = {} as any;
const language = setLanguage(req, res);
i18n.changeLanguage(language);
const wrapper = (
@ -53,8 +69,9 @@ server.get("/*", async (req, res) => {
res.send(`
<!DOCTYPE html>
<html ${helmet.htmlAttributes.toString()} lang="en">
<html ${helmet.htmlAttributes.toString()} lang="en" class="scroll-smooth">
<head>
${erudaInit()}
${helmet.title.toString()}
${helmet.meta.toString()}
@ -70,44 +87,11 @@ server.get("/*", async (req, res) => {
<!-- Styles -->
<link rel="stylesheet" type="text/css" href="/static/styles/styles.css" />
<!-- These don't work with the css minifier -->
<style>
@font-face {
font-family: 'CaviarDreams';
font-style: normal;
src: url('/static/assets/fonts/CaviarDreams.ttf') format('truetype');
}
.bg-image {
position: fixed;
left: 0;
right: 0;
z-index: -1;
display: block;
width: 100%;
height: 100%;
background:
linear-gradient(
rgba(0, 0, 0, 0.5),
rgba(0, 0, 0, 0.5)
),
url('/static/assets/images/main_img.webp');
-webkit-filter: blur(7px);
-moz-filter: blur(7px);
-o-filter: blur(7px);
-ms-filter: blur(7px);
filter: blur(7px);
<link rel="stylesheet" href="/static/assets/glide.core.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter">
/* Center and scale the image nicely */
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
</style>
<!-- Current theme and more -->
${helmet.link.toString()}
</head>
<body ${helmet.bodyAttributes.toString()}>
<div id='root'>${root}</div>
<script defer src='/static/js/client.js'></script>

View File

@ -0,0 +1,359 @@
export interface ApiLibrary {
name: string;
link: string;
description: string;
}
export enum SourceType {
Closed,
Open,
}
export interface AppDetails {
name: string;
description: string;
link: string;
icon?: string;
banner?: string;
links: AppLink[];
sourceType: SourceType;
}
export interface AppLink {
link: string;
icon: string;
}
export const API_LIBRARIES: ApiLibrary[] = [
{
name: "lemmy-js-client",
link: "https://github.com/LemmyNet/lemmy-js-client",
description: "a javascript / typescript client.",
},
{
name: "lemmy-dart client",
link: "https://github.com/LemmurOrg/lemmy_api_client",
description: "a dart / flutter client.",
},
{
name: "go-lemmy",
link: "https://github.com/Arsen6331/go-lemmy",
description: "a Go client.",
},
];
const voyagerApp: AppDetails = {
name: "Voyager",
description: "A Lemmy Client for iOS, Android and the web",
link: "https://github.com/aeharding/voyager",
icon: "/static/assets/images/voyager.png",
banner: "/static/assets/images/voyager_screen.webp",
links: [
{
link: "https://apps.apple.com/us/app/voyager-for-lemmy/id6451429762?platform=iphone",
icon: "appleinc",
},
{
link: "https://play.google.com/store/apps/details?id=app.vger.voyager",
icon: "googleplay",
},
{
link: "https://github.com/aeharding/voyager",
icon: "github",
},
],
sourceType: SourceType.Open,
};
const thunderApp: AppDetails = {
name: "Thunder",
description:
"An open-source cross-platform Lemmy client for iOS and Android built with Flutter",
link: "https://github.com/thunder-app/thunder",
icon: "/static/assets/images/thunder_logo.webp",
banner: "/static/assets/images/thunder_screen.webp",
links: [
{
link: "https://apt.izzysoft.de/fdroid/index/apk/com.hjiangsu.thunder",
icon: "f-droid",
},
{
link: "https://apps.apple.com/iq/app/thunder-for-lemmy/id6450518497",
icon: "appleinc",
},
{
link: "https://play.google.com/store/apps/details?id=com.hjiangsu.thunder",
icon: "googleplay",
},
{
link: "https://github.com/thunder-app/thunder",
icon: "github",
},
],
sourceType: SourceType.Open,
};
export const ANDROID_APPS: AppDetails[] = [
{
name: "Jerboa",
description: "A native Android app made by Lemmy's developers",
link: "https://github.com/dessalines/jerboa",
icon: "/static/assets/images/jerboa.svg",
banner: "/static/assets/images/jerboa_screen.webp",
links: [
{
link: "https://f-droid.org/en/packages/com.jerboa",
icon: "f-droid",
},
{
link: "https://play.google.com/store/apps/details?id=com.jerboa",
icon: "googleplay",
},
{
link: "https://github.com/dessalines/jerboa",
icon: "github",
},
],
sourceType: SourceType.Open,
},
{
name: "Eternity",
description: "A Lemmy client for Android written in Java.",
link: "https://codeberg.org/Bazsalanszky/Eternity",
icon: "/static/assets/images/eternity_icon.webp",
banner: "/static/assets/images/eternity_screen.webp",
links: [
{
link: "https://apt.izzysoft.de/fdroid/index/apk/eu.toldi.infinityforlemmy",
icon: "f-droid",
},
{
link: "https://play.google.com/store/apps/details?id=eu.toldi.infinityforlemmy",
icon: "googleplay",
},
{
link: "https://codeberg.org/Bazsalanszky/Eternity",
icon: "github",
},
],
sourceType: SourceType.Open,
},
{
name: "Combustible",
description: "An Open-Source Lemmy Client For Android",
link: "https://github.com/TheBrokenRail/Combustible",
icon: "/static/assets/images/combustible_logo.webp",
banner: "/static/assets/images/combustible_screen.webp",
links: [
{
link: "https://apt.izzysoft.de/fdroid/index/apk/com.thebrokenrail.combustible",
icon: "f-droid",
},
{
link: "https://github.com/TheBrokenRail/Combustible",
icon: "github",
},
],
sourceType: SourceType.Open,
},
{
name: "LiftOff!",
description: "A mobile client for lemmy",
link: "https://github.com/liftoff-app/liftoff",
icon: "/static/assets/images/liftoff_icon.svg",
banner: "/static/assets/images/liftoff_screen.webp",
links: [
{
link: "https://apt.izzysoft.de/fdroid/index/apk/com.liftoffapp.liftoff",
icon: "f-droid",
},
{
link: "https://play.google.com/store/apps/details?id=com.liftoffapp.liftoff&pli=1",
icon: "googleplay",
},
{
link: "https://github.com/liftoff-app/liftoff",
icon: "github",
},
],
sourceType: SourceType.Open,
},
voyagerApp,
thunderApp,
{
name: "Boost for Lemmy",
description: "A smooth app for Lemmy.",
link: "https://play.google.com/store/apps/details?id=com.rubenmayayo.lemmy",
icon: "/static/assets/images/boost_icon.webp",
banner: "/static/assets/images/boost_screen.webp",
links: [
{
link: "https://play.google.com/store/apps/details?id=com.rubenmayayo.lemmy",
icon: "googleplay",
},
],
sourceType: SourceType.Closed,
},
{
name: "Sync for Lemmy",
description: "A full-featured app for browsing Lemmy on the go.",
link: "https://play.google.com/store/apps/details?id=io.syncapps.lemmy_sync",
icon: "/static/assets/images/sync_icon.webp",
banner: "/static/assets/images/sync_screen.webp",
links: [
{
link: "https://play.google.com/store/apps/details?id=io.syncapps.lemmy_sync",
icon: "googleplay",
},
],
sourceType: SourceType.Closed,
},
];
export const IOS_APPS: AppDetails[] = [
{
name: "Mlem",
description: "A Lemmy Client for iOS.",
link: "https://github.com/mormaer/Mlem",
icon: "/static/assets/images/mlem.png",
banner: "/static/assets/images/mlem_screen.webp",
links: [
{
link: "https://testflight.apple.com/join/MelFP11Y",
icon: "appleinc",
},
{
link: "https://github.com/mlemgroup/mlem",
icon: "github",
},
],
sourceType: SourceType.Open,
},
{
name: "Lunar",
description: "A Lemmy Client for iOS written in Swift and SwiftUI",
link: "https://github.com/mani-sh-reddy/Lunar",
icon: "/static/assets/images/lunar_logo.webp",
banner: "/static/assets/images/lunar_screen.webp",
links: [
{
link: "https://testflight.apple.com/join/GEFCCQTb",
icon: "appleinc",
},
{
link: "https://github.com/mani-sh-reddy/Lunar",
icon: "github",
},
],
sourceType: SourceType.Open,
},
voyagerApp,
thunderApp,
{
name: "Memmy",
description:
"A Lemmy Client built in React Native for iOS available on the App Store.",
link: "https://github.com/Memmy-App/memmy",
icon: "/static/assets/images/memmy_icon.png",
banner: "/static/assets/images/memmy_banner.webp",
links: [
{
link: "https://apps.apple.com/us/app/memmy-for-lemmy/id6450204299?platform=iphone",
icon: "appleinc",
},
{
link: "https://github.com/Memmy-App/memmy",
icon: "github",
},
],
sourceType: SourceType.Open,
},
];
export const WEB_APPS: AppDetails[] = [
{
name: "lemmy-ui",
description: "The official web app for lemmy.",
link: "https://github.com/LemmyNet/lemmy-ui",
banner: "/static/assets/images/mobile_pic.webp",
links: [
{
link: "https://github.com/LemmyNet/lemmy-ui",
icon: "github",
},
],
sourceType: SourceType.Open,
},
{
name: "Photon",
description: "A sleek lemmy web UI.",
link: "https://github.com/Xyphyn/photon",
banner: "/static/assets/images/photon.webp",
icon: "/static/assets/images/photon-logo.svg",
links: [
{
link: "https://github.com/Xyphyn/photon",
icon: "github",
},
],
sourceType: SourceType.Open,
},
{
name: "Alexandrite",
description:
"A beautiful and convenient desktop-first alternate web UI for Lemmy.",
link: "https://github.com/sheodox/alexandrite",
icon: "/static/assets/images/alexandrite_logo.svg",
banner: "/static/assets/images/alexandrite_screen.webp",
links: [
{
link: "https://github.com/sheodox/alexandrite",
icon: "github",
},
],
sourceType: SourceType.Open,
},
{
name: "mlmym",
description: "A familiar desktop experience for lemmy",
link: "https://github.com/rystaf/mlmym",
banner: "/static/assets/images/mlmym_screen.webp",
links: [
{
link: "https://github.com/rystaf/mlmym",
icon: "github",
},
],
sourceType: SourceType.Open,
},
{
name: "lemmyBB",
description: "A lemmy frontend based on phpBB.",
link: "https://github.com/LemmyNet/lemmyBB",
banner: "/static/assets/images/lemmybb_2.webp",
links: [
{
link: "https://github.com/LemmyNet/lemmyBB",
icon: "github",
},
],
sourceType: SourceType.Open,
},
];
export const CLI_APPS: AppDetails[] = [
{
name: "neonmodem",
description: "BBS-style TUI client",
link: "https://github.com/mrusme/neonmodem",
banner: "/static/assets/images/neonmodem.webp",
links: [
{
link: "https://github.com/mrusme/neonmodem",
icon: "github",
},
],
sourceType: SourceType.Open,
},
];

View File

@ -1,50 +0,0 @@
import { Component } from "inferno";
import { Icon } from "./icon";
interface AppDetailsProps {
name: string;
description: string;
link: string;
icon?: string;
banner?: string;
links: AppLink[];
}
interface AppLink {
link: string;
icon: string;
}
export class AppDetails extends Component<AppDetailsProps, any> {
constructor(props: any, context: any) {
super(props, context);
}
render() {
let p = this.props;
let icon = p.icon || "/static/assets/images/lemmy.svg";
let banner = p.banner || "/static/assets/images/lemmy.svg";
return (
<>
<header class="is-center">
<img class="app-icon" src={icon} />
<h4>
<a href={p.link}>{p.name}</a>
</h4>
</header>
<div class="is-center">
<img class="app-banner" src={banner} />
</div>
<br />
<p class="is-center">{p.description}</p>
<footer class="is-center">
{p.links.map(l => (
<a class="button primary" href={l.link}>
<Icon icon={l.icon} />
</a>
))}
</footer>
</>
);
}
}

View File

@ -5,9 +5,8 @@ import { i18n } from "../i18next";
import { routes } from "../routes";
import { NoMatch } from "./no-match";
import { Symbols } from "./symbols";
import { Navbar } from "./navbar";
import { Footer } from "./footer";
import "./styles.scss";
import { Footer, Navbar } from "./navbar";
import { BACKGROUND_GRADIENT_1, BACKGROUND_GRADIENT_2 } from "./common";
export class App extends Component<any, any> {
constructor(props: any, context: any) {
@ -15,26 +14,28 @@ export class App extends Component<any, any> {
}
render() {
return (
<>
<div>
<div className={BACKGROUND_GRADIENT_1}>
<div className={BACKGROUND_GRADIENT_2}>
<Provider i18next={i18n}>
<Navbar />
<Switch>
{routes.map(({ path, exact, component: C, ...rest }) => (
<Route
key={path}
path={path}
exact={exact}
render={props => <C {...props} {...rest} />}
/>
))}
<Route render={props => <NoMatch {...props} />} />
</Switch>
<Footer />
<div className="min-h-screen">
<Navbar />
<Switch>
{routes.map(({ path, exact, component: C, ...rest }) => (
<Route
key={path}
path={path}
exact={exact}
render={props => C && <C {...props} {...rest} />}
/>
))}
<Route render={props => <NoMatch {...props} />} />
</Switch>
<Footer />
</div>
<Symbols />
</Provider>
</div>
</>
</div>
);
}
}

View File

@ -1,7 +1,146 @@
import { Component } from "inferno";
import { AppDetails } from "./app-details";
import { Helmet } from "inferno-helmet";
import { i18n } from "../i18next";
import { T } from "inferno-i18next";
import { BottomSpacer, TEXT_GRADIENT } from "./common";
import {
ANDROID_APPS,
API_LIBRARIES,
AppDetails,
AppLink,
CLI_APPS,
IOS_APPS,
SourceType,
WEB_APPS,
} from "./app-definitions";
import { Icon } from "./icon";
const TitleBlock = () => (
<div className="flex flex-col items-center pt-16 mb-8">
<T i18nKey="lemmy_apps" className="text-4xl font-bold mb-3">
#<span className={TEXT_GRADIENT}>#</span>
</T>
<p className="text-2xl text-gray-300 text-center">
{i18n.t("choose_from_apps")}
</p>
</div>
);
interface AppDetailsCardProps {
app: AppDetails;
}
const AppDetailsTitle = ({ app }: AppDetailsCardProps) => (
<div className="flex flex-row space-x-2 mb-2">
<img
src={app.icon || "/static/assets/images/lemmy.svg"}
className="rounded-xl w-7 h-7"
/>
<a href={app.link} className={`card-title text-2xl ${TEXT_GRADIENT}`}>
{app.name}
</a>
</div>
);
interface AppDetailsButtonsProps {
links: AppLink[];
}
const AppDetailsButtons = ({ links }: AppDetailsButtonsProps) => (
<div className="flex flex-row justify-between gap-2">
{links.map(l => (
<a
className="btn btn-sm btn-primary text-white normal-case"
href={l.link}
>
<Icon icon={l.icon} />
</a>
))}
</div>
);
const AppDetailsCard = ({ app }: AppDetailsCardProps) => (
<div className="card card-bordered bg-neutral-900 shadow-xl">
<div className="card-body items-center">
<AppDetailsTitle app={app} />
<img
src={app.banner || "/static/assets/images/lemmy.svg"}
className="rounded-xl max-h-96 mb-2"
/>
<p className="text-sm text-gray-300 mb-2">{app.description}</p>
{app.sourceType == SourceType.Closed && (
<div className="alert alert-warning">
<Icon icon="alert-octagon" />
<span>{i18n.t("closed_source_warning")}</span>
</div>
)}
<AppDetailsButtons links={app.links} />
</div>
</div>
);
const AppTitle = ({ title }) => (
<div className="text-2xl mb-3 text-gray-300">{title}</div>
);
const MobileAppsBlock = () => (
<div>
<AppTitle title={i18n.t("mobile_apps_for_android")} />
<AppGrid apps={ANDROID_APPS} />
<AppTitle title={i18n.t("mobile_apps_for_ios")} />
<AppGrid apps={IOS_APPS} />
</div>
);
const WebAppsBlock = () => (
<div>
<AppTitle title={i18n.t("web_apps")} />
<AppGrid apps={WEB_APPS} />
</div>
);
const CliAppsBlock = () => (
<div>
<AppTitle title={i18n.t("cli_apps")} />
<AppGrid apps={CLI_APPS} />
</div>
);
interface AppGridProps {
apps: AppDetails[];
}
const AppGrid = ({ apps }: AppGridProps) => (
<div className="grid md:grid-cols-2 grid-cols-1 gap-4 mb-16">
{apps.map(a => (
<AppDetailsCard app={a} />
))}
</div>
);
const ApiLibrariesBlock = () => (
<div>
<AppTitle title={i18n.t("api_libraries")} />
<div className="card card-bordered bg-neutral-900 shadow-xl md:w-1/2">
<div className="card-body">
<ul>
{API_LIBRARIES.map(a => (
<li>
<span className={`${TEXT_GRADIENT} mr-2`}></span>
<span>
<a href={a.link} className={`${TEXT_GRADIENT}`}>
{a.name}
</a>
</span>
<span className="text-gray-300 mx-2">-</span>
<span className="text-gray-300">{a.description}</span>
</li>
))}
</ul>
</div>
</div>
</div>
);
export class Apps extends Component<any, any> {
constructor(props: any, context: any) {
@ -15,223 +154,16 @@ export class Apps extends Component<any, any> {
render() {
const title = i18n.t("apps_title");
return (
<div>
<div className="container mx-auto px-4">
<Helmet title={title}>
<meta property={"title"} content={title} />
</Helmet>
<div class="container">
<h1>{i18n.t("lemmy_apps")}</h1>
<p>{i18n.t("choose_from_apps")}</p>
<div class="row">
<div class="card col-6">
<AppDetails
name="Jerboa"
description="A native Android app made by Lemmy's developers"
link="https://github.com/dessalines/jerboa"
icon="/static/assets/images/jerboa.svg"
banner="/static/assets/images/jerboa_screen.webp"
links={[
{
link: "https://f-droid.org/en/packages/com.jerboa",
icon: "f-droid",
},
{
link: "https://play.google.com/store/apps/details?id=com.jerboa",
icon: "googleplay",
},
{
link: "https://github.com/dessalines/jerboa",
icon: "github",
},
]}
/>
</div>
<div class="card col-6">
<AppDetails
name="Mlem"
description="A Lemmy Client for iOS."
link="https://github.com/mormaer/Mlem"
icon="/static/assets/images/mlem.png"
banner="/static/assets/images/mlem_screen.webp"
links={[
{
link: "https://testflight.apple.com/join/MelFP11Y",
icon: "appleinc",
},
{
link: "https://github.com/mlemgroup/mlem",
icon: "github",
},
]}
/>
</div>
<div class="card col-6">
<AppDetails
name="Memmy"
description="A Lemmy Client built in React Native for iOS available on the App Store."
link="https://github.com/Memmy-App/memmy"
icon="/static/assets/images/memmy_icon.png"
banner="/static/assets/images/memmy_banner.webp"
links={[
{
link: "https://apps.apple.com/us/app/memmy-for-lemmy/id6450204299?platform=iphone",
icon: "appleinc",
},
{
link: "https://github.com/Memmy-App/memmy",
icon: "github",
},
]}
/>
</div>
<div class="card col-6">
<AppDetails
name="Voyager"
description="A Lemmy Client for iOS, Android and the web"
link="https://github.com/aeharding/voyager"
icon="/static/assets/images/voyager.png"
banner="/static/assets/images/voyager_screen.webp"
links={[
{
link: "https://apps.apple.com/us/app/voyager-for-lemmy/id6451429762?platform=iphone",
icon: "appleinc",
},
{
link: "https://play.google.com/store/apps/details?id=app.vger.voyager",
icon: "googleplay",
},
{
link: "https://github.com/aeharding/voyager",
icon: "github",
},
]}
/>
</div>
<div class="card col-6">
<AppDetails
name="Lunar"
description="A Lemmy Client for iOS written in Swift and SwiftUI"
link="https://github.com/mani-sh-reddy/Lunar"
icon="/static/assets/images/lunar_logo.webp"
banner="/static/assets/images/lunar_screen.webp"
links={[
{
link: "https://testflight.apple.com/join/GEFCCQTb",
icon: "appleinc",
},
{
link: "https://github.com/mani-sh-reddy/Lunar",
icon: "github",
},
]}
/>
</div>
</div>
<h1>{i18n.t("web_apps")}</h1>
<div class="row">
<div class="card col-6">
<AppDetails
name="lemmy-ui"
description="The official web app for lemmy."
link="https://github.com/LemmyNet/lemmy-ui"
banner="/static/assets/images/mobile_pic.webp"
links={[
{
link: "https://github.com/LemmyNet/lemmy-ui",
icon: "github",
},
]}
/>
</div>
<div class="card col-6">
<AppDetails
name="lemmyBB"
description="A lemmy frontend based on phpBB."
link="https://github.com/LemmyNet/lemmyBB"
banner="/static/assets/images/lemmybb_2.webp"
links={[
{
link: "https://github.com/LemmyNet/lemmyBB",
icon: "github",
},
]}
/>
</div>
<div class="card col-6">
<AppDetails
name="Photon"
description="A sleek lemmy web UI."
link="https://github.com/Xyphyn/photon"
banner="/static/assets/images/photon.webp"
icon="/static/assets/images/photon-logo.svg"
links={[
{
link: "https://github.com/Xyphyn/photon",
icon: "github",
},
]}
/>
</div>
<div class="card col-6">
<AppDetails
name="lemmy-lite"
description="A static, JSless, touch-friendly Lemmy frontend built for legacy web clients and maximum performance"
link="https://github.com/IronOxidizer/lemmy-lite"
banner="/static/assets/images/lemmy_lite_screen.webp"
links={[
{
link: "https://github.com/IronOxidizer/lemmy-lite",
icon: "github",
},
]}
/>
</div>
</div>
<h1>{i18n.t("cli_apps")}</h1>
<div class="row">
<div class="card col-6">
<AppDetails
name="neonmodem"
description="BBS-style TUI client"
link="https://github.com/mrusme/neonmodem"
banner="/static/assets/images/neonmodem.webp"
links={[
{
link: "https://github.com/mrusme/neonmodem",
icon: "github",
},
]}
/>
</div>
</div>
<h1>{i18n.t("api_libraries")}</h1>
<ul>
<li>
<a href="https://github.com/LemmyNet/lemmy-js-client">
lemmy-js-client
</a>{" "}
- a javascript / typescript client.
</li>
<li>
<a href="https://github.com/LemmurOrg/lemmy_api_client">
lemmy-dart client
</a>{" "}
- a dart / flutter client.
</li>
<li>
<a href="https://gitea.elara.ws/Elara6331/go-lemmy">go-lemmy</a> -
a Go client.
</li>
</ul>
</div>
<TitleBlock />
<MobileAppsBlock />
<WebAppsBlock />
<CliAppsBlock />
<ApiLibrariesBlock />
<BottomSpacer />
</div>
);
}

View File

@ -0,0 +1,144 @@
import { Link } from "inferno-router";
import { i18n } from "../i18next";
import { T } from "inferno-i18next";
import classNames from "classnames";
import {
END_FUNDRAISER_DATE,
FUNDED_DEVS,
FUNDED_DEV_GOAL,
MEDIAN_DEV_SALARY,
TOTAL_RECURRING_MONTHLY_EUR,
TOTAL_SUPPORTERS,
} from "./donate-definitions";
import { NUMBER_FORMAT, monthsBetween } from "../utils";
export const TEXT_GRADIENT =
"bg-gradient-to-r bg-clip-text text-transparent from-[#69D066] to-[#03A80E]";
export const CARD_GRADIENT =
"bg-gradient-to-r from-[#797979]/[.05] via-[#07B0BA]/[.15] to-[#797979]/[.05]";
export const BACKGROUND_GRADIENT_1 =
"min-h-full bg-gradient-to-r from-transparent via-[#12D10E]/[.15] to-transparent";
export const BACKGROUND_GRADIENT_2 =
"min-h-full bg-gradient-to-b from-transparent to-black/[.30] to-20%";
export const SELECT_CLASSES =
"select select-sm select-ghost select-bordered text-gray-400";
export const Badge = ({ content, outline = false }) => (
<div
className={classNames("p-2 rounded-xl bg-neutral-800 text-gray-300 w-fit", {
"outline outline-primary": outline,
})}
>
{content}
</div>
);
export const DonateDesc = () => (
<p className="text-sm text-gray-300 mb-3">
<T i18nKey="donate_desc">
#
<Link className="link" to="/donate">
#
</Link>
#
</T>
</p>
);
export const DonateButtons = () => (
<div className="flex flex-row flex-wrap justify-between gap-2">
<a
className="btn btn-primary text-white max-md:btn-block normal-case"
href="https://liberapay.com/Lemmy"
>
<T i18nKey="support_on_liberapay">
#<span className="font-bold">#</span>
</T>
</a>
<a
className="btn btn-secondary text-white max-md:btn-block normal-case"
href="https://www.patreon.com/dessalines"
>
<T i18nKey="support_on_patreon">
#<span className="font-bold">#</span>
</T>
</a>
<a
className="btn btn-primary text-white max-md:btn-block normal-case"
href="https://opencollective.com/lemmy"
>
<T i18nKey="support_on_opencollective">
#<span className="font-bold">#</span>
</T>
</a>
<Link
className="btn btn-secondary text-white max-md:btn-block normal-case"
to="/crypto"
>
Crypto
</Link>
</div>
);
const FundingGoal = () => (
<div className="flex flex-col flex-wrap mb-3 gap-4">
<div className="flex flex-row flex-wrap justify-between gap-4">
<div>
<span className="text-xl font-bold">
{NUMBER_FORMAT.format(TOTAL_RECURRING_MONTHLY_EUR)}
</span>
<span className="text-gray-200 mr-3">
{i18n.t("per_month", { formattedCount: "" })}
</span>
</div>
<div
className="text-xl font-bold tooltip"
data-tip={i18n.t("based_on_salary", {
formattedCount: NUMBER_FORMAT.format(MEDIAN_DEV_SALARY),
})}
>
{i18n.t("devs_funded", {
formattedCount1: FUNDED_DEVS.toFixed(1),
formattedCount2: FUNDED_DEV_GOAL,
})}
*
</div>
</div>
<progress
className="progress progress-primary w-auto"
value={FUNDED_DEVS}
max={FUNDED_DEV_GOAL}
></progress>
<div className="flex flex-row flex-wrap justify-between gap-4">
<div className="text-sm text-gray-300">
{i18n.t("supporters", {
formattedCount: NUMBER_FORMAT.format(TOTAL_SUPPORTERS),
})}
</div>
<div className="text-sm text-gray-300">
{monthsBetween(new Date(), END_FUNDRAISER_DATE)} months remaining
</div>
</div>
</div>
);
export const DonateBlock = () => (
<div className="flex flex-col items-center pt-16">
<div className={`card card-bordered ${CARD_GRADIENT} shadow-xl`}>
<div className="card-body px-8 md:px-32 py-16">
<p className={`card-title text-4xl mb-3 ${TEXT_GRADIENT}`}>
{i18n.t("donate")}
</p>
<DonateDesc />
<FundingGoal />
<DonateButtons />
</div>
</div>
</div>
);
export const BottomSpacer = () => <div className="pb-32" />;

View File

@ -1,6 +1,42 @@
import { Component } from "inferno";
import { Helmet } from "inferno-helmet";
import { i18n } from "../i18next";
import { BottomSpacer, TEXT_GRADIENT } from "./common";
import { Icon } from "./icon";
const TitleBlock = () => (
<div className={`pt-16 text-center text-4xl mb-8 ${TEXT_GRADIENT}`}>
{i18n.t("contact")}
</div>
);
const ContactBlock = () => (
<div className="flex flex-col items-center">
<div className="card w-96 card-bordered bg-neutral-900 shadow-xl">
<div className="card-body items-center p-8">
<ContactBtn
title="/c/lemmy_support"
url="https://lemmy.ml/c/lemmy_support"
/>
<ContactBtn
title="Matrix"
url="https://matrix.to/#/#lemmy-space:matrix.org"
/>
<ContactBtn title="Github" url="https://github.com/LemmyNet" />
</div>
</div>
</div>
);
const ContactBtn = ({ title, url }) => (
<a
className="btn btn-block bg-neutral-800 mb-3 justify-start normal-case"
href={url}
>
<Icon icon="embed" classes={`fill-current text-primary`} />
<span className="underline">{title}</span>
</a>
);
export class Contact extends Component<any, any> {
constructor(props: any, context: any) {
@ -9,24 +45,13 @@ export class Contact extends Component<any, any> {
render() {
const title = i18n.t("contact_title");
return (
<div>
<div className="container mx-auto px-4">
<Helmet title={title}>
<meta property={"title"} content={title} />
</Helmet>
<div class="container">
<h1>{i18n.t("contact")}</h1>
<ul>
<li>
<a href="https://lemmy.ml/c/lemmy_support">/c/lemmy_support</a>
</li>
<li>
<a href="https://matrix.to/#/#lemmy-space:matrix.org">Matrix</a>
</li>
<li>
<a href="https://github.com/LemmyNet">GitHub</a>
</li>
</ul>
</div>
<TitleBlock />
<ContactBlock />
<BottomSpacer />
</div>
);
}

View File

@ -0,0 +1,117 @@
import { Helmet } from "inferno-helmet";
import { Badge } from "./common";
import { Component } from "inferno";
import * as QRCode from "qrcode";
import { Icon } from "./icon";
const title = "Crypto";
interface Cryptos {
name: string;
address: string;
}
const CRYPTOS: Cryptos[] = [
{
name: "bitcoin",
address: "1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK",
},
{
name: "ethereum",
address: "0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01",
},
{
name: "monero",
address:
"41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV",
},
{
name: "cardano",
address:
"addr1q858t89l2ym6xmrugjs0af9cslfwvnvsh2xxp6x4dcez7pf5tushkp4wl7zxfhm2djp6gq60dk4cmc7seaza5p3slx0sakjutm",
},
];
const QrModal = ({ name, imgData }) => (
<dialog id={`qr-modal-${name}`} className="modal">
<form method="dialog" className="modal-backdrop">
<button>X</button>
</form>
<div className="modal-box bg-neutral-800 w-auto">
<form method="dialog">
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
</button>
</form>
<div className="container mx-auto">
<img className="w-auto" src={imgData} />
</div>
</div>
</dialog>
);
interface State {
cryptoQr: Map<string, string>;
}
export class Crypto extends Component<any, State> {
state = { cryptoQr: new Map() };
constructor(props: any, context: any) {
super(props, context);
}
async componentDidMount() {
let cryptoQr = new Map<string, string>();
for (const c of CRYPTOS) {
cryptoQr.set(c.name, await QRCode.toDataURL(c.address));
}
this.setState({ cryptoQr });
}
render() {
return (
<div className="container mx-auto px-4">
<Helmet title={title}>
<meta property={"title"} content={title} />
</Helmet>
{Array.from(this.state.cryptoQr.entries()).map(e => (
<QrModal name={e[0]} imgData={e[1]} />
))}
<div className="pt-16 text-center text-4xl font-bold mb-8">{title}</div>
<div className="card card-bordered bg-neutral-900 shadow-xl">
<div className="card-body p-4">
<table className="table table-sm">
{CRYPTOS.map(c => (
<tr>
<td className="text-sm text-gray-300">{c.name}</td>
<td>
<Badge
content={
<code className="text-sm text-secondary break-all">
{c.address}
</code>
}
/>
</td>
<td>
<button
className="btn btn-ghost"
onClick={() =>
(
document.getElementById(`qr-modal-${c.name}`) as any
).showModal()
}
>
<Icon icon="qr_code" />
</button>
</td>
</tr>
))}
</table>
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,154 @@
import { donation_stats } from "../donation_stats";
export interface Coder {
name: string;
link?: string;
}
export interface LinkedSponsor {
name: string;
link: string;
}
export interface GoldSponsor {
name: string;
link: string;
avatar?: string;
}
export interface Translation {
lang: string;
country?: string;
translators: Translator[];
}
export interface Translator {
name: string;
link?: string;
}
interface FundingPlatform {
supporterCount: number;
monthlyEUR: number;
}
export const CODERS: Coder[] = [
{ name: "dessalines", link: "https://github.com/dessalines" },
{ name: "Nutomic", link: "https://github.com/nutomic" },
{ name: "phiresky", link: "https://github.com/phiresky" },
{ name: "SleeplessOne1917", link: "https://github.com/SleeplessOne1917" },
{ name: "asonix", link: "https://github.com/asonix" },
{ name: "MV-GH", link: "https://github.com/MV-GH" },
{ name: "dullbananas", link: "https://github.com/dullbananas" },
{ name: "sunaurus", link: "https://github.com/sunaurus" },
{ name: "shilangyu", link: "https://github.com/shilangyu" },
{ name: "eiknat", link: "https://github.com/eiknat" },
{ name: "ernestwisniewski", link: "https://github.com/ernestwisniewski" },
{ name: "zacanger", link: "https://github.com/zacanger" },
{ name: "iav", link: "https://github.com/iav" },
];
export const GOLD_SPONSORS: GoldSponsor[] = [
{
name: "Erlend Sogge Heggen",
link: "https://liberapay.com/~1776198/",
avatar:
"https://seccdn.libravatar.org/gravatarproxy/69fda0df8b4878fb6a18deffa972d26a?s=160&default=404",
},
{
name: "Justen Burdette",
link: "https://liberapay.com/~1825274/",
avatar:
"https://seccdn.libravatar.org/avatar/1939404ede3cf8d4ccd41e1f78faa104?s=160&d=404",
},
{
name: "#spacehost",
link: "https://www.patreon.com/user?u=98115693",
},
{
name: "Numair Faraz",
link: "https://www.patreon.com/user?u=7214767",
},
];
export const LATINUM_SPONSORS: GoldSponsor[] = [
{
name: "NLnet",
link: "https://nlnet.nl",
avatar: "https://nlnet.nl/image/logo_nlnet.svg",
},
];
export const SILVER_SPONSORS: LinkedSponsor[] = [
{
name: "Eric Betts",
link: "https://www.patreon.com/bettse",
},
{
name: "Mastodon.world Admins",
link: "https://www.patreon.com/mastodonworld",
},
{
name: "Rob Bradley",
link: "https://www.patreon.com/user?u=35339207",
},
{
name: "SaltyIceteaMaker",
link: "https://www.patreon.com/user?u=95322653",
},
];
export const HIGHLIGHTED_SPONSORS = [
"lazynooblet",
"anachronist",
"jbonomi",
"Adam Honse",
"Cassandra Comar",
"Chris Lam",
"Dew",
"Jams Hounshell",
"OliverLost",
"THE-DIESEL999",
];
// Don't do these until its automated
export const GENERAL_SPONSORS = [];
// Monthly counts in EUR
const liberapay: FundingPlatform = {
supporterCount: donation_stats[0].patrons,
monthlyEUR: donation_stats[0].amount,
};
const openCollective: FundingPlatform = {
supporterCount: donation_stats[1].patrons,
monthlyEUR: donation_stats[1].amount,
};
const patreon: FundingPlatform = {
supporterCount: donation_stats[2].patrons,
monthlyEUR: donation_stats[2].amount,
};
const fundingPlatforms = [liberapay, patreon, openCollective];
export const TOTAL_RECURRING_MONTHLY_EUR = fundingPlatforms
.map(f => f.monthlyEUR)
.reduce((a, b) => a + b, 0);
export const TOTAL_SUPPORTERS = fundingPlatforms
.map(f => f.supporterCount)
.reduce((a, b) => a + b, 0);
// From https://www.developersalary.com
export const MEDIAN_DEV_SALARY = 50_000;
export const MEDIAN_DEV_MONTHLY_EUR = MEDIAN_DEV_SALARY / 12;
// Number of devs funded off of recurring
export const FUNDED_DEVS = TOTAL_RECURRING_MONTHLY_EUR / MEDIAN_DEV_MONTHLY_EUR;
// Goal
export const FUNDED_DEV_GOAL = 3;
// End fundraiser date
export const END_FUNDRAISER_DATE = new Date("2024-12-01");

View File

@ -1,41 +0,0 @@
import { Component } from "inferno";
import { Link } from "inferno-router";
import { i18n } from "../i18next";
import { T } from "inferno-i18next";
export class DonateLines extends Component<any, any> {
constructor(props: any, context: any) {
super(props, context);
}
render() {
return (
<>
<p>
<T i18nKey="donate_desc">
#<Link to="/donate">#</Link>#
</T>
</p>
<div class="row is-horizontal-align">
<div class="col-3">
<a class="button primary" href="https://liberapay.com/Lemmy">
{i18n.t("support_on_liberapay")}
</a>
</div>
<div class="col-3">
<a class="button primary" href="https://www.patreon.com/dessalines">
{i18n.t("support_on_patreon")}
</a>
</div>
<div class="col-3">
<a
class="col button primary"
href="https://opencollective.com/lemmy"
>
{i18n.t("support_on_opencollective")}
</a>
</div>
</div>
</>
);
}
}

View File

@ -1,84 +1,245 @@
import { Component } from "inferno";
import { Helmet } from "inferno-helmet";
import { DonateLines } from "./donate-lines";
import { i18n } from "../i18next";
import { T } from "inferno-i18next";
import { translators } from "../translations/translators";
import { languagesAll, countries } from "countries-list";
import { isBrowser } from "../utils";
import { Badge, BottomSpacer, DonateBlock, TEXT_GRADIENT } from "./common";
import {
CODERS,
GOLD_SPONSORS,
SILVER_SPONSORS,
GoldSponsor,
HIGHLIGHTED_SPONSORS,
LATINUM_SPONSORS,
GENERAL_SPONSORS,
Translation,
} from "./donate-definitions";
import classNames from "classnames";
import { Icon } from "./icon";
const avatarSize = 40;
const bannerWidth = 240;
const bannerHeight = 101;
const SectionTitle = ({ title }) => (
<div className="text-2xl mb-3">{title}</div>
);
interface LinkedSponsor {
name: string;
link: string;
const ContributorsBlock = () => (
<div className="my-16">
<SectionTitle title={i18n.t("contributors")} />
<p className="text-sm text-gray-300 mb-3">{i18n.t("thanks_coders")}</p>
<CodersBlock />
<p className="text-sm text-gray-300 mt-6 mb-3">
{i18n.t("thanks_translators")}
</p>
<TranslatorsBlock />
<div className="card card-bordered bg-neutral-900 shadow-xl text-center text-secondary">
<div className="card-body p-4">
<p>
<T i18nKey="add_weblate">
#
<a
className="link"
href="https://weblate.join-lemmy.org/projects/lemmy/"
>
#
</a>
</T>
</p>
</div>
</div>
</div>
);
const CodersBlock = () => (
<div className="card card-bordered bg-neutral-900 shadow-xl">
<div className="card-body p-4">
<PersonBadges persons={CODERS} />
</div>
</div>
);
const TranslatorsBlock = () => {
// Split these into two cards for md
const transArr = convertTranslators();
const halfway = Math.floor(transArr.length / 2);
const first = transArr.slice(0, halfway);
const second = transArr.slice(halfway, transArr.length);
return (
<div className="mb-8">
<div className="max-md:hidden">
<div className="grid grid-cols-2 gap-4">
<TranslatorsCard translations={first} />
<TranslatorsCard translations={second} />
</div>
</div>
<div className="md:max-xl:hidden">
<TranslatorsCard translations={transArr} />
</div>
</div>
);
};
interface TranslatorsCardProps {
translations: Translation[];
}
interface GoldSponsor {
name: string;
link: string;
avatar: string;
const TranslatorsCard = ({ translations }: TranslatorsCardProps) => (
<div className="card card-bordered bg-neutral-900 shadow-xl">
<div className="card-body p-4">
<table>
{translations.map(t => (
<tr>
<td>
<div className="text-secondary">
<span>{languagesAll[t.lang].native}</span>
{t.country && <span> {countries[t.country].native}</span>}
<span>:</span>
</div>
</td>
<td>
<PersonBadges persons={t.translators} />
</td>
</tr>
))}
</table>
</div>
</div>
);
const SponsorsBlock = () => (
<div className="mb-16">
<SectionTitle title={i18n.t("sponsors")} />
<GoldSponsorCards
title={i18n.t("gold_pressed_latinum_sponsors_desc")}
sponsors={LATINUM_SPONSORS}
color="primary"
/>
<GoldSponsorCards
title={i18n.t("gold_sponsors_desc")}
sponsors={GOLD_SPONSORS}
color="secondary"
/>
<GoldSponsorCards
title={i18n.t("silver_sponsors_desc")}
sponsors={SILVER_SPONSORS}
color="warning"
/>
<GeneralSponsorCard />
</div>
);
interface GoldSponsorCardsProps {
title: string;
sponsors: GoldSponsor[];
color: string;
}
let goldSponsors: GoldSponsor[] = [];
const GoldSponsorCards = ({ title, sponsors, color }: GoldSponsorCardsProps) =>
sponsors.length > 0 && (
<div>
<p className="text-sm text-gray-300 mb-3">{title}</p>
<div className="flex flex-row flex-wrap gap-2 mb-2">
{sponsors.map(s => (
<a
className={`btn btn-${color} btn-outline w-32 h-16 normal-case`}
href={s.link}
>
<div className="flex flex-wrap flex-row justify-center">
{s.avatar && (
<div className="avatar w-auto h-8">
<img src={s.avatar} className="rounded" />
</div>
)}
<span className="text-xs">{s.name}</span>
</div>
</a>
))}
</div>
</div>
);
let latinumSponsors: GoldSponsor[] = [
{
name: "NLnet",
link: "https://nlnet.nl",
avatar: "https://nlnet.nl/image/logo_nlnet.svg",
},
];
const GeneralSponsorCard = () => {
const highlighted: PersonBadgeData[] = HIGHLIGHTED_SPONSORS.map(name => {
return { name, primaryOutline: true, primaryAt: true };
});
let silverSponsors: LinkedSponsor[] = [];
const general: PersonBadgeData[] = GENERAL_SPONSORS.map(name => {
return { name, primaryAt: true };
});
let highlightedSponsors = ["DQW", "John Knapp"];
let sponsors = [
"Anthony",
"Remi Rampin",
"Cameron C",
"Vegard",
"0ti.me",
"Brendan",
"mexicanhalloween .",
"Arthur Nieuwland",
"Forrest Weghorst",
"Luke Black",
"Brandon Abbott",
"Eon Gattignolo",
];
const combined = highlighted.concat(general);
export interface Coder {
return (
<div>
<p className="text-sm text-gray-300 mt-6 mb-3">
{i18n.t("general_sponsors_desc")}
</p>
<div className="card card-bordered bg-neutral-900 shadow-xl">
<div className="card-body p-4">
<PersonBadges persons={combined} />
</div>
</div>
</div>
);
};
interface PersonBadgeData {
name: string;
link?: string;
primaryOutline?: boolean;
primaryAt?: boolean;
}
let coders: Coder[] = [
{ name: "dessalines", link: "https://mastodon.social/@dessalines" },
{ name: "Nutomic", link: "https://lemmy.ml/u/nutomic" },
{ name: "asonix", link: "https://github.com/asonix" },
{ name: "krawieck", link: "https://github.com/krawieck" },
{ name: "shilangyu", link: "https://github.com/shilangyu" },
{ name: "uuttff8", link: "https://github.com/uuttff8" },
{ name: "eiknat", link: "https://github.com/eiknat" },
{ name: "ernestwisniewski", link: "https://github.com/ernestwisniewski" },
{ name: "zacanger", link: "https://github.com/zacanger" },
{ name: "iav", link: "https://github.com/iav" },
];
export interface Translation {
lang: string;
country?: string;
translators: Translator[];
interface PersonBadgeProps {
person: PersonBadgeData;
}
export interface Translator {
name: string;
link?: string;
const PersonBadge = ({ person }: PersonBadgeProps) =>
person.link ? (
<a href={person.link}>
<PersonBadgeInternal person={person} />
</a>
) : (
<PersonBadgeInternal person={person} />
);
const PersonBadgeInternal = ({ person }: PersonBadgeProps) => (
<Badge
content={
<div>
<Icon
icon="at-sign"
classes={classNames("fill-current text-gray-600", {
"text-primary": person.primaryAt,
})}
/>
<span
className={classNames("ml-1", {
[`${TEXT_GRADIENT}`]: person.link,
})}
>
{person.name}
</span>
</div>
}
outline={person.primaryOutline}
/>
);
interface PersonBadgesProps {
persons: PersonBadgeData[];
}
const PersonBadges = ({ persons }: PersonBadgesProps) => (
<div className="flex flex-row flex-wrap gap-2 mb-2">
{persons.map(p => (
<PersonBadge person={p} />
))}
</div>
);
export class Donate extends Component<any, any> {
constructor(props: any, context: any) {
super(props, context);
@ -91,185 +252,16 @@ export class Donate extends Component<any, any> {
}
render() {
const title = i18n.t("support_title");
const title = i18n.t("donate_title");
return (
<div>
<div className="container mx-auto px-4">
<Helmet title={title}>
<meta property={"title"} content={title} />
</Helmet>
<div class="container">
<div class="text-center">
<h1>{i18n.t("support_lemmy")}</h1>
<DonateLines />
</div>
<hr />
<div class="text-center">
<h2>{i18n.t("contributers")}</h2>
{this.codersLine()}
{this.translatorsLine()}
</div>
<div class="text-center">
<h2>{i18n.t("sponsors")}</h2>
{latinumSponsors.length > 0 && (
<div>
<p>{i18n.t("gold_pressed_latinum_sponsors_desc")}</p>
<div class="row is-horizontal-align">
{latinumSponsors.map(s => (
<div class="col-6">
<a class="button outline" href={s.link}>
<img
src={s.avatar}
width={bannerWidth}
height={bannerHeight}
/>
<div>{s.name}</div>
</a>
</div>
))}
</div>
<br />
</div>
)}
{goldSponsors.length > 0 && (
<div>
<p>{i18n.t("gold_sponsors_desc")}</p>
<div class="row is-horizontal-align">
{goldSponsors.map(s => (
<div class="col">
<a class="button outline gold" href={s.link}>
<img
class="is-rounded"
src={s.avatar}
width={avatarSize}
height={avatarSize}
/>
<div>{s.name}</div>
</a>
</div>
))}
</div>
<br />
</div>
)}
{silverSponsors.length > 0 && (
<div>
<p>{i18n.t("silver_sponsors_desc")}</p>
<div class="row is-horizontal-align">
{silverSponsors.map(s => (
<div class="col">
<a class="button outline primary" href={s.link}>
💎 {s.name}
</a>
</div>
))}
</div>
<br />
</div>
)}
<p>{i18n.t("general_sponsors_desc")}</p>
<div class="row is-horizontal-align">
{highlightedSponsors.map(s => (
<div class="col">
<div class="button outline primary">{s}</div>
</div>
))}
{sponsors.map(s => (
<div class="col">
<div class="button outline">{s}</div>
</div>
))}
</div>
</div>
<div class="text-center">
<h1>Crypto</h1>
<table>
<tr>
<td>bitcoin</td>
<td>
<code>1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK</code>
</td>
</tr>
<tr>
<td>ethereum</td>
<td>
<code>0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01</code>
</td>
</tr>
<tr>
<td>monero</td>
<td>
<code>
41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV
</code>
</td>
</tr>
<tr>
<td>cardano</td>
<td>
<code>
addr1q858t89l2ym6xmrugjs0af9cslfwvnvsh2xxp6x4dcez7pf5tushkp4wl7zxfhm2djp6gq60dk4cmc7seaza5p3slx0sakjutm
</code>
</td>
</tr>
</table>
</div>
</div>
</div>
);
}
translatorsLine() {
return (
<div>
<p>
<span>{i18n.t("thanks_translators")}</span>
{convertTranslators().map(t => (
<span>
<span class="text-error">{languagesAll[t.lang].native}</span>
{t.country && (
<span class="text-error"> {countries[t.country].native}</span>
)}
<span>: </span>
{t.translators.map((translator, i) => (
<span>
{translator.link ? (
<a href={translator.link}>{translator.name}</a>
) : (
<span>{translator.name}</span>
)}
<span>{i != t.translators.length - 1 ? ", " : " "}</span>
</span>
))}
</span>
))}
</p>
<p>
<T i18nKey="add_weblate">
#<a href="https://weblate.join-lemmy.org/projects/lemmy/">#</a>
</T>
</p>
</div>
);
}
codersLine() {
return (
<div>
<p>
<span>{i18n.t("thanks_coders")}</span>
{coders.map((coder, i) => (
<span>
{coder.link ? (
<a href={coder.link}>{coder.name}</a>
) : (
<span>{coder.name}</span>
)}
<span>{i != coders.length - 1 ? ", " : " "}</span>
</span>
))}
</p>
<DonateBlock />
<ContributorsBlock />
<SponsorsBlock />
<BottomSpacer />
</div>
);
}

View File

@ -1,35 +0,0 @@
import { Component } from "inferno";
import { LinkLine } from "./link-line";
import { T } from "inferno-i18next";
export class Footer extends Component<any, any> {
constructor(props: any, context: any) {
super(props, context);
}
render() {
return (
<footer>
<br />
<nav class="nav">
<div class="nav-left">
<p style="padding-left: 2rem">
<T i18nKey="footer_desc">
#
<a class="footer-name" href="https://infernojs.org">
#
</a>
<a class="footer-name" href="https://jenil.github.io/chota">
#
</a>
</T>
</p>
</div>
<div class="nav-right hide-sm hide-md hide-lg">
<LinkLine />
</div>
</nav>
</footer>
);
}
}

View File

@ -1,31 +1,23 @@
import { Component } from "inferno";
export enum IconSize {
Small = "w-3 h-3",
Medium = "w-4 h-4",
Large = "w-6 h-6",
Largest = "w-8 h-8",
}
interface IconProps {
icon: string;
size?: IconSize;
classes?: string;
}
export class Icon extends Component<IconProps, any> {
constructor(props: any, context: any) {
super(props, context);
}
export const Icon = ({ icon, size = IconSize.Medium, classes }: IconProps) => (
<svg className={`icon ${size} ${classes}`}>
<title>{icon}</title>
<use xlinkHref={`#icon-${icon}`}></use>
</svg>
);
render() {
return (
<svg class={`icon ${this.props.classes}`}>
<title>{this.props.icon}</title>
<use xlinkHref={`#icon-${this.props.icon}`}></use>
</svg>
);
}
}
export class Spinner extends Component<any, any> {
constructor(props: any, context: any) {
super(props, context);
}
render() {
return <Icon icon="spinner" classes="icon-spinner spin" />;
}
}
export const Spinner = () => (
<Icon icon="spinner" classes="icon-spinner spin" />
);

View File

@ -0,0 +1,140 @@
import classNames from "classnames";
import { Component, linkEvent } from "inferno";
import { All_TOPIC, TOPICS, Topic } from "./instances-definitions";
import { LANGUAGES, i18n } from "../i18next";
import { I18nKeys } from "i18next";
import { Icon } from "./icon";
enum Step {
Interest,
Language,
}
interface Props {
reset?: boolean;
}
interface State {
activeStep: Step;
topic?: Topic;
language?: string;
}
export class InstancePicker extends Component<Props, State> {
state: State = {
activeStep: Step.Interest,
};
constructor(props: any, context: any) {
super(props, context);
}
componentWillReceiveProps(): void {
this.setState({
activeStep: Step.Interest,
});
}
render() {
return (
<dialog id="picker" className="modal">
<form method="dialog" className="modal-backdrop">
<button>X</button>
</form>
<div className="modal-box bg-neutral-800">
<form method="dialog">
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
</button>
</form>
<div className="container mx-auto">
{this.state.activeStep == Step.Interest && (
<>
<p className="text-2xl font-bold text-center pb-4">
{i18n.t("what_topic")}
</p>
<div className="flex flex-row flex-wrap gap-4 pb-4">
{TOPICS.map(c => (
<button
className="btn btn-sm btn-outline normal-case"
value={c.name}
onClick={linkEvent(this, handleTopicChange)}
>
<Icon icon={c.icon} />
{i18n.t(c.name as I18nKeys)}
</button>
))}
</div>
</>
)}
{this.state.activeStep == Step.Language && (
<>
<p className="text-2xl font-bold text-center pb-4">
{i18n.t("what_language")}
</p>
<div className="flex flex-row flex-wrap gap-4 pb-4">
<button
className="btn btn-sm btn-outline normal-case"
value={"all"}
onClick={linkEvent(this, handleLanguageChange)}
>
{i18n.t("all_languages")}
</button>
{LANGUAGES.map(l => (
<button
className="btn btn-sm btn-outline normal-case"
value={l.code}
onClick={linkEvent(this, handleLanguageChange)}
>
{l.name}
</button>
))}
</div>
</>
)}
<ul className="steps steps-vertical lg:steps-horizontal w-full">
<li
onClick={linkEvent(this, handleResetInterests)}
className={classNames("step text-gray-300", {
"step-primary": this.state.activeStep == Step.Interest,
})}
>
{i18n.t("interests")}
</li>
<li
onClick={linkEvent(this, handleResetInterests)}
className={classNames("step text-gray-300", {
"step-primary": this.state.activeStep == Step.Language,
})}
>
{i18n.t("languages")}
</li>
</ul>
</div>
</div>
</dialog>
);
}
}
function handleTopicChange(i: InstancePicker, event: any) {
i.setState({
topic: TOPICS.find(c => c.name == event.target.value) ?? All_TOPIC,
activeStep: Step.Language,
});
}
function handleLanguageChange(i: InstancePicker, event: any) {
i.setState({ language: event.target.value });
const url = `/instances?topic=${i.state.topic?.name ?? All_TOPIC}&language=${
i.state.language
}&scroll=true`;
// Requires a page reload unfortunately
window.location.href = url;
}
function handleResetInterests(i: InstancePicker) {
i.setState({ topic: undefined, activeStep: Step.Interest });
}

View File

@ -0,0 +1,474 @@
export interface InstanceHelper {
name: string;
link: string;
}
export const INSTANCE_HELPERS: InstanceHelper[] = [
{
name: "Lemmy Community Explorer",
link: "https://lemmyverse.net/communities",
},
{
name: "Lemmy Fediverse Observer",
link: "https://lemmy.fediverse.observer/list",
},
{
name: "Awesome-Lemmy-Instances on GitHub",
link: "https://github.com/maltfield/awesome-lemmy-instances",
},
{
name: "Feddit's Lemmy Community Browser",
link: "https://browse.feddit.de/",
},
];
export interface Topic {
name: string;
icon: string;
}
export const All_TOPIC: Topic = {
name: "all_topics",
icon: "folder",
};
const GENERAL: Topic = {
name: "general",
icon: "box",
};
const TECHNOLOGY: Topic = {
name: "technology",
icon: "smartphone",
};
const POLITICS: Topic = {
name: "politics",
icon: "hammer2",
};
const RELIGION: Topic = {
name: "religion",
icon: "david-star",
};
const LGBTQ: Topic = {
name: "lgbtq",
icon: "transgender-alt",
};
const ART: Topic = {
name: "art",
icon: "edit",
};
const LITERATURE: Topic = {
name: "literature",
icon: "book",
};
const MUSIC: Topic = {
name: "music",
icon: "music",
};
const HOBBIES: Topic = {
name: "hobbies",
icon: "home",
};
const GAMING: Topic = {
name: "gaming",
icon: "videogame_asset",
};
const SPORTS: Topic = {
name: "sports",
icon: "futbol-o",
};
export const TOPICS: Topic[] = [
All_TOPIC,
GENERAL,
TECHNOLOGY,
POLITICS,
RELIGION,
LGBTQ,
ART,
LITERATURE,
MUSIC,
HOBBIES,
GAMING,
SPORTS,
];
export interface RecommendedInstance {
domain: string;
languages: string[];
topics: Topic[];
}
export const RECOMMENDED_INSTANCES: RecommendedInstance[] = [
{
domain: "lemmy.ml",
languages: ["en"],
topics: [GENERAL, TECHNOLOGY, POLITICS],
},
{
domain: "slrpnk.net",
languages: ["en"],
topics: [TECHNOLOGY, POLITICS],
},
{
domain: "lemmy.zip",
languages: ["en"],
topics: [GENERAL, TECHNOLOGY, GAMING],
},
{
domain: "lemmy.nz",
languages: ["en"],
topics: [GENERAL],
},
{
domain: "mander.xyz",
languages: ["en"],
topics: [ART, TECHNOLOGY, POLITICS],
},
{
domain: "infosec.pub",
languages: ["en"],
topics: [TECHNOLOGY],
},
{
domain: "lemmygrad.ml",
languages: ["en"],
topics: [GENERAL, POLITICS, LGBTQ],
},
{
domain: "sopuli.xyz",
languages: ["en", "fi"],
topics: [GENERAL],
},
{
domain: "lemmy.sdf.org",
languages: ["en"],
topics: [GENERAL, TECHNOLOGY],
},
{
domain: "lemmy.sdfeu.org",
languages: ["en"],
topics: [GENERAL, TECHNOLOGY],
},
{
domain: "pawb.social",
languages: ["en"],
topics: [LGBTQ],
},
{
domain: "lemmy.blahaj.zone",
languages: ["en"],
topics: [GENERAL, LGBTQ],
},
{
domain: "lemmy.dbzer0.com",
languages: ["en"],
topics: [TECHNOLOGY],
},
{
domain: "programming.dev",
languages: ["en"],
topics: [TECHNOLOGY],
},
{
domain: "lemmy.ca",
languages: ["en"],
topics: [GENERAL],
},
{
domain: "beehaw.org",
languages: ["en"],
topics: [GENERAL, POLITICS, GAMING],
},
{
domain: "hexbear.net",
languages: ["en"],
topics: [GENERAL, POLITICS, LGBTQ],
},
{
domain: "midwest.social",
languages: ["en"],
topics: [GENERAL],
},
{
domain: "lemmy.world",
languages: ["en"],
topics: [GENERAL],
},
{
domain: "startrek.website",
languages: ["en"],
topics: [TECHNOLOGY, ART, POLITICS],
},
{
domain: "lemmy.fmhy.ml",
languages: ["en"],
topics: [TECHNOLOGY],
},
{
domain: "discuss.tchncs.de",
languages: ["en", "de"],
topics: [GENERAL, TECHNOLOGY],
},
{
domain: "lemm.ee",
languages: ["en"],
topics: [GENERAL],
},
{
domain: "reddthat.com",
languages: ["en"],
topics: [GENERAL, TECHNOLOGY],
},
{
domain: "discuss.online",
languages: ["en"],
topics: [TECHNOLOGY],
},
{
domain: "lemmy.one",
languages: ["en"],
topics: [GENERAL],
},
{
domain: "links.dartboard.social",
languages: ["en"],
topics: [GENERAL],
},
{
domain: "poptalk.scrubbles.tech",
languages: ["en"],
topics: [MUSIC],
},
{
domain: "aiparadise.moe",
languages: ["en"],
topics: [TECHNOLOGY, ART],
},
{
domain: "jlai.lu",
languages: ["fr"],
topics: [GENERAL],
},
{
domain: "kerala.party",
languages: ["ml"],
topics: [GENERAL],
},
{
domain: "monero.town",
languages: ["en"],
topics: [TECHNOLOGY],
},
{
domain: "mujico.org",
languages: ["es"],
topics: [GENERAL],
},
{
domain: "partizle.com",
languages: ["en"],
topics: [GENERAL, TECHNOLOGY],
},
{
domain: "sub.wetshaving.social",
languages: ["en"],
topics: [HOBBIES],
},
{
domain: "iusearchlinux.fyi",
languages: ["en"],
topics: [TECHNOLOGY],
},
{
domain: "feddit.dk",
languages: ["da"],
topics: [GENERAL],
},
{
domain: "feddit.nu",
languages: ["sv"],
topics: [GENERAL],
},
{
domain: "mtgzone.com",
languages: ["en"],
topics: [HOBBIES],
},
{
domain: "lemmy.run",
languages: ["en"],
topics: [GENERAL],
},
{
domain: "rblind.com",
languages: ["en"],
topics: [GENERAL, TECHNOLOGY],
},
{
domain: "tucson.social",
languages: ["en"],
topics: [GENERAL],
},
{
domain: "lemmyonline.com",
languages: ["en"],
topics: [TECHNOLOGY],
},
{
domain: "lemmy.wtf",
languages: ["en"],
topics: [GENERAL],
},
{
domain: "thelemmy.club",
languages: ["en"],
topics: [GENERAL],
},
{
domain: "bookwormstory.social",
languages: ["en"],
topics: [ART, LITERATURE],
},
{
domain: "lib.lgbt",
languages: ["en"],
topics: [LGBTQ],
},
{
domain: "suppo.fi",
languages: ["en", "fi"],
topics: [GENERAL],
},
{
domain: "lemmy.studio",
languages: ["en"],
topics: [MUSIC, HOBBIES, ART],
},
{
domain: "lemmy.radio",
languages: ["en"],
topics: [MUSIC, HOBBIES],
},
{
domain: "feddit.ch",
languages: ["en"],
topics: [GENERAL],
},
{
domain: "preserve.games",
languages: ["en"],
topics: [GAMING],
},
{
domain: "lemmy.my.id",
languages: ["id"],
topics: [GENERAL],
},
{
domain: "lemmyfly.org",
languages: ["en"],
topics: [HOBBIES],
},
{
domain: "lemmy.spacestation14.com",
languages: ["en"],
topics: [GAMING],
},
{
domain: "eslemmy.es",
languages: ["es"],
topics: [GENERAL],
},
{
domain: "dmv.social",
languages: ["en"],
topics: [GENERAL],
},
{
domain: "feddit.ro",
languages: ["ro"],
topics: [GENERAL],
},
{
domain: "feddit.de",
languages: ["de"],
topics: [GENERAL],
},
{
domain: "lemmy.ninja",
languages: ["en"],
topics: [GENERAL],
},
{
domain: "discuss.tchncs.de",
languages: ["de"],
topics: [TECHNOLOGY],
},
{
domain: "sh.itjust.works",
languages: ["en", "fr"],
topics: [GENERAL, GAMING],
},
{
domain: "feddit.nl",
languages: ["nl"],
topics: [GENERAL],
},
{
domain: "aussie.zone",
languages: ["en"],
topics: [GENERAL],
},
{
domain: "lemmy.pt",
languages: ["pt"],
topics: [GENERAL],
},
{
domain: "lemmy.eco.br",
languages: ["pt"],
topics: [GENERAL],
},
{
domain: "feddit.cl",
languages: ["es"],
topics: [GENERAL],
},
{
domain: "lemmy.eus",
languages: ["eu"],
topics: [TECHNOLOGY],
},
{
domain: "tabinezumi.net",
languages: ["ja"],
topics: [TECHNOLOGY],
},
{
domain: "lm.korako.me",
languages: ["ja"],
topics: [TECHNOLOGY],
},
{
domain: "feddit.it",
languages: ["it"],
topics: [TECHNOLOGY],
},
{
domain: "feddit.uk",
languages: ["en"],
topics: [GENERAL],
},
];

View File

@ -1,152 +1,585 @@
import { Component } from "inferno";
import { Component, InfernoEventHandler, linkEvent } from "inferno";
import { Helmet } from "inferno-helmet";
import { i18n } from "../i18next";
import { i18n, LANGUAGES } from "../i18next";
import { T } from "inferno-i18next";
import { instance_stats } from "../instance_stats";
import { numToSI } from "../utils";
import { getQueryParams, mdToHtml, numToSI } from "../utils";
import { Badge, SELECT_CLASSES, TEXT_GRADIENT } from "./common";
import {
INSTANCE_HELPERS,
Topic,
RECOMMENDED_INSTANCES,
All_TOPIC,
TOPICS,
} from "./instances-definitions";
import { Icon, IconSize } from "./icon";
import { I18nKeys } from "i18next";
const TitleBlock = () => (
<div className="flex flex-col items-center pt-16 mb-16">
<T i18nKey="lemmy_servers" className="text-4xl font-bold mb-8">
#<span className={TEXT_GRADIENT}>#</span>
</T>
<div
className="tooltip"
data-tip={i18n.t("monthly_active_users", {
formattedCount: numToSI(instance_stats.stats.users_active_month),
})}
>
<div className="stats shadow mb-8">
<div className="stat">
<div className="stat-figure text-primary">
<Icon icon="globe" size={IconSize.Largest} />
</div>
<div className="stat-title">{i18n.t("servers")}</div>
<div className="stat-value">
{numToSI(instance_stats.stats.crawled_instances)}
</div>
<div className="stat-desc">{i18n.t("lemmyverse")}</div>
</div>
<div className="stat">
<div className="stat-figure text-secondary">
<Icon icon="users" size={IconSize.Largest} />
</div>
<div className="stat-title">{i18n.t("active_users")}*</div>
<div className="stat-value">
{numToSI(instance_stats.stats.users_active_month)}
</div>
<div className="stat-desc">{new Date().toLocaleDateString()}</div>
</div>
</div>
</div>
<p className="text-xl text-gray-300">{i18n.t("instance_disclaimer")}</p>
</div>
);
const ComparisonBlock = () => (
<div>
<div className="text-md text-gray-300 mb-3">
{i18n.t("instance_comparison")}:
</div>
<div className="card card-bordered bg-neutral-900 shadow-xl">
<div className="card-body p-4">
<div className="flex flex-row flex-wrap gap-4 mb-2">
{INSTANCE_HELPERS.map(i => (
<Badge
content={
<a href={i.link}>
<Icon
icon="embed"
classes={`fill-current text-primary mr-2`}
/>
<span className="text-gray-300">{i.name}</span>
</a>
}
/>
))}
</div>
</div>
</div>
</div>
);
const SectionTitle = ({ title }) => (
<div className="text-2xl mb-3">{title}</div>
);
interface InstanceCardGridProps {
title: string;
instances: any[];
}
const InstanceCardGrid = ({ instances }: InstanceCardGridProps) => (
<div className="grid md:grid-cols-3 grid-cols-1 gap-4">
{instances.map(i => (
<InstanceCard instance={i} />
))}
</div>
);
interface InstanceCardProps {
instance: any;
}
function buildUrl(domain: string): string {
return `https://${domain}`;
}
const InstanceCard = ({ instance }: InstanceCardProps) => {
const domain = instance.domain;
const description = instance.site_info.site_view.site.description;
const sidebar = instance.site_info.site_view.site.sidebar || description;
const icon =
instance.site_info.site_view.site.icon || "/static/assets/images/lemmy.svg";
const banner = instance.site_info.site_view.site.banner;
const users = instance.site_info.site_view.counts.users;
const comments = instance.site_info.site_view.counts.comments;
const monthlyUsers = instance.site_info.site_view.counts.users_active_month;
const registrationMode =
instance.site_info.site_view.local_site.registration_mode;
const modalId = `modal_${domain}`;
return (
<div className="card card-bordered bg-neutral-900 shadow-xl">
<div className="card-body p-4">
<div className="flex flex-row flex-wrap gap-4">
<DetailsModal
id={modalId}
domain={domain}
banner={banner}
users={users}
comments={comments}
monthlyUsers={monthlyUsers}
icon={icon}
sidebar={sidebar}
registrationMode={registrationMode}
/>
<InstanceIcon domain={domain} icon={icon} />
<InstanceStats
users={users}
comments={comments}
monthlyUsers={monthlyUsers}
/>
</div>
<a
href={buildUrl(domain)}
className={`text-2xl font-bold ${TEXT_GRADIENT}`}
>
{domain}
</a>
<p className="text-sm text-gray-300 mb-2">{description}</p>
<div className="flex flex-row flex-wrap justify-between gap-2">
<a
className="btn btn-primary text-white max-md:btn-block bg-gradient-to-r from-[#69D066] to-[#03A80E] normal-case"
href={`${buildUrl(domain)}/signup`}
>
{i18n.t("sign_up")}
</a>
<button
className="btn btn-secondary btn-outline text-white max-md:btn-block normal-case"
onClick={() =>
(document.getElementById(modalId) as any).showModal()
}
>
{i18n.t("more_information")}
</button>
</div>
</div>
</div>
);
};
const imgError =
"this.onError=null;this.src='/static/assets/images/lemmy.svg';" as unknown as InfernoEventHandler<HTMLImageElement>;
const InstanceIcon = ({ domain, icon }) => (
<a className="rounded-xl bg-neutral-800 p-4" href={buildUrl(domain)}>
<img className="w-24 h-24" src={icon} onError={imgError} />
</a>
);
const InstanceStats = ({ users, comments, monthlyUsers }) => (
<div className="flex flex-col flex-wrap justify-between">
<StatsBadges
users={users}
comments={comments}
monthlyUsers={monthlyUsers}
/>
</div>
);
export const StatsBadges = ({ users, comments, monthlyUsers }) => (
<>
<Badge
content={
<div
className="text-sm text-gray-500 tooltip"
data-tip={i18n.t("total_users", {
formattedCount: users.toLocaleString(),
})}
>
<Icon icon="users" classes="mr-2" />
<span>{users.toLocaleString()}</span>
</div>
}
/>
<Badge
content={
<div
className="text-sm text-gray-500 tooltip"
data-tip={i18n.t("total_comments", {
formattedCount: comments.toLocaleString(),
})}
>
<Icon icon="message-circle" classes="mr-2" />
<span>{comments.toLocaleString()}</span>
</div>
}
/>
<Badge
content={
<div
className="text-sm text-gray-500 tooltip"
data-tip={i18n.t("monthly_active_users", {
formattedCount: monthlyUsers.toLocaleString(),
})}
>
<Icon icon="user-check" classes="mr-2" />
<span>
{i18n.t("per_month", {
formattedCount: monthlyUsers.toLocaleString(),
})}
</span>
</div>
}
/>
</>
);
function registrationModeToString(registrationMode: string): string {
if (registrationMode == "Open") {
return i18n.t("registrations_open");
} else if (registrationMode == "Closed") {
return i18n.t("registrations_closed");
} else if (registrationMode == "RequireApplication") {
return i18n.t("requires_application");
} else {
return i18n.t("registrations_open");
}
}
export const DetailsModal = ({
id,
domain,
icon,
banner,
users,
comments,
monthlyUsers,
sidebar,
registrationMode,
}) => (
<dialog id={id} className="modal">
<form method="dialog" className="modal-backdrop">
<button>close</button>
</form>
<div className="modal-box w-10/12 max-w-5xl bg-neutral-900">
<form method="dialog">
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
</button>
</form>
{banner && (
<img
src={banner}
className="object-cover w-full h-48 rounded-xl mb-3"
/>
)}
<div className="flex flex-row flex-wrap gap-4 mb-3 items-center">
{icon && <img className="w-8 h-8" src={icon} onError={imgError} />}
<StatsBadges
users={users}
comments={comments}
monthlyUsers={monthlyUsers}
/>
<Badge
content={
<div className="text-sm text-gray-500">
<Icon icon="user-check" classes="mr-2" />
<span>{registrationModeToString(registrationMode)}</span>
</div>
}
/>
<div className="btn btn-primary btn-outline btn-sm normal-case">
<a href={buildUrl(domain)}>{i18n.t("browse_instance")}</a>
</div>
</div>
{sidebar && (
<article className="prose max-w-none prose-a:text-primary prose-h1:text-primary">
<div dangerouslySetInnerHTML={mdToHtml(sidebar)} />
</article>
)}
<a
className="btn btn-primary btn-block text-white normal-case"
href={`${buildUrl(domain)}/signup`}
>
{i18n.t("sign_up")}
</a>
</div>
</dialog>
);
interface Sort {
name: string;
icon: string;
}
const RANDOM_SORT: Sort = {
name: "random",
icon: "TBD",
};
const MOST_ACTIVE_SORT: Sort = {
name: "most_active",
icon: "TBD",
};
const LEAST_ACTIVE_SORT: Sort = {
name: "least_active",
icon: "TBD",
};
const SORTS: Sort[] = [RANDOM_SORT, MOST_ACTIVE_SORT, LEAST_ACTIVE_SORT];
function sortRandom(instances: any[]): any[] {
return instances
.map(value => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value);
}
function sortActive(instances: any[]): any[] {
return instances.sort(
(a, b) =>
b.site_info.site_view.counts.users_active_month -
a.site_info.site_view.counts.users_active_month,
);
}
interface State {
instances: any[];
sort: Sort;
language: string;
topic: Topic;
scroll: boolean;
}
interface Props {
sort: Sort;
language: string;
topic: Topic;
scroll: boolean;
}
function getSortFromQuery(sort?: string): Sort {
return SORTS.find(s => s.name == sort) ?? RANDOM_SORT;
}
function getTopicFromQuery(topic?: string): Topic {
return TOPICS.find(c => c.name == topic) ?? All_TOPIC;
}
function getInstancesQueryParams() {
return getQueryParams<Props>({
sort: getSortFromQuery,
language: d => d || "all",
topic: getTopicFromQuery,
scroll: d => !!d,
});
}
export class Instances extends Component<Props, State> {
state: State = {
instances: [],
sort: RANDOM_SORT,
language: "all",
topic: All_TOPIC,
scroll: false,
};
export class Instances extends Component<any, any> {
constructor(props: any, context: any) {
super(props, context);
}
biasedRandom(active_users, avg, max) {
// Lets introduce a better bias to random shuffle instances list
var influence = 1.25;
var rnd = Math.random() * (max / influence) + active_users;
var mix = Math.random() * influence;
return rnd * (1 - mix) + avg * mix;
// Set the filters by the query params if they exist
componentDidMount() {
this.setState(getInstancesQueryParams());
// This is browser intensive, so run in the background
setTimeout(() => {
this.buildInstanceList();
this.scrollToSearch();
}, 0);
}
averageFunc(values: any) {
return values.reduce((a, b) => a + b) / values.length;
scrollToSearch() {
if (this.state.scroll) {
const el = document.getElementById("search")?.offsetTop;
if (el) {
window.scrollTo({ top: el, behavior: "smooth" });
}
}
}
isOpenInstance(i: any): boolean {
return !(
i.site_info.site_view.local_site.registration_mode !== "Open" ||
i.site_info.site_view.local_site.captcha_enabled ||
i.site_info.site_view.local_site.require_email_verification
);
}
buildInstanceList() {
let instances = instance_stats.stats.instance_details;
const recommended = RECOMMENDED_INSTANCES;
// Language Filter
if (this.state.language !== "all") {
const languageRecs = recommended.filter(r =>
r.languages.includes(this.state.language),
);
instances = instances.filter(i =>
languageRecs.map(r => r.domain).includes(i.domain),
);
}
// Topic filter
if (this.state.topic !== All_TOPIC) {
const topicRecs = recommended.filter(r =>
r.topics.includes(this.state.topic),
);
instances = instances.filter(i =>
topicRecs.map(c => c.domain).includes(i.domain),
);
}
// Filter out all open instances (often used by bots)
instances = instances.filter(i => !this.isOpenInstance(i));
// Sort
if (this.state.sort == RANDOM_SORT) {
instances = sortRandom(instances);
} else if (this.state.sort == MOST_ACTIVE_SORT) {
instances = sortActive(instances);
} else {
instances = sortActive(instances).reverse();
}
this.setState({ instances });
}
render() {
const title = i18n.t("join_title");
var recommended_instances = instance_stats.recommended[i18n.language];
if (!recommended_instances) {
recommended_instances = instance_stats.recommended["en"];
}
var recommended = [];
var remaining = [];
var values = [];
for (var i of instance_stats.stats.instance_details) {
if (recommended_instances.indexOf(i.domain) > -1) {
recommended.push(i);
} else {
remaining.push(i);
}
values.push(i.site_info.site_view.counts.users_active_month);
}
// Use these values for the shuffle
const avgMonthlyUsers = this.averageFunc(values);
const maxMonthlyUsers = Math.max(...values);
let recommended2 = recommended
.map(value => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value);
// BIASED sorting for instances, based on the min/max of users_active_month
let remaining2 = remaining
.map(i => ({
instance: i,
sort: this.biasedRandom(
i.site_info.site_view.counts.users_active_month,
avgMonthlyUsers,
maxMonthlyUsers,
),
}))
.sort((a, b) => b.sort - a.sort)
.map(({ instance }) => instance);
return (
<div class="container">
<div className="container mx-auto px-4">
<Helmet title={title}>
<meta property={"title"} content={title} />
</Helmet>
<h1 class="is-marginless">{i18n.t("lemmy_servers")}</h1>
{this.header()}
<br />
<br />
{this.renderList(i18n.t("recommended_instances"), recommended2)}
{this.renderList(i18n.t("popular_instances"), remaining2)}
<TitleBlock />
<ComparisonBlock />
{this.filterAndTitleBlock()}
<div className="mt-4">
{this.state.instances.length > 0 ? (
<InstanceCardGrid
title={i18n.t("popular_instances")}
instances={this.state.instances}
/>
) : (
this.seeAllBtn()
)}
</div>
</div>
);
}
header() {
seeAllBtn() {
return (
<i>
{i18n.t("instance_totals", {
instances: numToSI(instance_stats.stats.crawled_instances),
users: numToSI(instance_stats.stats.users_active_month),
})}
<p>
{i18n.t("instance_comparison")}:
<ul>
<li>
<a href="https://github.com/maltfield/awesome-lemmy-instances">
Awesome-Lemmy-Instances on GitHub
</a>
</li>
<li>
<a href="https://the-federation.info/platform/73">
the-federation.info Lemmy Instances Page
</a>
</li>
<li>
<a href="https://lemmymap.feddit.de/">Feddit's Lemmymap</a>
</li>
</ul>
{i18n.t("instance_browser")}{" "}
<a href="https://browse.feddit.de/">
Feddit's Lemmy Community Browser
</a>
</p>
</i>
<div>
<p className="text-sm text-gray-300 mb-4">{i18n.t("none_found")}</p>
<button
className="btn btn-sm btn-secondary text-white normal-case"
onClick={linkEvent(this, handleSeeAll)}
>
{i18n.t("see_all_servers")}
</button>
</div>
);
}
renderList(header: string, instances: any[]) {
filterAndTitleBlock() {
return (
<div>
<h2>{header}</h2>
<div class="row">
{instances.map(instance => {
let domain = instance.domain;
let description = instance.site_info.site_view.site.description;
let icon = instance.site_info.site_view.site.icon;
return (
<div class="card col-6">
<header>
<div class="row">
<h4 class="col">{domain}</h4>
</div>
</header>
<div class="is-center">
<img
class="join-banner"
src={icon || "/static/assets/images/lemmy.svg"}
/>
</div>
<br />
<p class="join-desc">{description}</p>
<footer>
<a class="button primary" href={`https://${domain}`}>
{i18n.t("browse_instance")}
</a>
</footer>
</div>
);
})}
<div id="search" className="mt-16">
<div className="flex flex-row flex-wrap gap-4">
<div className="flex-none">
<SectionTitle title={i18n.t("join_title")} />
</div>
<div className="grow"></div>
<div>
<select
className={`${SELECT_CLASSES} mr-2`}
value={this.state.topic.name}
onChange={linkEvent(this, handleTopicChange)}
name="topic_select"
>
<option disabled selected>
{i18n.t("topic")}
</option>
{TOPICS.map(c => (
<option key={c.name} value={c.name}>
{i18n.t(c.name as I18nKeys)}
</option>
))}
</select>
<select
value={this.state.language}
onChange={linkEvent(this, handleLanguageChange)}
className={`${SELECT_CLASSES} mr-2`}
>
<option disabled>Languages</option>
<option key="all" value="all">
{i18n.t("all_languages")}
</option>
{LANGUAGES.map((l, i) => (
<option key={i} value={l.code}>
{l.name}
</option>
))}
</select>
<select
value={this.state.sort.name}
name="sort_select"
className={SELECT_CLASSES}
onChange={linkEvent(this, handleSortChange)}
>
<option disabled>{i18n.t("sort")}</option>
{SORTS.map(s => (
<option key={s.name} value={s.name}>
{i18n.t(s.name as I18nKeys)}
</option>
))}
</select>
</div>
</div>
</div>
);
}
}
function handleSortChange(i: Instances, event: any) {
i.setState({
sort: SORTS.find(s => s.name == event.target.value) ?? RANDOM_SORT,
});
i.buildInstanceList();
}
function handleTopicChange(i: Instances, event: any) {
i.setState({
topic: TOPICS.find(c => c.name == event.target.value) ?? All_TOPIC,
});
i.buildInstanceList();
}
function handleLanguageChange(i: Instances, event: any) {
i.setState({ language: event.target.value });
i.buildInstanceList();
}
function handleSeeAll(i: Instances) {
i.setState({
sort: RANDOM_SORT,
language: "all",
topic: All_TOPIC,
});
i.buildInstanceList();
}

View File

@ -1,21 +0,0 @@
import { Component } from "inferno";
import { Link } from "inferno-router";
import { i18n } from "../i18next";
export class LinkLine extends Component<any, any> {
constructor(props: any, context: any) {
super(props, context);
}
render() {
return (
<>
<Link to="/instances">{i18n.t("join_a_server")}</Link>
<Link to="/news">{i18n.t("news")}</Link>
<Link to="/apps">{i18n.t("apps")}</Link>
<Link to="/donate">{i18n.t("donate")}</Link>
<a href={`/docs/index.html`}>{i18n.t("docs")}</a>
<Link to="/contact">{i18n.t("contact")}</Link>
</>
);
}
}

View File

@ -1,255 +1,511 @@
import { Component } from "inferno";
import { Link } from "inferno-router";
import { Helmet } from "inferno-helmet";
import { DonateLines } from "./donate-lines";
import { i18n } from "../i18next";
import { T } from "inferno-i18next";
import { isBrowser } from "../utils";
import { getQueryParams, isBrowser } from "../utils";
import { Icon } from "./icon";
import {
BottomSpacer,
CARD_GRADIENT,
DonateBlock,
TEXT_GRADIENT,
} from "./common";
import { InstancePicker } from "./instance-picker";
import classNames from "classnames";
import Glide from "@glidejs/glide";
interface MainProps {
i: Main;
}
const TitleBlock = ({ i }: MainProps) => (
<div className="py-16 flex flex-col items-center">
<div className="flex flex-col items-center gap-4 mb-8">
<p className={`text-6xl font-bold ${TEXT_GRADIENT} p-2`}>Lemmy</p>
<p className="text-3xl font-medium text-center">{i18n.t("lemmy_desc")}</p>
</div>
<div className="flex flex-row justify-around gap-4">
<JoinServerButton i={i} />
<SeeAllServersButton />
</div>
</div>
);
const carouselImages = [
"/static/assets/images/main_screen_2.webp",
"/static/assets/images/main_screen_3.webp",
"/static/assets/images/main_screen_1.webp",
];
const CarouselBlock = () => (
<div>
<div class="glide p-8 space-x-8 rounded-box mt-16">
<div class="glide__track" data-glide-el="track">
<ul class="glide__slides">
{carouselImages.map((image, i) => (
<img
src={image}
className={classNames("rounded-box border-8 z-10 glide__slide", {
"border-primary/[.15]": i & 1,
"border-secondary/[.15]": !(i & 1),
})}
/>
))}
</ul>
</div>
<div
className="glide__bullets flex justify-center w-full py-2 gap-4"
data-glide-el="controls[nav]"
>
{carouselImages.map((_, i) => (
<button
data-glide-dir={`=${i}`}
className={`glide__bullet ${TEXT_GRADIENT}`}
>
</button>
))}
</div>
</div>
</div>
);
const JoinServerButton = ({ i }: MainProps) => (
<a
href="?showJoinModal=true"
className="btn btn-primary text-white normal-case z-10"
onClick={e => {
e.preventDefault();
i.setState({ resetInstancePicker: true });
showJoinModal();
}}
>
{i18n.t("join_a_server")}
</a>
);
const SeeAllServersButton = () => (
<Link
to="/instances"
className="btn btn-secondary text-white normal-case z-10"
>
{i18n.t("see_all_servers")}
</Link>
);
const FollowCommunitiesBlock = ({ i }: MainProps) => (
<div className="flex flex-col items-center">
<div className={`card card-bordered ${CARD_GRADIENT} shadow-xl`}>
<div className="card-body items-center px-8 md:px-32 py-16">
<T
i18nKey="follow_communities"
className="card-title font-bold text-4xl text-center mb-3 inline-block"
>
#<span className={TEXT_GRADIENT}>#</span>
</T>
<p className="text-sm text-gray-300 text-center mb-6">
{i18n.t("lemmy_long_desc")}
</p>
<JoinServerButton i={i} />
</div>
</div>
</div>
);
const FeatureCard = ({ pic, title, subtitle, classes }) => (
<div className={`card ${CARD_GRADIENT} shadow-xl ${classes}`}>
<div className="p-4">
<img src={pic} className="rounded-xl w-full object-fill min-h-[300px]" />
</div>
<div className="card-body pt-0">
<h2 className="card-title text-secondary">{title}</h2>
<p className="text-sm text-gray-300">{subtitle}</p>
</div>
</div>
);
const OpenSourceCard = ({ classes }) => (
<FeatureCard
classes={classes}
pic={"/static/assets/images/main_open_source.webp"}
title={i18n.t("open_source")}
subtitle={
<T i18nKey="open_source_desc">
#
<a className="link" href="https://github.com/LemmyNet">
#
</a>
<a className="link" href="https://en.wikipedia.org/wiki/Copyleft">
#
</a>
<a
className="link"
href="https://github.com/LemmyNet/lemmy/blob/master/LICENSE"
>
#
</a>
</T>
}
/>
);
const BlazingFastCard = ({ classes }) => (
<FeatureCard
classes={classes}
pic={"/static/assets/images/main_blazing_fast.webp"}
title={i18n.t("blazing_fast")}
subtitle={
<T i18nKey="blazing_fast_desc">
#
<a className="link" href="https://www.rust-lang.org">
#
</a>
<a className="link" href="https://actix.rs/">
#
</a>
<a className="link" href="http://diesel.rs/">
#
</a>
<a className="link" href="https://infernojs.org">
#
</a>
<a className="link" href="https://www.typescriptlang.org/">
#
</a>
</T>
}
/>
);
const ModToolsCard = ({ classes }) => (
<FeatureCard
classes={classes}
pic={"/static/assets/images/main_powerful.webp"}
title={i18n.t("mod_tools")}
subtitle={i18n.t("mod_tools_desc")}
/>
);
const CensorshipCard = ({ classes }) => (
<FeatureCard
classes={classes}
pic={"/static/assets/images/main_censorship.webp"}
title={i18n.t("censorship_resistant")}
subtitle={i18n.t("censorship_resistant_desc")}
/>
);
const FederationCard = ({ classes }) => (
<FeatureCard
classes={classes}
pic={"/static/assets/images/main_federation.webp"}
title={i18n.t("federation")}
subtitle={i18n.t("federation_desc")}
/>
);
const FeatureCardsBlock = () => (
<div className="grid md:grid-cols-12 grid-cols-1 gap-4 mt-16">
<OpenSourceCard classes="md:col-span-7" />
<BlazingFastCard classes="md:col-span-5" />
<ModToolsCard classes="md:col-span-4" />
<CensorshipCard classes="md:col-span-4" />
<FederationCard classes="md:col-span-4" />
</div>
);
const DiscussionPlatformBlock = () => (
<div className="flex flex-col items-center mt-16">
<div className="card card-bordered bg-gradient-to-r text-transparent from-primary to-secondary shadow-xl">
<div className="card-body items-center px-8 md:px-32 py-16">
<T
i18nKey="create_discussion_platform"
className="card-title font-medium text-4xl text-center text-white mb-3 inline-block"
>
#<span className="font-bold">#</span>
</T>
<T
i18nKey="create_discussion_platform_desc"
className="text-sm text-white text-center mb-6"
>
#
<a className="link" href={`/docs/administration/administration.html`}>
#
</a>
<i>#</i>
<a className="link" href="https://en.wikipedia.org/wiki/Fediverse">
#
</a>
</T>
<a
className="btn btn-primary bg-white text-primary normal-case"
href={`/docs/administration/administration.html`}
>
{i18n.t("run_a_server")}
</a>
</div>
</div>
</div>
);
const MoreFeaturesBlock = () => (
<div className="mt-16">
<T i18nKey="more_features" className={`text-center text-4xl mb-8`}>
#<span className={TEXT_GRADIENT}>#</span>
</T>
<div className="grid md:grid-cols-5 grid-cols-1 gap-4">
<MoreFeaturesCard
icons={<Icon icon="embed" />}
text={
<T i18nKey="self_hostable">
#
<a
className="link"
href={`/docs/administration/install_docker.html`}
>
#
</a>
<a
className="link"
href={`/docs/administration/install_ansible.html`}
>
#
</a>
</T>
}
/>
<MoreFeaturesCard
icons={
<div>
<Icon icon="clipboard" />
</div>
}
text={i18n.t("clean_interface")}
/>
<MoreFeaturesCard
icons={
<div>
<Icon icon="appleinc" /> <Icon icon="android" />
</div>
}
text={
<Link className="link" to="/apps">
{i18n.t("mobile_apps_for_ios_and_android")}
</Link>
}
/>
<MoreFeaturesCard
icons={
<div>
<Icon icon="smile" />
</div>
}
text={i18n.t("avatar_support")}
/>
<MoreFeaturesCard
icons={
<div>
<Icon icon="thumbs-up" /> <Icon icon="thumbs-down" />
</div>
}
text={
<T i18nKey="full_vote_scores">
#<code>#</code>#
</T>
}
/>
<MoreFeaturesCard
icons={
<div>
<Icon icon="moon" /> <Icon icon="sun" />
</div>
}
text={i18n.t("themes_including")}
/>
<MoreFeaturesCard
icons={<div>:</div>}
text={
<T i18nKey="emojis_autocomplete">
#<code>#</code>
</T>
}
/>
<MoreFeaturesCard
icons={
<div>
<Icon icon="at-sign" />
</div>
}
text={
<T i18nKey="user_tagging">
#<code>#</code>
<code>#</code>
</T>
}
/>
<MoreFeaturesCard
icons={
<div>
<Icon icon="image" />
</div>
}
text={i18n.t("integrated_image_uploading")}
/>
<MoreFeaturesCard
icons={
<div>
<Icon icon="bell" />
</div>
}
text={i18n.t("notifications_including")}
/>
<MoreFeaturesCard
icons={
<div>
<Icon icon="globe" />
</div>
}
text={
<T i18nKey="i18n_support">
#
<a
className="link"
href="https://weblate.join-lemmy.org/projects/lemmy/lemmy/"
>
#
</a>
</T>
}
/>
<MoreFeaturesCard
icons={
<div>
<Icon icon="rss" />
</div>
}
text={
<T i18nKey="rss_feeds">
#<code>#</code>
<code>#</code>
<code>#</code>
<code>#</code>
<code>#</code>
</T>
}
/>
<MoreFeaturesCard
icons={
<div>
<Icon icon="trash" />
</div>
}
text={i18n.t("can_fully_erase")}
/>
<MoreFeaturesCard
icons={
<div>
<Icon icon="alert-octagon" />
</div>
}
text={i18n.t("nsfw_support")}
/>
<MoreFeaturesCard
icons={
<div>
<Icon icon="eye-off" />
</div>
}
text={i18n.t("censorship_resistant_desc")}
/>
</div>
</div>
);
const MoreFeaturesCard = ({ icons, text }) => (
<div className="card card-bordered w-auto bg-neutral-800 shadow-xl">
<div className="card-body">
<div className="btn btn-sm btn-secondary w-fit mb-2 pointer-events-none">
{icons}
</div>
<p className="text-sm text-gray-300">{text}</p>
</div>
</div>
);
function getMainQueryParams() {
return getQueryParams<Props>({
showJoinModal: d => !!d,
});
}
interface Props {
showJoinModal?: boolean;
}
interface State {
resetInstancePicker: boolean;
showJoinModal?: boolean;
}
function showJoinModal() {
(document.getElementById("picker") as any).showModal();
}
export class Main extends Component<Props, State> {
state: State = {
resetInstancePicker: false,
};
export class Main extends Component<any, any> {
constructor(props: any, context: any) {
super(props, context);
}
componentDidMount() {
new Glide(".glide", {
type: "carousel",
gap: 50,
perView: 3,
breakpoints: {
800: {
perView: 1,
},
},
autoplay: 3000,
hoverpause: true,
}).mount();
if (isBrowser()) {
window.scrollTo(0, 0);
}
}
joinServer() {
return (
<Link className="button primary" to="/instances">
{i18n.t("join_a_server")}
</Link>
);
}
runServer() {
return (
<a
class="button primary"
href={`/docs/administration/administration.html`}
>
{i18n.t("run_a_server")}
</a>
);
this.setState(getMainQueryParams());
if (this.state.showJoinModal) {
showJoinModal();
}
}
render() {
const title = i18n.t("lemmy_title");
return (
<div>
<InstancePicker reset={this.state.resetInstancePicker} />
<Helmet title={title}>
<meta property={"title"} content={title} />
</Helmet>
<div class="bg-image"></div>
<div class="container">
<div class="text-center">
<h1 class="stylized">{i18n.t("lemmy")}</h1>
<h4>{i18n.t("lemmy_desc")}</h4>
<div class="row is-horizontal-align">
<div class="col-2-lg">{this.joinServer()}</div>
<div class="col-2-lg">{this.runServer()}</div>
</div>
</div>
<img
src="/static/assets/images/world_background.svg"
className="bg-top bg-no-repeat bg-contain opacity-20 absolute"
/>
<div className="container mx-auto px-4">
<TitleBlock i={this} />
<FollowCommunitiesBlock i={this} />
</div>
<br />
<div class="container">
<div class="text-center">
<h2>{i18n.t("follow_communities")}</h2>
<p>
<T i18nKey="lemmy_long_desc">
#<a href="https://github.com/LemmyNet">#</a>
<a href="https://reddit.com">#</a>
<a href="https://lobste.rs">#</a>
<a href="https://news.ycombinator.com/">#</a>
<b>#</b>
</T>
</p>
<p>{this.joinServer()}</p>
</div>
</div>
<br />
<div class="bg-success">
<br />
<div class="container">
<div class="row">
<div class="col-4">
<div>
<header class="is-center">
<img
height={180}
src="/static/assets/images/review_pic.webp"
/>
</header>
<br />
<h4 class="text-center">{i18n.t("open_source")}</h4>
<p>
<T i18nKey="open_source_desc">
#<a href="https://github.com/LemmyNet">#</a>
<a href="https://en.wikipedia.org/wiki/Copyleft">#</a>
<a href="https://github.com/LemmyNet/lemmy/blob/master/LICENSE">
#
</a>
</T>
</p>
</div>
</div>
<div class="col-4">
<div>
<header class="is-center">
<img
height={180}
src="/static/assets/images/code_pic.webp"
/>
</header>
<br />
<h4 class="text-center">{i18n.t("blazing_fast")}</h4>
<p>
<T i18nKey="blazing_fast_desc">
#<a href="https://www.rust-lang.org">#</a>
<a href="https://actix.rs/">#</a>
<a href="http://diesel.rs/">#</a>
<a href="https://infernojs.org">#</a>
<a href="https://www.typescriptlang.org/">#</a>
</T>
</p>
</div>
</div>
<div class="col-4">
<div>
<header class="is-center">
<img
height={180}
src="/static/assets/images/mod_pic.webp"
/>
</header>
<br />
<h4 class="text-center">{i18n.t("mod_tools")}</h4>
<p>{i18n.t("mod_tools_desc")}</p>
</div>
</div>
</div>
</div>
<br />
<br />
<div class="container">
<div class="text-center">
<h2>{i18n.t("create_discussion_platform")}</h2>
<p>
<T i18nKey="create_discussion_platform_desc">
#<a href={`/docs/administration/administration.html`}>#</a>
<i>#</i>
<a href="https://en.wikipedia.org/wiki/Fediverse">#</a>
</T>
</p>
<p>{this.runServer()}</p>
</div>
</div>
<br />
</div>
<br />
<div class="container">
<div class="row">
<div class="col-6">
<h4>{i18n.t("live_updates")}</h4>
<p>{i18n.t("live_updates_desc")}</p>
</div>
<div class="col-6 is-center">
<video height={325} autoPlay loop>
<source src="/static/assets/images/reply_vid.webm" />
</video>
</div>
</div>
</div>
<br />
<br />
<div class="container">
<div class="row">
<div class="col-6 is-center">
<img height={325} src="/static/assets/images/mobile_pic.webp" />
</div>
<div class="col-6">
<h4 class="is-marginless">{i18n.t("more_features")}</h4>
<ul class="is-marginless">
<li>
<T i18nKey="self_hostable">
#<a href={`/docs/administration/install_docker.html`}>#</a>
<a href={`/docs/administration/install_ansible.html`}>#</a>
</T>
</li>
<li>{i18n.t("clean_interface")}</li>
<li>
<Link to="/apps">
{i18n.t("mobile_apps_for_ios_and_android")}
</Link>
</li>
<li>{i18n.t("avatar_support")}</li>
<li>
<T i18nKey="full_vote_scores">
#<code>#</code>#
</T>
</li>
<li>{i18n.t("themes_including")}</li>
<li>
<T i18nKey="emojis_autocomplete">
#<code>#</code>
</T>
</li>
<li>
<T i18nKey="user_tagging">
#<code>#</code>
<code>#</code>
</T>
</li>
<li>{i18n.t("integrated_image_uploading")}</li>
<li>{i18n.t("notifications_including")}</li>
<li>
<T i18nKey="i18n_support">
#
<a href="https://weblate.join-lemmy.org/projects/lemmy/lemmy/">
#
</a>
</T>
</li>
<li>
<T i18nKey="rss_feeds">
#<code>#</code>
<code>#</code>
<code>#</code>
<code>#</code>
<code>#</code>
</T>
</li>
<li>{i18n.t("can_fully_erase")}</li>
<li>{i18n.t("nsfw_support")}</li>
</ul>
</div>
</div>
</div>
<br />
<div class="bg-success">
<br />
<div class="container">
<div class="text-center">
<h2>
<Link to="/donate">{i18n.t("support_donate")}</Link>
</h2>
<DonateLines />
</div>
</div>
<br />
<CarouselBlock />
<div className="container mx-auto px-4">
<FeatureCardsBlock />
<DiscussionPlatformBlock />
<MoreFeaturesBlock />
<DonateBlock />
<BottomSpacer />
</div>
</div>
);

View File

@ -1,69 +1,126 @@
import { Component, ChangeEvent, linkEvent } from "inferno";
import { ChangeEvent, linkEvent } from "inferno";
import { Link } from "inferno-router";
import { LinkLine } from "./link-line";
import { Icon } from "./icon";
import { i18n, languages } from "../i18next";
import { Icon, IconSize } from "./icon";
import { i18n, LANGUAGES } from "../i18next";
import classNames from "classnames";
import { SELECT_CLASSES } from "./common";
export class Navbar extends Component<any, any> {
constructor(props: any, context: any) {
super(props, context);
}
const NavLink = ({ content }) => <li className="text-gray-400">{content}</li>;
handleLanguageChange(_: any, event: ChangeEvent<HTMLSelectElement>) {
location.href = `/?lang=${event.target.value}`;
}
const NavLinks = () => (
<>
<NavLink
content={
<Link onClick={closeNavbarDropdown} to="/instances">
{i18n.t("join")}
</Link>
}
/>
<NavLink
content={
<Link onClick={closeNavbarDropdown} to="/news">
{i18n.t("news")}
</Link>
}
/>
<NavLink
content={
<Link onClick={closeNavbarDropdown} to="/apps">
{i18n.t("apps")}
</Link>
}
/>
<NavLink
content={
<Link onClick={closeNavbarDropdown} to="/donate">
{i18n.t("donate")}
</Link>
}
/>
<NavLink
content={
<a onClick={closeNavbarDropdown} href="/docs/index.html">
{i18n.t("docs")}
</a>
}
/>
<NavLink
content={
<Link onClick={closeNavbarDropdown} to="/contact">
{i18n.t("contact")}
</Link>
}
/>
</>
);
languageList() {
return Object.keys(i18n.services.resourceStore.data).sort();
}
render() {
return (
<>
<nav class="nav hide-xs hide-sm">
<div class="nav-left">
<Link className="brand" to="/">
<img
src="/static/assets/images/lemmy.svg"
height="32"
width="32"
/>
</Link>
<LinkLine />
</div>
<div class="nav-right">
<div>
<select
onChange={linkEvent(this, this.handleLanguageChange)}
class="text-light bd-dark language-selector"
>
{this.languageList().map((language, i) => (
<option
key={i}
value={language}
selected={i18n.language.startsWith(language)}
>
{languages.find(l => l.code.startsWith(language)).name}
</option>
))}
</select>
</div>
<a href="https://github.com/LemmyNet">
<Icon icon="github" />
</a>
<a href="https://matrix.to/#/#lemmy:matrix.org">
<Icon icon="matrix" />
</a>
</div>
</nav>
<nav class="nav hide-md hide-lg">
<div class="nav-center">
<Link className="brand" to="/">
<img src="/static/assets/images/lemmy.svg" />
</Link>
</div>
</nav>
</>
);
}
function closeNavbarDropdown() {
(document.activeElement as any).blur();
}
function handleLanguageChange(_: any, event: ChangeEvent<HTMLSelectElement>) {
location.href = `/?lang=${event.target.value}`;
}
export const Footer = () => <Navbar footer />;
export const Navbar = ({ footer = false }) => (
<div
className={classNames("navbar px-2", {
"sticky top-[100vh]": footer,
})}
>
<div className="navbar-start">
<Link className="btn btn-ghost normal-case text-xl" to="/">
<img src="/static/assets/images/lemmy.svg" className="h-12 w-12" />
</Link>
</div>
<div className="navbar-center hidden lg:flex">
<ul className="menu menu-horizontal px-1">
<NavLinks />
</ul>
</div>
<div className="navbar-end">
{footer ? (
<a
className="text-sm text-gray-600 max-md:hidden text-right"
href="https://github.com/LemmyNet/lemmy/blob/main/LICENSE"
>
{i18n.t("copyright_line")}
</a>
) : (
<>
<select
onChange={linkEvent(this, handleLanguageChange)}
className={SELECT_CLASSES}
>
{LANGUAGES.map((l, i) => (
<option
key={i}
value={l.code}
selected={i18n.language.startsWith(l.code)}
>
{l.name}
</option>
))}
</select>
</>
)}
<div
className={classNames("dropdown dropdown-end", {
"dropdown-top": footer,
})}
>
<label tabIndex={0} className="btn btn-ghost lg:hidden">
<Icon icon="align-right" size={IconSize.Large} />
</label>
<ul
tabIndex={0}
className="menu menu-sm dropdown-content z-[1] p-2 shadow bg-neutral-800 rounded-box w-52 items-center mt-3 "
>
<NavLinks />
</ul>
</div>
</div>
</div>
);

View File

@ -1,10 +1,8 @@
import { Component } from "inferno";
import { Helmet } from "inferno-helmet";
import { i18n } from "../i18next";
import { news_md } from "../translations/news";
import { isBrowser, mdToHtml } from "../utils";
const title = i18n.t("news");
import { BottomSpacer } from "./common";
export class NewsItem extends Component<any, any> {
constructor(props: any, context: any) {
@ -17,21 +15,28 @@ export class NewsItem extends Component<any, any> {
}
}
get markdown(): string {
get title(): string {
let title = decodeURIComponent(this.props.match.params.title);
title = title.replace(/_/g, " ");
return news_md.find(v => v.title == title).markdown;
return title;
}
get markdown(): string {
return news_md.find(v => v.title == this.title)?.markdown ?? "";
}
render() {
return (
<div>
<Helmet title={title}>
<meta property={"title"} content={title} />
<div className="container mx-auto px-4">
<Helmet title={this.title}>
<meta property={"title"} content={this.title} />
</Helmet>
<div class="container">
<div dangerouslySetInnerHTML={mdToHtml(this.markdown)} />
<div className="flex flex-col items-center pt-16">
<article className="prose prose-a:text-primary prose-h1:text-primary">
<div dangerouslySetInnerHTML={mdToHtml(this.markdown)} />
</article>
</div>
<BottomSpacer />
</div>
);
}

View File

@ -2,11 +2,86 @@ import { Component } from "inferno";
import { Link } from "inferno-router";
import { Helmet } from "inferno-helmet";
import { i18n } from "../i18next";
import { isBrowser } from "../utils";
import { isBrowser, mdToHtml } from "../utils";
import { news_md } from "../translations/news";
import { BottomSpacer, TEXT_GRADIENT } from "./common";
const title = i18n.t("news");
const newsReversed = news_md.reverse();
const news_reversed = news_md.reverse();
interface NewsInfo {
title: string;
dateStr: string;
preview: string;
url: string;
}
function buildNewsInfoArray(): Array<NewsInfo> {
return news_reversed.map(n => {
let split = n.title.split(" - ");
return {
dateStr: split[0],
title: split[1],
preview: split[2] || previewMarkdown(n.markdown),
url: `news/${titleToUrl(n.title)}`,
};
});
}
function titleToUrl(title: string): string {
return title.replace(/ /g, "_");
}
function previewMarkdown(markdown: string): string {
return markdown
.replace(/#/g, "")
.split(/\n/g)
.slice(3)
.join("\n")
.replace(/[\n\r]/g, " ")
.slice(0, 100)
.concat("...");
}
const TitleBlock = () => (
<div className="pt-16 text-center text-4xl font-bold mb-8">{title}</div>
);
const NewsCards = () => buildNewsInfoArray().map(n => <NewsCard news={n} />);
interface NewsProps {
news: NewsInfo;
}
const NewsCard = ({ news }: NewsProps) => (
<div className="card card-bordered bg-neutral-900 shadow-xl mb-3">
<div className="card-body">
<div className="grid md:grid-cols-12 grid-cols-1 gap-4">
<div className="md:col-span-10">
<div className="md:flex md:flex-row md:items-baseline md:space-x-3">
<Link to={news.url} className={`text-2xl ${TEXT_GRADIENT}`}>
{news.title}
</Link>
<div className="text-sm text-gray-500">{news.dateStr}</div>
</div>
{news.preview && (
<div
className="text-sm text-gray-300"
dangerouslySetInnerHTML={mdToHtml(news.preview)}
/>
)}
</div>
<Link
to={news.url}
className="md:col-span-2 btn btn-secondary normal-case"
>
{i18n.t("read_more")}
</Link>
</div>
</div>
</div>
);
export class News extends Component<any, any> {
constructor(props: any, context: any) {
@ -21,25 +96,14 @@ export class News extends Component<any, any> {
render() {
return (
<div>
<div className="container mx-auto px-4">
<Helmet title={title}>
<meta property={"title"} content={title} />
</Helmet>
<div class="container">
<h1>{title}</h1>
<ul>
{newsReversed.map(v => (
<li>
<Link to={`news/${titleToUrl(v.title)}`}>{v.title}</Link>
</li>
))}
</ul>
</div>
<TitleBlock />
<NewsCards />
<BottomSpacer />
</div>
);
}
}
function titleToUrl(title: string): string {
return title.replace(/ /g, "_");
}

View File

@ -7,8 +7,8 @@ export class NoMatch extends Component<any, any> {
render() {
return (
<div class="container">
<h1>404</h1>
<div className="container mx-auto px-4">
<div className="pt-16 text-center text-4xl font-bold mb-8">404</div>
</div>
);
}

View File

@ -1,2 +0,0 @@
@import "../../../node_modules/chota/dist/chota.min.css";
@import "../../assets/css/main.css";

View File

@ -20,6 +20,114 @@ export class Symbols extends Component<any, any> {
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<symbol id="icon-qr_code" viewBox="0 0 24 24">
<path d="M3 11.016h8.016v-8.016h-8.016v8.016zM5.016 5.016h3.984v3.984h-3.984v-3.984zM3 21h8.016v-8.016h-8.016v8.016zM5.016 15h3.984v3.984h-3.984v-3.984zM12.984 3v8.016h8.016v-8.016h-8.016zM18.984 9h-3.984v-3.984h3.984v3.984zM18.984 18.984h2.016v2.016h-2.016v-2.016zM12.984 12.984h2.016v2.016h-2.016v-2.016zM15 15h2.016v2.016h-2.016v-2.016zM12.984 17.016h2.016v1.969h-2.016v-1.969zM15 18.984h2.016v2.016h-2.016v-2.016zM17.016 17.016h1.969v1.969h-1.969v-1.969zM17.016 12.984h1.969v2.016h-1.969v-2.016zM18.984 15h2.016v2.016h-2.016v-2.016z"></path>
</symbol>
<symbol id="icon-home" viewBox="0 0 24 24">
<path d="M2.386 8.211c-0.236 0.184-0.386 0.469-0.386 0.789v11c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h14c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-11c-0.001-0.3-0.134-0.593-0.386-0.789l-9-7c-0.358-0.275-0.861-0.285-1.228 0zM16 21v-9c0-0.552-0.448-1-1-1h-6c-0.552 0-1 0.448-1 1v9h-3c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-10.511l8-6.222 8 6.222v10.511c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293zM10 21v-8h4v8z"></path>
</symbol>
<symbol id="icon-hammer2" viewBox="0 0 32 32">
<path d="M31.568 28.617l-17.145-15.608 0.798-0.8c0.653-0.655 1.006-1.501 1.060-2.363 0.031-0.014 0.063-0.028 0.092-0.045l3.218-2.013c0.435-0.512 0.404-1.321-0.071-1.797l-5.598-5.613c-0.475-0.476-1.281-0.508-1.792-0.071l-2.007 3.227c-0.016 0.030-0.031 0.061-0.045 0.093-0.859 0.054-1.703 0.408-2.356 1.063l-3.045 3.053c-0.653 0.655-1.006 1.501-1.060 2.363-0.031 0.014-0.063 0.029-0.093 0.045l-3.218 2.013c-0.436 0.512-0.404 1.321 0.071 1.797l5.598 5.613c0.475 0.476 1.281 0.508 1.792 0.071l2.007-3.227c0.017-0.030 0.031-0.061 0.045-0.093 0.859-0.054 1.703-0.408 2.356-1.063l0.884-0.887 15.566 17.191c0.451 0.498 1.147 0.579 1.546 0.178l1.574-1.578c0.399-0.4 0.319-1.098-0.178-1.55z"></path>
</symbol>
<symbol id="icon-david-star" viewBox="0 0 32 32">
<path d="M16 6.96l1.873 3.040h-3.745l1.872-3.040zM24.799 21.264h-3.725l1.853-3.018 1.872 3.018zM7.202 21.264l1.872-3.018 1.853 3.018h-3.725zM11.994 23l4.006 6.5 4.007-6.5h7.993l-4.006-6.49 4.006-6.51h-7.993l-4.007-6.5-4.006 6.5h-7.994l4.007 6.51-4.007 6.49h7.994zM24.799 11.736l-1.872 3.035-1.853-3.035h3.725zM16 26.039l-1.872-3.039h3.745l-1.873 3.039zM10.141 16.51l2.92-4.774h5.879l2.921 4.774-2.939 4.754h-5.86l-2.921-4.754zM7.202 11.736h3.725l-1.853 3.035-1.872-3.035z"></path>
</symbol>
<symbol id="icon-videogame_asset" viewBox="0 0 24 24">
<path d="M19.5 12q0.656 0 1.078-0.422t0.422-1.078-0.422-1.078-1.078-0.422-1.078 0.422-0.422 1.078 0.422 1.078 1.078 0.422zM15.516 15q0.609 0 1.055-0.422t0.445-1.078-0.445-1.078-1.055-0.422-1.055 0.422-0.445 1.078 0.445 1.078 1.055 0.422zM11.016 12.984v-1.969h-3v-3h-2.016v3h-3v1.969h3v3h2.016v-3h3zM21 6q0.797 0 1.406 0.609t0.609 1.406v7.969q0 0.797-0.609 1.406t-1.406 0.609h-18q-0.797 0-1.406-0.609t-0.609-1.406v-7.969q0-0.797 0.609-1.406t1.406-0.609h18z"></path>
</symbol>
<symbol id="icon-edit" viewBox="0 0 24 24">
<path d="M11 3h-7c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h14c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-7c0-0.552-0.448-1-1-1s-1 0.448-1 1v7c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-14c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-14c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h7c0.552 0 1-0.448 1-1s-0.448-1-1-1zM17.793 1.793l-9.5 9.5c-0.122 0.121-0.217 0.28-0.263 0.465l-1 4c-0.039 0.15-0.042 0.318 0 0.485 0.134 0.536 0.677 0.862 1.213 0.728l4-1c0.167-0.041 0.33-0.129 0.465-0.263l9.5-9.5c0.609-0.609 0.914-1.41 0.914-2.207s-0.305-1.598-0.914-2.207-1.411-0.915-2.208-0.915-1.598 0.305-2.207 0.914zM19.207 3.207c0.219-0.219 0.504-0.328 0.793-0.328s0.574 0.109 0.793 0.328 0.328 0.504 0.328 0.793-0.109 0.574-0.328 0.793l-9.304 9.304-2.114 0.529 0.529-2.114z"></path>
</symbol>
<symbol id="icon-box" viewBox="0 0 24 24">
<path d="M18.961 6.828l-6.961 4.027-6.961-4.027 6.456-3.689c0.112-0.064 0.232-0.105 0.355-0.124 0.218-0.034 0.445 0.003 0.654 0.124zM11.526 22.961c0.141 0.076 0.303 0.119 0.474 0.119 0.173 0 0.336-0.044 0.478-0.121 0.356-0.058 0.701-0.18 1.017-0.36l7.001-4.001c0.618-0.357 1.060-0.897 1.299-1.514 0.133-0.342 0.202-0.707 0.205-1.084v-8c0-0.478-0.113-0.931-0.314-1.334-0.022-0.071-0.052-0.14-0.091-0.207-0.046-0.079-0.1-0.149-0.162-0.21-0.031-0.043-0.064-0.086-0.097-0.127-0.23-0.286-0.512-0.528-0.831-0.715l-7.009-4.005c-0.61-0.352-1.3-0.465-1.954-0.364-0.363 0.057-0.715 0.179-1.037 0.363l-7.001 4.001c-0.383 0.221-0.699 0.513-0.941 0.85-0.060 0.060-0.114 0.13-0.159 0.207-0.039 0.068-0.070 0.138-0.092 0.21-0.040 0.080-0.076 0.163-0.108 0.246-0.132 0.343-0.201 0.708-0.204 1.078v8.007c0.001 0.71 0.248 1.363 0.664 1.878 0.23 0.286 0.512 0.528 0.831 0.715l7.009 4.005c0.324 0.187 0.67 0.307 1.022 0.362zM11 12.587v7.991l-6.495-3.711c-0.111-0.065-0.207-0.148-0.285-0.245-0.139-0.172-0.22-0.386-0.22-0.622v-7.462zM13 20.578v-7.991l7-4.049v7.462c-0.001 0.121-0.025 0.246-0.070 0.362-0.080 0.206-0.225 0.384-0.426 0.5z"></path>
</symbol>
<symbol id="icon-folder" viewBox="0 0 24 24">
<path d="M23 19v-11c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879h-8.465l-1.703-2.555c-0.182-0.27-0.486-0.445-0.832-0.445h-5c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h16c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121zM21 19c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-16c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-14c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h4.465l1.703 2.555c0.192 0.287 0.506 0.443 0.832 0.445h9c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707z"></path>
</symbol>
<symbol id="icon-book" viewBox="0 0 24 24">
<path d="M6.5 1c-0.966 0-1.843 0.393-2.475 1.025s-1.025 1.509-1.025 2.475v15c0 0.966 0.393 1.843 1.025 2.475s1.509 1.025 2.475 1.025h13.5c0.552 0 1-0.448 1-1v-20c0-0.552-0.448-1-1-1zM19 18v3h-12.5c-0.414 0-0.788-0.167-1.061-0.439s-0.439-0.647-0.439-1.061 0.167-0.788 0.439-1.061 0.647-0.439 1.061-0.439zM6.5 3h12.5v13h-12.5c-0.537 0-1.045 0.121-1.5 0.337v-11.837c0-0.414 0.167-0.788 0.439-1.061s0.647-0.439 1.061-0.439z"></path>
</symbol>
<symbol id="icon-music" viewBox="0 0 24 24">
<path d="M8 18c0 0.553-0.223 1.051-0.586 1.414s-0.861 0.586-1.414 0.586-1.051-0.223-1.414-0.586-0.586-0.861-0.586-1.414 0.223-1.051 0.586-1.414 0.861-0.586 1.414-0.586 1.051 0.223 1.414 0.586 0.586 0.861 0.586 1.414zM22 16v-13c0-0.050-0.004-0.107-0.014-0.164-0.091-0.545-0.606-0.913-1.151-0.822l-12 2c-0.476 0.081-0.835 0.492-0.835 0.986v9.535c-0.588-0.34-1.272-0.535-2-0.535-1.104 0-2.106 0.449-2.828 1.172s-1.172 1.724-1.172 2.828 0.449 2.106 1.172 2.828 1.724 1.172 2.828 1.172 2.106-0.449 2.828-1.172 1.172-1.724 1.172-2.828v-12.153l10-1.667v8.355c-0.588-0.34-1.272-0.535-2-0.535-1.104 0-2.106 0.449-2.828 1.172s-1.172 1.724-1.172 2.828 0.449 2.106 1.172 2.828 1.724 1.172 2.828 1.172 2.106-0.449 2.828-1.172 1.172-1.724 1.172-2.828zM20 16c0 0.553-0.223 1.051-0.586 1.414s-0.861 0.586-1.414 0.586-1.051-0.223-1.414-0.586-0.586-0.861-0.586-1.414 0.223-1.051 0.586-1.414 0.861-0.586 1.414-0.586 1.051 0.223 1.414 0.586 0.586 0.861 0.586 1.414z"></path>
</symbol>
<symbol id="icon-image" viewBox="0 0 24 24">
<path d="M5 2c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h14c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-14c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879zM11 8.5c0-0.69-0.281-1.316-0.732-1.768s-1.078-0.732-1.768-0.732-1.316 0.281-1.768 0.732-0.732 1.078-0.732 1.768 0.281 1.316 0.732 1.768 1.078 0.732 1.768 0.732 1.316-0.281 1.768-0.732 0.732-1.078 0.732-1.768zM9 8.5c0 0.138-0.055 0.262-0.146 0.354s-0.216 0.146-0.354 0.146-0.262-0.055-0.354-0.146-0.146-0.216-0.146-0.354 0.055-0.262 0.146-0.354 0.216-0.146 0.354-0.146 0.262 0.055 0.354 0.146 0.146 0.216 0.146 0.354zM7.414 20l8.586-8.586 4 4v3.586c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293zM20 12.586l-3.293-3.293c-0.391-0.391-1.024-0.391-1.414 0l-10.644 10.644c-0.135-0.050-0.255-0.129-0.356-0.23-0.182-0.182-0.293-0.431-0.293-0.707v-14c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h14c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707z"></path>
</symbol>
<symbol id="icon-smartphone" viewBox="0 0 24 24">
<path d="M7 1c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v16c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h10c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-16c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879zM7 3h10c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v16c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-10c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-16c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293zM12 19c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1z"></path>
</symbol>
<symbol id="icon-futbol-o" viewBox="0 0 28 28">
<path d="M9.516 12.75l4.484-3.25 4.484 3.25-1.703 5.25h-5.547zM14 0c7.734 0 14 6.266 14 14s-6.266 14-14 14-14-6.266-14-14 6.266-14 14-14zM23.672 21.094c1.469-2 2.328-4.438 2.328-7.094v-0.047l-1.594 1.391-3.75-3.5 0.984-5.047 2.094 0.187c-1.484-2.047-3.609-3.625-6.078-4.406l0.828 1.937-4.484 2.484-4.484-2.484 0.828-1.937c-2.469 0.781-4.594 2.359-6.078 4.406l2.109-0.187 0.969 5.047-3.75 3.5-1.594-1.391v0.047c0 2.656 0.859 5.094 2.328 7.094l0.469-2.063 5.094 0.625 2.172 4.656-1.813 1.078c1.172 0.391 2.438 0.609 3.75 0.609s2.578-0.219 3.75-0.609l-1.813-1.078 2.172-4.656 5.094-0.625z"></path>
</symbol>
<symbol id="icon-transgender-alt" viewBox="0 0 26 28">
<path d="M20 0.5c0-0.281 0.219-0.5 0.5-0.5h4.5c0.547 0 1 0.453 1 1v4.5c0 0.281-0.219 0.5-0.5 0.5h-1c-0.281 0-0.5-0.219-0.5-0.5v-2.094l-3.969 3.984c1.219 1.531 1.969 3.484 1.969 5.609 0 4.625-3.5 8.437-8 8.937v2.063h1.5c0.281 0 0.5 0.219 0.5 0.5v1c0 0.281-0.219 0.5-0.5 0.5h-1.5v1.5c0 0.281-0.219 0.5-0.5 0.5h-1c-0.281 0-0.5-0.219-0.5-0.5v-1.5h-1.5c-0.281 0-0.5-0.219-0.5-0.5v-1c0-0.281 0.219-0.5 0.5-0.5h1.5v-2.063c-4.5-0.5-8-4.312-8-8.937 0-2.125 0.75-4.078 1.969-5.609l-0.812-0.828-1.578 1.734c-0.187 0.203-0.5 0.219-0.703 0.047l-0.75-0.688c-0.203-0.172-0.219-0.5-0.031-0.703l1.641-1.797-1.734-1.75v2.094c0 0.281-0.219 0.5-0.5 0.5h-1c-0.281 0-0.5-0.219-0.5-0.5v-4.5c0-0.547 0.453-1 1-1h4.5c0.281 0 0.5 0.219 0.5 0.5v1c0 0.281-0.219 0.5-0.5 0.5h-2.078l1.656 1.672 1.344-1.469c0.187-0.203 0.5-0.219 0.703-0.047l0.75 0.688c0.203 0.172 0.219 0.5 0.031 0.703l-1.406 1.547 0.891 0.875c1.531-1.219 3.484-1.969 5.609-1.969s4.078 0.75 5.609 1.969l3.984-3.969h-2.094c-0.281 0-0.5-0.219-0.5-0.5v-1zM13 20c3.859 0 7-3.141 7-7s-3.141-7-7-7-7 3.141-7 7 3.141 7 7 7z"></path>
</symbol>
<symbol id="icon-eye-off" viewBox="0 0 24 24">
<path d="M10.128 5.214c0.651-0.152 1.296-0.221 1.86-0.214 1.758 0 3.309 0.559 4.658 1.393 1.119 0.692 2.089 1.567 2.894 2.448 0.644 0.705 1.175 1.405 1.585 2.001 0.327 0.475 0.57 0.874 0.733 1.155-0.546 0.953-1.16 1.821-1.778 2.542-0.359 0.419-0.311 1.051 0.108 1.41s1.051 0.311 1.41-0.108c0.818-0.954 1.611-2.112 2.283-3.37 0.148-0.279 0.163-0.618 0.013-0.919 0 0-0.396-0.789-1.12-1.843-0.451-0.656-1.038-1.432-1.757-2.218-0.894-0.979-2.004-1.987-3.319-2.8-1.595-0.985-3.506-1.691-5.686-1.691-0.734-0.009-1.54 0.079-2.34 0.266-0.538 0.126-0.872 0.664-0.746 1.202s0.664 0.872 1.202 0.746zM10.027 11.442l2.531 2.531c-0.182 0.061-0.372 0.094-0.563 0.101-0.513 0.018-1.030-0.159-1.434-0.536s-0.617-0.88-0.635-1.393c-0.008-0.238 0.025-0.476 0.101-0.704zM5.983 7.397l2.553 2.553c-0.434 0.691-0.636 1.484-0.608 2.266 0.036 1.022 0.463 2.033 1.271 2.785s1.846 1.107 2.868 1.071c0.692-0.024 1.379-0.228 1.984-0.608l2.322 2.322c-1.378 0.799-2.895 1.196-4.384 1.214-1.734 0-3.285-0.559-4.634-1.393-1.119-0.692-2.089-1.567-2.894-2.448-0.644-0.705-1.175-1.405-1.585-2.001-0.326-0.475-0.57-0.873-0.732-1.154 1.050-1.822 2.376-3.379 3.841-4.607zM0.293 1.707l4.271 4.271c-1.731 1.479-3.269 3.358-4.445 5.549-0.148 0.279-0.164 0.619-0.013 0.92 0 0 0.396 0.789 1.12 1.843 0.451 0.656 1.038 1.432 1.757 2.218 0.894 0.979 2.004 1.987 3.319 2.8 1.595 0.986 3.506 1.692 5.71 1.692 1.993-0.024 4.019-0.601 5.815-1.759l4.466 4.466c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-8.876-8.876c-0.002-0.002-0.005-0.005-0.007-0.007l-4.209-4.21c-0.008-0.007-0.016-0.016-0.024-0.024l-8.884-8.883c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z"></path>
</symbol>
<symbol id="icon-alert-octagon" viewBox="0 0 24 24">
<path d="M7.86 1c-0.256 0-0.512 0.098-0.707 0.293l-5.86 5.86c-0.181 0.181-0.293 0.431-0.293 0.707v8.28c0 0.256 0.098 0.512 0.293 0.707l5.86 5.86c0.181 0.181 0.431 0.293 0.707 0.293h8.28c0.256 0 0.512-0.098 0.707-0.293l5.86-5.86c0.181-0.181 0.293-0.431 0.293-0.707v-8.28c0-0.256-0.098-0.512-0.293-0.707l-5.86-5.86c-0.181-0.181-0.431-0.293-0.707-0.293zM8.274 3h7.452l5.274 5.274v7.452l-5.274 5.274h-7.452l-5.274-5.274v-7.452zM11 8v4c0 0.552 0.448 1 1 1s1-0.448 1-1v-4c0-0.552-0.448-1-1-1s-1 0.448-1 1zM12 17c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1z"></path>
</symbol>
<symbol id="icon-trash" viewBox="0 0 24 24">
<path d="M18 7v13c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-10c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-13zM17 5v-1c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879h-4c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v1h-4c-0.552 0-1 0.448-1 1s0.448 1 1 1h1v13c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h10c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-13h1c0.552 0 1-0.448 1-1s-0.448-1-1-1zM9 5v-1c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h4c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v1z"></path>
</symbol>
<symbol id="icon-rss" viewBox="0 0 24 24">
<path d="M4 12c2.209 0 4.208 0.894 5.657 2.343s2.343 3.448 2.343 5.657c0 0.552 0.448 1 1 1s1-0.448 1-1c0-2.761-1.12-5.263-2.929-7.071s-4.31-2.929-7.071-2.929c-0.552 0-1 0.448-1 1s0.448 1 1 1zM4 5c4.142 0 7.891 1.678 10.607 4.393s4.393 6.465 4.393 10.607c0 0.552 0.448 1 1 1s1-0.448 1-1c0-4.694-1.904-8.946-4.979-12.021s-7.327-4.979-12.021-4.979c-0.552 0-1 0.448-1 1s0.448 1 1 1zM7 19c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586-1.053 0.225-1.414 0.586-0.586 0.862-0.586 1.414 0.225 1.053 0.586 1.414 0.862 0.586 1.414 0.586 1.053-0.225 1.414-0.586 0.586-0.862 0.586-1.414z"></path>
</symbol>
<symbol id="icon-globe" viewBox="0 0 24 24">
<path d="M16.951 11c-0.214-2.69-1.102-5.353-2.674-7.71 1.57 0.409 2.973 1.232 4.087 2.346 1.408 1.408 2.351 3.278 2.581 5.364zM14.279 20.709c1.483-2.226 2.437-4.853 2.669-7.709h3.997c-0.23 2.086-1.173 3.956-2.581 5.364-1.113 1.113-2.516 1.936-4.085 2.345zM7.049 13c0.214 2.69 1.102 5.353 2.674 7.71-1.57-0.409-2.973-1.232-4.087-2.346-1.408-1.408-2.351-3.278-2.581-5.364zM9.721 3.291c-1.482 2.226-2.436 4.853-2.669 7.709h-3.997c0.23-2.086 1.173-3.956 2.581-5.364 1.114-1.113 2.516-1.936 4.085-2.345zM12.004 1c0 0 0 0 0 0-3.044 0.001-5.794 1.233-7.782 3.222-1.99 1.989-3.222 4.741-3.222 7.778s1.232 5.789 3.222 7.778c1.988 1.989 4.738 3.221 7.774 3.222 0 0 0 0 0 0 3.044-0.001 5.793-1.233 7.782-3.222 1.99-1.989 3.222-4.741 3.222-7.778s-1.232-5.789-3.222-7.778c-1.988-1.989-4.738-3.221-7.774-3.222zM14.946 13c-0.252 2.788-1.316 5.36-2.945 7.451-1.729-2.221-2.706-4.818-2.945-7.451zM11.999 3.549c1.729 2.221 2.706 4.818 2.945 7.451h-5.89c0.252-2.788 1.316-5.36 2.945-7.451z"></path>
</symbol>
<symbol id="icon-bell" viewBox="0 0 24 24">
<path d="M17 8c0 4.011 0.947 6.52 1.851 8h-13.702c0.904-1.48 1.851-3.989 1.851-8 0-1.381 0.559-2.63 1.464-3.536s2.155-1.464 3.536-1.464 2.63 0.559 3.536 1.464 1.464 2.155 1.464 3.536zM19 8c0-1.933-0.785-3.684-2.050-4.95s-3.017-2.050-4.95-2.050-3.684 0.785-4.95 2.050-2.050 3.017-2.050 4.95c0 6.127-2.393 8.047-2.563 8.174-0.453 0.308-0.573 0.924-0.269 1.381 0.192 0.287 0.506 0.443 0.832 0.445h18c0.552 0 1-0.448 1-1 0-0.339-0.168-0.638-0.429-0.821-0.176-0.13-2.571-2.050-2.571-8.179zM12.865 20.498c-0.139 0.239-0.359 0.399-0.608 0.465s-0.52 0.037-0.759-0.101c-0.162-0.094-0.283-0.222-0.359-0.357-0.274-0.48-0.884-0.647-1.364-0.373s-0.647 0.884-0.373 1.364c0.25 0.439 0.623 0.823 1.093 1.096 0.716 0.416 1.535 0.501 2.276 0.304s1.409-0.678 1.824-1.394c0.277-0.478 0.114-1.090-0.363-1.367s-1.090-0.114-1.367 0.363z"></path>
</symbol>
<symbol id="icon-image" viewBox="0 0 24 24">
<path d="M5 2c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h14c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-14c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879zM11 8.5c0-0.69-0.281-1.316-0.732-1.768s-1.078-0.732-1.768-0.732-1.316 0.281-1.768 0.732-0.732 1.078-0.732 1.768 0.281 1.316 0.732 1.768 1.078 0.732 1.768 0.732 1.316-0.281 1.768-0.732 0.732-1.078 0.732-1.768zM9 8.5c0 0.138-0.055 0.262-0.146 0.354s-0.216 0.146-0.354 0.146-0.262-0.055-0.354-0.146-0.146-0.216-0.146-0.354 0.055-0.262 0.146-0.354 0.216-0.146 0.354-0.146 0.262 0.055 0.354 0.146 0.146 0.216 0.146 0.354zM7.414 20l8.586-8.586 4 4v3.586c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293zM20 12.586l-3.293-3.293c-0.391-0.391-1.024-0.391-1.414 0l-10.644 10.644c-0.135-0.050-0.255-0.129-0.356-0.23-0.182-0.182-0.293-0.431-0.293-0.707v-14c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h14c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707z"></path>
</symbol>
<symbol id="icon-moon" viewBox="0 0 24 24">
<path d="M21.996 12.882c0.022-0.233-0.038-0.476-0.188-0.681-0.325-0.446-0.951-0.544-1.397-0.219-0.95 0.693-2.060 1.086-3.188 1.162-1.368 0.092-2.765-0.283-3.95-1.158-1.333-0.985-2.139-2.415-2.367-3.935s0.124-3.124 1.109-4.456c0.142-0.191 0.216-0.435 0.191-0.691-0.053-0.55-0.542-0.952-1.092-0.898-2.258 0.22-4.314 1.18-5.895 2.651-1.736 1.615-2.902 3.847-3.137 6.386-0.254 2.749 0.631 5.343 2.266 7.311s4.022 3.313 6.772 3.567 5.343-0.631 7.311-2.266 3.313-4.022 3.567-6.772zM19.567 14.674c-0.49 1.363-1.335 2.543-2.416 3.441-1.576 1.309-3.648 2.016-5.848 1.813s-4.108-1.278-5.417-2.854-2.016-3.648-1.813-5.848c0.187-2.032 1.117-3.814 2.507-5.106 0.782-0.728 1.71-1.3 2.731-1.672-0.456 1.264-0.577 2.606-0.384 3.899 0.303 2.023 1.38 3.934 3.156 5.247 1.578 1.167 3.448 1.668 5.272 1.545 0.752-0.050 1.496-0.207 2.21-0.465z"></path>
</symbol>
<symbol id="icon-sun" viewBox="0 0 24 24">
<path d="M18 12c0-1.657-0.673-3.158-1.757-4.243s-2.586-1.757-4.243-1.757-3.158 0.673-4.243 1.757-1.757 2.586-1.757 4.243 0.673 3.158 1.757 4.243 2.586 1.757 4.243 1.757 3.158-0.673 4.243-1.757 1.757-2.586 1.757-4.243zM16 12c0 1.105-0.447 2.103-1.172 2.828s-1.723 1.172-2.828 1.172-2.103-0.447-2.828-1.172-1.172-1.723-1.172-2.828 0.447-2.103 1.172-2.828 1.723-1.172 2.828-1.172 2.103 0.447 2.828 1.172 1.172 1.723 1.172 2.828zM11 1v2c0 0.552 0.448 1 1 1s1-0.448 1-1v-2c0-0.552-0.448-1-1-1s-1 0.448-1 1zM11 21v2c0 0.552 0.448 1 1 1s1-0.448 1-1v-2c0-0.552-0.448-1-1-1s-1 0.448-1 1zM3.513 4.927l1.42 1.42c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-1.42-1.42c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414zM17.653 19.067l1.42 1.42c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-1.42-1.42c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414zM1 13h2c0.552 0 1-0.448 1-1s-0.448-1-1-1h-2c-0.552 0-1 0.448-1 1s0.448 1 1 1zM21 13h2c0.552 0 1-0.448 1-1s-0.448-1-1-1h-2c-0.552 0-1 0.448-1 1s0.448 1 1 1zM4.927 20.487l1.42-1.42c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-1.42 1.42c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0zM19.067 6.347l1.42-1.42c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-1.42 1.42c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0z"></path>
</symbol>
<symbol id="icon-thumbs-up" viewBox="0 0 24 24">
<path d="M13 9c0 0.552 0.448 1 1 1h5.679c0.065 0.002 0.153 0.011 0.153 0.011 0.273 0.041 0.502 0.188 0.655 0.396s0.225 0.47 0.184 0.742l-1.38 8.998c-0.037 0.239-0.156 0.448-0.325 0.6-0.18 0.161-0.415 0.256-0.686 0.253h-10.28v-9.788l3.608-8.118c0.307 0.098 0.582 0.268 0.806 0.492 0.363 0.363 0.586 0.861 0.586 1.414zM15 8v-3c0-1.104-0.449-2.106-1.172-2.828s-1.724-1.172-2.828-1.172c-0.405 0-0.754 0.241-0.914 0.594l-4 9c-0.060 0.134-0.087 0.275-0.086 0.406v11c0 0.552 0.448 1 1 1h11.28c0.767 0.009 1.482-0.281 2.021-0.763 0.505-0.452 0.857-1.076 0.967-1.783l1.38-9.002c0.125-0.82-0.096-1.614-0.55-2.231s-1.147-1.063-1.965-1.187c-0.165-0.025-0.333-0.037-0.492-0.034zM7 21h-3c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-7c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h3c0.552 0 1-0.448 1-1s-0.448-1-1-1h-3c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v7c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h3c0.552 0 1-0.448 1-1s-0.448-1-1-1z"></path>
</symbol>
<symbol id="icon-thumbs-down" viewBox="0 0 24 24">
<path d="M11 15c0-0.552-0.448-1-1-1h-5.679c-0.065-0.002-0.153-0.011-0.153-0.011-0.273-0.041-0.502-0.188-0.655-0.396s-0.225-0.47-0.184-0.742l1.38-8.998c0.037-0.239 0.156-0.448 0.325-0.6 0.179-0.161 0.415-0.256 0.686-0.253h10.28v9.788l-3.608 8.118c-0.307-0.098-0.582-0.268-0.806-0.492-0.363-0.363-0.586-0.861-0.586-1.414zM9 16v3c0 1.104 0.449 2.106 1.172 2.828s1.724 1.172 2.828 1.172c0.405 0 0.754-0.241 0.914-0.594l4-9c0.060-0.134 0.087-0.275 0.086-0.406v-11c0-0.552-0.448-1-1-1h-11.28c-0.767-0.009-1.482 0.281-2.021 0.763-0.505 0.452-0.857 1.076-0.967 1.783l-1.38 9.002c-0.125 0.82 0.096 1.614 0.55 2.231s1.147 1.063 1.965 1.187c0.165 0.025 0.333 0.037 0.492 0.034zM17 3h2.67c0.361-0.006 0.674 0.119 0.912 0.332 0.213 0.191 0.364 0.45 0.418 0.746v6.787c-0.037 0.34-0.208 0.63-0.455 0.833-0.235 0.194-0.537 0.306-0.861 0.301l-2.684 0.001c-0.552 0-1 0.448-1 1s0.448 1 1 1h2.656c0.81 0.012 1.569-0.27 2.16-0.756 0.622-0.511 1.059-1.251 1.176-2.11 0.005-0.040 0.008-0.087 0.008-0.134v-7c0-0.042-0.003-0.089-0.009-0.137-0.111-0.803-0.505-1.51-1.075-2.020-0.6-0.537-1.397-0.858-2.246-0.842h-2.67c-0.552 0-1 0.448-1 1s0.448 1 1 1z"></path>
</symbol>
<symbol id="icon-smile" viewBox="0 0 24 24">
<path d="M23 12c0-3.037-1.232-5.789-3.222-7.778s-4.741-3.222-7.778-3.222-5.789 1.232-7.778 3.222-3.222 4.741-3.222 7.778 1.232 5.789 3.222 7.778 4.741 3.222 7.778 3.222 5.789-1.232 7.778-3.222 3.222-4.741 3.222-7.778zM21 12c0 2.486-1.006 4.734-2.636 6.364s-3.878 2.636-6.364 2.636-4.734-1.006-6.364-2.636-2.636-3.878-2.636-6.364 1.006-4.734 2.636-6.364 3.878-2.636 6.364-2.636 4.734 1.006 6.364 2.636 2.636 3.878 2.636 6.364zM7.2 14.6c0 0 0.131 0.173 0.331 0.383 0.145 0.153 0.338 0.341 0.577 0.54 0.337 0.281 0.772 0.59 1.297 0.853 0.705 0.352 1.579 0.624 2.595 0.624s1.89-0.272 2.595-0.624c0.525-0.263 0.96-0.572 1.297-0.853 0.239-0.199 0.432-0.387 0.577-0.54 0.2-0.21 0.331-0.383 0.331-0.383 0.331-0.442 0.242-1.069-0.2-1.4s-1.069-0.242-1.4 0.2c-0.041 0.050-0.181 0.206-0.181 0.206-0.1 0.105-0.237 0.239-0.408 0.382-0.243 0.203-0.549 0.419-0.91 0.6-0.48 0.239-1.050 0.412-1.701 0.412s-1.221-0.173-1.701-0.413c-0.36-0.18-0.667-0.397-0.91-0.6-0.171-0.143-0.308-0.277-0.408-0.382-0.14-0.155-0.181-0.205-0.181-0.205-0.331-0.442-0.958-0.531-1.4-0.2s-0.531 0.958-0.2 1.4zM9 10c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1zM15 10c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1z"></path>
</symbol>
<symbol id="icon-android" viewBox="0 0 32 32">
<path d="M28 12c-1.1 0-2 0.9-2 2v8c0 1.1 0.9 2 2 2s2-0.9 2-2v-8c0-1.1-0.9-2-2-2zM4 12c-1.1 0-2 0.9-2 2v8c0 1.1 0.9 2 2 2s2-0.9 2-2v-8c0-1.1-0.9-2-2-2zM7 23c0 1.657 1.343 3 3 3v0 4c0 1.1 0.9 2 2 2s2-0.9 2-2v-4h4v4c0 1.1 0.9 2 2 2s2-0.9 2-2v-4c1.657 0 3-1.343 3-3v-11h-18v11z"></path>
<path d="M24.944 10c-0.304-2.746-1.844-5.119-4.051-6.551l1.001-2.001c0.247-0.494 0.047-1.095-0.447-1.342s-1.095-0.047-1.342 0.447l-1.004 2.009-0.261-0.104c-0.893-0.297-1.848-0.458-2.84-0.458s-1.947 0.161-2.84 0.458l-0.261 0.104-1.004-2.009c-0.247-0.494-0.848-0.694-1.342-0.447s-0.694 0.848-0.447 1.342l1.001 2.001c-2.207 1.433-3.747 3.805-4.051 6.551v1h17.944v-1h-0.056zM13 8c-0.552 0-1-0.448-1-1s0.447-0.999 0.998-1c0.001 0 0.002 0 0.003 0s0.001-0 0.002-0c0.551 0.001 0.998 0.448 0.998 1s-0.448 1-1 1zM19 8c-0.552 0-1-0.448-1-1s0.446-0.999 0.998-1c0 0 0.001 0 0.002 0s0.002-0 0.003-0c0.551 0.001 0.998 0.448 0.998 1s-0.448 1-1 1z"></path>
</symbol>
<symbol id="icon-appleinc" viewBox="0 0 32 32">
<path d="M24.734 17.003c-0.040-4.053 3.305-5.996 3.454-6.093-1.88-2.751-4.808-3.127-5.851-3.171-2.492-0.252-4.862 1.467-6.127 1.467-1.261 0-3.213-1.43-5.28-1.392-2.716 0.040-5.221 1.579-6.619 4.012-2.822 4.897-0.723 12.151 2.028 16.123 1.344 1.944 2.947 4.127 5.051 4.049 2.026-0.081 2.793-1.311 5.242-1.311s3.138 1.311 5.283 1.271c2.18-0.041 3.562-1.981 4.897-3.931 1.543-2.255 2.179-4.439 2.216-4.551-0.048-0.022-4.252-1.632-4.294-6.473zM20.705 5.11c1.117-1.355 1.871-3.235 1.665-5.11-1.609 0.066-3.559 1.072-4.713 2.423-1.036 1.199-1.942 3.113-1.699 4.951 1.796 0.14 3.629-0.913 4.747-2.264z"></path>
</symbol>
<symbol id="icon-clipboard" viewBox="0 0 24 24">
<path d="M7 5c0 0.552 0.225 1.053 0.586 1.414s0.862 0.586 1.414 0.586h6c0.552 0 1.053-0.225 1.414-0.586s0.586-0.862 0.586-1.414h1c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707v14c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-12c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-14c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293zM9 1c-0.552 0-1.053 0.225-1.414 0.586s-0.586 0.862-0.586 1.414h-1c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h12c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-14c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879h-1c0-0.552-0.225-1.053-0.586-1.414s-0.862-0.586-1.414-0.586zM9 3h6v2h-6z"></path>
</symbol>
<symbol id="icon-align-right" viewBox="0 0 24 24">
<path d="M21 9h-14c-0.552 0-1 0.448-1 1s0.448 1 1 1h14c0.552 0 1-0.448 1-1s-0.448-1-1-1zM21 5h-18c-0.552 0-1 0.448-1 1s0.448 1 1 1h18c0.552 0 1-0.448 1-1s-0.448-1-1-1zM21 13h-18c-0.552 0-1 0.448-1 1s0.448 1 1 1h18c0.552 0 1-0.448 1-1s-0.448-1-1-1zM21 17h-14c-0.552 0-1 0.448-1 1s0.448 1 1 1h14c0.552 0 1-0.448 1-1s-0.448-1-1-1z"></path>
</symbol>
<symbol id="icon-user-check" viewBox="0 0 24 24">
<path d="M17 21v-2c0-1.38-0.561-2.632-1.464-3.536s-2.156-1.464-3.536-1.464h-7c-1.38 0-2.632 0.561-3.536 1.464s-1.464 2.156-1.464 3.536v2c0 0.552 0.448 1 1 1s1-0.448 1-1v-2c0-0.829 0.335-1.577 0.879-2.121s1.292-0.879 2.121-0.879h7c0.829 0 1.577 0.335 2.121 0.879s0.879 1.292 0.879 2.121v2c0 0.552 0.448 1 1 1s1-0.448 1-1zM13.5 7c0-1.38-0.561-2.632-1.464-3.536s-2.156-1.464-3.536-1.464-2.632 0.561-3.536 1.464-1.464 2.156-1.464 3.536 0.561 2.632 1.464 3.536 2.156 1.464 3.536 1.464 2.632-0.561 3.536-1.464 1.464-2.156 1.464-3.536zM11.5 7c0 0.829-0.335 1.577-0.879 2.121s-1.292 0.879-2.121 0.879-1.577-0.335-2.121-0.879-0.879-1.292-0.879-2.121 0.335-1.577 0.879-2.121 1.292-0.879 2.121-0.879 1.577 0.335 2.121 0.879 0.879 1.292 0.879 2.121zM16.293 11.707l2 2c0.391 0.391 1.024 0.391 1.414 0l4-4c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-3.293 3.293-1.293-1.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z"></path>
</symbol>
<symbol id="icon-users" viewBox="0 0 24 24">
<path d="M18 21v-2c0-1.38-0.561-2.632-1.464-3.536s-2.156-1.464-3.536-1.464h-8c-1.38 0-2.632 0.561-3.536 1.464s-1.464 2.156-1.464 3.536v2c0 0.552 0.448 1 1 1s1-0.448 1-1v-2c0-0.829 0.335-1.577 0.879-2.121s1.292-0.879 2.121-0.879h8c0.829 0 1.577 0.335 2.121 0.879s0.879 1.292 0.879 2.121v2c0 0.552 0.448 1 1 1s1-0.448 1-1zM14 7c0-1.38-0.561-2.632-1.464-3.536s-2.156-1.464-3.536-1.464-2.632 0.561-3.536 1.464-1.464 2.156-1.464 3.536 0.561 2.632 1.464 3.536 2.156 1.464 3.536 1.464 2.632-0.561 3.536-1.464 1.464-2.156 1.464-3.536zM12 7c0 0.829-0.335 1.577-0.879 2.121s-1.292 0.879-2.121 0.879-1.577-0.335-2.121-0.879-0.879-1.292-0.879-2.121 0.335-1.577 0.879-2.121 1.292-0.879 2.121-0.879 1.577 0.335 2.121 0.879 0.879 1.292 0.879 2.121zM24 21v-2c-0.001-1.245-0.457-2.385-1.215-3.261-0.652-0.753-1.528-1.311-2.529-1.576-0.534-0.141-1.081 0.177-1.222 0.711s0.177 1.081 0.711 1.222c0.607 0.161 1.136 0.498 1.528 0.952 0.454 0.526 0.726 1.206 0.727 1.952v2c0 0.552 0.448 1 1 1s1-0.448 1-1zM15.752 4.099c0.803 0.206 1.445 0.715 1.837 1.377s0.531 1.47 0.325 2.273c-0.176 0.688-0.575 1.256-1.105 1.652-0.314 0.235-0.675 0.409-1.063 0.511-0.534 0.14-0.854 0.687-0.713 1.221s0.687 0.854 1.221 0.713c0.637-0.167 1.232-0.455 1.752-0.844 0.884-0.66 1.552-1.613 1.845-2.758 0.342-1.337 0.11-2.689-0.542-3.788s-1.725-1.953-3.062-2.296c-0.535-0.137-1.080 0.186-1.217 0.721s0.186 1.080 0.721 1.217z"></path>
</symbol>
<symbol id="icon-message-circle" viewBox="0 0 24 24">
<path d="M22 11.497v-0.497c0-0.017-0.001-0.038-0.002-0.058-0.136-2.338-1.113-4.461-2.642-6.051-1.602-1.667-3.814-2.752-6.301-2.889-0.016-0.001-0.036-0.002-0.055-0.002h-0.489c-1.405-0.016-2.882 0.31-4.264 1.008-1.223 0.621-2.291 1.488-3.139 2.535-1.322 1.634-2.107 3.705-2.108 5.946-0.014 1.275 0.253 2.61 0.824 3.877l-1.772 5.317c-0.066 0.196-0.072 0.418 0 0.632 0.175 0.524 0.741 0.807 1.265 0.632l5.314-1.771c1.16 0.527 2.484 0.826 3.876 0.823 1.372-0.009 2.714-0.308 3.941-0.866 1.912-0.871 3.54-2.373 4.541-4.375 0.644-1.249 1.015-2.715 1.011-4.261zM20 11.503c0.003 1.225-0.292 2.375-0.789 3.339-0.801 1.602-2.082 2.785-3.592 3.472-0.97 0.442-2.035 0.679-3.126 0.686-1.221 0.003-2.371-0.292-3.335-0.789-0.249-0.129-0.528-0.142-0.775-0.060l-3.803 1.268 1.268-3.803c0.088-0.263 0.060-0.537-0.056-0.767-0.552-1.094-0.804-2.254-0.792-3.338 0.001-1.789 0.619-3.42 1.663-4.709 0.671-0.829 1.518-1.517 2.49-2.010 1.092-0.552 2.252-0.804 3.336-0.792h0.456c1.962 0.107 3.704 0.961 4.969 2.277 1.202 1.251 1.972 2.916 2.086 4.753z"></path>
</symbol>
<symbol id="icon-at-sign" viewBox="0 0 24 24">
<path d="M15 12c0 0.829-0.335 1.577-0.879 2.121s-1.292 0.879-2.121 0.879-1.577-0.335-2.121-0.879-0.879-1.292-0.879-2.121 0.335-1.577 0.879-2.121 1.292-0.879 2.121-0.879 1.577 0.335 2.121 0.879 0.879 1.292 0.879 2.121zM15.74 15.318c0.13 0.182 0.274 0.353 0.431 0.51 0.723 0.723 1.725 1.172 2.829 1.172s2.106-0.449 2.828-1.172 1.172-1.724 1.172-2.828v-1c0-3.037-1.233-5.789-3.222-7.778s-4.741-3.222-7.779-3.221-5.788 1.232-7.778 3.222-3.221 4.741-3.221 7.778 1.233 5.789 3.222 7.778 4.741 3.222 7.778 3.221c2.525 0 4.855-0.852 6.69-2.269 0.437-0.337 0.518-0.965 0.18-1.403s-0.965-0.518-1.403-0.18c-1.487 1.148-3.377 1.844-5.435 1.852-2.54-0.009-4.776-1.014-6.398-2.636-1.627-1.629-2.634-3.877-2.634-6.363s1.006-4.734 2.636-6.364 3.878-2.636 6.364-2.636 4.734 1.006 6.364 2.636 2.636 3.878 2.636 6.363v1c0 0.553-0.223 1.051-0.586 1.414s-0.861 0.586-1.414 0.586-1.051-0.223-1.414-0.586-0.586-0.861-0.586-1.414v-5c0-0.552-0.448-1-1-1s-1 0.448-1 1c-0.835-0.627-1.875-1-3-1-1.38 0-2.632 0.561-3.536 1.464s-1.464 2.156-1.464 3.536 0.561 2.632 1.464 3.536 2.156 1.464 3.536 1.464 2.632-0.561 3.536-1.464c0.070-0.070 0.139-0.143 0.205-0.217z"></path>
</symbol>
<symbol id="icon-embed" viewBox="0 0 40 32">
<path d="M26 23l3 3 10-10-10-10-3 3 7 7z"></path>
<path d="M14 9l-3-3-10 10 10 10 3-3-7-7z"></path>
<path d="M21.916 4.704l2.171 0.592-6 22.001-2.171-0.592 6-22.001z"></path>
</symbol>
<symbol id="icon-f-droid" viewBox="0 0 32 32">
<path d="M27.296 13.441h-22.592c-1.169 0-2.119 0.948-2.119 2.119v14.12c0 1.169 0.948 2.119 2.119 2.119h22.592c1.169 0 2.119-0.948 2.119-2.119v-14.12c0-1.171-0.949-2.119-2.119-2.119zM16 30.033c-4.088 0-7.413-3.325-7.413-7.413s3.325-7.413 7.413-7.413 7.413 3.325 7.413 7.413-3.325 7.413-7.413 7.413zM16 16.548c-3.348 0-6.072 2.724-6.072 6.072s2.724 6.072 6.072 6.072 6.072-2.724 6.072-6.072-2.724-6.072-6.072-6.072zM16 27.032c-2.084 0-3.841-1.471-4.295-3.424h2.227c0.367 0.775 1.145 1.305 2.068 1.305 1.28 0 2.295-1.015 2.295-2.295s-1.015-2.295-2.295-2.295c-0.865 0-1.6 0.469-1.991 1.165h-2.269c0.504-1.883 2.225-3.283 4.26-3.283 2.424 0 4.412 1.988 4.412 4.412 0 2.425-1.988 4.413-4.412 4.413zM31.799 0.528c-0.001 0.001-0.003 0.003-0.003 0.004-0.003-0.003-0.005-0.004-0.008-0.007 0.001-0.001 0.003-0.004 0.005-0.005-0.155-0.183-0.372-0.308-0.692-0.317-0.269 0.007-0.521 0.129-0.683 0.345l-2.424 3.137c-0.219-0.077-0.452-0.127-0.697-0.127h-22.593c-0.245 0-0.477 0.051-0.697 0.127l-2.424-3.139c-0.161-0.216-0.413-0.337-0.683-0.345-0.32 0.008-0.537 0.133-0.692 0.317 0.001 0.001 0.003 0.004 0.005 0.005-0.004 0.003-0.007 0.005-0.009 0.008 0-0.001-0.001-0.003-0.003-0.004-0.088 0.104-0.396 0.568-0.016 1.099l2.545 3.295c-0.089 0.235-0.144 0.488-0.144 0.755v4.943c0 1.169 0.948 2.119 2.119 2.119h22.592c1.169 0 2.119-0.948 2.119-2.119v-4.943c0-0.267-0.055-0.52-0.145-0.755l2.545-3.295c0.379-0.531 0.071-0.995-0.017-1.099zM9.205 10.971c-1.316 0-2.383-1.067-2.383-2.383s1.067-2.383 2.383-2.383 2.383 1.067 2.383 2.383-1.067 2.383-2.383 2.383zM22.972 10.971c-1.316 0-2.383-1.067-2.383-2.383s1.067-2.383 2.383-2.383 2.383 1.067 2.383 2.383-1.067 2.383-2.383 2.383z"></path>
</symbol>

View File

@ -0,0 +1,17 @@
export const donation_stats = [
{
name_: "liberapay",
patrons: 292,
amount: 1545.9725682543303,
},
{
name_: "opencollective",
patrons: 274,
amount: 867.060770338,
},
{
name_: "patreon",
patrons: 490,
amount: 1424.156049036,
},
];

View File

@ -25,7 +25,7 @@ import { pt_BR } from "./translations/pt_BR";
import { ru } from "./translations/ru";
import { zh } from "./translations/zh";
export const languages = [
export const LANGUAGES = [
{ resource: bg, code: "bg", name: "Български" },
{ resource: da, code: "da", name: "Dansk" },
{ resource: de, code: "de", name: "Deutsch" },
@ -52,7 +52,7 @@ export const languages = [
];
const resources: Resource = {};
languages.forEach(l => (resources[l.code] = l.resource));
LANGUAGES.forEach(l => (resources[l.code] = l.resource));
function format(value: any, format: any): any {
return format === "uppercase" ? value.toUpperCase() : value;

File diff suppressed because one or more lines are too long

View File

@ -6,6 +6,7 @@ import { Contact } from "./components/contact";
import { Donate } from "./components/donate";
import { News } from "./components/news";
import { NewsItem } from "./components/news-item";
import { Crypto } from "./components/crypto";
export const routes: IRouteProps[] = [
{
@ -48,4 +49,9 @@ export const routes: IRouteProps[] = [
exact: true,
component: Donate,
},
{
path: `/crypto`,
exact: true,
component: Crypto,
},
];

View File

@ -24,3 +24,36 @@ export const md = new markdown_it({
export function mdToHtml(text: string) {
return { __html: md.render(text) };
}
export function getQueryParams<T extends Record<string, any>>(processors: {
[K in keyof T]: (param: string) => T[K];
}): T {
if (isBrowser()) {
const searchParams = new URLSearchParams(window.location.search);
return Array.from(Object.entries(processors)).reduce(
(acc, [key, process]) => ({
...acc,
[key]: process(searchParams.get(key)),
}),
{} as T,
);
}
return {} as T;
}
export const NUMBER_FORMAT = new Intl.NumberFormat("en", {
maximumFractionDigits: 0,
});
export function monthsBetween(startDate: Date, endDate: Date) {
// The number of milliseconds in one day
const oneMonth = (1000 * 60 * 60 * 24 * 365) / 12;
// Calculate the difference in milliseconds
const differenceMs = Math.abs(startDate.getTime() - endDate.getTime());
// Convert back to days and return
return Math.round(differenceMs / oneMonth);
}

12
src/style.css Normal file
View File

@ -0,0 +1,12 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
.icon {
display: inline-block;
max-width: 4rem;
max-height: 4rem;
stroke-width: 0;
stroke: currentColor;
fill: currentColor;
}

25
tailwind.config.js Normal file
View File

@ -0,0 +1,25 @@
/** @type {import('tailwindcss').Config} */
const defaultTheme = require("tailwindcss/defaultTheme");
module.exports = {
daisyui: {
themes: [
{
halloween: {
...require("daisyui/src/theming/themes")["[data-theme=halloween]"],
primary: "#12D10E",
secondary: "#06AFC6",
"base-content": "#ffffff",
},
},
],
},
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
fontFamily: {
sans: ["Inter", ...defaultTheme.fontFamily.sans],
},
extend: {},
},
plugins: [require("@tailwindcss/typography"), require("daisyui")],
};

View File

@ -3,6 +3,7 @@
"pretty": true,
"target": "esnext",
"module": "esnext",
"strictNullChecks": true,
"allowSyntheticDefaultImports": true,
"preserveConstEnums": true,
"sourceMap": true,

48
update_donations.mjs Normal file
View File

@ -0,0 +1,48 @@
import fs from "fs";
import fetch from "node-fetch";
const donationStatsFile = "src/shared/donation_stats.ts";
const USDtoEURUrl =
"https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/usd/eur.json";
const liberaPayUrl = "https://liberapay.com/Lemmy/public.json";
const openCollectiveUrl = "https://opencollective.com/lemmy.json";
const patreonUrl = "https://www.patreon.com/api/campaigns/2692831";
const usdToEurRes = await fetch(USDtoEURUrl);
const usdToEur = (await usdToEurRes.json()).eur;
// In weekly USD
const liberaPayRes = await fetch(liberaPayUrl);
const liberaPayData = await liberaPayRes.json();
// In yearly USD, decimal
const openCollectiveRes = await fetch(openCollectiveUrl);
const openCollectiveData = await openCollectiveRes.json();
// In monthly USD, decimal
const patreonRes = await fetch(patreonUrl);
const patreonData = await patreonRes.json();
const donationData = [
{
name_: "liberapay",
patrons: liberaPayData.npatrons,
amount: Number(liberaPayData.receiving.amount) * 4.348214 * usdToEur,
},
{
name_: "opencollective",
patrons: openCollectiveData.backersCount,
amount: (Number(openCollectiveData.yearlyIncome) / 100 / 12) * usdToEur,
},
{
name_: "patreon",
patrons: patreonData.data.attributes.patron_count,
amount: (Number(patreonData.data.attributes.pledge_sum) / 100) * usdToEur,
},
];
let data = `export const donation_stats = \n `;
data += JSON.stringify(donationData, null, 2) + ";";
fs.writeFileSync(donationStatsFile, data);

View File

@ -1,5 +1,4 @@
const webpack = require("webpack");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const nodeExternals = require("webpack-node-externals");
const CopyPlugin = require("copy-webpack-plugin");
const RunNodeWebpackPlugin = require("run-node-webpack-plugin");
@ -25,10 +24,6 @@ const base = {
},
module: {
rules: [
{
test: /\.(scss|css)$/i,
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
},
{
test: /\.(js|jsx|tsx|ts)$/, // All ts and tsx files will be process by
exclude: /node_modules/, // ignore node_modules
@ -44,9 +39,6 @@ const base = {
],
},
plugins: [
new MiniCssExtractPlugin({
filename: "styles/styles.css",
}),
new CopyPlugin({
patterns: [{ from: "./src/assets", to: "./assets" }],
}),

2175
yarn.lock

File diff suppressed because it is too large Load Diff