refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View File

@@ -1,40 +0,0 @@
# Build artifacts
build/
*.beam
*.o
*.a
# Dependencies & tooling
/node_modules
/.cache
/.venv
/.idea/
.vscode/
# Git & CI
.git/
.github/
.gitignore
.env
.env*
# OS/editor temp files
.DS_Store
Thumbs.db
*.swp
*.swo
*~
# Documentation
README.md
*.md
!priv/**/*.md
# Test files
test/
*_test.gleam
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -1,52 +1,85 @@
ARG BUILD_TIMESTAMP=0
FROM erlang:27.1.1.0-alpine AS builder
COPY --from=ghcr.io/gleam-lang/gleam:nightly-erlang /bin/gleam /bin/gleam
RUN apk add --no-cache git curl
WORKDIR /app
COPY gleam.toml manifest.toml ./
COPY src ./src
COPY priv ./priv
COPY tailwind.css ./
COPY locales ./locales
RUN apk add --no-cache gettext
RUN gleam deps download
RUN gleam export erlang-shipment
RUN sh -c 'mkdir -p priv/locales && cd locales && for d in */; do mkdir -p ../priv/locales/$d/LC_MESSAGES && msgfmt -o ../priv/locales/$d/LC_MESSAGES/messages.mo $d/messages.po; done'
ARG TAILWIND_VERSION=v4.1.17
RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "x86_64" ]; then \
TAILWIND_ARCH="x64"; \
elif [ "$ARCH" = "aarch64" ]; then \
TAILWIND_ARCH="arm64"; \
else \
TAILWIND_ARCH="x64"; \
fi && \
echo "Downloading Tailwind CSS $TAILWIND_VERSION for Alpine Linux: linux-$TAILWIND_ARCH-musl" && \
curl -sSLf -o /tmp/tailwindcss "https://github.com/tailwindlabs/tailwindcss/releases/download/${TAILWIND_VERSION}/tailwindcss-linux-${TAILWIND_ARCH}-musl" && \
chmod +x /tmp/tailwindcss && \
/tmp/tailwindcss -i ./tailwind.css -o ./priv/static/app.css --minify
FROM erlang:27.1.1.0-alpine
ARG BUILD_SHA
ARG BUILD_NUMBER
ARG BUILD_TIMESTAMP
ARG RELEASE_CHANNEL=nightly
RUN apk add --no-cache openssl ncurses-libs curl
FROM node:24-bookworm-slim AS base
WORKDIR /app
WORKDIR /usr/src/app
COPY --from=builder /app/build/erlang-shipment /app
COPY --from=builder /app/priv ./priv
RUN corepack enable && corepack prepare pnpm@10.26.0 --activate
FROM base AS deps
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY patches/ ./patches/
COPY packages/ ./packages/
COPY fluxer_marketing/package.json ./fluxer_marketing/
RUN pnpm install --frozen-lockfile
FROM deps AS build
COPY tsconfigs /usr/src/app/tsconfigs
COPY fluxer_marketing/tsconfig.json ./fluxer_marketing/
COPY fluxer_marketing/src ./fluxer_marketing/src
WORKDIR /usr/src/app
RUN pnpm --filter @fluxer/config generate
RUN pnpm --filter @fluxer/marketing build:css \
&& mkdir -p fluxer_marketing/public/static \
&& cp -r packages/marketing/public/static/. fluxer_marketing/public/static/
FROM base AS prod-deps
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY patches/ ./patches/
COPY packages/ ./packages/
COPY fluxer_marketing/package.json ./fluxer_marketing/
RUN pnpm install --frozen-lockfile --prod
COPY --from=build /usr/src/app/packages/marketing/public /usr/src/app/packages/marketing/public
FROM node:24-bookworm-slim
ARG BUILD_SHA
ARG BUILD_NUMBER
ARG BUILD_TIMESTAMP
ARG RELEASE_CHANNEL
WORKDIR /usr/src/app/fluxer_marketing
RUN apt-get update && apt-get install -y --no-install-recommends \
curl && \
rm -rf /var/lib/apt/lists/*
RUN corepack enable && corepack prepare pnpm@10.26.0 --activate
COPY --from=prod-deps /usr/src/app/node_modules /usr/src/app/node_modules
COPY --from=prod-deps /usr/src/app/fluxer_marketing/node_modules ./node_modules
COPY --from=prod-deps /usr/src/app/packages /usr/src/app/packages
COPY --from=build /usr/src/app/packages/config/src/ConfigSchema.json /usr/src/app/packages/config/src/ConfigSchema.json
COPY --from=build /usr/src/app/packages/config/src/MasterZodSchema.generated.tsx /usr/src/app/packages/config/src/MasterZodSchema.generated.tsx
COPY tsconfigs /usr/src/app/tsconfigs
COPY --from=build /usr/src/app/fluxer_marketing/tsconfig.json ./tsconfig.json
COPY --from=build /usr/src/app/fluxer_marketing/src ./src
COPY --from=build /usr/src/app/fluxer_marketing/public ./public
COPY fluxer_marketing/package.json ./
RUN mkdir -p /usr/src/app/.cache/corepack && \
chown -R nobody:nogroup /usr/src/app
ENV HOME=/usr/src/app
ENV COREPACK_HOME=/usr/src/app/.cache/corepack
ENV NODE_ENV=production
ENV FLUXER_MARKETING_PORT=8080
ENV BUILD_SHA=${BUILD_SHA}
ENV BUILD_NUMBER=${BUILD_NUMBER}
ENV BUILD_TIMESTAMP=${BUILD_TIMESTAMP}
ENV RELEASE_CHANNEL=${RELEASE_CHANNEL}
USER nobody
EXPOSE 8080
ENV PORT=8080
ENV BUILD_TIMESTAMP=${BUILD_TIMESTAMP}
CMD ["/app/entrypoint.sh", "run"]
CMD ["pnpm", "start"]

View File

@@ -1,24 +0,0 @@
FROM ghcr.io/gleam-lang/gleam:v1.13.0-erlang-alpine
WORKDIR /workspace
# Install dependencies
RUN apk add --no-cache curl gettext
# Download gleam dependencies
COPY gleam.toml manifest.toml* ./
RUN gleam deps download
# Copy source code
COPY . .
# Download and setup tailwindcss, then build CSS
RUN mkdir -p build/bin && \
curl -sLo build/bin/tailwindcss https://github.com/tailwindlabs/tailwindcss/releases/download/v4.1.17/tailwindcss-linux-x64-musl && \
chmod +x build/bin/tailwindcss && \
build/bin/tailwindcss -i ./tailwind.css -o ./priv/static/app.css
# Compile i18n message catalogs
RUN sh -c 'mkdir -p priv/locales && cd locales && for d in */; do mkdir -p ../priv/locales/$d/LC_MESSAGES && msgfmt -o ../priv/locales/$d/LC_MESSAGES/messages.mo $d/messages.po; done'
CMD ["gleam", "run"]

View File

@@ -1,22 +0,0 @@
name = "fluxer_marketing"
version = "1.0.0"
[dependencies]
gleam_stdlib = ">= 0.63.2 and < 1.0.0"
gleam_http = ">= 4.2.0 and < 5.0.0"
gleam_erlang = ">= 1.0.0 and < 2.0.0"
wisp = ">= 1.5.0 and < 2.0.0"
mist = ">= 5.0.0 and < 6.0.0"
lustre = ">= 5.0.0 and < 6.0.0"
kielet = ">= 3.0.0 and < 4.0.0"
nibble = ">= 1.1.4 and < 2.0.0"
simplifile = ">= 2.3.0 and < 3.0.0"
dot_env = ">= 1.2.0 and < 2.0.0"
gleam_regexp = ">= 1.1.1 and < 2.0.0"
gleam_httpc = ">= 5.0.0 and < 6.0.0"
gleam_json = ">= 3.0.2 and < 4.0.0"
envoy = ">= 1.0.2 and < 2.0.0"
[dev-dependencies]
gleeunit = ">= 1.6.1 and < 2.0.0"
glailglind = ">= 2.2.0 and < 3.0.0"

View File

@@ -1,10 +0,0 @@
module fluxer.dev/fluxer_marketing
go 1.25.5
require (
github.com/chai2010/webp v1.4.0
github.com/disintegration/imaging v1.6.2
github.com/gen2brain/avif v0.4.4
github.com/schollz/progressbar/v3 v3.18.0
)

View File

@@ -1,15 +0,0 @@
github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/gen2brain/avif v0.4.4/go.mod h1:/XCaJcjZraQwKVhpu9aEd9aLOssYOawLvhMBtmHVGqk=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -1,47 +0,0 @@
default:
@just --list
build:
gleam build
run:
just css && gleam run
test:
gleam test
css:
./build/bin/tailwindcss -i ./tailwind.css -o ./priv/static/app.css
css-watch:
./build/bin/tailwindcss -i ./tailwind.css -o ./priv/static/app.css --watch
clean:
rm -rf build/
rm -rf priv/static/app.css
deps:
gleam deps download
format:
gleam format
check: format build test
install-tailwind:
gleam run -m tailwind/install
setup: deps install-tailwind css i18n-compile
preprocess-images desktop mobile:
cd scripts && go run ./cmd/preprocess-images -desktop {{desktop}} -mobile {{mobile}}
i18n-compile:
#!/usr/bin/env bash
set -euo pipefail
for po in locales/*/messages.po; do
locale_dir=$(dirname "$po")
locale_name=$(basename "$locale_dir")
mkdir -p "priv/locales/$locale_name/LC_MESSAGES"
msgfmt -o "priv/locales/$locale_name/LC_MESSAGES/messages.mo" "$po"
done

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +0,0 @@
# This file was generated by Gleam
# You typically do not need to edit this file
packages = [
{ name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" },
{ name = "dot_env", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "simplifile"], otp_app = "dot_env", source = "hex", outer_checksum = "F2B4815F1B5AF8F20A6EADBB393E715C4C35203EBD5BE8200F766EA83A0B18DE" },
{ name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" },
{ name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" },
{ name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
{ name = "glailglind", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_httpc", "gleam_stdlib", "shellout", "simplifile", "tom"], otp_app = "glailglind", source = "hex", outer_checksum = "B0306F2C0A03A5A03633FC2BDF2D52B1E76FCAED656FB3F5EBCB7C31770E2524" },
{ name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" },
{ name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" },
{ name = "gleam_http", version = "4.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "FFE29C3832698AC3EF6202922EC534EE19540152D01A7C2D22CB97482E4AF211" },
{ name = "gleam_httpc", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C545172618D07811494E97AAA4A0FB34DA6F6D0061FDC8041C2F8E3BE2B2E48F" },
{ name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" },
{ name = "gleam_otp", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "7987CBEBC8060B88F14575DEF546253F3116EBE2A5DA6FD82F38243FCE97C54B" },
{ name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" },
{ name = "gleam_stdlib", version = "0.63.2", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "962B25C667DA07F4CAB32001F44D3C41C1A89E58E3BBA54F183B482CF6122150" },
{ name = "gleam_time", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "DCDDC040CE97DA3D2A925CDBBA08D8A78681139745754A83998641C8A3F6587E" },
{ name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" },
{ name = "gleeunit", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "FDC68A8C492B1E9B429249062CD9BAC9B5538C6FBF584817205D0998C42E1DAC" },
{ name = "glisten", version = "8.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "534BB27C71FB9E506345A767C0D76B17A9E9199934340C975DC003C710E3692D" },
{ name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" },
{ name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" },
{ name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
{ name = "iv", version = "1.3.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_yielder"], otp_app = "iv", source = "hex", outer_checksum = "1FE22E047705BE69EA366E3A2E73C2E1310CBCB27DDE767DE17AE3FA86499947" },
{ name = "kielet", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "nibble"], otp_app = "kielet", source = "hex", outer_checksum = "B91E685DA1C9FEC8896E864FE406C64F5AAED391D34235969F3064325A7077BB" },
{ name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" },
{ name = "lustre", version = "5.3.5", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "5CBB5DD2849D8316A2101792FC35AEB58CE4B151451044A9C2A2A70A2F7FCEB8" },
{ name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" },
{ name = "mist", version = "5.0.3", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7C4BE717A81305323C47C8A591E6B9BA4AC7F56354BF70B4D3DF08CC01192668" },
{ name = "nibble", version = "1.1.4", build_tools = ["gleam"], requirements = ["gleam_regexp", "gleam_stdlib", "iv"], otp_app = "nibble", source = "hex", outer_checksum = "06397501730FF486AE6F99299982A33F5EA9F8945B5A25920C82C8F924CEA481" },
{ name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" },
{ name = "shellout", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "shellout", source = "hex", outer_checksum = "1BDC03438FEB97A6AF3E396F4ABEB32BECF20DF2452EC9A8C0ACEB7BDDF70B14" },
{ name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" },
{ name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" },
{ name = "tom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "74D0C5A3761F7A7D06994755D4D5AD854122EF8E9F9F76A3E7547606D8C77091" },
{ name = "wisp", version = "1.8.0", build_tools = ["gleam"], requirements = ["directories", "exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "houdini", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "0FE9049AFFB7C8D5FC0B154EEE2704806F4D51B97F44925D69349B3F4F192957" },
]
[requirements]
dot_env = { version = ">= 1.2.0 and < 2.0.0" }
envoy = { version = ">= 1.0.2 and < 2.0.0" }
glailglind = { version = ">= 2.2.0 and < 3.0.0" }
gleam_erlang = { version = ">= 1.0.0 and < 2.0.0" }
gleam_http = { version = ">= 4.2.0 and < 5.0.0" }
gleam_httpc = { version = ">= 5.0.0 and < 6.0.0" }
gleam_json = { version = ">= 3.0.2 and < 4.0.0" }
gleam_regexp = { version = ">= 1.1.1 and < 2.0.0" }
gleam_stdlib = { version = ">= 0.63.2 and < 1.0.0" }
gleeunit = { version = ">= 1.6.1 and < 2.0.0" }
kielet = { version = ">= 3.0.0 and < 4.0.0" }
lustre = { version = ">= 5.0.0 and < 6.0.0" }
mist = { version = ">= 5.0.0 and < 6.0.0" }
nibble = { version = ">= 1.1.4 and < 2.0.0" }
simplifile = { version = ">= 2.3.0 and < 3.0.0" }
wisp = { version = ">= 1.5.0 and < 2.0.0" }

View File

@@ -0,0 +1,29 @@
{
"name": "fluxer_marketing",
"private": true,
"type": "module",
"scripts": {
"build:css": "pnpm --filter @fluxer/marketing build:css",
"build:css:watch": "pnpm --filter @fluxer/marketing build:css:watch",
"dev": "tsx watch --clear-screen=false src/index.tsx",
"start": "pnpm build:css && tsx src/index.tsx",
"typecheck": "tsgo --noEmit"
},
"dependencies": {
"@fluxer/cache": "workspace:*",
"@fluxer/config": "workspace:*",
"@fluxer/hono": "workspace:*",
"@fluxer/initialization": "workspace:*",
"@fluxer/kv_client": "workspace:*",
"@fluxer/logger": "workspace:*",
"@fluxer/marketing": "workspace:*",
"@fluxer/rate_limit": "workspace:*",
"hono": "catalog:",
"tsx": "catalog:"
},
"devDependencies": {
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:"
},
"packageManager": "pnpm@10.29.3"
}

View File

@@ -1,41 +0,0 @@
Fluxer Platform AB is a Swedish limited liability company registered with the Swedish Companies Registration Office (Bolagsverket) under the laws of Sweden.
### Company Registration
**Organization Number:** 559537-3993
**VAT ID:** SE559537399301
### Registered Address
Fluxer Platform AB
Norra Kronans Gata 430
136 76 Brandbergen
Stockholm County, Sweden
### Contact Information
**Email:** support@fluxer.app (for account-related matters, please contact us from the email address associated with your Fluxer account where possible)
**Website:** [https://fluxer.app](https://fluxer.app)
### Authorized Representative
Hampus Kraft, Founder & CEO
### Specialized Contact Information
- **General Inquiries:** support@fluxer.app
- **Press & Media:** press@fluxer.app
- **Privacy & Data Protection:** privacy@fluxer.app (primary contact for privacy and data protection questions; we have not formally appointed a Data Protection Officer under GDPR)
- **Security Vulnerabilities:** See our [Security Bug Bounty page](/security) for disclosure guidance and contact information.
- **Copyright (DMCA):** dmca@fluxer.app
- **Trust & Safety:** safety@fluxer.app
- **Legal Requests:** legal@fluxer.app
- **Partnership Inquiries:** partners@fluxer.app
- **Accessibility:** accessibility@fluxer.app
- **Account Appeals:** appeals@fluxer.app
For information about how we handle legal and law-enforcement requests for user data, please see the "Law Enforcement and Legal Requests" section of our [Privacy Policy](/privacy).
For all account-related inquiries and support requests, we normally provide assistance only when you contact us from the email address associated with your Fluxer account. This is our primary method of verifying your identity and ensuring secure communication. If you no longer have access to that email address, we may require additional verification and might not always be able to help with account changes or recovery.
Fluxer will never request your password, complete payment card details, or other sensitive security credentials via email. All official Fluxer communications originate from email addresses ending in `@fluxer.app`. Please be vigilant against phishing attempts using similar-looking domains or requesting sensitive information.

View File

@@ -1,418 +0,0 @@
**Effective: 1 January 2026**
**Last updated: 1 January 2026**
## Table of Contents
- [Welcome to Fluxer](#welcome-to-fluxer)
- [The Golden Rule](#the-golden-rule)
- [Positive Participation](#positive-participation)
- [Prohibited Conduct](#prohibited-conduct)
- [Reporting Violations](#reporting-violations)
- [Enforcement Actions](#enforcement-actions)
- [Appeals Process](#appeals-process)
- [Special Considerations](#special-considerations)
- [Why These Guidelines Matter](#why-these-guidelines-matter)
- [Updates to These Guidelines](#updates-to-these-guidelines)
- [Final Thoughts](#final-thoughts)
- [Need Help?](#need-help)
- [Law Enforcement Requests](#law-enforcement-requests)
- [Safety and Crisis Resources](#safety-and-crisis-resources)
## Welcome to Fluxer
Fluxer is a platform designed for communication, connection, and community building. These community guidelines help ensure that everyone can use the platform safely and respectfully. They form an integral part of our Terms of Service, and violations may result in warnings, content removal, restrictions, or account suspension or termination.
These guidelines apply to all users of Fluxer without exception. They govern all interactions on the platform, including (but not limited to):
- direct messages;
- community communications;
- voice and video chats;
- user profiles and statuses; and
- any other areas where users interact with one another or share content.
Individual Communities may adopt their own rules, but those rules must always be consistent with these guidelines and our Terms of Service. Where there is a conflict, these guidelines and our Terms of Service take precedence.
## The Golden Rule
**Treat others with respect and consideration. If you would not want something done or said to you, do not do or say it to someone else.**
Fluxer is built for real people. Behind every username is a person who deserves to be treated with basic dignity.
## Positive Participation
We want Fluxer to be safe, welcoming, and constructive. You can help by:
- **Assuming good intent**
When something is unclear, ask for clarification before reacting.
- **Using content warnings and age markings where appropriate**
If you discuss potentially distressing, graphic, or adult topics, label them clearly and place them in age-appropriate spaces.
- **Setting and following clear community rules**
If you run a Community:
- make your rules clear, accessible, and easy to find;
- ensure they align with these guidelines and local law;
- enforce them fairly and consistently.
- **Onboarding new members kindly**
Help new members understand your Community rules. Avoid dogpiling or retaliation. Use your moderation tools instead.
- **Protecting privacy (yours and others')**
Share only what is necessary, and be cautious about exposing personal information that could affect safety.
- **Disagreeing constructively**
Challenge ideas, not people. It is fine to disagree, but personal attacks, harassment, or demeaning behavior are not acceptable.
- **Keeping safety in mind**
If you see behavior that looks dangerous, abusive, or clearly inappropriate, please report it rather than amplify it.
## Prohibited Conduct
These rules apply platform-wide. Communities can set stricter rules, but never more permissive ones.
The following behaviors are strictly prohibited on Fluxer and may result in enforcement action, including content removal, account restrictions, or bans.
### 1. Harassment and Bullying
Do not engage in harassment, bullying, or threatening behavior toward any person or group. This includes, but is not limited to:
- sustained harassment or coordinated attacks against individuals or groups;
- threats of any kind, whether direct or implied;
- doxxing (sharing someone's personal or identifying information without their explicit consent);
- targeted harassment, including repeated unwanted contact after being asked to stop;
- sexual harassment of any nature, including unwanted sexual comments, advances, or innuendo; and
- encouraging others to engage in harmful behavior toward someone.
We consider factors such as frequency, severity, power imbalances, and whether someone has clearly asked you to stop.
### 2. Hate Speech and Discrimination
We do not tolerate hate speech or discrimination based on any protected characteristics, including:
- race, ethnicity, or national origin;
- religious beliefs or practices;
- gender, gender identity, or gender expression;
- sexual orientation;
- disability or medical condition;
- age or generational status;
- immigration or citizenship status; or
- any other characteristic protected under applicable law.
This prohibition includes, for example:
- hate symbols and imagery used in a praising or celebratory way;
- derogatory slurs or dehumanizing language about protected groups;
- content that promotes or glorifies hateful ideologies; and
- calls for violence, segregation, or exclusion of protected groups.
Context matters (for example, historical discussion or condemnation of hate). However, we may remove content even when intent is ambiguous if it uses slurs, symbols, or narratives that are commonly understood as hateful.
### 3. Violence and Graphic Content
Do not share or promote:
- graphic depictions of violence, gore, or mutilation;
- content that promotes, encourages, or provides instructions for self-harm or suicide;
- detailed instructions or encouragement for violence or illegal activities;
- content depicting or glorifying animal cruelty or abuse; or
- content that glorifies, celebrates, or promotes violence, violent extremism, or acts of terrorism.
Non-graphic discussion of difficult topics may be allowed in appropriate contexts (for example, news or educational content) but may require content warnings and, in some cases, restriction to age-gated spaces.
### 4. Sexual Content and Protection of Minors
We have a zero-tolerance stance on child sexual exploitation.
- **Users under 18:**
If you are under 18, you must not engage with, share, or distribute any sexual or sexually suggestive content.
- **Content involving minors (real or fictional):**
No user may share, distribute, request, or possess sexual or sexually suggestive content involving minors (whether real, fictional, or simulated). This includes "age play" or any portrayal that sexualizes minors.
- **Child sexual abuse material (CSAM):**
CSAM is strictly prohibited and will be reported to law enforcement authorities as required by law. We use automated tools and safety systems to detect and prevent CSAM in media where technically feasible and take immediate action when we identify it.
- **Adult content restrictions:**
Adult content is only allowed in clearly marked 18+ spaces. Communities must:
- apply appropriate age gates and clear labels; and
- ensure minors cannot access or participate in 18+ spaces.
We may restrict or remove Communities that fail to enforce these requirements.
- **Non-consensual intimate media:**
Never share intimate images, videos, or recordings of any person without their explicit consent. This includes "deepfakes", edited or simulated content that depicts someone in an intimate context without their permission.
- **Revenge porn and sexual exploitation:**
Sharing intimate media to shame, coerce, or harm someone, or to obtain money, favors, or other benefits, is strictly prohibited.
### 5. Illegal Activities
Do not use Fluxer to facilitate, promote, or engage in illegal activities. This includes, but is not limited to:
- distribution or promotion of malware, viruses, or harmful software;
- fraud, scams, or deceptive practices (including phishing, impersonation, and financial scams);
- sale, distribution, or promotion of illegal goods, services, or controlled substances;
- copyright infringement or other intellectual property violations at scale or in a clearly abusive way;
- hacking, unauthorized access, or cyberattacks;
- money laundering, terrorist financing, or similar activities;
- evasion of lawful blocks or sanctions using the service (for example, using Fluxer where export control or sanctions laws prohibit it); or
- any other activity that violates applicable local, national, or international law.
We may cooperate with law enforcement where required by law or where we believe it is necessary to protect individuals from serious harm.
### 6. Spam and Platform Abuse
Do not abuse or misuse the Fluxer platform in the following ways:
- sending spam, bulk messages, or unsolicited commercial content;
- creating fake accounts or impersonating other individuals or entities;
- artificially inflating community member counts, engagement metrics, or reactions;
- buying, selling, renting, or trading Fluxer accounts or Communities;
- abusing our free tier as unlimited cloud storage rather than for legitimate communication;
- initiating fraudulent chargebacks or payment disputes to obtain free services or benefits; or
- using automation, bots, scrapers, or scripts to:
- evade limits;
- scrape or harvest data;
- mass-create accounts; or
- disrupt normal user experiences.
Limited automation that complies with our policies and applicable law may be allowed where explicitly permitted by Fluxer. Otherwise, automated abuse is prohibited.
### 7. Harmful Misinformation
Do not deliberately spread harmful misinformation that could:
- endanger public health or safety;
- interfere with democratic processes or civic participation;
- cause direct physical harm to individuals or communities; or
- damage critical infrastructure or essential services.
Examples include:
- false medical "cures" that could lead someone to avoid necessary treatment;
- calls to disable public safety systems or emergency services; or
- fabricated election procedures intended to confuse or disenfranchise voters.
We may remove or limit the reach of content that is demonstrably false and likely to cause harm. When assessing content, we consider context, source credibility, intent, and potential real-world impact.
### 8. Privacy Violations
Respect the privacy rights of all users. Do not:
- share private conversations or communications without explicit permission from all parties, except where necessary to report abuse or illegal behavior to us or to authorities;
- record voice or video communications without consent where legally required;
- circumvent, bypass, or attempt to defeat privacy settings, user blocks, or safety features; or
- engage in stalking, doxxing, surveillance, or other invasive monitoring of users on or off the platform in connection with their use of Fluxer.
If you are unsure whether something violates someone's privacy, err on the side of caution and do not share it.
## Reporting Violations
If you observe conduct or content that appears to violate these guidelines or our Terms of Service, please report it. You can:
- use the in-app reporting features available throughout the platform; and/or
- email our safety team at safety@fluxer.app.
When possible, please include:
- relevant screenshots or message excerpts;
- direct links to the content, message, or Community;
- user IDs or usernames; and
- a brief description of what is happening and why it concerns you.
Please do not engage in "vigilante justice". Do not harass, threaten, or doxx others in response to violations. Report issues to us and allow our moderation team to handle them appropriately.
We may not always be able to share the outcome of our actions with you, but we review reports in good faith and prioritize situations that present higher risk of harm.
## Enforcement Actions
When we identify violations of these community guidelines or our Terms of Service, we may take one or more of the following enforcement actions, depending on severity, context, and risk:
- issuing informal or formal warnings to the account holder;
- removing or restricting access to violating content;
- temporarily limiting or disabling specific features (for example, messaging or community creation);
- temporarily suspending account access;
- permanently banning accounts from the platform;
- restricting the ability to create, own, or manage Communities;
- deleting Communities that repeatedly or seriously violate our guidelines;
- restricting access to specific cosmetic items, premium services, or subscriptions; and/or
- reporting illegal content or serious threats to appropriate law enforcement or relevant organizations.
### How We Decide
When deciding on enforcement, we consider factors such as:
- the **severity** of the violation and the potential or actual harm caused;
- the **intent** of the user (for example, malicious vs. accidental);
- the user's **prior history** of violations or warnings;
- the **risk of future harm** if no action is taken;
- whether the content affects minors or vulnerable individuals; and
- whether applicable law requires us to act in a particular way.
General principles:
- We typically start with less severe measures (warnings, content removal, temporary restrictions) for minor or first-time violations.
- We may act immediately and permanently for egregious violations, such as:
- child sexual exploitation or CSAM;
- credible threats of serious violence; or
- large-scale or clearly malicious abuse, fraud, or hacking.
Automated tools may flag content or behavior for review, but enforcement decisions are made by humans, except in limited cases where we must automatically block certain content (for example, known CSAM hashes) or where automated integrity systems limit access as described in our [Privacy Policy](/privacy).
Some safety and integrity protections (such as automated CSAM blocking, spam defenses, and regional access restrictions based on IP geolocation) operate automatically. These systems are designed to comply with applicable law, including any rights you may have in relation to automated decision-making as described in our [Privacy Policy](/privacy).
No moderation system is perfect. We may make mistakes, and that is why we provide an appeals process.
## Appeals Process
If you believe we made an error in enforcing these guidelines against your account or content, you may appeal our decision.
To appeal:
1. Send an email to appeals@fluxer.app from the email address associated with your Fluxer account.
2. Clearly state:
- the enforcement action you are appealing (for example, "7-day suspension on [date]" or "Community deletion"); and
- why you believe the decision was incorrect, incomplete, or disproportionate.
3. Include any relevant context or evidence (for example, message IDs, timestamps, or clarifications).
For security and verification reasons:
- We can only process appeals submitted from the email address associated with the affected account.
- We cannot accept appeals submitted on behalf of another user, except where supported by law (for example, authorized legal representatives).
Additional notes:
- **One appeal per enforcement action.** Submitting multiple appeals about the same decision will not expedite review.
- **Appeal window:** Please submit your appeal within 60 days of receiving the enforcement notice.
- **During review:** Temporary actions generally remain in place while we review your appeal.
- **Response times:** We aim to review and respond to appeals within 14 days where feasible, but this may vary depending on volume and complexity.
After review, our decision on the appeal is generally final. However, we may revisit past decisions if new, material information comes to light or if we update our policies in relevant ways.
## Special Considerations
### For Teenage Users
Users must meet the **Minimum Age** to use Fluxer as described in our [Terms of Service](/terms) and [Privacy Policy](/privacy). This Minimum Age is typically 13 but may be higher in some countries.
We determine eligibility based on your approximate geographic location and applicable laws. Users who are above the Minimum Age but under the age of legal majority in their jurisdiction (for example, under 18 in many countries) are treated as younger users for safety purposes.
- We may enable enhanced safety features by default for users we identify as under 18 (for example, stricter privacy defaults or restricted access to certain features).
- Certain types of content or Communities may be restricted based on age (for example, 18+ spaces and adult content).
- Communities focused on dating or romantic relationships between minors, or that sexualize minors in any way, are strictly prohibited.
- We maintain heightened vigilance regarding the safety of underage users and may take swift action in response to reports involving minors.
If you are under 18, please be especially cautious about sharing personal information, and do not meet people from Fluxer in person without involving a trusted adult and ensuring your safety.
### For Community Owners
If you own, create, or administer a Community:
- You are responsible for the content and behavior within your Community, including user-generated content and moderation practices.
- Use available tools to keep your Community safe and compliant, such as:
- moderation roles and permissions;
- content and membership controls; and
- age gates, labels, and clear descriptions for 18+ or sensitive spaces.
- Set clear, visible rules that are aligned with these guidelines and with the law, and enforce them fairly.
- You may set stricter rules than Fluxer's baseline, but not more permissive ones.
- Failure to moderate or address serious, repeated violations can lead to:
- restrictions on your Community;
- removal of your Community; and/or
- enforcement action against your account.
If you are unsure how to handle a safety issue that affects your Community, you can always report it to us or contact safety@fluxer.app for guidance.
### For Parents and Guardians
We understand that online safety is especially important for younger users.
- We provide safety resources and guidance on our website to help parents and guardians understand how Fluxer works and how to support young users.
- If you have concerns about your teenager's account, you can contact our support team. We may need to verify your relationship to the user before we can discuss or take action on a specific account.
- We take child safety extremely seriously and maintain strict policies to protect young users, including zero tolerance for child sexual exploitation and grooming.
If you believe a child is in immediate danger, please contact local emergency services first, then notify us.
### Self-Harm and Crisis Content
- Do not glorify, encourage, or provide instructions for self-harm, suicide, or eating disorders.
- Supportive, empathetic conversations about mental health are allowed, but they must not:
- include detailed methods or plans;
- encourage others to harm themselves; or
- shame or attack people who are struggling.
- If you see content that suggests someone may be at imminent risk of self-harm or harm to others:
- report it via in-app tools or email safety@fluxer.app; and
- if you know the person and can safely do so, encourage them to seek professional support or contact local emergency services.
Fluxer is not a substitute for professional mental health care or emergency services.
## Why These Guidelines Matter
We maintain these community guidelines to ensure that Fluxer remains:
- **Safe:** Everyone should feel as secure as reasonably possible while using our platform.
- **Welcoming:** People from many backgrounds and perspectives should feel respected and able to participate.
- **Enjoyable:** Communication platforms should be fun, engaging, and positive experiences, not sources of fear or stress.
- **Legal and sustainable:** We must comply with applicable laws and regulations in all jurisdictions where we operate and maintain a platform that can exist long-term.
By following these guidelines and our Terms of Service, you help us maintain a space that works for everyone.
## Updates to These Guidelines
We may update these community guidelines from time to time as:
- new features are introduced;
- community norms evolve; or
- laws and regulations change.
If we make significant changes:
- we will provide at least 30 days' notice where reasonably practicable, for example via email or in-app notifications; and
- we will maintain a changelog or archive of prior versions for reference.
Your continued use of Fluxer after updated guidelines take effect constitutes your acceptance of those changes. If you do not agree with updated guidelines, you should stop using Fluxer and may delete your account.
## Final Thoughts
The vast majority of Fluxer users engage with our platform responsibly and never have any issues with these guidelines.
If you:
- treat others with respect;
- use common sense and good judgment; and
- remember that there is a real person on the other side of every interaction,
you are very unlikely to experience any enforcement action from us.
Thank you for helping us keep Fluxer a safe, welcoming, and enjoyable place to connect and communicate.
## Need Help?
### General Questions
- **Email:** support@fluxer.app
### Safety Concerns
- **Email:** safety@fluxer.app
If you are unsure whether something violates these guidelines, you can ask our support or safety teams for clarification.
## Law Enforcement Requests
We recognize that law enforcement and other authorities may, in some circumstances, require information from us.
For more detailed information about how we handle such requests, please see the "Law Enforcement and Legal Requests" section of our [Privacy Policy](/privacy).
- Direct lawful process and urgent preservation requests to legal@fluxer.app.
- Requests must clearly identify the requesting authority, the legal basis, and the specific data requested.
- We may notify affected users of requests when permitted by law and where doing so would not pose a risk to safety, security, or legal obligations.
- We may reject or narrow overbroad, unsupported, or non-compliant requests.
- We handle all such requests in accordance with applicable law and our Privacy Policy.
## Safety and Crisis Resources
- If you or someone else is in **immediate danger**, contact your local emergency services first.
- For other urgent safety concerns on Fluxer (for example, threats, self-harm indications, or serious harassment), please:
- use in-app reporting tools; and/or
- email safety@fluxer.app with as much detail as possible so we can review quickly.
Fluxer cannot provide medical, psychological, or legal advice. However, we will do our best to respond to safety-related reports promptly and, where appropriate, may work with relevant services or authorities consistent with applicable law.

View File

@@ -1,49 +0,0 @@
---
title: How to change your date of birth
description: How to update your date of birth on Fluxer by contacting our support team
category: account-settings
category_title: Account Settings
category_icon: gear
order: 10
snowflake_id: 1426347609450086400
---
Your date of birth helps us keep your account secure and apply age-based features correctly. Because of that, it can't be edited directly in the app. If something is wrong and you need it updated, our support team can help.
## What you'll need
When you contact support, please have:
- A valid government-issued ID so we can verify your identity
- The correct date of birth you want on your account
- A short explanation of why it needs to be changed (for example, a typo during sign-up)
## How to contact support
1. Email us at support@fluxer.app from the email address linked to your Fluxer account.
2. Use the subject line: `Date of birth change request`.
3. In the email, include:
- The correct date of birth
- Why the current date of birth is wrong
- A clear photo of your government-issued ID as an attachment
## After you send your request
1. Our support team will review your request, usually within **2448 hours**.
2. We may ask for additional details if something is unclear.
3. Once everything is verified, we'll update the date of birth on your account.
4. You'll get a confirmation email when the change is complete.
Most requests are fully processed within **23 business days**.
## Things to know
- **Limited changes**: For security reasons, we generally allow only one date of birth change per account.
- **Age requirements**: If the new date of birth changes your eligibility for certain features, your access may be updated to match.
- **ID handling**: Any ID you send is handled securely and deleted after the verification process is complete.
## Age-restricted features
Changing your date of birth may affect access to:
- Age-restricted channels and communities
- Certain platform features
- Content filters and safety settings

View File

@@ -1,54 +0,0 @@
---
title: How to delete or disable your account
description: Learn how to permanently delete or temporarily disable your Fluxer account, and what happens to your data
category: account-settings
category_title: Account Settings
category_icon: gear
order: 20
snowflake_id: 1445724566704881664
---
If you want to take a break from Fluxer or leave the platform entirely, you can either disable or delete your account. Here's what each option does and how to access them.
## How to access account options
1. Log in to your Fluxer account at [web.fluxer.app/login](https://web.fluxer.app/login).
2. Press the cogwheel icon in the bottom-left corner of the screen to open Settings.
3. Navigate to **Security & Login**.
4. Choose either **Delete account** or **Disable account**.
## Deleting your account
Deleting your account schedules a permanent deletion in **14 days**. During this period, you can cancel the pending deletion by logging into your account again.
After the 14-day period, your account and associated data will be permanently removed and cannot be recovered.
**Important**: Accounts are automatically deleted if they remain inactive for 2 years.
### What happens to your messages
When you delete your account, all your messages will remain on the platform unless you delete them first. You have several options:
- **Delete all messages**: Use the Privacy Dashboard to delete all your messages at once. See our guide on <% article 1445730947679911936 %> for detailed instructions.
- **Export your data first**: Before deleting messages, you may want to export your data to keep a personal backup. Learn more about <% article 1445731738851475456 %>.
- **Request specific data deletion**: Contact **privacy@fluxer.app** from your registered email address if you want to delete specific data without deleting everything.
**Important**: You cannot delete your messages after you've deleted your account. Be sure to handle your data before initiating account deletion.
## Disabling your account
Disabling your account simply logs you out of all devices. Your account remains on the platform but is inaccessible until you log in again.
You can log in at any time in the future to re-enable your account and pick up where you left off.
## Summary
| Option | What it does | Reversible? |
|--------|--------------|-------------|
| **Disable account** | Logs you out of all devices | Yes, log in to re-enable |
| **Delete account** | Schedules permanent deletion in 14 days | Yes, within 14 days by logging in |
| **Data management** | Export or delete your messages and data | See linked guides |
For more information about managing your data, see:
- <% article 1445730947679911936 %>
- <% article 1445731738851475456 %>

View File

@@ -1,56 +0,0 @@
---
title: Requesting data deletion
description: Learn how to delete your messages and other data from Fluxer
category: account-settings
category_title: Account Settings
category_icon: gear
order: 21
snowflake_id: 1445730947679911936
---
Fluxer gives you control over your data. You can delete your messages and other content at any time through the Privacy Dashboard or by contacting our privacy team.
## What happens to your messages
When you delete your account, all your messages will remain on the platform unless you opt to delete them first. We provide several ways to delete your messages:
### Delete all messages through Privacy Dashboard
The quickest way to delete all your messages is through the Privacy Dashboard:
1. Log in to your Fluxer account at [web.fluxer.app/login](https://web.fluxer.app/login).
2. Press the cogwheel icon in the bottom-left corner to open Settings.
3. Navigate to **Privacy Dashboard**.
4. Select the **Data Deletion** tab.
5. Click **Delete all my messages**.
Mass-deletion requests are processed in the background and will complete fully as soon as possible, relative to the total volume of messages you have sent.
### Request specific data deletion
If you want to delete specific data without deleting all your messages, you can email **privacy@fluxer.app** from your account's registered email address. Our privacy team will process your request and assist you with deleting the specific data you indicate.
## Important information
### Attachments and deletion
When you delete a message containing attachments, the attachments will also be deleted. If you wish to keep your attachments, make sure to <% article 1445731738851475456 %> before deleting messages. Attachments also expire based on size; see <% article 1447193503661555712 %> for how long links last and how to extend them.
### Timing matters
You cannot delete your messages after you've deleted your account. If you want to remove your messages from the platform, be sure to do it before initiating account deletion.
For more information about account deletion, see our guide on <% article 1445724566704881664 %>.
## Automatic account deletion
Accounts are automatically deleted if they remain inactive for 2 years. This helps us maintain platform security and manage resources efficiently.
## Summary
| Method | What you can delete | Processing time |
|--------|---------------------|-----------------|
| **Privacy Dashboard** | All your messages | Background processing, completes ASAP |
| **Email privacy@fluxer.app** | Specific data you request | Handled by privacy team |
| **Account deletion** | Your account after 14 days | Cannot delete messages afterward |
| **Automatic deletion** | Inactive accounts after 2 years | Automatic |

View File

@@ -1,59 +0,0 @@
---
title: Exporting your account data
description: Learn how to request and download a complete export of your Fluxer data
category: account-settings
category_title: Account Settings
category_icon: gear
order: 22
snowflake_id: 1445731738851475456
---
Fluxer allows you to request a complete export of your account data, including all messages you've sent and information about any attachments.
## How to request a data export
1. Log in to your Fluxer account at [web.fluxer.app/login](https://web.fluxer.app/login).
2. Press the cogwheel icon in the bottom-left corner to open Settings.
3. Navigate to **Privacy Dashboard**.
4. Select the **Data Export** tab.
5. Click **Request Data Export**.
## What's included in your data export
Your data export package contains:
- All your user account information
- All messages you have sent across the platform
- URLs to download any attachments from your messages
## Downloading attachments
The data export includes URLs for downloading your attachments. However, you must download these attachments **before** deleting the messages that contain them.
**Important**: Deleting a message that contains attachments will also delete those attachments. If you want to preserve your attachments, download them from the URLs provided in your data export before deleting any messages.
## Receiving your data export
Once you request a data export:
1. Your request will be processed and a data package will be prepared
2. When ready, you'll receive an email at your account's registered email address
3. The email will contain a download link to a zip file
4. This download link is valid for **7 days**
## Request frequency
You can request a data export once every **7 days**. This helps us manage server resources while ensuring you have regular access to your data.
## What to do next
After exporting your data, you may want to:
- Review your message history
- Download important attachments before deleting messages. Attachments expire based on size; see <% article 1447193503661555712 %>.
- Keep a personal backup of your Fluxer data
- Understand what information you've shared on the platform
For information about deleting your data after export, see our guide on <% article 1445730947679911936 %>.
For information about account deletion, see our guide on <% article 1445724566704881664 %>.

View File

@@ -1,58 +0,0 @@
---
title: How attachment expiry works on Fluxer
description: How we set attachment expiry, how access can extend it, and what to do before a file is removed
category: files-and-attachments
category_title: Files & Attachments
category_icon: paperclip
order: 1
snowflake_id: 1447193503661555712
---
Fluxer automatically expires older attachments. Smaller files stay available longer; bigger ones expire sooner. If people view a message with a file when it is close to expiring, we extend it so it stays available.
## How expiry is decided
- The clock starts when you upload. Viewing or downloading later does not restart it; we only refresh the expiry if the file is close to expiring (see below).
- Files **5 MB or smaller** keep links for about **3 years** (the longest window).
- Files near **500 MB** keep links for about **14 days** (the shortest window).
- Between **5 MB and 500 MB**, larger files get shorter windows and smaller files get longer ones.
- Files **over 500 MB** are not accepted on the current plan.
## Extending availability when accessed
- We only extend when a file is close to expiring: if a message with the file is loaded and the remaining time is inside the renewal window, we push the expiry forward.
- The renewal window depends on size. Small files can renew up to about **30 days**; the largest files renew up to about **7 days**. We cap the total lifetime to the size-based budget, so a 500 MB file will not suddenly gain an extra month.
- Multiple views inside the same window do not stack; one view is enough to refresh it. You do not have to click or download the file for this to happen.
- As long as people keep viewing the message before the file expires (and within the size-appropriate renewal window), it stays available. If no one views it and it reaches expiry, the link disappears.
## What happens after expiry
We regularly sweep expired attachments and delete them from our CDN and storage. There can be a short delay after the expiry time before removal.
## Why we expire attachments
- **Storage and bandwidth fairness**: Large media is costly to keep forever; expiring it keeps things fair for everyone.
- **Safety and privacy**: Clearing out long-lived uploads reduces the chance that old sensitive files linger.
- **Predictable limits**: Clear timeframes help you download what you need to keep.
## Keeping important files
- Download attachments you need before they expire.
- For full account exports (including attachment URLs), see <% article 1445731738851475456 %>. For deletion requests, see <% article 1445730947679911936 %>.
## Frequently asked questions
**Q: What if I have Plutonium, do my files stay for longer?**
At this time, all users, whether they have Plutonium or not, are subject to the same limits.
**Q: What if my workflow relies on guaranteed, persistent access to large files I am not regularly accessing?**
Fluxer is a messaging service, not a cloud storage platform. We suggest advanced users host files themselves if this is a concern. There are tools that let you upload screenshots or files to your own cloud storage and domain, then share those links on Fluxer or elsewhere while retaining full control.
**Q: Do I need to click or download a file to keep it available?**
No. If a message with the file is viewed in chat or search while the file is near expiry, we refresh its expiry window even if you do not click or download it.
**Q: What about Saved Media?**
Fluxer has a Saved Media feature that lets you keep up to **50** media files (or **500** if you have Plutonium)—images, videos, GIFs, or audio—to access across your accounts. Saved Media is **not** subject to attachment expiry; items you save there stay until you delete them.
**Q: Can I hide the "Expires on" note under attachments?**
Yes. Go to User Settings (cogwheel bottom left) > Messages & Media > Media and toggle off "Show Attachment Expiry Indicator" if you prefer not to see it.

View File

@@ -1,38 +0,0 @@
---
title: Reporting a bug to Fluxer
description: How to file clear, high-quality bug reports for Fluxer Support or our GitHub
category: support
category_title: Support
category_icon: question
order: 1
snowflake_id: 1447264362996695040
---
Use this guide to file a clear report so we can reproduce and fix the bug quickly. Screenshots, short screen recordings, and relevant logs or files speed up diagnosis.
## Bug report template
**Title:** Be specific, e.g., `Bug: media upload stalls at 95%`.
**Steps to reproduce:**
1. Step one (include exact clicks/taps, inputs, or shortcuts).
2. Step two.
3. Step three (note timing or ordering details).
**Expected result:** What you expected to happen.
**Actual result:** What happened instead (include exact errors or on-screen messages).
**System and client settings:** In User Settings, tap your client info at the bottom of the sidebar to copy it, then paste it here.
## Add evidence
Include anything that shows the issue—screenshots, short videos, logs, or sample files/exports. More detail means faster help.
## Submit your report
Email support@fluxer.app with the filled template. A concise subject, such as `Bug report: media upload stuck`, helps us triage faster. Prefer GitHub? File issues in the [Fluxer GitHub repository](https://github.com/fluxerapp/fluxer); use the correct template and check for existing reports first. High-quality, non-duplicate reports we approve—whether emailed or filed on GitHub—may earn an invite to our private Fluxer Testers community, where you can unlock the Bug Hunter badge.
## Security issues
If you believe the issue is security-related, visit our [Security Bug Bounty page](/security) instead of emailing Support. Follow the guidance there, include clear steps, why you think it is a security risk, and any impact you see. We respond quickly to assess, coordinate a fix, and discuss disclosure expectations.

View File

@@ -1,44 +0,0 @@
---
title: Fluxer Copyright & Intellectual Property Policy
description: How to report suspected copyright or intellectual property violations on Fluxer
category: support
category_title: Support
category_icon: question
order: 50
snowflake_id: 1453792298436395008
---
Fluxer respects the intellectual property of every creator. We expect the same level of respect from the whole Fluxer community.
## Copyright complaints
If you believe content on Fluxer infringes your copyright, the quickest way to ask for removal is to use our report form at [https://web.fluxer.app/report](https://web.fluxer.app/report) and choose the copyright or intellectual property option. You can also email **dmca@fluxer.app** with the subject line "DMCA Takedown Request." In either case, your notice should include:
- A description of the copyrighted work you believe has been infringed.
- The precise location of the allegedly infringing material on Fluxer (message links, channel IDs, etc.).
- A statement that you have a good faith belief that the disputed use is not authorized by you, your agent, or the law, and that the information in the notice is accurate.
- A statement, made under penalty of perjury, that you are the copyright or intellectual property owner or authorized to act on their behalf.
- Your contact information (mailing address, telephone number, and/or email) so we can reach you.
- Your physical or electronic signature.
Do not mislead our support or legal teams. Please do not file false or malicious reports, submit multiple reports about the same material, or ask other people to report the same content for you. Repeated violations of this guideline may lead to warnings or other penalties on your account.
## Counter-notices
If you believe the content that was removed or disabled did not infringe your rights or was taken down by mistake, send a counter-notice via **dmca@fluxer.app**. A complete counter-notice must contain:
- Identification of the content that was removed or disabled and its location before the removal (message links, channel IDs, etc.).
- A statement that you have a good faith belief the content was removed or disabled because of a mistake or misidentification.
- Your contact information (mailing address, telephone number, and/or email).
- A statement that you consent to the jurisdiction of the Federal District Court for the district where you live, or if you live outside the United States, the Northern District of California, and that you will accept service of process from the person who notified Fluxer of the alleged infringement.
- Your physical or electronic signature.
We cannot process counter-notices with missing or defective elements. If we identify any issues, we will notify you and ask you to submit a corrected notice.
Once we receive a valid counter-notice, we will forward it to the party that filed the complaint. If the dispute persists, the parties must resolve it in court. Unless the complaining rights holder tells us they have filed a court action against the content provider, member, or user, we may restore the removed material 10 to 14 business days (or longer) after we receive the counter-notice, at our discretion.
There are legal and financial consequences for fraudulent or bad faith submissions. Please make sure you are the rights holder or are authorized to act on their behalf before submitting a notice or counter-notice.
## Infringement consequences
We may delete or disable access to content that is alleged to infringe someone elses intellectual property. Repeat copyright or IP infringers may have their accounts terminated, and in serious cases we may take stronger enforcement actions.

View File

@@ -1,63 +0,0 @@
---
title: How long Fluxer keeps your information
description: Understand how long Fluxer retains different types of information and why we keep it
category: support
category_title: Support
category_icon: question
order: 51
snowflake_id: 1453793207971217408
---
Fluxer collects certain information while you use the platform. Our [Privacy Policy](/privacy) covers what we collect, how we use it, and the controls you have. This guide explains how long we keep different kinds of data and why we retain it.
## What must stay on file
We keep the information that helps us create your account, deliver Fluxer, meet our commitments, and comply with the law. This includes your username, email address, phone number, and basic usage data. The rest is optional, and you can remove it if you choose.
## Information you can delete directly from the services
You can remove anything you have posted while you still have access to the space where you posted it. For example:
- Delete individual messages or attachments directly inside Fluxer.
- Use our Privacy Dashboard article on <% article 1445730947679911936 %> to bulk delete your messages before deleting your account.
- If you are about to delete your account, follow <% article 1445724566704881664 %> for the full account deletion flow.
Deleted content is no longer visible to other users, though cached copies may take a short time to clear.
If you'd like our help deleting specific content, email **privacy@fluxer.app** from the email address associated with your account and let us know what you need removed.
## Information retained until your account is deleted
Most personal information stays with us for as long as you have an active Fluxer account. This helps us understand how people use the platform, fix problems, and build new features. Accounts that remain inactive for two years may be deleted automatically to keep the platform healthy.
Account deletion requests are completed after a 14-day waiting period so that deletions can be reversed in case they were made by mistake. During that time you can log in to cancel the deletion. Once the 14 days pass, we begin deleting identifying information and anonymizing the rest.
## Retention periods for specific purposes
We sometimes keep certain information longer to fulfill legal or operational obligations:
- **Age verification:** If you send an ID for an age verification appeal, we delete it within 60 days after the appeal is closed.
- **Backups:** Database backups are stored for 3045 days and then deleted.
- **Legal and tax compliance:** Transactional records are kept for the time periods required by law (for example, tax or accounting rules).
- **Safety and security:** We retain contact details (including emails and phone numbers) for 180 days after deletion for trust and safety purposes. If an account is flagged for Terms of Service violations, we may keep related information for up to two years to prevent repeat abuse and keep Fluxer safe.
- **Legal claims:** When you send us a support request about your data, we keep that conversation for up to five years after the ticket closes so we can defend or exercise legal rights if necessary.
- **Continuity of the service:** Even after you delete your account, the content you shared with others may remain for their access, even though it is no longer linked to you. Delete anything you still control before removing your account, or ask privacy@fluxer.app for help.
## What happens when you delete your account
Once you confirm deletion, we typically hold your account for 14 days before permanently removing it. This grace period lets you recover the account if the deletion was accidental. After 14 days, we delete identifying information and anonymize or aggregate what remains. Identifying data in backups can take up to 45 days to disappear.
We keep aggregated or anonymized information (information that can no longer identify you) even after deletion so we can understand platform trends and improve Fluxer.
## Your rights
Every user can control their information directly inside Fluxer or by contacting **privacy@fluxer.app**. If you live in the European Union, the GDPR gives you additional rights, which you can exercise through your account or by emailing us:
- Right of access to your personal data
- Right to rectify inaccurate personal data
- Right to erase your personal data
- Right to limit the processing of your personal data
- Right to data portability
- Right to object to the processing of your personal data
If you have questions about how long we keep something, want to exercise one of these rights, or need help deleting specific content, please contact **privacy@fluxer.app**.

View File

@@ -1,32 +0,0 @@
---
title: EU DSA Dispute Resolution Options
description: How EU users covered by the Digital Services Act can exercise their rights on Fluxer
category: support
category_title: Support
category_icon: question
order: 52
snowflake_id: 1453793221464293376
---
This article explains how EU users covered by the European Unions Digital Services Act (DSA) can exercise certain rights. Parts of the DSA only apply to online platforms and to specific types of decisions, so not every report or action on Fluxer is eligible for DSA handling.
## Reporting illegal content under the DSA
We work hard to remove violative content and bad actors from Fluxer. The easiest way to report content that violates our Terms of Service or Community Guidelines is through our in-app reporting surfaces inside Fluxer. Certain EU users also have the option to flag illegal content under the DSA through our web form at [https://web.fluxer.app/report](https://web.fluxer.app/report).
## DSA appeals rights
If the DSA applies to you and you disagree with one of the decisions listed below, you have six months from the date of the violation notice to appeal through our internal appeals process. You can find the link to appeal on the notice itself.
The DSA appeals mechanisms only apply to decisions that are based on a finding that the information provided is illegal or violates our Terms of Service, such as:
- removing or disabling access to content, or restricting its visibility;
- suspending or terminating access to Fluxer (either in whole or in part);
- suspending or terminating a users Fluxer account;
- suspending, terminating, or restricting a users ability to monetize their Fluxer activity.
## Out-of-court dispute settlement
You may also have the option of choosing an out-of-court settlement body that has been certified by a Digital Services Coordinator in an EU Member State to help resolve a dispute related to any of the decisions listed above. [The European Commission](https://commission.europa.eu/index) maintains a website that lists those settlement bodies as they become certified.
Fluxer will cooperate with such a settlement body when required by law, but we are not bound by the decisions they issue. We also reserve the right not to engage with an out-of-court settlement body if the same dispute (same information and grounds) has already been resolved.

View File

@@ -1,561 +0,0 @@
**Effective: 1 January 2026**
**Last updated: 1 January 2026**
## Table of Contents
- [Privacy Policy Summary](#privacy-policy-summary)
- [1. Who We Are](#1-who-we-are)
- [2. Information We Collect](#2-information-we-collect)
- [3. How We Use Your Information](#3-how-we-use-your-information)
- [4. Who We Share Information With](#4-who-we-share-information-with)
- [5. Content Scanning for Safety](#5-content-scanning-for-safety)
- [6. Data Storage and International Transfers](#6-data-storage-and-international-transfers)
- [7. Data Retention](#7-data-retention)
- [8. Your Privacy Controls](#8-your-privacy-controls)
- [9. Data Security](#9-data-security)
- [10. Your Privacy Rights](#10-your-privacy-rights)
- [11. Children's Privacy](#11-childrens-privacy)
- [12. Cookies and Similar Technologies](#12-cookies-and-similar-technologies)
- [13. Third-Party Services and Links](#13-third-party-services-and-links)
- [14. Law Enforcement and Legal Requests](#14-law-enforcement-and-legal-requests)
- [15. Changes to This Privacy Policy](#15-changes-to-this-privacy-policy)
- [16. Contact Us](#16-contact-us)
## Privacy Policy Summary
We take your privacy seriously and are committed to protecting your personal information. This Privacy Policy explains what data we collect, how we use it, and the choices you have.
In summary:
- We do not sell, rent, or trade your personal data to third parties. We also do not "sell" or "share" personal information for cross-context behavioral advertising as defined by the CCPA/CPRA in relation to Fluxer. When you interact with third-party content such as embedded YouTube videos, those third parties may collect and use information under their own privacy policies, which may include advertising in their own services.
- The service is not end-to-end encrypted, but we use strong encryption for data in transit and at rest.
- We aim to collect only the minimum data necessary to provide, secure, and improve our service.
- We do not train AI models on your messages, files, or any other content you create or share on Fluxer.
- You can export, manage, and delete your data through your privacy dashboard and related tools.
- We handle personal data in line with applicable privacy laws, such as GDPR in the EEA/UK and CCPA/CPRA in California, based on your location and use of our services.
- We log limited feature usage and operational events to keep the service reliable and secure. We do not use this data for behavioral advertising or cross-site tracking.
- Where we use automated systems that significantly affect your access to Fluxer (for example, regional eligibility checks based on IP geolocation), we do so in line with applicable law and describe your rights in this policy.
This summary is provided for convenience. You should read the full policy below to understand how we handle your information.
## 1. Who We Are
We are **Fluxer Platform AB**, a Swedish limited liability company (Swedish organization number: 559537-3993). We operate the Fluxer chat platform and related services.
Fluxer Platform AB is the "data controller" for your personal data when you use Fluxer, meaning we determine how and why your personal data is processed.
As a company based in Sweden, we are subject to and comply with European Union data protection laws, including the General Data Protection Regulation (GDPR).
You can find our contact details and our privacy contact's details in [Section 16](#16-contact-us).
## 2. Information We Collect
We collect information in three main ways: (1) information you provide to us, (2) information we collect automatically, and (3) information we receive from other sources.
### 2.1 Information You Provide to Us
This includes:
- **Account information:**
Email address, username, password (stored using a modern, secure password hashing algorithm, currently Argon2id), date of birth, and optionally your phone number for two-factor authentication (2FA) or account recovery where available.
- **Content and communications:**
Messages, files, images, voice and video communications (where supported), community information, reactions, and profile data (such as avatar, bio, and other information you choose to display). This also includes information about Communities you create or administer.
- **Support and correspondence:**
Information you provide when you contact our support team or interact with customer service, including the content of messages, attachments, and any additional details you choose to provide.
- **Payment information:**
If you purchase premium features (such as Fluxer Plutonium), payment processing is handled securely by Stripe. We do not store your full payment card details. Stripe provides us with limited information necessary to record and manage your purchases (for example, billing country, partial card details, payment status, and timestamps).
We do not require you to provide special categories of personal data (such as information about your health, religion, or political beliefs). If you choose to share such information in your messages or profile, you do so at your own discretion.
### 2.2 Information We Collect Automatically
When you use Fluxer, we automatically collect certain technical and usage information, including:
- **Device and technical information:**
IP address, browser type and version, operating system, device type, device identifiers, language settings, and similar technical data.
- **Usage information:**
Information about how you interact with Fluxer, such as:
- pages and screens visited within the app or site;
- features used (for example, voice calls, file uploads, reactions);
- timestamps and duration of sessions;
- approximate counts and types of events (for example, messages sent, communities joined), without reading message content for analytics; and
- crash reports and performance metrics.
- **Security and operational logs:**
Data generated by our systems to maintain security and reliability, such as:
- login attempts and authentication events;
- changes to account settings;
- rate limits, API errors, and system errors;
- IP-based signals related to spam, abuse, or unusual behavior.
We use this information to secure the service, detect and prevent abuse, improve performance, and understand which features are being used so we can prioritize support and improvements. We do not use this information for behavioral advertising or cross-site tracking.
### 2.3 Information From Other Sources
We may also receive information about you from:
- **Other users:**
When other users mention you, add you to Communities, send you messages, share content involving you, or otherwise interact with your account.
- **Service providers:**
Limited information from our service providers, such as delivery status of emails or SMS messages (for example, from Twilio), payment confirmations from Stripe, or security-related alerts from infrastructure providers.
- **Public or third-party sources:**
In some cases, we may receive information from publicly available sources or trusted partners for security, anti-fraud, or compliance purposes (for example, checking whether an IP address is associated with known abuse).
We combine this information with the information we collect directly and automatically to help operate, secure, and improve our services.
## 3. How We Use Your Information
We use your information for the following purposes:
- To provide, operate, and maintain the Fluxer platform and services.
- To create and manage your account.
- To deliver your messages, media, and other content to the intended recipients.
- To secure your account and prevent unauthorized access.
- To detect, investigate, and prevent abuse, fraud, spam, and violations of our terms or community guidelines.
- To send important service updates, security alerts, and administrative messages.
- To process payments, manage subscriptions, and handle financial transactions.
- To understand which features are used and how the service performs, so we can prioritize support, fix issues, and plan improvements.
- To comply with legal obligations and respond to lawful requests.
- To protect the safety, rights, and property of our users, the public, and Fluxer.
We do not use your messages, files, or any other content you create or share on Fluxer for targeted advertising or for training AI models.
### 3.1 Lawful Bases for Processing (GDPR)
If you are in the EEA, UK, or another jurisdiction with similar requirements, our legal bases for processing your personal data include:
- **Contract necessity:**
We process data that is necessary to provide the services you have requested under our Terms of Service, such as delivering messages, operating Communities, maintaining your account, and providing support.
- **Legitimate interests:**
We process data based on our legitimate interests, such as:
- securing the platform and preventing fraud and abuse;
- maintaining and improving service reliability and performance;
- understanding how features are used at an aggregate level; and
- communicating with you about changes to our services or policies.
When we rely on legitimate interests, we balance our interests against your rights and expectations and implement safeguards to protect your privacy.
- **Legal obligations:**
We process data when necessary to comply with legal obligations, such as:
- accounting, tax, and record-keeping;
- child protection and CSAM reporting obligations;
- responding to lawful requests from public authorities; and
- complying with applicable data protection, security, and consumer laws.
- **Consent:**
In limited cases, we may rely on your consent, for example:
- where required to send certain types of optional communications; or
- where local law requires consent for specific processing or cookie use on our marketing site.
Where we rely on consent, you can withdraw it at any time in your settings or by contacting us. Withdrawing consent does not affect the lawfulness of processing that took place before the withdrawal.
### 3.2 IP Address Geolocation
We may geolocate your IP address at a coarse level (city, state/region, and country) for the following purposes:
- providing security alerts when logins occur from new or unusual locations;
- showing you where your account is currently logged in for security monitoring;
- preventing fraud, detecting abuse, and protecting platform integrity;
- determining regional age requirements and access eligibility based on local laws; and
- meeting legal obligations related to export control and sanctions.
We use IP geolocation databases that run locally on our servers for internal platform functions. To determine whether you can access the platform based on your region, we also use a geolocation service provided by our content delivery network (CDN), which processes your IP address on their infrastructure using code we control.
If local laws require invasive age verification methods that we are not able or willing to implement (for example, requiring government ID uploads or biometric scans), we may restrict access to the platform from those regions. These restrictions are enforced using automated systems that rely primarily on IP geolocation and similar signals.
We generally do not manually override these automated determinations, but you may contact us if you believe your access has been restricted in error. Where applicable data protection laws grant you rights in relation to automated decision-making (for example, under GDPR), we will honor those rights as described in [Section 10](#10-your-privacy-rights).
## 4. Who We Share Information With
We do not sell your personal data. We share information only in the following limited circumstances:
### 4.1 When You Direct Us to Share
We share your information when you intentionally interact with others on Fluxer, for example:
- when you send messages to other users;
- when you join or participate in Communities;
- when you share content publicly or with specific groups;
- when your profile information is visible to others according to your settings; and
- when you choose to connect to or use integrations or third-party services (where available).
In these cases, other users can see the information you choose to share, and they may further share or store it outside Fluxer. We encourage you to be mindful about the content you share and with whom.
### 4.2 With Service Providers
We work with trusted third-party service providers who process data on our behalf to help us operate Fluxer. These providers include:
- **OVHcloud** primary hosting of our servers in Vint Hill, Virginia, USA.
- **Backblaze** encrypted backups stored in Amsterdam, Netherlands.
- **Cloudflare** content delivery network (CDN) and security services, including:
- delivery and caching of user-generated content on the `fluxerusercontent.com` domain;
- IP geolocation services to determine regional platform access eligibility; and
- CSAM scanning for user-uploaded media, as described in [Section 5](#5-content-scanning-for-safety).
- **Stripe** payment processing for subscriptions and other purchases.
- **Google** Tenor GIF search and YouTube embeds. GIF searches are sent from our servers to the Tenor API so Google does not see your IP address or device identifiers. For YouTube embeds, we fetch metadata server-side and only load a YouTube iframe from your device if and when you choose to play a video.
- **hCaptcha** backup CAPTCHA provider; users can choose hCaptcha instead of Cloudflare Turnstile for bot prevention challenges.
- **Cloudflare Turnstile** primary CAPTCHA provider for bot prevention.
- **Twilio** transactional emails and SMS notifications (for example, login codes, alerts).
- **Fastmail** support email infrastructure.
- **Porkbun** domain registration services.
Many of these providers (for example, OVHcloud, Backblaze, Stripe, Twilio, Fastmail, and Porkbun in its role as registrar) act as processors and process personal data only on our behalf, according to our instructions.
Other providers, such as Google (for YouTube videos and Tenor GIFs), Cloudflare Turnstile, and hCaptcha, may also process some data as independent controllers when you interact directly with their services (for example, when you play an embedded YouTube video or complete a CAPTCHA). In those cases, your use of those services is also governed by their own terms and privacy policies.
Where required by law, we have data processing agreements and standard contractual clauses or equivalent safeguards in place with providers that process personal data on our behalf, and we take steps to minimize the amount of personal data shared with all providers (for example, by proxying GIF searches and fetching YouTube metadata server-side).
### 4.3 When Required by Law or to Protect Rights
We may disclose your information if we reasonably believe it is necessary to:
- comply with a valid legal obligation, legal process, or enforceable governmental request;
- enforce our Terms of Service or other agreements;
- protect the safety, rights, or property of our users, the public, or Fluxer; or
- detect, prevent, or otherwise address fraud, security, or technical issues.
Where legally permitted and appropriate, we will attempt to notify you before disclosing your information in response to legal requests, especially if the request concerns your account or content.
### 4.4 Business Transfers
If we are involved in a merger, acquisition, reorganization, sale of assets, or similar transaction, your information may be transferred as part of that transaction. We will continue to protect your information in accordance with this policy and will notify you of any significant changes to how your data is handled.
## 5. Content Scanning for Safety
We use automated tools and, in limited circumstances, human review to help keep Fluxer safe and compliant with the law.
### 5.1 CSAM Scanning
We use Cloudflare's CSAM scanning capabilities on user-uploaded media delivered via our `fluxerusercontent.com` content delivery network (CDN) to help identify and block child sexual abuse material (CSAM). These tools compare media against hash lists from organizations such as the National Center for Missing & Exploited Children (NCMEC).
Key points:
- All user-uploaded media (for example, avatars, emojis, stickers, and file attachments) is routed through this CDN and is subject to CSAM scanning before or as it is delivered.
- Text messages and the main Fluxer application traffic do not route through this media CDN and are not scanned by this tool.
- When prohibited content is detected, Cloudflare blocks access to the content and notifies us so we can take appropriate action, which may include account suspension and reporting to relevant authorities.
### 5.2 Other Safety and Abuse Detection
In addition to CSAM scanning, we may use automated systems to:
- detect and block malware, phishing, spam, and other harmful or abusive content;
- prevent and mitigate harassment, raiding, or coordinated abuse;
- detect suspicious login attempts or access patterns; and
- enforce our Terms of Service and Community Guidelines.
These systems primarily rely on metadata, patterns, and signals such as frequency of messages, links, and other non-content indicators. We do not use your private messages or files to build behavioral advertising profiles.
Where necessary for enforcing our policies, ensuring safety, or responding to user reports, authorized staff may review specific content, subject to strict access controls and audit logging.
## 6. Data Storage and International Transfers
### 6.1 Where Your Data Is Stored
We store and process data in multiple locations in order to provide a reliable and performant service:
- **Primary servers:**
OVHcloud data center in Vint Hill, Virginia, USA.
- **Encrypted backups:**
Backblaze data center in Amsterdam, Netherlands.
- **User-generated content CDN:**
Cloudflare edge locations worldwide (for content on the `fluxerusercontent.com` domain).
### 6.2 International Data Transfers
Because we operate globally and use service providers located in different countries, your data may be transferred to and processed in countries outside your own, including the United States. These countries may have data protection laws that are different from the laws of your country.
Where required by law (for example, under GDPR), we ensure that appropriate safeguards are in place for international transfers, such as:
- standard contractual clauses approved by the European Commission or UK authorities;
- data processing agreements with appropriate security and privacy commitments; and
- additional technical and organizational measures, such as encryption and access controls.
We do not transfer your data to third parties for their own independent advertising or marketing purposes without your explicit consent.
You can contact us for more information about the safeguards we use for international data transfers (see [Section 16](#16-contact-us)).
## 7. Data Retention
We retain your personal data only for as long as necessary to fulfill the purposes described in this policy, to comply with legal obligations, to resolve disputes, and to enforce our agreements.
### 7.1 Active Accounts
For active accounts, we generally keep your personal data for as long as you use Fluxer. This includes your messages, Communities, and other User Content while your account remains active, unless you delete specific content yourself.
### 7.2 Attachments and Expiry
Attachments may remain available only for a limited time. Their availability can depend on factors such as file size, age, and how often they are accessed.
Saved Media (your personal library of pinned files) is handled differently and is not subject to the same expiry rules as regular attachments.
For the current details of how attachment availability, expiry, and Saved Media work, see <% article 1447193503661555712 %>.
### 7.3 Deleted Content
When you delete messages or other content:
- We remove them from our active systems within a reasonable period of time.
- Deleted data may remain in our backup systems and certain logs for a limited period before being permanently removed, except where we are required by law to retain specific data for longer.
For user-generated content cached on Cloudflare's edge network, we use [Cloudflare's Instant Purge](https://developers.cloudflare.com/cache/how-to/purge-cache/) to invalidate cached attachments as soon as possible after deletion. In practice, there may be short delays due to rate limits and global propagation.
The current retention periods for deleted messages, account data, and related backups are described in our help articles, including <% article 1445724566704881664 %> and <% article 1445730947679911936 %>.
### 7.4 Inactive Accounts and Account Deletion
We may delete accounts that are inactive for an extended period, and we also delete accounts when you request it or following certain enforcement actions, in line with our deletion procedures.
- For information about how we define inactivity, the steps that occur before deletion (including any notice periods), and what happens when you delete your account, see <% article 1445724566704881664 %>.
- For information about bulk deletion of your messages and other content before or after account deletion, see <% article 1445730947679911936 %>.
When an account is deleted:
- you lose access to the account and its associated data; and
- messages and content you posted in Communities or direct messages may remain visible to other users unless you delete them first (for example, via your Privacy Dashboard or the tools described in <% article 1445730947679911936 %>).
### 7.5 Payment and Transaction Data
We retain transaction records and related data for the period required by accounting, tax, and other legal obligations. We do not store full payment card numbers. Stripe and other payment providers may retain payment data in accordance with their own legal obligations and policies.
### 7.6 Logs and Security Data
Security logs, audit logs, and usage logs are retained only for as long as necessary for security, fraud prevention, troubleshooting, and compliance, and are then deleted or anonymized. We may retain certain logs longer where necessary to investigate security incidents, comply with legal obligations, or resolve disputes. Where relevant, we describe retention periods for particular types of data in our help articles.
## 8. Your Privacy Controls
You have several tools and settings to help you manage your data and privacy on Fluxer.
### 8.1 Privacy Dashboard and In-App Controls
Through your Privacy Dashboard and account settings, you can:
- **Request a data export:**
Request a full export of your account data (including messages and URLs to download attachments) via the Data Export tab. See <% article 1445731738851475456 %> for current limits and details.
- **Download attachments:**
Use the URLs in your data export to download attachments before deleting related messages or before attachments expire.
- **Delete messages:**
Delete individual messages in-app. When you delete a message, any attachments in that message are also deleted from active systems and eventually from caches and backups, as described above.
- **Bulk deletion:**
Delete all of your messages via the Data Deletion tab in the Privacy Dashboard. Processing may run in the background and can take time for large accounts. See <% article 1445730947679911936 %> for details.
- **Account deletion:**
Schedule deletion of your account, which completes after a grace period unless you sign in during that period to cancel. See <% article 1445724566704881664 %> for how this works.
Attachments have expiry windows; if you want to keep specific files, download or move them (or save them to Saved Media) before they expire.
### 8.2 Requests by Email
If you need specific data removed or modified without deleting everything:
- Email privacy@fluxer.app from the email address associated with your Fluxer account.
- Clearly describe what you want us to do (for example, delete specific content, correct account information, or provide a data copy).
- We may ask for additional information to verify your identity and confirm that you control the account.
If you want a copy of your data before deleting messages or your account, request an export first and wait for the export to complete.
## 9. Data Security
We implement technical and organizational measures to protect your personal data against accidental or unlawful destruction, loss, alteration, unauthorized disclosure, or access.
These measures include:
- industry-standard encryption for data in transit (TLS);
- strong encryption for data at rest on our servers and backups;
- secure, professionally managed data centers with physical security controls;
- regular security updates, patch management, and infrastructure hardening;
- rate limiting, anomaly detection, and other protections against abuse and attacks;
- strict access controls so that only authorized staff can access user data, on a need-to-know basis and for limited purposes; and
- regular encrypted backups for disaster recovery.
No online service can guarantee perfect security. We work continuously to improve our security posture and reduce risks.
### 9.1 Responsible Disclosure and Security Reports
If you discover a potential security vulnerability or issue:
- Visit our [Security Bug Bounty page](/security) and follow the reporting instructions there so we have enough detail to reproduce the issue.
- Do not publicly disclose the vulnerability before we have had a reasonable opportunity to investigate and address it.
- We appreciate responsible disclosure and may acknowledge your contribution if you wish (subject to your consent).
### 9.2 Data Breaches
If we become aware of a data breach that has a material impact on your personal data, we will:
- investigate and take appropriate remedial steps;
- notify you without undue delay when legally required; and
- notify relevant supervisory authorities when required by law.
Notifications will include information about what happened, what data may be affected, and steps you can take to protect yourself.
## 10. Your Privacy Rights
Depending on where you live, you may have certain rights regarding your personal data. These rights may include, for example under GDPR (EEA/UK) or CCPA/CPRA (California):
- **Right of access:**
Request confirmation of whether we process your personal data and, if so, receive a copy.
- **Right to rectification:**
Request correction of inaccurate or incomplete personal data.
- **Right to deletion ("right to be forgotten"):**
Request deletion of your personal data in certain circumstances, for example where it is no longer necessary for the purposes for which it was collected.
- **Right to restriction:**
Request that we restrict processing of your personal data in certain circumstances (for example, while we verify its accuracy or assess an objection).
- **Right to object:**
Object to certain processing of your personal data, including processing based on legitimate interests, and we will consider your objection in line with applicable law.
- **Right to data portability:**
Request a copy of certain personal data in a structured, commonly used, machine-readable format and ask us to transmit it to another controller where technically feasible.
- **Right to withdraw consent:**
Where processing is based on your consent, you can withdraw that consent at any time. This will not affect the lawfulness of processing that took place before you withdrew consent.
- **Rights under CCPA/CPRA (for California residents):**
- right to know what personal information we collect, use, disclose, and share;
- right to delete personal information in certain circumstances;
- right to correct inaccurate personal information;
- right to opt out of the sale or sharing of personal information (we do not sell or share personal information for cross-context behavioral advertising in relation to Fluxer); and
- right to be free from discrimination for exercising your rights.
### 10.1 Exercising Your Rights
You can exercise many of your rights directly through your Privacy Dashboard and account settings (for example, export, deletion, and correction of certain information).
You can also contact us at privacy@fluxer.app to exercise your rights. When you do so:
- we may need to verify your identity (for example, by asking you to reply from your registered email or provide additional details);
- we will respond as required by applicable law (typically within 30 days, or up to 45 days where permitted and necessary); and
- if we cannot fully comply with your request (for example, due to legal obligations, technical limitations, or the rights of others), we will explain why and what options you still have.
You may also authorize an agent to submit requests on your behalf where permitted by law. We may require proof that the agent is validly authorized.
### 10.2 Complaints to Supervisory Authorities
If you are in the EEA, UK, or another jurisdiction with a data protection authority, you have the right to lodge a complaint with your local supervisory authority if you believe your privacy rights have been violated.
If you are in Sweden, the relevant authority is the Swedish Authority for Privacy Protection (Integritetsskyddsmyndigheten). You can also contact your local authority in your country of residence.
We encourage you to contact us first so we can try to resolve your concerns directly.
### 10.3 Automated Decision-Making
We use automated systems in certain limited ways that may affect your use of Fluxer, such as:
- determining whether your approximate location is in a region where access to Fluxer is permitted based on IP geolocation and applicable laws; and
- detecting potential fraud, spam, or abusive behavior.
These systems can influence, for example, whether you can access Fluxer from a given region or whether certain actions are temporarily blocked while we investigate potential abuse.
Where applicable law (such as GDPR) grants you rights related to automated decision-making — such as the right to obtain human review, to express your point of view, or to contest certain decisions — you can contact us at privacy@fluxer.app. We will handle such requests in line with those laws and our legal obligations.
## 11. Children's Privacy
Users must meet the minimum age requirement in their region to create and use a Fluxer account. We determine eligibility based on your self-reported information and your approximate geographic location.
For the purposes of this policy and our Terms of Service:
- the **minimum age to use Fluxer** (the "Minimum Age") is the lowest age at which applicable law in your country permits you to use an online service like Fluxer. This is typically 13 years old, but it varies by country and may be higher in certain jurisdictions; and
- users who are **above the Minimum Age but under the age of legal majority** in their jurisdiction (for example, under 18 in many countries) may use Fluxer, but our Terms require a parent or guardian to review and agree to them on the user's behalf.
We use IP geolocation and similar signals to determine whether access is allowed from your region and to apply regional age-related rules.
Some jurisdictions require age verification methods that we are not able or willing to implement (for example, requiring government ID uploads or biometric scanning). Where such requirements apply, we may restrict account registration and platform access from those regions.
We generally do not manually override these automated regional determinations, but you may contact us if you believe your access has been restricted in error. Where applicable law grants minors or their guardians specific rights regarding automated decisions, we will comply with those obligations.
We do not knowingly collect personal information from children under the Minimum Age in their region. If we become aware that we have inadvertently collected personal information from a child who does not meet the Minimum Age requirement, we will take steps to delete that information and, where appropriate, delete the account.
If you are a parent or legal guardian and believe that your child has used Fluxer without your consent or does not meet the Minimum Age requirement, you may contact privacy@fluxer.app from the child's registered email address (or with sufficient proof of guardianship) to request deletion of their account and associated data.
## 12. Cookies and Similar Technologies
### 12.1 Fluxer Application
We do not use third-party advertising or tracking cookies for our own analytics or advertising in the Fluxer application. Any cookies we set are strictly necessary for the operation and security of the service. We also do not run advertising trackers or analytics SDKs in the app. Operational logging and limited feature-usage telemetry are stored server-side to keep the service reliable and secure and to understand which features are used. This server-side data is not used for advertising or cross-site profiling.
When you interact with embedded third-party content in the app (for example, a YouTube video or a CAPTCHA challenge), those third parties may set their own cookies or similar technologies under their own privacy policies.
### 12.2 Marketing Site
On our marketing website (`fluxer.app`):
- we set a single language-preference cookie to remember your language choice; and
- we do not use third-party advertising trackers or fingerprinting technologies.
If we introduce additional analytics on the site in the future, we will update this notice and, where required, seek your consent (for example, via a cookie banner or similar interface).
You can control cookie usage through your browser settings, which may allow you to block or delete cookies. Note that some site functionality may be affected if you disable cookies entirely.
## 13. Third-Party Services and Links
Fluxer may contain links to third-party websites, services, or content (for example, YouTube videos, GIF search, or other embedded content). When you use these features:
- we route requests through our servers where technically possible to reduce the amount of data sent directly from your device to third parties;
- for GIF search, your search terms are sent from our servers to the Tenor API, so Google does not see your IP address or device identifiers for those searches;
- for YouTube links, we fetch basic metadata from the YouTube API on our servers so we can render previews without your device contacting YouTube until you choose to play the video; and
- if you play an embedded YouTube video or interact with other embedded third-party content, that content is loaded directly from the third party (for example, in an iframe), and the third party may collect information from your device under its own terms and privacy policy.
Third-party services may have their own privacy policies and data practices that are separate from ours. We are not responsible for the privacy practices, content, or security of third-party services, and we encourage you to review the privacy policies of any third-party services you use.
## 14. Law Enforcement and Legal Requests
We take the privacy and security of our users seriously and carefully consider all legal requests for data.
- We respond to valid legal processes and requests that comply with applicable law, which may include court orders, warrants, or other legally binding requests.
- Requests should be directed to legal@fluxer.app and should clearly identify the requesting authority, legal basis, and scope of data requested.
- We may refuse or narrow requests that are overly broad, not legally valid, or inconsistent with applicable law.
- Where legally permitted and where it would not create a risk to safety, security, or legal obligations, we will attempt to notify affected users before disclosing their data so they have an opportunity to object.
We may disclose data without notice where we reasonably believe it is necessary to prevent harm, protect the safety of individuals, or respond to emergencies, in accordance with applicable law.
For additional operational details on how law enforcement requests interact with community safety and enforcement, you can also refer to the "Law Enforcement Requests" section of our [Community Guidelines](/guidelines).
## 15. Changes to This Privacy Policy
We may update this Privacy Policy from time to time to reflect changes in our practices, services, or legal obligations.
If we make significant changes, we will:
- provide at least 30 days' advance notice, where reasonably practicable, via email, in-app notifications, or notices on our website; and
- indicate the effective date at the top of this policy.
We maintain a changelog or archive of prior versions of this Privacy Policy for reference.
Your continued use of Fluxer after the updated policy takes effect constitutes your acceptance of the changes. If you do not agree with the updated policy, you should stop using Fluxer and, if you wish, delete your account.
## 16. Contact Us
For account-related and privacy-related requests, you should contact us from the email address associated with your Fluxer account wherever possible. This helps us verify your identity and protect your account.
### Privacy and Data Protection Contact
- Email: privacy@fluxer.app
- Contact person: Hampus Kraft, founder and CEO
- This is our primary contact point for privacy and data protection questions. We have not formally appointed a Data Protection Officer under GDPR.
You can use this address to exercise your privacy rights, ask questions about this policy, or raise concerns about how your data is handled.
### General Support
- Email: support@fluxer.app
- Website: [https://fluxer.app](https://fluxer.app)
### Postal Address
Fluxer Platform AB
Norra Kronans Gata 430
136 76 Brandbergen
Stockholm County, Sweden
Organization number: 559537-3993

View File

@@ -1,118 +0,0 @@
If you believe you have found a security vulnerability in Fluxer, please report it responsibly. This policy explains what is in scope, how to submit a report, what we need from you, and what you can expect from us.
## Safe harbor
If you follow this policy, act in good faith, and avoid privacy violations or service disruption, Fluxer will not pursue legal action against you for your security research.
## Table of contents
- [Who should read this](#who-should-read-this)
- [Scope](#scope)
- [In scope](#in-scope)
- [Out of scope](#out-of-scope)
- [How to report](#how-to-report)
- [What we need from you](#what-we-need-from-you)
- [Rewards and recognition](#rewards-and-recognition)
- [What to expect from us](#what-to-expect-from-us)
- [Safe testing rules](#safe-testing-rules)
## Who should read this
Security researchers, community members, and anyone who discovers a potential security issue in Fluxer should read this policy before sending a report. It explains what is in scope, how we triage findings, and how we acknowledge and reward responsible disclosures.
## Scope
### In scope
Fluxer websites, applications, and services operated by Fluxer Platform AB, including the domains below:
| In-scope domains |
| -------------------------------------------------- |
| `fluxer.gg`, `*.fluxer.gg` |
| `fluxer.gift`, `*.fluxer.gift` |
| `fluxerapp.com`, `*.fluxerapp.com` |
| `fluxer.dev`, `*.fluxer.dev` |
| `fluxerusercontent.com`, `*.fluxerusercontent.com` |
| `fluxerstatic.com`, `*.fluxerstatic.com` |
| `fluxer.media`, `*.fluxer.media` |
| `fluxer.app`, `*.fluxer.app` |
Also in scope:
- Infrastructure, systems, and operational services directly managed by Fluxer that impact authentication, authorization, payments, community data, or the processing of security- or privacy-relevant data (including user identifiers, account metadata, logs, analytics, telemetry, and similar signals).
- Abuse cases that enable unauthorized persistence, privilege escalation, or data disclosure when triggered through officially supported product features.
- Self-hosted Fluxer instances that declare trust in Fluxer security guidance, provided:
- the issue is reproducible on the latest official release as we ship it, and
- the issue is not solely caused by third-party modifications or local misconfiguration.
If you are unsure whether a target is in scope, email us and ask.
### Out of scope
The following are out of scope (not an exhaustive list):
- Third-party services, infrastructure, or integrations we do not control (for example partner communities' independent integrations, bots, or external hosting providers).
- Vulnerabilities that require physical access to facilities, servers, or devices.
- Social engineering, phishing, bribery, coercion, or attempts to manipulate Fluxer staff or users.
- Denial-of-service (DoS) attacks, traffic flooding, rate-limit exhaustion, or resource exhaustion testing.
- Automated scanning or bulk testing that produces noisy/low-signal findings, especially without a clear security impact and a reliable reproduction path.
- General UI bugs, feature requests, or non-security support issues (email support@fluxer.app for those).
- Issues in forked, modified, or outdated self-hosted deployments that are not reproducible on the latest official release.
In addition, we generally do not prioritize low-impact reports (for example missing best-practice headers or minor configuration issues) unless you can demonstrate a concrete security impact.
## How to report
Email your report to security@fluxer.ap.
Please include:
- A short, descriptive title.
- Why the issue is a security concern (impact, affected users/systems, and realistic attack scenario).
- Step-by-step reproduction instructions and any proof of concept (screenshots, logs, recordings, or curl commands are helpful).
Please do not publicly disclose the vulnerability until we have acknowledged your report and had a reasonable opportunity to investigate and ship a fix. We may coordinate a disclosure timeline with you.
## What we need from you
To help us validate and fix the issue quickly, include as much of the following as you can:
- A clear summary of the issue and its impact.
- Step-by-step reproduction instructions.
- The environment you used (browser, operating system, client version, region, logged-in state, etc.).
- Any mitigations you tried, and whether the issue persists after clearing caches, using a private window, or restarting clients.
- A severity estimate (for example a CVSS score), or a plain-language assessment of the access/impact the issue enables.
## Rewards and recognition
Depending on validity, severity, and impact, we may award:
- A Bug Hunter badge on your Fluxer profile.
- Plutonium gift codes on fluxer.app so you can access premium features.
Higher-severity findings receive more recognition. We intend to add cash payouts in the future once our payments tooling is ready. At this time, we do not guarantee monetary rewards, but we do credit valid research that follows this policy.
### Credit and eligibility
- Please report findings privately to security@fluxer.app.
- Public disclosure before we acknowledge and address the issue may make the report ineligible for rewards or recognition.
- If multiple reports describe the same underlying issue, we typically credit the first report that clearly explains the vulnerability and enables reliable reproduction.
## What to expect from us
- **Acknowledgement:** We aim to acknowledge reports within five business days (often sooner).
- **Triage and updates:** We will review your report, prioritize critical issues, and keep you updated as we investigate.
- **Resolution and disclosure:** After a fix is available, we typically coordinate disclosure and credit with the reporter, unless you prefer to remain anonymous.
- **If we cannot reproduce:** We will share what we tried and may ask for additional details, environment information, or a clearer proof of concept.
## Safe testing rules
- Only test against accounts, communities, and data you own or have explicit permission to use.
- Community-level testing (roles, permissions, invites, moderation tools, settings, data access, etc.) must be performed only in communities you own/admin, or where you have explicit permission from the community owner/admin.
- Do not access, modify, or attempt to view other users' or other communities' data without consent.
- Do not use automated flooding, scraping, brute forcing, or other disruptive techniques.
- Do not use scanners or automated tools in ways that degrade reliability or create noisy/low-signal reports.
- If your testing could trigger real user notifications, support workflows, emails, billing events, or payments, contact us first so we can monitor.
- Follow applicable laws where you live and where the systems operate. If you are unsure, err on the side of caution and ask before escalating a high-impact test.
Thank you for helping keep Fluxer secure.

View File

@@ -1,490 +0,0 @@
**Effective: 1 January 2026**
**Last updated: 1 January 2026**
## Table of Contents
- [Welcome to Fluxer](#welcome-to-fluxer)
- [Definitions](#definitions)
- [1. Eligibility and Accounts](#1-eligibility-and-accounts)
- [2. Using Fluxer](#2-using-fluxer)
- [3. Your Content](#3-your-content)
- [4. Privacy and Data Protection](#4-privacy-and-data-protection)
- [5. Acceptable Use and Platform Integrity](#5-acceptable-use-and-platform-integrity)
- [6. Paid Services and Subscriptions](#6-paid-services-and-subscriptions)
- [7. Third-Party Services](#7-third-party-services)
- [8. Account Termination and Inactivity](#8-account-termination-and-inactivity)
- [9. Disclaimers, Limitation of Liability, and Indemnification](#9-disclaimers-limitation-of-liability-and-indemnification)
- [10. Dispute Resolution and Governing Law](#10-dispute-resolution-and-governing-law)
- [11. Changes to These Terms](#11-changes-to-these-terms)
- [12. Account Communications and Verification](#12-account-communications-and-verification)
- [13. Contact Information](#13-contact-information)
- [14. Export Controls and Sanctions](#14-export-controls-and-sanctions)
## Welcome to Fluxer
These terms of service ("Terms") constitute a legally binding contract between you and Fluxer Platform AB (Swedish organization number: 559537-3993). We are based in Stockholm County, Sweden, and we operate the Fluxer chat platform and related services. Throughout these Terms, references to "Fluxer", "we", "us" or "our" refer to Fluxer Platform AB and our Services.
By creating an account or using Fluxer in any way, you agree to be bound by these Terms, our [Privacy Policy](/privacy), and our [Community Guidelines](/guidelines). If you do not agree to these Terms, you must not use Fluxer.
If there is any conflict between these Terms and any applicable local mandatory law, the mandatory law will prevail.
## Definitions
For the purposes of these Terms:
- **"Services"** means the Fluxer applications (web, mobile, desktop), HTTP and WebSocket APIs, related websites and domains, and any other products, software, features, or services provided by Fluxer.
- **"User Content"** means any data, text, messages, media, files, communities, reactions, or metadata you or other users submit, upload, transmit, store, or display on or through the Services.
- **"Plutonium"** means Fluxer's optional paid subscription offering that provides enhanced features and benefits.
- **"Community"** means a server, space, or similar environment created or administered on Fluxer where users can communicate or share content.
- **"Community Owners"** means users who create, own, or administer Communities and who are responsible for setting rules (subject to these Terms and our Community Guidelines) and moderating those Communities.
- **"Account"** means a user account registered with Fluxer that is associated with a unique identifier and, typically, an email address.
- **"Privacy Policy"** means our [Privacy Policy](/privacy), which explains how we collect, use, and protect personal data.
- **"Community Guidelines"** means our [Community Guidelines](/guidelines), which govern acceptable behavior and content on Fluxer.
Capitalized terms that are not defined in this section have the meaning given to them elsewhere in these Terms.
## 1. Eligibility and Accounts
### 1.1 Eligibility to Use Fluxer
You may use the Services only if:
- you meet the minimum age required in your place of residence (the **"Minimum Age"**); and
- you are legally capable of entering into a binding contract with us, or your parent or legal guardian agrees to these Terms on your behalf as described below.
We determine your eligibility based on your self-reported information and your approximate geographic location. In many jurisdictions, the Minimum Age is 13 years, but it may be higher where you live.
If you are under the age of legal majority in your jurisdiction (for example, under 18 in many countries) but at or above the Minimum Age, your parent or legal guardian must review and agree to these Terms on your behalf before you use the Services.
By allowing a minor to use your Account or to use the Services with your permission, you confirm that you are the minor's parent or legal guardian, that you have reviewed and agreed to these Terms, and that you are responsible for the minor's activity on the Services.
Some countries, regions, or U.S. states have laws that require age-verification methods we are not able or willing to implement (for example, requirements to submit government ID or biometric data). Where such requirements apply, we may restrict or disable account registration and access to the Services from those regions.
We use automated systems (such as IP geolocation and similar technologies) to determine whether access to the Services is allowed from your region and to enforce regional restrictions. These determinations may be re-evaluated over time as laws or our capabilities change. We generally do not manually override these determinations, but you may contact us if you believe your access has been restricted in error. Where applicable law provides you with rights related to automated decision-making, we will honor those rights as described in our [Privacy Policy](/privacy).
You must not use the Services if:
- applicable law in your jurisdiction prohibits you from doing so;
- you are subject to relevant export control or sanctions restrictions, as described in Section 14; or
- your Account has previously been terminated or disabled by us for breach of these Terms, unless we have expressly agreed in writing to allow you to use the Services again.
### 1.2 Your Account and Security
To use most features of Fluxer, you must create an Account. You are solely responsible for:
- maintaining the confidentiality and security of your login credentials;
- all activities that occur under your Account, whether or not authorized by you, **except where applicable law provides that you are not responsible (for example, where activity results from a security incident for which we are responsible);**
- providing accurate, current, and complete information during registration; and
- keeping your Account information up to date when it changes.
You must promptly notify us at support@fluxer.app if you become aware of any unauthorized access to or use of your Account.
We strongly recommend that you:
- use a strong, unique password and change it periodically; and
- enable two-factor authentication (2FA) where available.
You are also responsible for securing the devices and software you use to access the Services and for keeping them free of malware.
Nothing in this Section 1.2 affects any non-waivable rights you may have under applicable consumer or payment laws in relation to unauthorized charges or security incidents.
### 1.3 Consumer Use and Custom Contracts
These Terms govern your use of Fluxer as a consumer and for general personal or community use.
If you or your organization negotiate and sign a separate written enterprise, business, or custom agreement with us that expressly supersedes these Terms, that agreement will govern to the extent it conflicts with these Terms. In all other respects, these Terms will continue to apply.
## 2. Using Fluxer
### 2.1 Permitted Uses
Fluxer is a communication and community platform where you can:
- send and receive messages, files, and media;
- create, manage, and participate in Communities;
- engage in voice and video communications with other users;
- build and moderate Communities around shared interests, subject to these Terms and our [Community Guidelines](/guidelines); and
- subscribe to Plutonium for access to premium features and benefits.
Your use of the Services must always comply with these Terms, our [Community Guidelines](/guidelines), and applicable laws and regulations.
### 2.2 Prohibited Uses
You must not use the Services to:
- violate any applicable local, national, or international law or regulation;
- promote, glorify, encourage, or provide instructions for self-harm or harm to others, or threaten, harass, or bully other users;
- violate our [Community Guidelines](/guidelines) in any way;
- abuse our free Services or platform resources (see Section 5 below);
- infringe, misappropriate, or violate the intellectual property or other rights of others;
- distribute malware, viruses, or other harmful code, or engage in cyberattacks or unauthorized access attempts;
- impersonate any person, entity, or organization, or otherwise misrepresent your affiliation with a person or entity;
- circumvent, disable, or interfere with security-related features or access controls; or
- engage in any activity that is otherwise prohibited by these Terms or our [Community Guidelines](/guidelines).
### 2.3 Service Changes and Availability
- The Services are provided without any service-level agreement (SLA). Outages, interruptions, and performance issues may occur.
- We may add, modify, or remove features, or discontinue parts of the Services at any time. When we make significant changes, we will provide notice where reasonably practicable (for example, via email, in-app notifications, or update notes).
- Certain features may be limited, unavailable, or different depending on your region, device, account type, or applicable law.
- We may temporarily limit or suspend access to the Services or certain features for maintenance, security, legal, or technical reasons.
- If we make changes that significantly and adversely affect paid features you have already purchased, you may have additional rights under applicable consumer laws, including rights to refunds or price reductions. Nothing in these Terms limits any such mandatory rights.
## 3. Your Content
### 3.1 Content Ownership
You retain full ownership of all User Content you create and share on or through the Services. Except as described in Section 3.2, we do not claim ownership of your User Content.
You are responsible for ensuring that you have all necessary rights, licenses, and permissions to submit, upload, or share User Content on the Services and to grant the rights described in these Terms.
### 3.2 License to Fluxer
To operate, secure, and improve the Services, you grant Fluxer a limited license to your User Content.
Specifically, by submitting, uploading, or otherwise making User Content available on or through the Services, you grant Fluxer a worldwide, non-exclusive, royalty-free, transferable, and sublicensable license to:
- host, cache, store, reproduce, and display your User Content;
- use your User Content to operate, maintain, secure, and provide the Services;
- adapt and modify your User Content as reasonably necessary for technical purposes (such as compression, transcoding, formatting, or re-encoding media); and
- sublicense these rights to our service providers solely for the purpose of helping us provide the Services, subject to appropriate contractual safeguards.
We will not use your User Content to:
- train AI models; or
- share it with third parties for their own independent purposes without your explicit consent, except where we are required to do so by law or legal process, or as otherwise described in our [Privacy Policy](/privacy).
You can generally revoke this license for specific User Content by deleting that content from the Services, subject to the retention and backup periods described in these Terms and in our [Privacy Policy](/privacy), and subject to any obligations we may have to retain certain data under applicable law.
### 3.3 Content Deletion and Retention
When you delete User Content:
- we will remove it from our active systems within a reasonable period of time; and
- the content may continue to exist in encrypted backups and certain system logs for limited periods, after which it is deleted in accordance with our data retention practices.
In particular:
- Deleted content may persist in backup systems and certain logs for a limited period before it is fully removed.
- Cached files on our content delivery networks (CDNs) may require additional time to clear from all global locations.
- Attachments may only remain available for a limited time and may expire based on factors such as file size and age. Items you save to Saved Media are treated differently and are not subject to the same expiry rules as regular attachments. For the most up-to-date details on how attachment availability and expiry work, see <% article 1447193503661555712 %>.
If you plan to delete messages or your Account, download any attachments or other content you wish to keep before deletion or expiry. For information about exporting your data, see <% article 1445731738851475456 %>.
We may retain certain information even after you delete content or close your Account where we have a legitimate business need or legal obligation to do so, such as for security, fraud prevention, or compliance with legal obligations. Such retained data is handled in accordance with our [Privacy Policy](/privacy).
### 3.4 Content Scanning and Safety
We use automated systems and, where required, human review to help keep the Services safe and compliant with the law.
In particular:
- We scan user-uploaded media (such as avatars, emojis, stickers, and attachments) to help detect and prevent child sexual abuse material (CSAM) and to comply with our legal obligations, using tools provided by our content delivery network infrastructure. When prohibited content is detected, access to the media is blocked and we take appropriate action, which may include reporting to relevant authorities and terminating Accounts.
- We may also use automated tools and signals to detect spam, malware, and other harmful or abusive content or behavior, as described in our [Privacy Policy](/privacy) and our [Community Guidelines](/guidelines).
Our safety and moderation systems are designed to protect both users and the integrity of the platform. However, no system is perfect, and we cannot guarantee that all harmful content or behaviors will be identified or prevented.
### 3.5 Copyright and Intellectual Property
If you believe that content on Fluxer infringes your copyrights, please notify us at dmca@fluxer.app with the following information:
- your full name and contact details;
- a description of the copyrighted work you claim has been infringed;
- the specific URL(s) or location(s) of the allegedly infringing material on the Services;
- a statement that you have a good-faith belief that the use is not authorized by you, your agent, or the law; and
- a statement that the information in your notice is accurate and that you are the copyright owner or authorized to act on their behalf, plus your physical or electronic signature.
We may remove or disable access to allegedly infringing material and may notify the user who posted it. Where appropriate and in accordance with applicable law, repeat infringers may have their Accounts terminated.
If we remove content in response to a copyright notice, we may, where permitted by law, offer the affected user an opportunity to submit a counter-notice.
## 4. Privacy and Data Protection
We take your privacy seriously. Our [Privacy Policy](/privacy) explains in detail what personal data we collect, how we use it, and the rights you may have in relation to your data.
In summary:
- We do not sell, rent, or trade your personal data.
- The Services are not end-to-end encrypted. However, we use strong encryption for data in transit (for example, Transport Layer Security) and for data at rest.
- We aim to collect only the minimum data necessary to operate, secure, and improve the Services.
- Your data is primarily stored in data centers in the United States and the European Union/EEA, and certain user-generated content may be cached on content delivery networks with edge locations worldwide, as described in our [Privacy Policy](/privacy).
- We may geolocate your IP address for security features, regional compliance, fraud prevention, and to determine eligibility for the Services.
- You can export, manage, and delete your data through your privacy dashboard and by using the tools described in our [Privacy Policy](/privacy).
- Depending on where you live, you may have rights such as access, rectification, deletion, data portability, objection, and restriction of processing.
Please read our [Privacy Policy](/privacy) carefully. If there is any conflict between these Terms and the Privacy Policy concerning the handling of personal data, the Privacy Policy will control to the extent of the conflict.
## 5. Acceptable Use and Platform Integrity
### 5.1 Acceptable Use of Free Services
We offer a generous free tier, but we expect you to use it reasonably and for its intended communication and community purposes.
We may take enforcement action if you:
- use Fluxer primarily as unlimited cloud storage rather than for communication or community activities;
- accumulate excessive amounts of data or create unusual loads that negatively impact service quality or availability for other users;
- distribute malware or illegal content, or use the Services for malicious, fraudulent, or abusive purposes;
- use Fluxer infrastructure for command-and-control of malware, botnets, or other harmful systems; or
- deliberately attempt to stress test, overload, or degrade our infrastructure without our prior written permission.
We will not target normal users who are using Fluxer in good faith for its intended purposes. This policy exists to prevent abuse that harms the platform and other users.
### 5.2 Platform Integrity Protection
To protect the integrity and security of the Services, we actively monitor for and take action against:
- automated spam, bulk messaging, and abuse;
- large-scale or unauthorized data scraping, harvesting, or indexing operations;
- manipulation of platform metrics or engagement statistics;
- coordinated inauthentic behavior and fake engagement;
- community raiding, brigading, or mass harassment campaigns; and
- attempts to bypass or defeat our safety, moderation, or rate-limiting systems.
Violations of platform integrity may result in immediate content removal, feature restrictions, or Account suspension or termination, with or without prior warning, depending on the severity and risk.
## 6. Paid Services and Subscriptions
### 6.1 Payment Authorization
By providing a payment method to Fluxer, you:
- authorize us to charge your payment method for any Services you purchase, including recurring subscription fees where applicable;
- confirm that you have the legal right and authority to use that payment method;
- authorize our payment processor (currently Stripe) to store your payment information securely for current and future transactions, and authorize us to receive and store limited billing information (such as billing country, partial card details, and payment status) necessary to manage your purchases;
- authorize us to retry failed payments or charge backup payment methods if you have added them to your Account; and
- agree that we may share necessary payment information with Stripe and other payment providers solely to process transactions, prevent fraud, and comply with legal obligations.
You are responsible for all applicable taxes, fees, and charges related to your purchases, except where we are required by law to collect and remit them.
### 6.2 Fluxer Plutonium Subscription
We offer an optional premium subscription service called **Fluxer Plutonium** that provides enhanced features and benefits.
#### 6.2.1 Automatic Renewal
By subscribing to Plutonium, you agree to recurring automatic payments. Unless you cancel, your subscription will automatically renew at the end of each billing period (for example, monthly or annually), and we will automatically charge your payment method at each renewal date for the applicable subscription fee and any applicable taxes.
You may cancel your subscription at any time through your Account settings. Cancellation takes effect at the end of your current billing period, and you will retain access to premium features until that time. You will not receive a refund or credit for any partial billing period unless required by law or as otherwise described in these Terms.
#### 6.2.2 Price Changes
We may change subscription prices from time to time, including introducing different prices for new subscriptions, regions, or promotional offers.
If you already have an active, continuously renewing Plutonium subscription:
- price increases will **not** apply to your existing subscription while it remains active and in good standing; you will continue to be charged the price that applied when you started or last changed that subscription (excluding any temporary discounts that have expired); and
- we may, at our discretion, reduce your subscription price or apply discounts, and we will clearly indicate when such changes occur.
If your subscription is canceled, expires, or lapses (for example, due to non-payment) and you later start a new Plutonium subscription, the then-current price for new subscriptions will apply and will be shown to you before you confirm the new subscription.
We may provide advance notice of price changes, especially where they are significant, but changes to prices for new subscriptions do not affect the price of your existing active subscription.
Nothing in this section limits any mandatory rights you may have under applicable consumer protection laws.
### 6.3 Refunds and Restrictions
#### 6.3.1 Plutonium Subscription Refunds
You may request a refund within 14 days of purchasing a Plutonium subscription by emailing support@fluxer.app from your registered Account email address, unless applicable law grants you a longer or different cooling-off period.
After receiving a refund:
- your Plutonium subscription will be canceled, and access to premium features will end when the refund is processed; and
- we may temporarily restrict your ability to purchase a new Plutonium subscription for a period of up to 30 days to reduce the risk of payment abuse.
If you request refunds for Plutonium subscriptions multiple times, we may restrict or block your ability to purchase Plutonium again if we reasonably believe your refund requests are abusive or present an unacceptable risk of payment abuse. In exceptional cases, we may restore purchasing access at our discretion, taking into account your account history and the circumstances of the refunds.
Nothing in this section limits any mandatory refund, withdrawal, or cancellation rights that you may have under applicable consumer laws.
#### 6.3.2 Gift Code Refunds
If you purchase a Plutonium gift code and subsequently request a refund or initiate a chargeback for that purchase:
- the user who redeemed the gift code will lose access to their premium benefits, typically from the time we process the refund or receive notice of the chargeback; and
- we may notify the affected user via email or in-app notification explaining that their premium access was revoked due to a refund or chargeback related to the underlying purchase.
### 6.4 Failed Payments
If a payment fails to process:
- we will automatically attempt to retry charging your payment method a reasonable number of times;
- we may charge any backup payment methods you have added to your Account;
- we may suspend or downgrade your access to premium features until payment is successfully processed; and
- you remain responsible for any unpaid amounts owed to Fluxer.
We are not responsible for any fees, charges, penalties, or interest imposed by your bank, payment provider, or financial institution in connection with failed payments or chargebacks.
### 6.5 Chargebacks and Payment Disputes
If you believe there is an error or unauthorized charge on your Account, we encourage you to contact us first at support@fluxer.app so we can investigate and attempt to resolve the issue quickly.
If you initiate a chargeback or payment dispute with your bank or payment provider for a payment made to Fluxer, we may:
- temporarily suspend or restrict your access to paid features while the dispute is investigated; and
- limit your ability to make future purchases if we reasonably believe the dispute is unfounded or forms part of a pattern of abusive behavior.
If a chargeback is resolved against us and we have a reasonable basis to believe it was made in bad faith or as part of a pattern of abuse or fraud, we may close your Account or permanently restrict your ability to purchase Plutonium or other paid Services. We will consider any information you provide when deciding these measures.
Chargebacks can cause significant costs and processing burdens, including penalty fees and increased fraud scrutiny from payment providers. Please contact us before initiating a chargeback whenever possible so we can try to resolve the issue.
Stripe or your payment provider may also contact you directly with receipts, alerts, or dispute notices.
## 7. Third-Party Services
Fluxer integrates with and relies upon various third-party services to provide the Services, including hosting and infrastructure providers, payment processors, content delivery networks, security services, and communication platforms.
For a description of the categories of third-party services we use and how they handle data, please see our [Privacy Policy](/privacy).
These third-party services have their own independent terms of service and privacy policies. Your use of such services may be subject to those additional terms. Fluxer is not responsible for the content, availability, policies, or practices of third-party services.
Some integrations involve you interacting directly with third-party content (for example, playing an embedded YouTube video). In those cases, the third party may receive information directly from your device and process it as an independent controller under its own terms and privacy policy.
Where required by law or by the terms of our agreements with providers that process personal data on our behalf, we may share limited information with them to provide the Services, prevent fraud, and comply with legal obligations. We will do so in accordance with our [Privacy Policy](/privacy).
## 8. Account Termination and Inactivity
### 8.1 Voluntary Account Deletion
You may delete your Fluxer Account at any time through your Account settings. While we will be sorry to see you go, we respect your choice and will process your deletion request in accordance with our data retention practices and applicable law. For details about the deletion process, including any grace period during which you can cancel deletion, see <% article 1445724566704881664 %>.
### 8.2 Suspension and Termination by Fluxer
We may suspend or permanently terminate Accounts, or restrict access to the Services, if we reasonably believe that:
- the Account or user has violated these Terms or our [Community Guidelines](/guidelines);
- the Account has been used to engage in illegal activities or to facilitate unlawful conduct;
- the Services are being abused, including through spam, fraud, or other malicious behavior;
- the Account is involved in fraudulent chargebacks, payment disputes, or other payment abuse; or
- it is necessary to do so for security, platform integrity, or legal compliance reasons.
We will typically provide advance warning before suspending or terminating an Account, unless the violation is severe, poses immediate risk to our platform or users, involves illegal conduct, or we are legally prohibited from providing notice.
If we terminate your Account for cause, you may lose access to your User Content and any associated data, subject to applicable law and our data retention practices.
If you believe we have made a mistake in suspending or terminating your Account, you may appeal in accordance with our [Community Guidelines](/guidelines), for example by emailing appeals@fluxer.app from the email address associated with your Account.
### 8.3 Account Inactivity and Deletion
To protect user privacy and manage resources, we may delete inactive Accounts in accordance with our data retention and deletion practices.
- When an Account has been inactive (no sign-ins or other meaningful activity) for an extended period, it may be scheduled for deletion.
- Where feasible, we will provide advance notice to the registered email address of the Account before deletion due to inactivity.
- The current criteria for inactivity, any applicable notice periods, and deletion timelines are described in our help articles <% article 1445724566704881664 %> and <% article 1445730947679911936 %>.
When an Account is deleted:
- messages and content you posted in Communities or direct messages may remain visible to other users, unless you delete them first (for example, via your Privacy Dashboard or other tools we provide); and
- you may no longer be able to access your User Content, unless you exported or downloaded it beforehand.
You can remove messages and other content through your Privacy Dashboard or by contacting privacy@fluxer.app before deletion, subject to technical and legal limitations.
## 9. Disclaimers, Limitation of Liability, and Indemnification
### 9.1 No Warranties
We work diligently to maintain Fluxer as a reliable and stable platform, but the Services are provided on an "as is" and "as available" basis.
To the fullest extent permitted by law, Fluxer and its affiliates make no express or implied warranties or representations about the Services, including but not limited to warranties of merchantability, fitness for a particular purpose, non-infringement, or that the Services will be uninterrupted, secure, or error-free.
We cannot guarantee:
- 100% uptime or availability;
- that the Services will be free from defects, bugs, or vulnerabilities; or
- that content, information, or communications transmitted through the Services will always be delivered or stored.
### 9.2 Limitation of Liability
To the maximum extent permitted by applicable law, Fluxer and its affiliates will not be liable for:
- any indirect, incidental, consequential, special, or punitive damages; or
- any loss of profits, revenues, data, goodwill, or other intangible losses;
arising out of or in connection with your use of, or inability to use, the Services, whether based on contract, tort (including negligence), statutory law, or any other legal theory, even if we have been advised of the possibility of such damages.
To the extent we are liable under applicable law, our total aggregate liability for all claims arising out of or relating to the Services or these Terms will be limited to the greater of:
- EUR 100; or
- the total amount you have paid to Fluxer for the Services during the twelve (12) months immediately preceding the event giving rise to the claim.
Nothing in these Terms limits or excludes:
- any liability that cannot be limited or excluded under applicable law, such as liability for gross negligence, willful misconduct, or for death or personal injury caused by our negligence; or
- any non-waivable rights or remedies you may have under mandatory consumer protection laws.
If you are a consumer residing in the EU/EEA, the United Kingdom, or another jurisdiction with mandatory consumer protection laws, the limitations and exclusions in this Section 9.2 apply only to the extent permitted by those laws and do not affect your statutory rights.
### 9.3 Indemnification
To the extent permitted by applicable law, you agree to defend, indemnify, and hold harmless Fluxer, its officers, directors, employees, and agents from and against any claims, demands, actions, damages, losses, and expenses (including reasonable legal fees) arising out of or related to:
- your violation of these Terms or our [Community Guidelines](/guidelines);
- your violation of any applicable law or regulation; or
- your User Content, including any claim that your User Content infringes or misappropriates the intellectual property or other rights of a third party.
This indemnity obligation does not apply to the extent that a claim arises due to our own breach of these Terms or our negligence, willful misconduct, or other liability that cannot be excluded under applicable law.
## 10. Dispute Resolution and Governing Law
We want to resolve disputes fairly and efficiently.
- **Informal resolution first:** If you have a concern or dispute with us, please contact support@fluxer.app first. We will try to work with you in good faith to resolve the issue informally.
- **Governing law and courts:** Unless otherwise required by mandatory local law, these Terms and any disputes arising out of or relating to them or the Services are governed by Swedish law, without regard to its conflict-of-law rules. Any dispute, controversy, or claim arising out of or in connection with these Terms or the Services will be submitted to the courts of Stockholm, Sweden, which will have exclusive jurisdiction, subject to the small-claims provision below.
- **Small claims:** Either party may bring an individual claim in a competent small-claims court in a jurisdiction where venue is proper, instead of in the courts of Stockholm, Sweden.
- **Consumers in the EU/EEA and other regions:** If you are a consumer residing in the EU, EEA, or another jurisdiction that grants you mandatory rights to bring claims in the courts of your country of residence, nothing in these Terms limits those rights.
- **Class and representative actions:** To the maximum extent permitted by applicable law, any claims must be brought on an individual basis, and not as a plaintiff or class member in any class, collective, or representative action. If a waiver of class or representative actions is not enforceable for a particular claim, that claim may proceed as required by law in a court with proper jurisdiction, and the class or representative aspects of that claim will be handled as that court determines.
Nothing in this Section 10 prevents either party from seeking interim or injunctive relief from a court of competent jurisdiction to prevent or stop ongoing or imminent harm.
## 11. Changes to These Terms
We may update or modify these Terms from time to time, for example to reflect changes in our Services, legal requirements, or business practices.
If we make significant changes to these Terms, we will:
- provide at least 30 days' advance notice, where reasonably practicable, via email, in-app notifications, or by posting a notice on our website; and
- make the updated Terms available, indicating the date on which they take effect.
We maintain a changelog or archive of prior versions of these Terms for reference.
Your continued use of the Services after the updated Terms take effect constitutes your acceptance of the changes. If you do not agree to the updated Terms, you must stop using the Services and, if you wish, delete your Account before the changes take effect.
## 12. Account Communications and Verification
All account-related communications with Fluxer should be sent from the email address associated with your Account. We use this as our primary method of verifying your identity and ensuring we are communicating with the actual Account holder.
For security reasons, we normally only provide account support, share sensitive information, or make changes to your Account when you contact us from that email address. If you lose access to your registered email address, we may need additional information to verify your identity and might not always be able to recover or modify your Account.
Fluxer will never ask you to provide your password, full payment card number, or other highly sensitive security information via email. All official Fluxer emails originate from addresses ending in `@fluxer.app`. Be cautious of phishing attempts using similar-looking domain names or requesting sensitive information.
If you receive a suspicious message claiming to be from Fluxer, please do not click any links or provide any information. Instead, contact us directly at support@fluxer.app.
## 13. Contact Information
If you have questions, concerns, or need assistance with the Services or these Terms, you can contact us at:
- **Support email:** support@fluxer.app (for account-related matters, please contact us from the email address associated with your Account where possible)
- **Privacy email:** privacy@fluxer.app (for privacy and data protection requests)
- **Website:** [https://fluxer.app](https://fluxer.app)
### Postal Address
Fluxer Platform AB
Norra Kronans Gata 430
136 76 Brandbergen
Stockholm County, Sweden
Organization number: 559537-3993
For additional contact details (including press, security, and legal requests), see our company information page or the [Privacy Policy](/privacy).
## 14. Export Controls and Sanctions
You must comply with all applicable export control, sanctions, and related laws and regulations when using the Services.
In particular:
- You may not use the Services if you are located in, or are ordinarily resident in, a country or region subject to comprehensive embargoes or similar sanctions, or if you are on any applicable sanctions, denied-party, or restricted lists maintained by relevant authorities.
- You agree not to export, re-export, transfer, or otherwise make the Services or any related technology available in violation of applicable export control or sanctions laws.
- We may restrict or terminate access to the Services, and block or close Accounts, to comply with applicable export control and sanctions requirements or if we have reason to believe that you are subject to relevant restrictions.
If you have questions about how export controls or sanctions may apply to your use of the Services, you should seek your own legal advice, as we cannot provide legal advice regarding your specific obligations.

File diff suppressed because it is too large Load Diff

View File

@@ -1,171 +0,0 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (
"flag"
"fmt"
"image"
"image/png"
"os"
"path/filepath"
"github.com/chai2010/webp"
"github.com/disintegration/imaging"
"github.com/gen2brain/avif"
)
type imageConfig struct {
input string
name string
sizes []int
}
var defaultSizes = map[string][]int{
"desktop": {480, 768, 1024, 1920, 2560},
"mobile": {480, 768},
}
func main() {
desktop := flag.String("desktop", "", "Path to desktop screenshot (required)")
mobile := flag.String("mobile", "", "Path to mobile screenshot (required)")
outputDir := flag.String("output", "priv/static/screenshots", "Output directory")
flag.Parse()
if *desktop == "" || *mobile == "" {
fmt.Fprintln(os.Stderr, "Usage: preprocess-images -desktop <path> -mobile <path> [-output <dir>]")
flag.PrintDefaults()
os.Exit(1)
}
if err := os.MkdirAll(*outputDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create output directory: %v\n", err)
os.Exit(1)
}
images := []imageConfig{
{input: *desktop, name: "desktop", sizes: defaultSizes["desktop"]},
{input: *mobile, name: "mobile", sizes: defaultSizes["mobile"]},
}
fmt.Println("Starting image preprocessing...")
fmt.Printf("Output directory: %s\n\n", *outputDir)
totalFiles := 0
for _, img := range images {
if _, err := os.Stat(img.input); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "Input file not found: %s\n", img.input)
continue
}
fmt.Printf("Processing %s...\n", img.name)
src, err := imaging.Open(img.input)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to open %s: %v\n", img.input, err)
continue
}
bounds := src.Bounds()
fmt.Printf(" Original size: %dx%d\n\n", bounds.Dx(), bounds.Dy())
for _, width := range img.sizes {
if err := processImage(src, img.name, width, *outputDir); err != nil {
fmt.Fprintf(os.Stderr, "Error processing %s at %d: %v\n", img.name, width, err)
} else {
totalFiles += 3 // avif, webp, png
}
}
fmt.Println()
}
fmt.Println("Image preprocessing complete!")
fmt.Printf("Generated %d image files\n", totalFiles)
}
func processImage(src image.Image, name string, width int, outputDir string) error {
fmt.Printf("Processing %s at %dpx width...\n", name, width)
// Resize maintaining aspect ratio
resized := imaging.Resize(src, width, 0, imaging.Lanczos)
// AVIF
if err := saveAVIF(resized, filepath.Join(outputDir, fmt.Sprintf("%s-%dw.avif", name, width))); err != nil {
fmt.Printf(" Failed to generate avif: %v\n", err)
} else {
printFileSize(filepath.Join(outputDir, fmt.Sprintf("%s-%dw.avif", name, width)))
}
// WebP
if err := saveWebP(resized, filepath.Join(outputDir, fmt.Sprintf("%s-%dw.webp", name, width))); err != nil {
fmt.Printf(" Failed to generate webp: %v\n", err)
} else {
printFileSize(filepath.Join(outputDir, fmt.Sprintf("%s-%dw.webp", name, width)))
}
// PNG
if err := savePNG(resized, filepath.Join(outputDir, fmt.Sprintf("%s-%dw.png", name, width))); err != nil {
fmt.Printf(" Failed to generate png: %v\n", err)
} else {
printFileSize(filepath.Join(outputDir, fmt.Sprintf("%s-%dw.png", name, width)))
}
return nil
}
func saveAVIF(img image.Image, path string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return avif.Encode(f, img, avif.Options{Quality: 80, Speed: 6})
}
func saveWebP(img image.Image, path string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return webp.Encode(f, img, &webp.Options{Quality: 85})
}
func savePNG(img image.Image, path string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
encoder := png.Encoder{CompressionLevel: png.BestCompression}
return encoder.Encode(f, img)
}
func printFileSize(path string) {
info, err := os.Stat(path)
if err != nil {
return
}
sizeKB := float64(info.Size()) / 1024
fmt.Printf(" OK %s (%.2f KB)\n", filepath.Base(path), sizeKB)
}

View File

@@ -1,171 +0,0 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
package main
import (
"flag"
"fmt"
"image"
"image/png"
"os"
"path/filepath"
"github.com/chai2010/webp"
"github.com/disintegration/imaging"
"github.com/gen2brain/avif"
)
type imageConfig struct {
input string
name string
sizes []int
}
func main() {
android := flag.String("android", "", "Path to Android screenshot (required)")
ios := flag.String("ios", "", "Path to iOS screenshot (required)")
desktop := flag.String("desktop", "", "Path to Desktop screenshot (required)")
outputDir := flag.String("output", "../fluxer_static/marketing/pwa-install", "Output directory")
flag.Parse()
if *android == "" || *ios == "" || *desktop == "" {
fmt.Fprintln(os.Stderr, "Usage: preprocess-pwa-images -android <path> -ios <path> -desktop <path> [-output <dir>]")
flag.PrintDefaults()
os.Exit(1)
}
if err := os.MkdirAll(*outputDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create output directory: %v\n", err)
os.Exit(1)
}
// For dialog thumbnails, we use smaller sizes
// Android is portrait (360x780), so we use height-based sizing
// iOS and Desktop are landscape, so we use width-based sizing
images := []imageConfig{
{input: *android, name: "android", sizes: []int{240, 320, 480}},
{input: *ios, name: "ios", sizes: []int{320, 480, 640}},
{input: *desktop, name: "desktop", sizes: []int{320, 480, 640, 960}},
}
fmt.Println("Starting PWA image preprocessing...")
fmt.Printf("Output directory: %s\n\n", *outputDir)
totalFiles := 0
for _, img := range images {
if _, err := os.Stat(img.input); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "Input file not found: %s\n", img.input)
continue
}
fmt.Printf("Processing %s...\n", img.name)
src, err := imaging.Open(img.input)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to open %s: %v\n", img.input, err)
continue
}
bounds := src.Bounds()
fmt.Printf(" Original size: %dx%d\n\n", bounds.Dx(), bounds.Dy())
for _, width := range img.sizes {
if err := processImage(src, img.name, width, *outputDir); err != nil {
fmt.Fprintf(os.Stderr, "Error processing %s at %d: %v\n", img.name, width, err)
} else {
totalFiles += 3 // avif, webp, png
}
}
fmt.Println()
}
fmt.Println("PWA image preprocessing complete!")
fmt.Printf("Generated %d image files\n", totalFiles)
}
func processImage(src image.Image, name string, width int, outputDir string) error {
fmt.Printf("Processing %s at %dpx width...\n", name, width)
// Resize maintaining aspect ratio
resized := imaging.Resize(src, width, 0, imaging.Lanczos)
// AVIF
if err := saveAVIF(resized, filepath.Join(outputDir, fmt.Sprintf("%s-%dw.avif", name, width))); err != nil {
fmt.Printf(" Failed to generate avif: %v\n", err)
} else {
printFileSize(filepath.Join(outputDir, fmt.Sprintf("%s-%dw.avif", name, width)))
}
// WebP
if err := saveWebP(resized, filepath.Join(outputDir, fmt.Sprintf("%s-%dw.webp", name, width))); err != nil {
fmt.Printf(" Failed to generate webp: %v\n", err)
} else {
printFileSize(filepath.Join(outputDir, fmt.Sprintf("%s-%dw.webp", name, width)))
}
// PNG
if err := savePNG(resized, filepath.Join(outputDir, fmt.Sprintf("%s-%dw.png", name, width))); err != nil {
fmt.Printf(" Failed to generate png: %v\n", err)
} else {
printFileSize(filepath.Join(outputDir, fmt.Sprintf("%s-%dw.png", name, width)))
}
return nil
}
func saveAVIF(img image.Image, path string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return avif.Encode(f, img, avif.Options{Quality: 80, Speed: 6})
}
func saveWebP(img image.Image, path string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return webp.Encode(f, img, &webp.Options{Quality: 85})
}
func savePNG(img image.Image, path string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
encoder := png.Encoder{CompressionLevel: png.BestCompression}
return encoder.Encode(f, img)
}
func printFileSize(path string) {
info, err := os.Stat(path)
if err != nil {
return
}
sizeKB := float64(info.Size()) / 1024
fmt.Printf(" OK %s (%.2f KB)\n", filepath.Base(path), sizeKB)
}

View File

@@ -1,20 +0,0 @@
module fluxer_marketing/scripts
go 1.25.5
require (
github.com/chai2010/webp v1.4.0
github.com/disintegration/imaging v1.6.2
github.com/gen2brain/avif v0.4.4
github.com/schollz/progressbar/v3 v3.18.0
)
require (
github.com/ebitengine/purego v0.8.3 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/term v0.28.0 // indirect
)

View File

@@ -1,38 +0,0 @@
github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko=
github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gen2brain/avif v0.4.4 h1:Ga/ss7qcWWQm2bxFpnjYjhJsNfZrWs5RsyklgFjKRSE=
github.com/gen2brain/avif v0.4.4/go.mod h1:/XCaJcjZraQwKVhpu9aEd9aLOssYOawLvhMBtmHVGqk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,90 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Config} from '@app/Config';
import {Logger} from '@app/Logger';
import type {ICacheService} from '@fluxer/cache/src/ICacheService';
import {KVCacheProvider} from '@fluxer/cache/src/providers/KVCacheProvider';
import {createServiceTelemetry} from '@fluxer/hono/src/middleware/TelemetryAdapters';
import type {IKVProvider} from '@fluxer/kv_client/src/IKVProvider';
import {KVClient} from '@fluxer/kv_client/src/KVClient';
import {createMarketingApp} from '@fluxer/marketing/src/App';
import {resolveMarketingPublicDir} from '@fluxer/marketing/src/PublicDir';
import {throwKVRequiredError} from '@fluxer/rate_limit/src/KVRequiredError';
import {RateLimitService} from '@fluxer/rate_limit/src/RateLimitService';
import type {Context, Hono} from 'hono';
export interface MarketingApp {
app: Hono;
shutdown: () => void;
}
export function createApp(): MarketingApp {
let rateLimitService: RateLimitService | null = null;
if (Config.rateLimit) {
if (!Config.kvUrl) {
throwKVRequiredError({
serviceName: 'fluxer_marketing',
configPath: 'Config.kvUrl',
});
}
const kvProvider = createKVProvider(Config.kvUrl, Logger);
const cacheService: ICacheService = new KVCacheProvider({client: kvProvider});
rateLimitService = new RateLimitService(cacheService);
}
const telemetry = createServiceTelemetry({
serviceName: 'fluxer-marketing',
skipPaths: ['/_health', '/static'],
});
const {app, shutdown} = createMarketingApp({
config: {
env: Config.env,
port: Config.port,
host: Config.host,
secretKeyBase: Config.secretKeyBase,
basePath: Config.basePath,
apiEndpoint: Config.apiEndpoint,
appEndpoint: Config.appEndpoint,
staticCdnEndpoint: Config.staticCdnEndpoint,
marketingEndpoint: Config.marketingEndpoint,
apiRpcHost: Config.apiRpcHost,
gatewayRpcSecret: Config.gatewayRpcSecret,
releaseChannel: Config.releaseChannel,
buildTimestamp: Config.buildTimestamp,
rateLimit: Config.rateLimit,
},
logger: Logger,
publicDir: resolveMarketingPublicDir(),
rateLimitService: rateLimitService ?? undefined,
metricsCollector: telemetry.metricsCollector,
tracing: telemetry.tracing,
});
app.get('/_health', (c: Context) => c.json({status: 'ok'}));
return {app, shutdown};
}
function createKVProvider(kvUrl: string, _logger: typeof Logger): IKVProvider {
return new KVClient({url: kvUrl});
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {loadConfig} from '@fluxer/config/src/ConfigLoader';
import {
extractBaseServiceConfig,
extractBuildInfoConfig,
extractKVClientConfig,
} from '@fluxer/config/src/ServiceConfigSlices';
import {normalizeBasePath} from '@fluxer/marketing/src/UrlUtils';
const master = await loadConfig();
const marketingConfig = master.services.marketing;
if (!marketingConfig) {
throw new Error('services.marketing configuration is required for standalone marketing service');
}
export const Config = {
...extractBaseServiceConfig(master),
...extractKVClientConfig(master),
...extractBuildInfoConfig(),
port: marketingConfig.port,
host: marketingConfig.host,
secretKeyBase: marketingConfig.secret_key_base,
basePath: normalizeBasePath(marketingConfig.base_path),
apiEndpoint: master.endpoints.api,
appEndpoint: master.endpoints.app,
staticCdnEndpoint: master.endpoints.static_cdn,
marketingEndpoint: stripPath(master.endpoints.marketing),
apiRpcHost: master.gateway.rpc_endpoint,
gatewayRpcSecret: master.gateway.rpc_secret,
rateLimit: null,
};
export type Config = typeof Config;
function stripPath(value: string): string {
const url = new URL(value);
url.pathname = '';
url.search = '';
url.hash = '';
return url.toString().replace(/\/$/, '');
}

28
fluxer_marketing/src/HonoJsx.d.ts vendored Normal file
View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {JSX as HonoJSX} from 'hono/jsx';
declare global {
namespace JSX {
type Element = HonoJSX.Element;
interface IntrinsicAttributes extends HonoJSX.IntrinsicAttributes {}
interface IntrinsicElements extends HonoJSX.IntrinsicElements {}
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Config} from '@app/Config';
import {createServiceInstrumentation} from '@fluxer/initialization/src/CreateServiceInstrumentation';
export const shutdownInstrumentation = createServiceInstrumentation({
serviceName: 'fluxer-marketing',
config: Config,
});

View File

@@ -0,0 +1,23 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {createLogger, type Logger as FluxerLogger} from '@fluxer/logger/src/Logger';
export const Logger = createLogger('fluxer-marketing');
export type Logger = FluxerLogger;

View File

@@ -1,189 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/badge_proxy
import fluxer_marketing/config
import fluxer_marketing/geoip
import fluxer_marketing/i18n
import fluxer_marketing/locale.{type Locale, get_locale_from_code}
import fluxer_marketing/metrics
import fluxer_marketing/middleware/cache_middleware
import fluxer_marketing/router
import fluxer_marketing/visionary_slots
import fluxer_marketing/web
import gleam/erlang/process
import gleam/http/request
import gleam/list
import gleam/result
import gleam/string
import mist
import wisp
import wisp/wisp_mist
pub fn main() {
wisp.configure_logger()
let assert Ok(cfg) = config.load_config()
let i18n_db = i18n.setup_database()
let slots_cache =
visionary_slots.start(visionary_slots.Settings(
api_host: cfg.api_host,
rpc_secret: cfg.gateway_rpc_secret,
))
let badge_featured_cache =
badge_proxy.start_cache(badge_proxy.product_hunt_featured_url)
let badge_top_post_cache =
badge_proxy.start_cache(badge_proxy.product_hunt_top_post_url)
let assert Ok(_) =
wisp_mist.handler(
handle_request(
_,
i18n_db,
cfg,
slots_cache,
badge_featured_cache,
badge_top_post_cache,
),
cfg.secret_key_base,
)
|> mist.new
|> mist.bind("0.0.0.0")
|> mist.port(cfg.port)
|> mist.start
process.sleep_forever()
}
fn handle_request(
req: wisp.Request,
i18n_db,
cfg: config.Config,
slots_cache: visionary_slots.Cache,
badge_featured_cache: badge_proxy.Cache,
badge_top_post_cache: badge_proxy.Cache,
) -> wisp.Response {
let locale = get_request_locale(req)
let base_url = cfg.marketing_endpoint <> cfg.base_path
let country_code =
geoip.country_code(
req,
geoip.Settings(api_host: cfg.api_host, rpc_secret: cfg.gateway_rpc_secret),
)
let user_agent = case request.get_header(req, "user-agent") {
Ok(ua) -> ua
Error(_) -> ""
}
let platform = web.detect_platform(user_agent)
let architecture = web.detect_architecture(user_agent, platform)
let ctx =
web.Context(
locale: locale,
i18n_db: i18n_db,
static_directory: "priv/static",
base_url: base_url,
country_code: country_code,
api_endpoint: cfg.api_endpoint,
app_endpoint: cfg.app_endpoint,
cdn_endpoint: cfg.cdn_endpoint,
asset_version: cfg.build_timestamp,
base_path: cfg.base_path,
platform: platform,
architecture: architecture,
release_channel: cfg.release_channel,
visionary_slots: visionary_slots.current(slots_cache),
metrics_endpoint: cfg.metrics_endpoint,
badge_featured_cache: badge_featured_cache,
badge_top_post_cache: badge_top_post_cache,
)
use <- wisp.log_request(req)
let start = monotonic_milliseconds()
let response = case wisp.path_segments(req) {
["static", ..] -> {
use <- wisp.serve_static(
req,
under: "/static",
from: ctx.static_directory,
)
router.handle_request(req, ctx)
}
_ -> router.handle_request(req, ctx)
}
let duration = monotonic_milliseconds() - start
metrics.track_request(ctx, req, response.status, duration)
response |> cache_middleware.add_cache_headers
}
type TimeUnit {
Millisecond
}
@external(erlang, "erlang", "monotonic_time")
fn erlang_monotonic_time(unit: TimeUnit) -> Int
fn monotonic_milliseconds() -> Int {
erlang_monotonic_time(Millisecond)
}
fn get_request_locale(req: wisp.Request) -> Locale {
case wisp.get_cookie(req, "locale", wisp.PlainText) {
Ok(locale_code) ->
get_locale_from_code(locale_code) |> result.unwrap(locale.EnUS)
Error(_) -> detect_browser_locale(req)
}
}
fn detect_browser_locale(req: wisp.Request) -> Locale {
case request.get_header(req, "accept-language") {
Ok(header) -> parse_accept_language(header)
Error(_) -> locale.EnUS
}
}
fn parse_accept_language(header: String) -> Locale {
header
|> string.split(",")
|> list.map(string.trim)
|> list.map(fn(lang) {
case string.split(lang, ";") {
[code, ..] -> string.trim(code) |> string.lowercase
_ -> ""
}
})
|> list.filter(fn(code) { code != "" })
|> list.find_map(fn(code) {
let clean_code = case string.split(code, "-") {
[lang, region] -> lang <> "-" <> string.uppercase(region)
[lang] -> lang
_ -> code
}
get_locale_from_code(clean_code)
})
|> result.unwrap(locale.EnUS)
}

View File

@@ -1,219 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import gleam/erlang/process
import gleam/http/request
import gleam/httpc
import gleam/option.{type Option}
import wisp.{type Response}
pub const product_hunt_featured_url = "https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1057558&theme=light"
pub const product_hunt_top_post_url = "https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=1057558&theme=light&period=daily&t=1767529639613"
const stale_after_ms = 300_000
const receive_timeout_ms = 5000
const fetch_timeout_ms = 4500
pub opaque type Cache {
Cache(subject: process.Subject(ServerMessage))
}
type ServerMessage {
Get(process.Subject(Option(String)))
RefreshDone(fetched_at: Int, svg: Option(String))
}
type CacheEntry {
CacheEntry(svg: String, fetched_at: Int)
}
type State {
State(cache: Option(CacheEntry), is_refreshing: Bool)
}
pub fn start_cache(url: String) -> Cache {
let started = process.new_subject()
let _ = process.spawn_unlinked(fn() { run(started, url) })
let assert Ok(subject) = process.receive(started, within: 1000)
Cache(subject: subject)
}
fn run(started: process.Subject(process.Subject(ServerMessage)), url: String) {
let subject = process.new_subject()
process.send(started, subject)
let initial = State(cache: option.None, is_refreshing: False)
loop(subject, url, initial)
}
fn loop(subject: process.Subject(ServerMessage), url: String, state: State) {
let new_state = case process.receive(subject, within: stale_after_ms) {
Ok(Get(reply_to)) -> handle_get(subject, reply_to, url, state)
Ok(RefreshDone(fetched_at, svg)) ->
handle_refresh_done(fetched_at, svg, state)
Error(_) -> maybe_refresh_in_background(subject, url, state)
}
loop(subject, url, new_state)
}
fn handle_get(
subject: process.Subject(ServerMessage),
reply_to: process.Subject(Option(String)),
url: String,
state: State,
) -> State {
let now = monotonic_time_ms()
case state.cache {
option.None -> {
let svg = fetch_badge_svg(url)
process.send(reply_to, svg)
let new_cache = case svg {
option.Some(content) ->
option.Some(CacheEntry(svg: content, fetched_at: now))
option.None -> option.None
}
State(cache: new_cache, is_refreshing: False)
}
option.Some(entry) -> {
let is_stale = now - entry.fetched_at > stale_after_ms
process.send(reply_to, option.Some(entry.svg))
case is_stale && !state.is_refreshing {
True -> {
spawn_refresh(subject, url)
State(..state, is_refreshing: True)
}
False -> state
}
}
}
}
fn handle_refresh_done(
fetched_at: Int,
svg: Option(String),
state: State,
) -> State {
let new_cache = case svg {
option.Some(content) ->
option.Some(CacheEntry(svg: content, fetched_at: fetched_at))
option.None -> state.cache
}
State(cache: new_cache, is_refreshing: False)
}
fn maybe_refresh_in_background(
subject: process.Subject(ServerMessage),
url: String,
state: State,
) -> State {
let now = monotonic_time_ms()
case state.cache, state.is_refreshing {
option.Some(entry), False if now - entry.fetched_at > stale_after_ms -> {
spawn_refresh(subject, url)
State(..state, is_refreshing: True)
}
_, _ -> state
}
}
fn spawn_refresh(subject: process.Subject(ServerMessage), url: String) {
let _ =
process.spawn_unlinked(fn() {
let fetched_at = monotonic_time_ms()
let svg = fetch_badge_svg(url)
process.send(subject, RefreshDone(fetched_at, svg))
})
Nil
}
pub fn get_badge(cache: Cache) -> Option(String) {
let reply_to = process.new_subject()
process.send(cache.subject, Get(reply_to))
case process.receive(reply_to, within: receive_timeout_ms) {
Ok(svg) -> svg
Error(_) -> option.None
}
}
pub fn product_hunt(cache: Cache) -> Response {
case get_badge(cache) {
option.Some(content) -> {
wisp.response(200)
|> wisp.set_header("content-type", "image/svg+xml")
|> wisp.set_header(
"cache-control",
"public, max-age=300, stale-while-revalidate=600",
)
|> wisp.set_header("vary", "Accept")
|> wisp.string_body(content)
}
option.None -> {
wisp.response(503)
|> wisp.set_header("content-type", "text/plain")
|> wisp.set_header("retry-after", "60")
|> wisp.string_body("Badge temporarily unavailable")
}
}
}
fn fetch_badge_svg(url: String) -> Option(String) {
let assert Ok(req0) = request.to(url)
let req =
req0
|> request.prepend_header("accept", "image/svg+xml")
|> request.prepend_header("user-agent", "FluxerMarketing/1.0")
let config =
httpc.configure()
|> httpc.timeout(fetch_timeout_ms)
case httpc.dispatch(config, req) {
Ok(resp) if resp.status >= 200 && resp.status < 300 ->
option.Some(resp.body)
_ -> option.None
}
}
type TimeUnit {
Millisecond
}
@external(erlang, "erlang", "monotonic_time")
fn erlang_monotonic_time(unit: TimeUnit) -> Int
fn monotonic_time_ms() -> Int {
erlang_monotonic_time(Millisecond)
}

View File

@@ -1,121 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/i18n
import fluxer_marketing/web.{type Context}
import gleam/list
import kielet.{gettext as g_}
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn render(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
let coming_features = [
g_(i18n_ctx, "Federation"),
g_(i18n_ctx, "Opt-in E2EE messaging"),
g_(i18n_ctx, "Slash commands"),
g_(i18n_ctx, "Message components"),
g_(i18n_ctx, "DM folders"),
g_(i18n_ctx, "Threads and forums"),
g_(i18n_ctx, "Community templates"),
g_(i18n_ctx, "Publish forums to the web"),
g_(i18n_ctx, "RSS/Atom feeds for forums"),
g_(i18n_ctx, "Polls & events"),
g_(i18n_ctx, "Stage channels"),
g_(i18n_ctx, "Event tickets"),
g_(i18n_ctx, "Discovery"),
g_(i18n_ctx, "Public profile URLs"),
g_(i18n_ctx, "Profile connections"),
g_(i18n_ctx, "Activity sharing"),
g_(i18n_ctx, "Creator monetization"),
g_(i18n_ctx, "Theme marketplace"),
g_(i18n_ctx, "Global voice regions"),
g_(i18n_ctx, "Better noise cancellation"),
g_(i18n_ctx, "E2EE calls"),
g_(i18n_ctx, "Call pop-out"),
g_(i18n_ctx, "Soundboard"),
g_(i18n_ctx, "Streamer mode"),
]
html.section(
[
attribute.class(
"bg-gradient-to-b from-gray-50 to-white px-6 py-24 md:py-40",
),
],
[
html.div([attribute.class("mx-auto max-w-7xl")], [
html.div([attribute.class("mb-20 md:mb-24 text-center")], [
html.h2(
[
attribute.class(
"display mb-8 md:mb-10 text-black text-4xl md:text-5xl lg:text-6xl",
),
],
[
html.text(g_(i18n_ctx, "What's coming next")),
],
),
html.p(
[
attribute.class(
"lead mx-auto max-w-3xl text-gray-700 text-xl md:text-2xl",
),
],
[
html.text(g_(
i18n_ctx,
"The future is being built right now. These features are coming soon.",
)),
],
),
]),
html.div(
[
attribute.class("max-w-6xl mx-auto"),
],
[
html.div(
[
attribute.id("coming-features-list"),
attribute.class("coming-features-masonry"),
],
coming_features
|> list.map(fn(feature) {
html.span(
[
attribute.class(
"feature-pill inline-block rounded-xl sm:rounded-2xl bg-white px-4 py-2.5 sm:px-6 sm:py-3.5 md:px-7 md:py-4.5 text-sm sm:text-base md:text-lg font-semibold text-gray-900 shadow-md border border-gray-200 whitespace-nowrap",
),
],
[html.text(feature)],
)
}),
),
],
),
]),
],
)
}

View File

@@ -1,113 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/components/community_type
import fluxer_marketing/i18n
import fluxer_marketing/web.{type Context}
import kielet.{gettext as g_}
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn render(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
html.section(
[
attribute.class(
"bg-gradient-to-b from-white to-gray-50 px-6 py-24 md:py-40",
),
],
[
html.div([attribute.class("mx-auto max-w-7xl text-center")], [
html.h2(
[
attribute.class(
"display mb-16 md:mb-20 text-black text-4xl md:text-5xl lg:text-6xl",
),
],
[
html.text(g_(i18n_ctx, "Built for communities of all kinds.")),
],
),
html.div(
[
attribute.class(
"mb-16 md:mb-20 grid grid-cols-5 gap-2 sm:gap-4 md:gap-6 max-w-6xl mx-auto",
),
],
[
community_type.render(
"game-controller",
"blue",
g_(i18n_ctx, "Gaming"),
),
community_type.render(
"video-camera",
"purple",
g_(i18n_ctx, "Creators"),
),
community_type.render(
"graduation-cap",
"green",
g_(i18n_ctx, "Education"),
),
community_type.render(
"users-three",
"orange",
g_(i18n_ctx, "Hobbyists"),
),
community_type.render("code", "red", g_(i18n_ctx, "Developers")),
],
),
html.p(
[
attribute.class(
"lead lead-soft mx-auto max-w-4xl text-gray-700 leading-relaxed",
),
],
[
html.text(g_(
i18n_ctx,
"A chat platform that answers to you, not investors. It's ad-free, open source, community-funded, and never sells your data or nags you with upgrade pop-ups.",
)),
],
),
html.div(
[
attribute.class("my-6 mx-auto flex flex-col items-center gap-3"),
attribute.attribute("aria-hidden", "true"),
],
[],
),
html.p(
[
attribute.class(
"lead lead-soft mx-auto max-w-4xl text-gray-700 leading-relaxed",
),
],
[
html.text(g_(
i18n_ctx,
"Over time, we'd love to explore optional monetization tools that help creators and communities earn, with a small, transparent fee that keeps the app sustainable.",
)),
],
),
]),
],
)
}

View File

@@ -1,80 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/icons
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn render(icon: String, color: String, label: String) -> Element(a) {
let bg_class = case color {
"blue" -> "bg-gradient-to-br from-blue-400 to-blue-600"
"purple" -> "bg-gradient-to-br from-purple-400 to-purple-600"
"green" -> "bg-gradient-to-br from-emerald-400 to-emerald-600"
"orange" -> "bg-gradient-to-br from-orange-400 to-orange-600"
"red" -> "bg-gradient-to-br from-rose-400 to-rose-600"
_ -> "bg-gradient-to-br from-gray-400 to-gray-600"
}
let icon_element = case icon {
"game-controller" -> icons.game_controller
"video-camera" -> icons.video_camera
"graduation-cap" -> icons.graduation_cap
"users-three" -> icons.users_three
"code" -> icons.code_icon
_ -> fn(_) { html.div([], []) }
}
html.div([], [
html.div(
[
attribute.class(
"lg:hidden w-full aspect-square flex items-center justify-center rounded-xl p-3 sm:p-4 "
<> bg_class,
),
],
[
icon_element([
attribute.class(
"w-3/5 h-3/5 sm:w-1/2 sm:h-1/2 text-white flex-shrink-0",
),
]),
],
),
html.div(
[
attribute.class(
"hidden lg:flex w-full aspect-square flex-col items-center justify-center rounded-2xl p-6 xl:p-8 "
<> bg_class,
),
],
[
icon_element([
attribute.class("w-2/5 h-2/5 text-white flex-shrink-0"),
]),
html.p(
[
attribute.class(
"subtitle text-white font-bold text-center mt-4 xl:mt-5 whitespace-nowrap overflow-hidden text-ellipsis",
),
],
[html.text(label)],
),
],
),
])
}

View File

@@ -1,164 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/components/feature_card
import fluxer_marketing/i18n
import fluxer_marketing/web.{type Context}
import kielet.{gettext as g_}
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn render(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
html.section(
[
attribute.class(
"bg-gradient-to-b from-[#4641D9] to-[#3832B8] px-6 py-20 md:py-32",
),
],
[
html.div([attribute.class("mx-auto max-w-7xl")], [
html.div([attribute.class("mb-16 md:mb-20 text-center")], [
html.h2(
[
attribute.class(
"display mb-6 md:mb-8 text-white text-4xl md:text-5xl lg:text-6xl",
),
],
[
html.text(g_(i18n_ctx, "What's available today")),
],
),
html.p(
[
attribute.class("lead mx-auto max-w-3xl text-white/90"),
],
[
html.text(g_(
i18n_ctx,
"All the basics you expect, plus a few things you don't.",
)),
],
),
]),
html.div(
[attribute.class("grid gap-8 md:gap-10 grid-cols-1 lg:grid-cols-2")],
[
feature_card.render(
ctx,
"chats",
g_(i18n_ctx, "Messaging"),
g_(
i18n_ctx,
"DM your friends, chat with groups, or build communities with channels.",
),
[
g_(i18n_ctx, "Full Markdown support in messages"),
g_(i18n_ctx, "Private DMs and group chats"),
g_(i18n_ctx, "Organized channels for communities"),
g_(i18n_ctx, "Share files and preview links"),
],
"dark",
),
feature_card.render(
ctx,
"microphone",
g_(i18n_ctx, "Voice & video"),
g_(
i18n_ctx,
"Hop in a call with friends or share your screen to work together.",
),
[
g_(i18n_ctx, "Join from multiple devices at once"),
g_(i18n_ctx, "Screen sharing built in"),
g_(i18n_ctx, "Noise suppression and echo cancellation"),
g_(i18n_ctx, "Mute, deafen, and camera controls"),
],
"dark",
),
feature_card.render(
ctx,
"gear",
g_(i18n_ctx, "Moderation tools"),
g_(
i18n_ctx,
"Keep your community running smoothly with roles, permissions, and logs.",
),
[
g_(i18n_ctx, "Granular roles and permissions"),
g_(i18n_ctx, "Moderation actions and tools"),
g_(i18n_ctx, "Audit logs for transparency"),
g_(i18n_ctx, "Webhooks and bot support"),
],
"dark",
),
feature_card.render(
ctx,
"magnifying-glass",
g_(i18n_ctx, "Search & quick switcher"),
g_(
i18n_ctx,
"Find old messages or jump between communities and channels in seconds.",
),
[
g_(i18n_ctx, "Search through message history"),
g_(i18n_ctx, "Filter by users, dates, and more"),
g_(i18n_ctx, "Quick switcher with keyboard shortcuts"),
g_(i18n_ctx, "Manage friends and block users"),
],
"dark",
),
feature_card.render(
ctx,
"palette",
g_(i18n_ctx, "Customization"),
g_(
i18n_ctx,
"Add custom emojis, save media for later, and style the app with custom CSS.",
),
[
g_(i18n_ctx, "Upload custom emojis and stickers"),
g_(i18n_ctx, "Save images, videos, GIFs, and audio"),
g_(i18n_ctx, "Custom CSS themes"),
g_(i18n_ctx, "Compact mode and display options"),
],
"dark",
),
feature_card.render(
ctx,
"server",
g_(i18n_ctx, "Self-hosting"),
g_(
i18n_ctx,
"Run the Fluxer backend on your own hardware and connect with our apps.",
),
[
g_(i18n_ctx, "Fully open source (AGPLv3)"),
g_(i18n_ctx, "Host your own instance"),
g_(i18n_ctx, "Use our desktop client (mobile coming soon)"),
g_(i18n_ctx, "Switch between multiple instances"),
],
"dark",
),
],
),
]),
],
)
}

View File

@@ -1,155 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/icons
import fluxer_marketing/web.{type Context}
import gleam/list
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn render(
_ctx: Context,
icon: String,
title: String,
description: String,
features: List(String),
theme: String,
) -> Element(a) {
let card_bg = case theme {
"light" -> "bg-white"
"dark" -> "bg-white/95 backdrop-blur-sm"
_ -> "bg-white/95 backdrop-blur-sm"
}
let text_color = case theme {
"light" -> "text-gray-900"
"dark" -> "text-gray-900"
_ -> "text-gray-900"
}
let description_color = case theme {
"light" -> "text-gray-600"
"dark" -> "text-gray-700"
_ -> "text-gray-700"
}
html.div(
[
attribute.class(
"flex h-full flex-col rounded-2xl "
<> card_bg
<> " p-8 md:p-10 shadow-md border border-gray-100",
),
],
[
html.div([attribute.class("mb-6")], [
html.div(
[
attribute.class(
"inline-flex items-center justify-center w-16 h-16 md:w-20 md:h-20 rounded-2xl bg-gradient-to-br from-[#4641D9]/10 to-[#4641D9]/5 mb-5",
),
],
[
case icon {
"chats" ->
icons.chats([
attribute.class("h-8 w-8 md:h-10 md:w-10 text-[#4641D9]"),
])
"microphone" ->
icons.microphone([
attribute.class("h-8 w-8 md:h-10 md:w-10 text-[#4641D9]"),
])
"palette" ->
icons.palette([
attribute.class("h-8 w-8 md:h-10 md:w-10 text-[#4641D9]"),
])
"magnifying-glass" ->
icons.magnifying_glass([
attribute.class("h-8 w-8 md:h-10 md:w-10 text-[#4641D9]"),
])
"devices" ->
icons.devices([
attribute.class("h-8 w-8 md:h-10 md:w-10 text-[#4641D9]"),
])
"gear" ->
icons.gear([
attribute.class("h-8 w-8 md:h-10 md:w-10 text-[#4641D9]"),
])
"heart" ->
icons.heart([
attribute.class("h-8 w-8 md:h-10 md:w-10 text-[#4641D9]"),
])
"lightning" ->
icons.lightning([
attribute.class("h-8 w-8 md:h-10 md:w-10 text-[#4641D9]"),
])
"globe" ->
icons.globe([
attribute.class("h-8 w-8 md:h-10 md:w-10 text-[#4641D9]"),
])
"server" ->
icons.globe([
attribute.class("h-8 w-8 md:h-10 md:w-10 text-[#4641D9]"),
])
"shopping-cart" ->
icons.shopping_cart([
attribute.class("h-8 w-8 md:h-10 md:w-10 text-[#4641D9]"),
])
"newspaper" ->
icons.newspaper([
attribute.class("h-8 w-8 md:h-10 md:w-10 text-[#4641D9]"),
])
"brain" ->
icons.brain([
attribute.class("h-8 w-8 md:h-10 md:w-10 text-[#4641D9]"),
])
_ -> html.div([], [])
},
],
),
html.h3([attribute.class("title " <> text_color <> " mb-3")], [
html.text(title),
]),
html.p([attribute.class("body-lg " <> description_color)], [
html.text(description),
]),
]),
html.div([attribute.class("flex-1 mt-2")], [
html.ul(
[attribute.class("space-y-3")],
features
|> list.map(fn(feature) {
html.li([attribute.class("flex items-start gap-3")], [
html.span(
[
attribute.class(
"mt-[.7em] h-1.5 w-1.5 rounded-full bg-[#4641D9] shrink-0",
),
],
[],
),
html.span([attribute.class("body-lg " <> text_color)], [
html.text(feature),
]),
])
}),
),
]),
],
)
}

View File

@@ -1,56 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub type Status {
Live
ComingSoon
}
pub type Theme {
Light
Dark
}
pub fn render(text: String, _status: Status) -> Element(a) {
html.span(
[
attribute.class(
"inline-block rounded-lg bg-white px-3 py-2 sm:px-4 sm:py-3 text-sm sm:text-base font-medium text-[#4641D9] shadow-sm border border-white/20",
),
],
[html.text(text)],
)
}
pub fn render_with_theme(
text: String,
_status: Status,
theme: Theme,
) -> Element(a) {
let pill_class = case theme {
Light ->
"inline-block rounded-lg bg-[#4641D9] px-3 py-2 sm:px-4 sm:py-3 text-sm sm:text-base font-medium text-white shadow-sm border border-gray-200"
Dark ->
"inline-block rounded-lg bg-white px-3 py-2 sm:px-4 sm:py-3 text-sm sm:text-base font-medium text-[#4641D9] shadow-sm border border-white/20"
}
html.span([attribute.class(pill_class)], [html.text(text)])
}

View File

@@ -1,70 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/components/feature_pill
import fluxer_marketing/i18n
import fluxer_marketing/web.{type Context}
import gleam/list
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn render(
ctx: Context,
title: String,
description: String,
features: List(#(String, feature_pill.Status)),
theme: feature_pill.Theme,
) -> Element(a) {
let _i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
let #(bg_class, text_class, desc_class) = case theme {
feature_pill.Light -> #("bg-white", "text-black", "text-gray-600")
feature_pill.Dark -> #("bg-[#4641D9]", "text-white", "text-white/80")
}
html.section([attribute.class(bg_class <> " px-6 py-20 md:py-32")], [
html.div([attribute.class("mx-auto max-w-7xl")], [
html.div([attribute.class("mb-16 text-center")], [
html.h2(
[
attribute.class(
"mb-6 text-3xl font-bold " <> text_class <> " md:text-4xl",
),
],
[html.text(title)],
),
html.p(
[
attribute.class(
"mx-auto max-w-3xl text-lg " <> desc_class <> " md:text-xl",
),
],
[html.text(description)],
),
]),
html.div(
[attribute.class("flex flex-wrap justify-center gap-3")],
features
|> list.map(fn(feature) {
let #(text, status) = feature
feature_pill.render_with_theme(text, status, theme)
}),
),
]),
])
}

View File

@@ -1,88 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/i18n
import fluxer_marketing/web.{type Context}
import kielet.{gettext as g_}
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn render(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
html.section(
[
attribute.class("bg-gradient-to-b from-white to-gray-50"),
],
[
html.div(
[
attribute.class(
"rounded-t-3xl bg-gradient-to-b from-[#4641D9] to-[#3832B8] text-white",
),
],
[
html.div(
[
attribute.class(
"px-4 py-24 md:px-8 md:py-32 lg:py-40 text-center",
),
],
[
html.h2(
[
attribute.class(
"display mb-8 md:mb-10 text-white text-5xl md:text-7xl lg:text-8xl font-bold",
),
],
[
html.text(g_(
i18n_ctx,
"We need your support to make this work.",
)),
],
),
html.p(
[
attribute.class(
"lead mb-12 md:mb-14 text-white/90 text-xl md:text-2xl",
),
],
[
html.text(g_(
i18n_ctx,
"Create an account and help us build something good.",
)),
],
),
html.a(
[
attribute.href(ctx.app_endpoint <> "/register"),
attribute.class(
"label inline-block rounded-xl bg-white px-10 py-5 md:px-12 md:py-6 text-lg md:text-xl text-[#4641D9] transition-colors hover:bg-opacity-90 shadow-lg",
),
],
[html.text(g_(i18n_ctx, "Join Fluxer"))],
),
],
),
],
),
],
)
}

View File

@@ -1,360 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/help_center
import fluxer_marketing/i18n
import fluxer_marketing/icons
import fluxer_marketing/web.{type Context, href}
import kielet.{gettext as g_}
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn render(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
let help_data = help_center.load_help_articles(ctx.locale)
let bug_article_href =
help_center.article_href(ctx.locale, help_data, "1447264362996695040")
html.footer(
[
attribute.class(
"bg-gradient-to-b from-[#4641D9] to-[#3832B8] px-4 py-20 md:py-24 text-white md:px-8",
),
],
[
html.div([attribute.class("mx-auto max-w-7xl")], [
html.div(
[
attribute.class(
"mb-12 md:mb-16 rounded-2xl bg-white/10 backdrop-blur-sm border border-white/20 px-6 py-6 md:px-10 md:py-10",
),
],
[
html.div(
[
attribute.class(
"flex flex-col md:flex-row items-center md:items-center justify-between gap-6 md:gap-8 text-center md:text-left",
),
],
[
html.div(
[
attribute.class(
"flex flex-col md:flex-row items-center md:items-center gap-3 md:gap-4",
),
],
[
icons.heart([attribute.class("h-7 w-7 text-white")]),
html.p([attribute.class("body-lg text-white/90")], [
html.text(g_(
i18n_ctx,
"Help support an independent communication platform. Your donation funds the platform's infrastructure and development.",
)),
]),
],
),
html.a(
[
href(ctx, "/donate"),
attribute.class(
"inline-flex items-center justify-center rounded-xl bg-white px-8 py-4 text-base md:text-lg font-semibold text-[#4641D9] transition-colors hover:bg-white/90 shadow-lg whitespace-nowrap",
),
],
[html.text(g_(i18n_ctx, "Donate to Fluxer"))],
),
],
),
],
),
html.div(
[attribute.class("grid grid-cols-1 gap-12 md:gap-16 md:grid-cols-4")],
[
html.div([attribute.class("md:col-span-1")], [
icons.fluxer_logo_wordmark([attribute.class("h-10 md:h-12")]),
]),
html.div(
[
attribute.class(
"grid grid-cols-1 gap-8 md:gap-12 sm:grid-cols-3 md:col-span-3",
),
],
[
html.div([], [
html.h3([attribute.class("title mb-4 md:mb-6 text-white")], [
html.text(g_(i18n_ctx, "Fluxer")),
]),
html.ul([attribute.class("space-y-3")], [
html.li([], [
html.a(
[
href(ctx, "/plutonium"),
attribute.class(
"body-lg text-white/90 hover:text-white hover:underline transition-colors",
),
],
[html.text(g_(i18n_ctx, "Pricing"))],
),
]),
html.li([], [
html.a(
[
href(ctx, "/partners"),
attribute.class(
"body-lg text-white/90 hover:text-white hover:underline transition-colors",
),
],
[html.text(g_(i18n_ctx, "Partners"))],
),
]),
html.li([], [
html.a(
[
href(ctx, "/download"),
attribute.class(
"body-lg text-white/90 hover:text-white hover:underline transition-colors",
),
],
[html.text(g_(i18n_ctx, "Download"))],
),
]),
html.li([], [
html.a(
[
attribute.href("https://github.com/fluxerapp/fluxer"),
attribute.class(
"body-lg text-white/90 hover:text-white hover:underline transition-colors",
),
],
[html.text(g_(i18n_ctx, "Source Code"))],
),
]),
html.li([], [
html.div([attribute.class("flex items-center gap-2")], [
html.a(
[
attribute.href(
"https://bsky.app/profile/fluxer.app",
),
attribute.class(
"body-lg text-white/90 hover:text-white hover:underline transition-colors",
),
],
[html.text(g_(i18n_ctx, "Bluesky"))],
),
html.a(
[
attribute.href(
"https://bsky.app/profile/fluxer.app/rss",
),
attribute.title("RSS Feed"),
attribute.target("_blank"),
attribute.rel("noopener noreferrer"),
attribute.class(
"text-white/90 hover:text-white transition-colors",
),
],
[
icons.rss([
attribute.class("h-[1em] w-[1em]"),
]),
],
),
]),
]),
html.li([], [
html.a(
[
href(ctx, "/help"),
attribute.class(
"body-lg text-white/90 hover:text-white hover:underline transition-colors",
),
],
[html.text(g_(i18n_ctx, "Help Center"))],
),
]),
html.li([], [
html.a(
[
href(ctx, "/press"),
attribute.class(
"body-lg text-white/90 hover:text-white hover:underline transition-colors",
),
],
[html.text(g_(i18n_ctx, "Press"))],
),
]),
html.li([], [
html.a(
[
attribute.href("https://docs.fluxer.app"),
attribute.class(
"body-lg text-white/90 hover:text-white hover:underline transition-colors",
),
],
[html.text(g_(i18n_ctx, "Developer Docs"))],
),
]),
html.li([], [
html.a(
[
href(ctx, "/careers"),
attribute.class(
"body-lg text-white/90 hover:text-white hover:underline transition-colors",
),
],
[html.text(g_(i18n_ctx, "Careers"))],
),
]),
]),
]),
html.div([], [
html.h3([attribute.class("title mb-4 md:mb-6 text-white")], [
html.text(g_(i18n_ctx, "Policies")),
]),
html.ul([attribute.class("space-y-3")], [
html.li([], [
html.a(
[
href(ctx, "/terms"),
attribute.class(
"body-lg text-white/90 hover:text-white hover:underline transition-colors",
),
],
[html.text(g_(i18n_ctx, "Terms of Service"))],
),
]),
html.li([], [
html.a(
[
href(ctx, "/privacy"),
attribute.class(
"body-lg text-white/90 hover:text-white hover:underline transition-colors",
),
],
[html.text(g_(i18n_ctx, "Privacy Policy"))],
),
]),
html.li([], [
html.a(
[
href(ctx, "/guidelines"),
attribute.class(
"body-lg text-white/90 hover:text-white hover:underline transition-colors",
),
],
[html.text(g_(i18n_ctx, "Community Guidelines"))],
),
]),
html.li([], [
html.a(
[
href(ctx, "/security"),
attribute.class(
"body-lg text-white/90 hover:text-white hover:underline transition-colors",
),
],
[html.text(g_(i18n_ctx, "Security Bug Bounty"))],
),
]),
html.li([], [
html.a(
[
href(ctx, "/company-information"),
attribute.class(
"body-lg text-white/90 hover:text-white hover:underline transition-colors",
),
],
[html.text(g_(i18n_ctx, "Company Information"))],
),
]),
]),
]),
html.div([], [
html.h3([attribute.class("title mb-4 md:mb-6 text-white")], [
html.text(g_(i18n_ctx, "Connect")),
]),
html.ul([attribute.class("space-y-3")], [
html.li([], [
html.a(
[
attribute.href("mailto:press@fluxer.app"),
attribute.class(
"body-lg text-white/90 hover:text-white hover:underline transition-colors",
),
],
[html.text("press@fluxer.app")],
),
]),
html.li([], [
html.a(
[
attribute.href("mailto:support@fluxer.app"),
attribute.class(
"body-lg text-white/90 hover:text-white hover:underline transition-colors",
),
],
[html.text("support@fluxer.app")],
),
]),
html.li([], [
html.a(
[
href(ctx, bug_article_href),
attribute.class(
"body-lg text-white/90 hover:text-white hover:underline transition-colors",
),
],
[html.text(g_(i18n_ctx, "Report a bug"))],
),
]),
]),
]),
],
),
],
),
html.div([attribute.class("mt-12 border-t border-white/20 pt-8")], [
html.div([attribute.class("flex flex-col gap-2")], [
html.p([attribute.class("body-sm text-white/80")], [
html.text(g_(
i18n_ctx,
"© Fluxer Platform AB (Swedish limited liability company: 559537-3993)",
)),
]),
html.p([attribute.class("body-sm text-white/80")], [
html.text(g_(
i18n_ctx,
"This product includes GeoLite2 Data created by MaxMind, available from ",
)),
html.a(
[
attribute.href("https://www.maxmind.com"),
attribute.target("_blank"),
attribute.rel("noopener noreferrer"),
attribute.class("hover:underline"),
],
[html.text("MaxMind")],
),
html.text("."),
]),
]),
]),
]),
],
)
}

View File

@@ -1,202 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/components/support_card
import fluxer_marketing/help_center
import fluxer_marketing/i18n
import fluxer_marketing/icons
import fluxer_marketing/web.{type Context}
import kielet.{gettext as g_}
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn render(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
let help_data = help_center.load_help_articles(ctx.locale)
let bug_article_href =
help_center.article_href(ctx.locale, help_data, "1447264362996695040")
html.section(
[
attribute.class(
"bg-gradient-to-b from-[#4641D9] to-[#3832B8] px-6 py-24 md:py-40",
),
attribute.id("get-involved"),
attribute.style("scroll-margin-top", "8rem"),
],
[
html.div([attribute.class("mx-auto max-w-7xl")], [
html.div([attribute.class("mb-20 md:mb-24 text-center")], [
html.h2(
[
attribute.class(
"display mb-8 md:mb-10 text-white text-4xl md:text-5xl lg:text-6xl",
),
],
[html.text(g_(i18n_ctx, "Get involved"))],
),
html.p(
[
attribute.class(
"lead mx-auto max-w-3xl text-white/90 text-xl md:text-2xl",
),
],
[
html.text(g_(
i18n_ctx,
"We're building a complete platform with all the features you'd expect. But we can't do it without your help!",
)),
],
),
]),
html.div(
[
attribute.class("grid gap-10 md:gap-12 grid-cols-1 md:grid-cols-2"),
],
[
support_card.render(
ctx,
"rocket-launch",
g_(i18n_ctx, "Join and spread the word"),
g_(
i18n_ctx,
"We're limiting registrations during beta. Once you're in, you can give friends a code to skip the queue.",
),
g_(i18n_ctx, "Register now"),
ctx.app_endpoint <> "/register",
),
support_card.render(
ctx,
"chat-centered-text",
g_(i18n_ctx, "Join Fluxer HQ"),
g_(
i18n_ctx,
"Get updates, see upcoming features, discuss suggestions, and chat with the team.",
),
g_(i18n_ctx, "Join Fluxer HQ"),
"https://fluxer.gg/fluxer-hq",
),
html.div(
[
attribute.class(
"flex h-full flex-col rounded-3xl bg-white/95 backdrop-blur-sm p-10 md:p-12 shadow-lg border border-white/50",
),
],
[
html.div([attribute.class("mb-8 text-center")], [
html.div(
[
attribute.class(
"inline-flex items-center justify-center w-24 h-24 md:w-28 md:h-28 rounded-3xl bg-[#4641D9] mb-6",
),
],
[
icons.bluesky([
attribute.class("h-12 w-12 md:h-14 md:w-14 text-white"),
]),
],
),
html.h3(
[
attribute.class(
"title text-gray-900 mb-4 text-xl md:text-2xl",
),
],
[html.text(g_(i18n_ctx, "Follow us on Bluesky"))],
),
html.p(
[attribute.class("body-lg text-gray-700 leading-relaxed")],
[
html.text(g_(
i18n_ctx,
"Stay updated on news, service status, and what's happening. You can also follow ",
)),
html.a(
[
attribute.href(
"https://bsky.app/profile/fluxer.app/rss",
),
attribute.class(
"underline hover:text-gray-900 transition-colors",
),
attribute.target("_blank"),
attribute.rel("noopener noreferrer"),
],
[html.text(g_(i18n_ctx, "our RSS feed"))],
),
html.text("."),
],
),
]),
html.div(
[attribute.class("mt-auto flex flex-col items-center")],
[
html.a(
[
attribute.href("https://bsky.app/profile/fluxer.app"),
attribute.class(
"label inline-block rounded-xl bg-[#4641D9] px-8 py-4 text-base md:text-lg text-white transition-colors hover:bg-opacity-90 shadow-md w-full text-center",
),
attribute.target("_blank"),
attribute.rel("noopener noreferrer"),
],
[html.text(g_(i18n_ctx, "Follow @fluxer.app"))],
),
],
),
],
),
support_card.render(
ctx,
"bug",
g_(i18n_ctx, "Report bugs"),
g_(
i18n_ctx,
"Approved reports grant access to Fluxer Testers, where you can earn points for Plutonium codes and the Bug Hunter badge.",
),
g_(i18n_ctx, "Read the Guide"),
bug_article_href,
),
support_card.render(
ctx,
"code",
g_(i18n_ctx, "Contribute code"),
g_(
i18n_ctx,
"Fluxer is open source (AGPLv3). Contribute directly on GitHub by opening pull requests.",
),
g_(i18n_ctx, "View repository"),
"https://github.com/fluxerapp/fluxer",
),
support_card.render(
ctx,
"shield-check",
g_(i18n_ctx, "Found a security issue?"),
g_(
i18n_ctx,
"We appreciate responsible disclosure via our Security Bug Bounty page. We offer Plutonium codes and Bug Hunter badges based on severity.",
),
g_(i18n_ctx, "Security Bug Bounty"),
"/security",
),
],
),
]),
],
)
}

View File

@@ -1,31 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn render() -> Element(a) {
html.div(
[
attribute.class(
"h-px bg-gradient-to-r from-transparent via-gray-300 to-transparent",
),
],
[],
)
}

View File

@@ -1,50 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/i18n
import fluxer_marketing/web.{type Context}
import kielet.{gettext as g_}
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn render(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
html.div(
[
attribute.class(
"mt-8 inline-flex items-center gap-3 rounded-full bg-white/10 backdrop-blur-sm border border-white/20 px-4 py-2.5 md:px-5",
),
],
[
html.p([attribute.class("text-sm md:text-base text-white/90")], [
html.text(g_(i18n_ctx, "Try it without an email at")),
html.text(" "),
html.a(
[
attribute.href("https://fluxer.gg/fluxer-hq"),
attribute.class(
"font-semibold text-white underline underline-offset-2 hover:no-underline",
),
],
[html.text("fluxer.gg/fluxer-hq")],
),
]),
],
)
}

View File

@@ -1,296 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/components/hackernews_banner
import fluxer_marketing/components/platform_download_button
import fluxer_marketing/i18n
import fluxer_marketing/locale
import fluxer_marketing/web.{type Context, prepend_base_path}
import kielet.{gettext as g_}
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn render(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
html.main(
[
attribute.class(
"flex flex-col items-center justify-center px-6 pb-16 pt-52 md:pb-20 md:pt-64 lg:pb-24",
),
],
[
html.div([attribute.class("max-w-4xl space-y-6 text-center")], [
case ctx.locale {
locale.Ja ->
html.div([attribute.class("mb-2 flex justify-center")], [
html.span([attribute.class("text-3xl font-bold text-white")], [
html.text("Fluxerフラクサー"),
]),
])
_ -> element.none()
},
html.div([attribute.class("mb-4 flex justify-center")], [
html.span(
[
attribute.class(
"rounded-full bg-white/20 px-4 py-2 backdrop-blur-sm",
),
],
[html.text(g_(i18n_ctx, "Public Beta"))],
),
]),
html.h1([attribute.class("hero")], [
html.text(g_(i18n_ctx, "A chat app that puts you first")),
]),
html.p([attribute.class("lead text-white/90")], [
html.text(g_(
i18n_ctx,
"Fluxer is an open-source, independent instant messaging and VoIP platform. Built for friends, groups, and communities.",
)),
]),
html.div(
[
attribute.class(
"flex flex-col items-center justify-center gap-4 pt-4 sm:flex-row sm:items-stretch",
),
],
[
platform_download_button.render_with_overlay(ctx),
html.a(
[
attribute.href(ctx.app_endpoint <> "/channels/@me"),
attribute.class(
"hidden sm:inline-flex items-center justify-center gap-3 rounded-2xl bg-white/10 backdrop-blur-sm border-2 border-white/30 px-8 md:px-10 text-lg md:text-xl font-semibold text-white transition-colors hover:bg-white/20 shadow-lg",
),
],
[html.text(g_(i18n_ctx, "Open in Browser"))],
),
],
),
hackernews_banner.render(ctx),
html.div(
[
attribute.class(
"mt-6 flex flex-wrap items-center justify-center gap-4",
),
],
[
html.a(
[
attribute.href(
"https://www.producthunt.com/products/fluxer?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-fluxer",
),
attribute.target("_blank"),
attribute.attribute("rel", "noopener noreferrer"),
],
[
html.img([
attribute.alt(
"Fluxer - Open-source Discord-like instant messaging & VoIP platform | Product Hunt",
),
attribute.attribute("width", "250"),
attribute.attribute("height", "54"),
attribute.src(prepend_base_path(
ctx,
"/api/badges/product-hunt",
)),
]),
],
),
html.a(
[
attribute.href(
"https://www.producthunt.com/products/fluxer?embed=true&utm_source=badge-top-post-badge&utm_medium=badge&utm_campaign=badge-fluxer",
),
attribute.target("_blank"),
attribute.attribute("rel", "noopener noreferrer"),
],
[
html.img([
attribute.alt(
"Fluxer - Open-source Discord-like instant messaging & VoIP platform | Product Hunt Top Post",
),
attribute.attribute("width", "250"),
attribute.attribute("height", "54"),
attribute.src(prepend_base_path(
ctx,
"/api/badges/product-hunt-top-post",
)),
]),
],
),
],
),
]),
html.div(
[
attribute.class(
"mt-16 flex w-full max-w-7xl items-end justify-center gap-4 px-6 md:mt-24 md:gap-8",
),
],
[
html.div(
[attribute.class("hidden w-full md:block md:w-3/4 lg:w-2/3")],
[
element.element("picture", [], [
element.element(
"source",
[
attribute.type_("image/avif"),
attribute.attribute(
"srcset",
ctx.cdn_endpoint
<> "/marketing/screenshots/desktop-480w.avif?v=4 480w, "
<> ctx.cdn_endpoint
<> "/marketing/screenshots/desktop-768w.avif?v=4 768w, "
<> ctx.cdn_endpoint
<> "/marketing/screenshots/desktop-1024w.avif?v=4 1024w, "
<> ctx.cdn_endpoint
<> "/marketing/screenshots/desktop-1920w.avif?v=4 1920w, "
<> ctx.cdn_endpoint
<> "/marketing/screenshots/desktop-2560w.avif?v=4 2560w",
),
attribute.attribute(
"sizes",
"(max-width: 768px) 100vw, 75vw",
),
],
[],
),
element.element(
"source",
[
attribute.type_("image/webp"),
attribute.attribute(
"srcset",
ctx.cdn_endpoint
<> "/marketing/screenshots/desktop-480w.webp?v=4 480w, "
<> ctx.cdn_endpoint
<> "/marketing/screenshots/desktop-768w.webp?v=4 768w, "
<> ctx.cdn_endpoint
<> "/marketing/screenshots/desktop-1024w.webp?v=4 1024w, "
<> ctx.cdn_endpoint
<> "/marketing/screenshots/desktop-1920w.webp?v=4 1920w, "
<> ctx.cdn_endpoint
<> "/marketing/screenshots/desktop-2560w.webp?v=4 2560w",
),
attribute.attribute(
"sizes",
"(max-width: 768px) 100vw, 75vw",
),
],
[],
),
html.img([
attribute.src(
ctx.cdn_endpoint
<> "/marketing/screenshots/desktop-1920w.png?v=4",
),
attribute.attribute(
"srcset",
ctx.cdn_endpoint
<> "/marketing/screenshots/desktop-480w.png?v=4 480w, "
<> ctx.cdn_endpoint
<> "/marketing/screenshots/desktop-768w.png?v=4 768w, "
<> ctx.cdn_endpoint
<> "/marketing/screenshots/desktop-1024w.png?v=4 1024w, "
<> ctx.cdn_endpoint
<> "/marketing/screenshots/desktop-1920w.png?v=4 1920w, "
<> ctx.cdn_endpoint
<> "/marketing/screenshots/desktop-2560w.png?v=4 2560w",
),
attribute.attribute("sizes", "(max-width: 768px) 100vw, 75vw"),
attribute.alt("Fluxer desktop interface"),
attribute.class(
"aspect-video w-full rounded-lg border-2 border-white/50",
),
]),
]),
],
),
html.div(
[
attribute.class(
"w-full max-w-[320px] md:w-1/4 md:max-w-none lg:w-1/5",
),
],
[
element.element("picture", [], [
element.element(
"source",
[
attribute.type_("image/avif"),
attribute.attribute(
"srcset",
ctx.cdn_endpoint
<> "/marketing/screenshots/mobile-480w.avif?v=4 480w, "
<> ctx.cdn_endpoint
<> "/marketing/screenshots/mobile-768w.avif?v=4 768w",
),
attribute.attribute(
"sizes",
"(max-width: 768px) 320px, 25vw",
),
],
[],
),
element.element(
"source",
[
attribute.type_("image/webp"),
attribute.attribute(
"srcset",
ctx.cdn_endpoint
<> "/marketing/screenshots/mobile-480w.webp?v=4 480w, "
<> ctx.cdn_endpoint
<> "/marketing/screenshots/mobile-768w.webp?v=4 768w",
),
attribute.attribute(
"sizes",
"(max-width: 768px) 320px, 25vw",
),
],
[],
),
html.img([
attribute.src(
ctx.cdn_endpoint
<> "/marketing/screenshots/mobile-768w.png?v=4",
),
attribute.attribute(
"srcset",
ctx.cdn_endpoint
<> "/marketing/screenshots/mobile-480w.png?v=4 480w, "
<> ctx.cdn_endpoint
<> "/marketing/screenshots/mobile-768w.png?v=4 768w",
),
attribute.attribute("sizes", "(max-width: 768px) 320px, 25vw"),
attribute.alt("Fluxer mobile interface"),
attribute.class(
"aspect-[9/19] w-full rounded-3xl border-2 border-white/50",
),
]),
]),
],
),
],
),
],
)
}

View File

@@ -1,68 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub type HeroConfig(a) {
HeroConfig(
icon: Element(a),
title: String,
description: String,
extra_content: Element(a),
custom_padding: String,
)
}
pub fn default_padding() -> String {
"px-6 pt-48 md:pt-60 pb-16 md:pb-20 lg:pb-24 text-white"
}
pub fn render(config: HeroConfig(a)) -> Element(a) {
html.section([attribute.class(config.custom_padding)], [
html.div([attribute.class("mx-auto max-w-5xl text-center")], [
html.div([attribute.class("mb-8 flex justify-center")], [
html.div(
[
attribute.class(
"inline-flex items-center justify-center w-28 h-28 md:w-36 md:h-36 rounded-3xl bg-white/10 backdrop-blur-sm",
),
],
[config.icon],
),
]),
html.h1(
[
attribute.class(
"hero mb-8 md:mb-10 text-5xl md:text-6xl lg:text-7xl font-bold",
),
],
[html.text(config.title)],
),
html.p(
[
attribute.class(
"lead text-white/90 text-xl md:text-2xl max-w-4xl mx-auto",
),
],
[html.text(config.description)],
),
config.extra_content,
]),
])
}

View File

@@ -1,253 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/flags
import fluxer_marketing/i18n
import fluxer_marketing/icons
import fluxer_marketing/locale
import fluxer_marketing/web.{type Context, href}
import gleam/list
import kielet.{gettext as g_}
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
import lustre/element/svg
pub fn render_trigger(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
html.a(
[
attribute.class(
"locale-toggle flex items-center justify-center p-2 rounded-lg hover:bg-gray-100 transition-colors",
),
attribute.attribute("aria-label", g_(i18n_ctx, "Change language")),
attribute.id("locale-button"),
href(ctx, "#locale-modal-backdrop"),
],
[icons.translate([attribute.class("h-5 w-5")])],
)
}
pub fn render_modal(ctx: Context, current_path: String) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
let current_locale = ctx.locale
let all_locales = locale.all_locales()
html.div(
[
attribute.id("locale-modal-backdrop"),
attribute.class("locale-modal-backdrop"),
],
[
html.div([attribute.class("locale-modal")], [
html.div([attribute.class("flex flex-col h-full")], [
html.div(
[attribute.class("flex items-center justify-between p-6 pb-0")],
[
html.h2([attribute.class("text-xl font-bold text-gray-900")], [
html.text(g_(i18n_ctx, "Choose your language")),
]),
html.a(
[
attribute.class(
"p-2 hover:bg-gray-100 rounded-lg text-gray-600 hover:text-gray-900",
),
attribute.id("locale-close"),
attribute.attribute("aria-label", "Close"),
href(ctx, "#"),
],
[
svg.svg(
[
attribute.class("w-5 h-5"),
attribute.attribute("fill", "none"),
attribute.attribute("stroke", "currentColor"),
attribute.attribute("viewBox", "0 0 24 24"),
],
[
svg.path([
attribute.attribute("stroke-linecap", "round"),
attribute.attribute("stroke-linejoin", "round"),
attribute.attribute("stroke-width", "2"),
attribute.attribute("d", "M6 18L18 6M6 6l12 12"),
]),
],
),
],
),
],
),
html.p(
[
attribute.class("px-6 pb-2 text-xs text-gray-500 leading-relaxed"),
],
[
html.text(g_(
i18n_ctx,
"All translations are currently LLM-generated with minimal human revision. We'd love to get real people to help us out localizing Fluxer into your language! To do so, shoot an email to i18n@fluxer.app and we'll be happy to accept your contributions.",
)),
],
),
html.div([attribute.class("flex-1 overflow-y-auto p-6 pt-4")], [
html.div(
[
attribute.class(
"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-3",
),
],
list.map(all_locales, fn(loc) {
let is_current = loc == current_locale
let locale_code = locale.get_code_from_locale(loc)
let native_name = locale.get_locale_name(loc)
let localized_name = case loc {
locale.Ar -> g_(i18n_ctx, "Arabic")
locale.Bg -> g_(i18n_ctx, "Bulgarian")
locale.Cs -> g_(i18n_ctx, "Czech")
locale.Da -> g_(i18n_ctx, "Danish")
locale.De -> g_(i18n_ctx, "German")
locale.El -> g_(i18n_ctx, "Greek")
locale.EnGB -> g_(i18n_ctx, "English")
locale.EnUS -> g_(i18n_ctx, "English (US)")
locale.EsES -> g_(i18n_ctx, "Spanish (Spain)")
locale.Es419 -> g_(i18n_ctx, "Spanish (Latin America)")
locale.Fi -> g_(i18n_ctx, "Finnish")
locale.Fr -> g_(i18n_ctx, "French")
locale.He -> g_(i18n_ctx, "Hebrew")
locale.Hi -> g_(i18n_ctx, "Hindi")
locale.Hr -> g_(i18n_ctx, "Croatian")
locale.Hu -> g_(i18n_ctx, "Hungarian")
locale.Id -> g_(i18n_ctx, "Indonesian")
locale.It -> g_(i18n_ctx, "Italian")
locale.Ja -> g_(i18n_ctx, "Japanese")
locale.Ko -> g_(i18n_ctx, "Korean")
locale.Lt -> g_(i18n_ctx, "Lithuanian")
locale.Nl -> g_(i18n_ctx, "Dutch")
locale.No -> g_(i18n_ctx, "Norwegian")
locale.Pl -> g_(i18n_ctx, "Polish")
locale.PtBR -> g_(i18n_ctx, "Portuguese (Brazil)")
locale.Ro -> g_(i18n_ctx, "Romanian")
locale.Ru -> g_(i18n_ctx, "Russian")
locale.SvSE -> g_(i18n_ctx, "Swedish")
locale.Th -> g_(i18n_ctx, "Thai")
locale.Tr -> g_(i18n_ctx, "Turkish")
locale.Uk -> g_(i18n_ctx, "Ukrainian")
locale.Vi -> g_(i18n_ctx, "Vietnamese")
locale.ZhCN -> g_(i18n_ctx, "Chinese (Simplified)")
locale.ZhTW -> g_(i18n_ctx, "Chinese (Traditional)")
}
html.form(
[
attribute.action(web.prepend_base_path(ctx, "/_locale")),
attribute.method("POST"),
attribute.class("contents locale-form"),
],
[
html.input([
attribute.type_("hidden"),
attribute.name("locale"),
attribute.value(locale_code),
]),
html.input([
attribute.type_("hidden"),
attribute.name("redirect"),
attribute.value(current_path),
]),
html.button(
[
attribute.type_("submit"),
attribute.class(
"relative flex flex-col items-center gap-3 p-4 rounded-xl border-2 hover:bg-gray-50 transition-colors text-center min-h-[120px] justify-center "
<> case is_current {
True -> "border-blue-500 bg-blue-50"
False -> "border-gray-200"
},
),
],
[
case is_current {
True ->
html.div(
[
attribute.class(
"absolute top-2 right-2 w-6 h-6 bg-blue-600 rounded-full flex items-center justify-center",
),
],
[
svg.svg(
[
attribute.class("w-4 h-4 text-white"),
attribute.attribute("fill", "none"),
attribute.attribute(
"stroke",
"currentColor",
),
attribute.attribute("viewBox", "0 0 24 24"),
attribute.attribute("stroke-width", "2"),
],
[
svg.path([
attribute.attribute(
"stroke-linecap",
"round",
),
attribute.attribute(
"stroke-linejoin",
"round",
),
attribute.attribute("d", "M5 13l4 4L19 7"),
]),
],
),
],
)
False -> html.text("")
},
flags.flag_svg(loc, ctx, [
attribute.class("w-8 h-8 rounded"),
]),
html.div(
[
attribute.class(
"font-semibold text-gray-900 text-sm",
),
],
[html.text(native_name)],
),
html.div([attribute.class("text-xs text-gray-500")], [
html.text(localized_name),
]),
],
),
],
)
}),
),
]),
]),
]),
],
)
}
pub fn render(ctx: Context, current_path: String) -> Element(a) {
html.div([], [
render_trigger(ctx),
render_modal(ctx, current_path),
])
}

View File

@@ -1,503 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/components/locale_selector
import fluxer_marketing/components/platform_download_button
import fluxer_marketing/i18n
import fluxer_marketing/icons
import fluxer_marketing/web.{type Context, href}
import kielet.{gettext as g_}
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
import wisp.{type Request}
pub fn render(ctx: Context, _req: Request) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
let #(drawer_download_url, drawer_download_label, drawer_download_icon) =
platform_download_button.get_platform_download_info(ctx)
html.nav(
[
attribute.id("navbar"),
attribute.class("fixed left-0 right-0 z-40"),
attribute.style("top", "var(--banner-height, 60px)"),
],
[
html.input([
attribute.type_("checkbox"),
attribute.id("nav-toggle"),
attribute.class("hidden peer"),
]),
html.div([attribute.class("px-4 py-6 md:px-8 md:py-8")], [
html.div(
[
attribute.class(
"mx-auto max-w-7xl rounded-2xl bg-white/95 backdrop-blur-lg shadow-lg border border-gray-200/60 px-4 py-3 md:px-6 md:py-3",
),
],
[
html.div([attribute.class("flex items-center justify-between")], [
html.div([attribute.class("flex items-center gap-8 xl:gap-12")], [
html.a(
[
href(ctx, "/"),
attribute.class(
"flex items-center transition-opacity hover:opacity-80 relative z-10",
),
attribute.attribute("aria-label", "Fluxer home"),
],
[
icons.fluxer_logo_wordmark([
attribute.class("h-8 md:h-9 text-[#4641D9]"),
]),
],
),
html.div(
[
attribute.class(
"hidden lg:flex items-center gap-6 xl:gap-8",
),
],
[
html.a(
[
href(ctx, "/download"),
attribute.class(
"body-lg text-gray-900/90 font-semibold hover:text-gray-900 transition-colors",
),
],
[html.text(g_(i18n_ctx, "Download"))],
),
html.a(
[
href(ctx, "/plutonium"),
attribute.class(
"body-lg text-gray-900/90 font-semibold hover:text-gray-900 transition-colors",
),
],
[html.text(g_(i18n_ctx, "Pricing"))],
),
html.a(
[
href(ctx, "/help"),
attribute.class(
"body-lg text-gray-900/90 font-semibold hover:text-gray-900 transition-colors",
),
],
[html.text(g_(i18n_ctx, "Help"))],
),
html.a(
[
attribute.href("https://docs.fluxer.app"),
attribute.class(
"body-lg text-gray-900/90 font-semibold hover:text-gray-900 transition-colors",
),
],
[html.text(g_(i18n_ctx, "Docs"))],
),
html.a(
[
href(ctx, "/donate"),
attribute.class(
"body-lg text-gray-900/90 font-semibold hover:text-gray-900 transition-colors",
),
],
[html.text(g_(i18n_ctx, "Donate"))],
),
],
),
]),
html.div([attribute.class("flex items-center gap-3")], [
html.a(
[
attribute.href("https://bsky.app/profile/fluxer.app"),
attribute.class(
"hidden lg:flex items-center p-2 rounded-lg text-[#4641D9] hover:text-[#3d38c7] hover:bg-gray-100 transition-colors",
),
attribute.target("_blank"),
attribute.rel("noopener noreferrer"),
attribute.attribute("aria-label", "Bluesky"),
],
[icons.bluesky([attribute.class("h-5 w-5")])],
),
html.a(
[
attribute.href("https://github.com/fluxerapp/fluxer"),
attribute.class(
"hidden lg:flex items-center p-2 rounded-lg text-[#4641D9] hover:text-[#3d38c7] hover:bg-gray-100 transition-colors",
),
attribute.target("_blank"),
attribute.rel("noopener noreferrer"),
attribute.attribute("aria-label", "GitHub"),
],
[icons.github([attribute.class("h-5 w-5")])],
),
html.a(
[
attribute.href("https://bsky.app/profile/fluxer.app/rss"),
attribute.class(
"hidden lg:flex items-center p-2 rounded-lg text-[#4641D9] hover:text-[#3d38c7] hover:bg-gray-100 transition-colors",
),
attribute.target("_blank"),
attribute.rel("noopener noreferrer"),
attribute.attribute("aria-label", "RSS Feed"),
],
[icons.rss([attribute.class("h-5 w-5")])],
),
html.div([attribute.class("hidden lg:block text-[#4641D9]")], [
locale_selector.render_trigger(ctx),
]),
html.a(
[
attribute.href(ctx.app_endpoint <> "/channels/@me"),
attribute.class(
"hidden lg:inline-flex whitespace-nowrap rounded-xl bg-[#4641D9] px-5 py-2.5 text-base font-semibold text-white transition-colors hover:bg-opacity-90 shadow-md",
),
],
[html.text(g_(i18n_ctx, "Open Fluxer"))],
),
html.label(
[
attribute.for("nav-toggle"),
attribute.class(
"lg:hidden flex items-center justify-center p-2 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer relative z-10",
),
],
[
icons.menu([
attribute.class(
"h-6 w-6 text-gray-900 peer-checked:hidden",
),
]),
],
),
]),
]),
],
),
]),
html.div(
[
attribute.class(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm opacity-0 pointer-events-none peer-checked:opacity-100 peer-checked:pointer-events-auto transition-opacity lg:hidden",
),
],
[
html.label(
[attribute.for("nav-toggle"), attribute.class("absolute inset-0")],
[],
),
],
),
html.div(
[
attribute.class(
"fixed top-0 right-0 bottom-0 z-50 w-full sm:w-[420px] sm:max-w-[90vw] bg-white rounded-none sm:rounded-l-3xl shadow-2xl transform translate-x-full peer-checked:translate-x-0 transition-transform lg:hidden overflow-y-auto",
),
],
[
html.div([attribute.class("flex h-full flex-col p-6")], [
html.div(
[attribute.class("mb-4 flex items-center justify-between")],
[
html.a(
[
href(ctx, "/"),
attribute.class(
"flex items-center gap-3 rounded-xl px-2 py-1 hover:bg-gray-50 transition-colors",
),
attribute.attribute("aria-label", "Fluxer home"),
],
[
icons.fluxer_logo_wordmark([
attribute.class("h-7 text-[#4641D9]"),
]),
],
),
html.label(
[
attribute.for("nav-toggle"),
attribute.class(
"p-2 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer",
),
],
[icons.x([attribute.class("h-6 w-6 text-gray-900")])],
),
],
),
html.div([attribute.class("mb-4 text-gray-900")], [
html.a(
[
href(ctx, "#locale-modal-backdrop"),
attribute.class(
"flex items-center gap-2 rounded-lg px-2 py-2 text-base font-semibold text-gray-900 hover:bg-gray-100 transition-colors",
),
attribute.attribute(
"aria-label",
g_(i18n_ctx, "Change language"),
),
],
[
icons.translate([attribute.class("h-5 w-5")]),
html.span([], [html.text(g_(i18n_ctx, "Change language"))]),
],
),
]),
html.div([attribute.class("flex-1 overflow-y-auto")], [
html.div([attribute.class("flex flex-col gap-4 py-2")], [
html.div([], [
html.p(
[
attribute.class(
"mb-1 text-xs font-semibold uppercase tracking-wide text-gray-500",
),
],
[html.text(g_(i18n_ctx, "Product"))],
),
html.div([attribute.class("flex flex-col gap-1")], [
html.a(
[
href(ctx, "/download"),
attribute.class(
"px-2 py-2 text-base font-semibold text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",
),
],
[html.text(g_(i18n_ctx, "Download"))],
),
html.a(
[
href(ctx, "/plutonium"),
attribute.class(
"px-2 py-2 text-base font-semibold text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",
),
],
[html.text(g_(i18n_ctx, "Pricing"))],
),
html.a(
[
href(ctx, "/partners"),
attribute.class(
"px-2 py-2 text-base font-semibold text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",
),
],
[html.text(g_(i18n_ctx, "Partners"))],
),
]),
]),
html.div([], [
html.p(
[
attribute.class(
"mb-1 text-xs font-semibold uppercase tracking-wide text-gray-500",
),
],
[html.text(g_(i18n_ctx, "Resources"))],
),
html.div([attribute.class("flex flex-col gap-1")], [
html.a(
[
attribute.href("https://docs.fluxer.app"),
attribute.class(
"px-2 py-2 text-base font-semibold text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",
),
],
[html.text(g_(i18n_ctx, "Docs"))],
),
html.a(
[
href(ctx, "/help"),
attribute.class(
"px-2 py-2 text-base font-semibold text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",
),
],
[html.text(g_(i18n_ctx, "Help Center"))],
),
html.a(
[
href(ctx, "/press"),
attribute.class(
"px-2 py-2 text-base font-semibold text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",
),
],
[html.text(g_(i18n_ctx, "Press"))],
),
]),
]),
html.div([], [
html.p(
[
attribute.class(
"mb-1 text-xs font-semibold uppercase tracking-wide text-gray-500",
),
],
[html.text(g_(i18n_ctx, "Company"))],
),
html.div([attribute.class("flex flex-col gap-1")], [
html.a(
[
href(ctx, "/careers"),
attribute.class(
"px-2 py-2 text-base font-semibold text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",
),
],
[html.text(g_(i18n_ctx, "Careers"))],
),
html.a(
[
href(ctx, "/donate"),
attribute.class(
"px-2 py-2 text-base font-semibold text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",
),
],
[html.text(g_(i18n_ctx, "Donate"))],
),
html.a(
[
href(ctx, "/company-information"),
attribute.class(
"px-2 py-2 text-base font-semibold text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",
),
],
[html.text(g_(i18n_ctx, "Company Information"))],
),
]),
]),
html.div([], [
html.p(
[
attribute.class(
"mb-1 text-xs font-semibold uppercase tracking-wide text-gray-500",
),
],
[html.text(g_(i18n_ctx, "Policies"))],
),
html.div([attribute.class("flex flex-col gap-1")], [
html.a(
[
href(ctx, "/terms"),
attribute.class(
"px-2 py-2 text-base font-semibold text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",
),
],
[html.text(g_(i18n_ctx, "Terms of Service"))],
),
html.a(
[
href(ctx, "/privacy"),
attribute.class(
"px-2 py-2 text-base font-semibold text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",
),
],
[html.text(g_(i18n_ctx, "Privacy Policy"))],
),
html.a(
[
href(ctx, "/guidelines"),
attribute.class(
"px-2 py-2 text-base font-semibold text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",
),
],
[html.text(g_(i18n_ctx, "Community Guidelines"))],
),
html.a(
[
href(ctx, "/security"),
attribute.class(
"px-2 py-2 text-base font-semibold text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",
),
],
[html.text(g_(i18n_ctx, "Security Bug Bounty"))],
),
]),
]),
html.div([], [
html.p(
[
attribute.class(
"mb-1 text-xs font-semibold uppercase tracking-wide text-gray-500",
),
],
[html.text(g_(i18n_ctx, "Connect"))],
),
html.div([attribute.class("flex flex-col gap-1")], [
html.a(
[
attribute.href("https://github.com/fluxerapp/fluxer"),
attribute.class(
"px-2 py-2 text-base font-semibold text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",
),
attribute.target("_blank"),
attribute.rel("noopener noreferrer"),
],
[html.text(g_(i18n_ctx, "Source Code"))],
),
html.a(
[
attribute.href("https://bsky.app/profile/fluxer.app"),
attribute.class(
"px-2 py-2 text-base font-semibold text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",
),
attribute.target("_blank"),
attribute.rel("noopener noreferrer"),
],
[html.text(g_(i18n_ctx, "Bluesky"))],
),
html.a(
[
attribute.href("mailto:support@fluxer.app"),
attribute.class(
"px-2 py-2 text-base font-semibold text-gray-900 hover:bg-gray-100 rounded-lg transition-colors",
),
],
[html.text("support@fluxer.app")],
),
]),
]),
]),
]),
html.div([attribute.class("mt-6")], [
html.div([attribute.class("flex flex-col gap-3")], [
html.a(
[
attribute.href(ctx.app_endpoint <> "/channels/@me"),
attribute.class(
"flex items-center justify-center rounded-xl border border-gray-300 px-5 py-3 text-base font-semibold text-gray-900 transition-colors hover:bg-gray-50",
),
],
[html.text(g_(i18n_ctx, "Open in Browser"))],
),
html.a(
[
attribute.href(drawer_download_url),
attribute.class(
"flex items-center justify-center gap-2 rounded-xl bg-[#4641D9] px-5 py-3 text-base font-semibold text-white transition-colors hover:bg-opacity-90 shadow-md",
),
],
[
drawer_download_icon,
html.span([], [html.text(drawer_download_label)]),
],
),
]),
]),
]),
],
),
],
)
}

View File

@@ -1,158 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/i18n
import fluxer_marketing/icons
import fluxer_marketing/web.{type Context, href}
import gleam/option.{None}
import kielet.{gettext as g_}
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn render(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
html.section(
[
attribute.class(
"bg-gradient-to-b from-gray-50 to-white px-6 pb-24 md:pb-40",
),
],
[
html.div(
[
attribute.class(
"mx-auto max-w-6xl rounded-3xl bg-gradient-to-br from-black to-gray-900 p-10 text-white md:p-16 lg:p-20 shadow-xl",
),
],
[
html.div([attribute.class("mb-10 md:mb-12 text-center")], [
html.div(
[
attribute.class(
"inline-flex items-center justify-center w-20 h-20 md:w-24 md:h-24 rounded-2xl bg-white/10 backdrop-blur-sm mb-6 md:mb-8",
),
],
[
icons.fluxer_partner([
attribute.class("h-10 w-10 md:h-12 md:w-12"),
]),
],
),
html.h2(
[
attribute.class(
"display mb-6 md:mb-8 text-white text-3xl md:text-4xl lg:text-5xl",
),
],
[
html.text(g_(i18n_ctx, "Become a Fluxer Partner")),
],
),
html.p([attribute.class("lead mx-auto max-w-3xl text-white/90")], [
html.text(g_(
i18n_ctx,
"Content creators and community owners: unlock exclusive perks including free Plutonium, a Partner badge, custom vanity URLs, and more.",
)),
]),
]),
html.div(
[
attribute.class(
"mb-10 md:mb-12 grid gap-6 md:gap-8 grid-cols-1 sm:grid-cols-3 max-w-3xl mx-auto",
),
],
[
html.div(
[attribute.class("flex flex-col items-center text-center")],
[
html.div(
[
attribute.class(
"inline-flex items-center justify-center w-16 h-16 md:w-20 md:h-20 rounded-xl bg-white/10 backdrop-blur-sm mb-4",
),
],
[
icons.fluxer_premium(None, [
attribute.class("h-8 w-8 md:h-10 md:w-10"),
]),
],
),
html.p([attribute.class("body-lg font-semibold text-white")], [
html.text(g_(i18n_ctx, "Free Plutonium")),
]),
],
),
html.div(
[attribute.class("flex flex-col items-center text-center")],
[
html.div(
[
attribute.class(
"inline-flex items-center justify-center w-16 h-16 md:w-20 md:h-20 rounded-xl bg-white/10 backdrop-blur-sm mb-4",
),
],
[
icons.seal_check([
attribute.class("h-8 w-8 md:h-10 md:w-10"),
]),
],
),
html.p([attribute.class("body-lg font-semibold text-white")], [
html.text(g_(i18n_ctx, "Verified Community")),
]),
],
),
html.div(
[attribute.class("flex flex-col items-center text-center")],
[
html.div(
[
attribute.class(
"inline-flex items-center justify-center w-16 h-16 md:w-20 md:h-20 rounded-xl bg-white/10 backdrop-blur-sm mb-4",
),
],
[
icons.chats_circle([
attribute.class("h-8 w-8 md:h-10 md:w-10"),
]),
],
),
html.p([attribute.class("body-lg font-semibold text-white")], [
html.text(g_(i18n_ctx, "Direct Team Access")),
]),
],
),
],
),
html.div([attribute.class("text-center")], [
html.a(
[
href(ctx, "/partners"),
attribute.class(
"label inline-block rounded-lg bg-white px-8 py-4 text-black transition-colors hover:bg-opacity-90",
),
],
[html.text(g_(i18n_ctx, "Become a Partner"))],
),
]),
],
),
],
)
}

View File

@@ -1,144 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import gleam/int
import gleam/string
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn render(
src: String,
alt: String,
class: String,
sizes: String,
width_1x: Int,
width_2x: Int,
) -> Element(a) {
render_with_priority(src, alt, class, sizes, width_1x, width_2x, False)
}
pub fn render_priority(
src: String,
alt: String,
class: String,
sizes: String,
width_1x: Int,
width_2x: Int,
) -> Element(a) {
render_with_priority(src, alt, class, sizes, width_1x, width_2x, True)
}
fn render_with_priority(
src: String,
alt: String,
class: String,
sizes: String,
width_1x: Int,
width_2x: Int,
high_priority: Bool,
) -> Element(a) {
let base_path = case string.split(src, ".") {
[] -> src
parts -> {
parts
|> remove_last
|> string.join(".")
}
}
let width_1x_str = int.to_string(width_1x)
let width_2x_str = int.to_string(width_2x)
html.picture([], [
html.source([
attribute.attribute(
"srcset",
base_path
<> "-1x.avif "
<> width_1x_str
<> "w, "
<> base_path
<> "-2x.avif "
<> width_2x_str
<> "w",
),
attribute.attribute("sizes", sizes),
attribute.attribute("type", "image/avif"),
]),
html.source([
attribute.attribute(
"srcset",
base_path
<> "-1x.webp "
<> width_1x_str
<> "w, "
<> base_path
<> "-2x.webp "
<> width_2x_str
<> "w",
),
attribute.attribute("sizes", sizes),
attribute.attribute("type", "image/webp"),
]),
html.img(case high_priority {
True -> [
attribute.attribute(
"srcset",
base_path
<> "-1x.png "
<> width_1x_str
<> "w, "
<> base_path
<> "-2x.png "
<> width_2x_str
<> "w",
),
attribute.attribute("sizes", sizes),
attribute.attribute("fetchpriority", "high"),
attribute.src(base_path <> "-1x.png"),
attribute.alt(alt),
attribute.class(class),
]
False -> [
attribute.attribute(
"srcset",
base_path
<> "-1x.png "
<> width_1x_str
<> "w, "
<> base_path
<> "-2x.png "
<> width_2x_str
<> "w",
),
attribute.attribute("sizes", sizes),
attribute.src(base_path <> "-1x.png"),
attribute.alt(alt),
attribute.class(class),
]
}),
])
}
fn remove_last(list: List(a)) -> List(a) {
case list {
[] -> []
[_] -> []
[first, ..rest] -> [first, ..remove_last(rest)]
}
}

View File

@@ -1,543 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/i18n
import fluxer_marketing/icons
import fluxer_marketing/web.{type Context}
import gleam/list
import gleam/option.{type Option, None, Some}
import kielet.{gettext as g_}
import kielet/context.{type Context as I18nContext}
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub type Platform {
Windows
MacOS
Linux
IOS
Android
}
pub type ButtonStyle {
Light
Dark
}
const light_bg = "bg-white"
const light_text = "text-[#4641D9]"
const light_hover = "hover:bg-gray-50"
const light_helper = "text-sm text-[#4641D9]/60"
const dark_bg = "bg-[#4641D9]"
const dark_text = "text-white"
const dark_hover = "hover:bg-[#3a36b0]"
const dark_helper = "text-sm text-white/70"
const btn_base = "download-link flex flex-col items-start justify-center gap-1 rounded-l-2xl px-6 py-5 md:px-8 md:py-6 transition-colors shadow-lg"
const chevron_base = "overlay-toggle flex items-center self-stretch rounded-r-2xl px-4 transition-colors shadow-lg"
const mobile_btn_base = "inline-flex flex-col items-center justify-center gap-1 rounded-2xl px-6 py-5 md:px-8 md:py-6 transition-colors shadow-lg"
fn channel_segment(ctx: Context) -> String {
case web.is_canary(ctx) {
True -> "canary"
False -> "stable"
}
}
fn desktop_redirect_url(
ctx: Context,
plat: String,
arch: String,
fmt: String,
) -> String {
web.api_url(
ctx,
"/dl/desktop/"
<> channel_segment(ctx)
<> "/"
<> plat
<> "/"
<> arch
<> "/latest/"
<> fmt,
)
}
fn get_desktop_btn_classes(style: ButtonStyle) -> #(String, String, String) {
case style {
Light -> #(
btn_base <> " " <> light_bg <> " " <> light_text <> " " <> light_hover,
chevron_base
<> " "
<> light_bg
<> " border-l border-gray-200 "
<> light_text
<> " "
<> light_hover,
light_helper,
)
Dark -> #(
btn_base <> " " <> dark_bg <> " " <> dark_text <> " " <> dark_hover,
chevron_base
<> " "
<> dark_bg
<> " border-l border-white/20 "
<> dark_text
<> " "
<> dark_hover,
dark_helper,
)
}
}
fn get_mobile_btn_classes(style: ButtonStyle) -> #(String, String) {
case style {
Light -> #(
mobile_btn_base
<> " "
<> light_bg
<> " "
<> light_text
<> " hover:bg-white/90",
"text-xs text-[#4641D9]/70",
)
Dark -> #(
mobile_btn_base <> " " <> dark_bg <> " " <> dark_text <> " " <> dark_hover,
"text-xs text-white/70",
)
}
}
fn format_arch_label(
i18n_ctx: I18nContext,
platform: Platform,
arch: String,
fmt: String,
) -> String {
case platform {
MacOS ->
case arch {
"arm64" -> g_(i18n_ctx, "Apple Silicon") <> " · " <> fmt
_ -> g_(i18n_ctx, "Intel") <> " · " <> fmt
}
_ -> arch <> " · " <> fmt
}
}
fn format_overlay_label(
i18n_ctx: I18nContext,
platform: Platform,
arch: String,
fmt: String,
) -> String {
case platform {
MacOS ->
case arch {
"arm64" -> g_(i18n_ctx, "Apple Silicon") <> " (" <> fmt <> ")"
_ -> g_(i18n_ctx, "Intel") <> " (" <> fmt <> ")"
}
_ -> fmt <> " (" <> arch <> ")"
}
}
fn default_architecture(ctx: Context, platform: Platform) -> String {
case platform {
MacOS ->
case ctx.architecture {
web.ARM64 -> "arm64"
web.ArchUnknown -> "arm64"
_ -> "x64"
}
_ ->
case ctx.architecture {
web.ARM64 -> "arm64"
_ -> "x64"
}
}
}
pub fn get_platform_download_info(ctx: Context) -> #(String, String, Element(a)) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
case ctx.platform {
web.Windows -> {
let arch = default_architecture(ctx, Windows)
#(
desktop_redirect_url(ctx, "win32", arch, "setup"),
g_(i18n_ctx, "Download for Windows"),
icons.windows([attribute.class("h-5 w-5")]),
)
}
web.MacOS -> {
let arch = default_architecture(ctx, MacOS)
#(
desktop_redirect_url(ctx, "darwin", arch, "dmg"),
g_(i18n_ctx, "Download for macOS"),
icons.apple([attribute.class("h-5 w-5")]),
)
}
web.Linux -> {
let arch = default_architecture(ctx, Linux)
#(
desktop_redirect_url(ctx, "linux", arch, "deb"),
g_(i18n_ctx, "Choose Linux distribution"),
icons.linux([attribute.class("h-5 w-5")]),
)
}
web.IOS -> #(
web.prepend_base_path(ctx, "/download"),
g_(i18n_ctx, "Mobile apps are underway"),
icons.download([attribute.class("h-5 w-5")]),
)
web.Android -> #(
web.prepend_base_path(ctx, "/download"),
g_(i18n_ctx, "Mobile apps are underway"),
icons.download([attribute.class("h-5 w-5")]),
)
web.Unknown -> #(
web.prepend_base_path(ctx, "/download"),
g_(i18n_ctx, "Download"),
icons.download([attribute.class("h-5 w-5")]),
)
}
}
pub fn get_system_requirements(ctx: Context, platform: Platform) -> String {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
case platform {
Windows -> g_(i18n_ctx, "Windows 10+")
MacOS -> g_(i18n_ctx, "macOS 10.15+")
Linux -> ""
IOS -> g_(i18n_ctx, "iOS 15+")
Android -> g_(i18n_ctx, "Android 8+")
}
}
fn get_detected_platform_requirements(ctx: Context) -> String {
case ctx.platform {
web.Windows -> get_system_requirements(ctx, Windows)
web.MacOS -> get_system_requirements(ctx, MacOS)
web.Linux -> get_system_requirements(ctx, Linux)
web.IOS -> get_system_requirements(ctx, IOS)
web.Android -> get_system_requirements(ctx, Android)
web.Unknown -> ""
}
}
pub fn render_with_overlay(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
let requirements = get_detected_platform_requirements(ctx)
let button = case ctx.platform {
web.Windows ->
render_desktop_button(ctx, Windows, Light, None, False, False)
web.MacOS -> render_desktop_button(ctx, MacOS, Light, None, False, False)
web.Linux -> render_desktop_button(ctx, Linux, Light, None, False, False)
web.IOS -> render_mobile_redirect_button(ctx, Light)
web.Android -> render_mobile_redirect_button(ctx, Light)
web.Unknown ->
html.a(
[
attribute.href(web.prepend_base_path(ctx, "/download")),
attribute.class(
"inline-flex items-center justify-center gap-3 rounded-2xl "
<> light_bg
<> " px-8 py-5 md:px-10 md:py-6 text-lg md:text-xl font-semibold "
<> light_text
<> " transition-colors hover:bg-white/90 shadow-lg",
),
],
[
icons.download([attribute.class("h-6 w-6 shrink-0")]),
html.span([], [html.text(g_(i18n_ctx, "Download Fluxer"))]),
],
)
}
case requirements {
"" -> button
req ->
html.div([attribute.class("relative")], [
button,
html.p(
[
attribute.class(
"absolute left-1/2 -translate-x-1/2 top-full mt-2 text-xs text-white/50 text-center whitespace-nowrap",
),
],
[html.text(req)],
),
])
}
}
fn render_mobile_redirect_button(ctx: Context, style: ButtonStyle) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
let #(btn_class, helper_class) = get_mobile_btn_classes(style)
html.a(
[
attribute.class(btn_class),
attribute.href(web.prepend_base_path(ctx, "/download")),
],
[
html.div([attribute.class("flex items-center gap-3")], [
icons.download([attribute.class("h-6 w-6 shrink-0")]),
html.span([attribute.class("text-base md:text-lg font-semibold")], [
html.text(g_(i18n_ctx, "Mobile apps are underway")),
]),
]),
html.span([attribute.class(helper_class)], [
html.text(g_(i18n_ctx, "Use Fluxer in your mobile browser for now")),
]),
],
)
}
fn linux_download_options(ctx: Context) -> List(#(String, String, String)) {
[
#("x64", "AppImage", desktop_redirect_url(ctx, "linux", "x64", "appimage")),
#(
"arm64",
"AppImage",
desktop_redirect_url(ctx, "linux", "arm64", "appimage"),
),
#("x64", "DEB", desktop_redirect_url(ctx, "linux", "x64", "deb")),
#("arm64", "DEB", desktop_redirect_url(ctx, "linux", "arm64", "deb")),
#("x64", "RPM", desktop_redirect_url(ctx, "linux", "x64", "rpm")),
#("arm64", "RPM", desktop_redirect_url(ctx, "linux", "arm64", "rpm")),
#("x64", "tar.gz", desktop_redirect_url(ctx, "linux", "x64", "tar_gz")),
#("arm64", "tar.gz", desktop_redirect_url(ctx, "linux", "arm64", "tar_gz")),
]
}
fn get_platform_config(
ctx: Context,
platform: Platform,
i18n_ctx: I18nContext,
) -> #(String, String, Element(a), List(#(String, String, String))) {
case platform {
Windows -> #(
"windows",
g_(i18n_ctx, "Windows"),
icons.windows([attribute.class("h-6 w-6 shrink-0")]),
[
#("x64", "EXE", desktop_redirect_url(ctx, "win32", "x64", "setup")),
#("arm64", "EXE", desktop_redirect_url(ctx, "win32", "arm64", "setup")),
],
)
MacOS -> #(
"macos",
g_(i18n_ctx, "macOS"),
icons.apple([attribute.class("h-6 w-6 shrink-0")]),
[
#("arm64", "DMG", desktop_redirect_url(ctx, "darwin", "arm64", "dmg")),
#("x64", "DMG", desktop_redirect_url(ctx, "darwin", "x64", "dmg")),
],
)
Linux -> #(
"linux",
g_(i18n_ctx, "Linux"),
icons.linux([attribute.class("h-6 w-6 shrink-0")]),
linux_download_options(ctx),
)
_ -> #("", "", element.none(), [])
}
}
pub fn render_desktop_button(
ctx: Context,
platform: Platform,
style: ButtonStyle,
id_prefix: Option(String),
compact: Bool,
full_width: Bool,
) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
let #(base_platform_id, platform_name, icon, options) =
get_platform_config(ctx, platform, i18n_ctx)
let platform_id = case id_prefix {
Some(prefix) -> prefix <> "-" <> base_platform_id
None -> base_platform_id
}
let default_arch = default_architecture(ctx, platform)
let #(default_arch_label, default_fmt, default_url) =
list.find(options, fn(opt) {
let #(arch, _, _) = opt
arch == default_arch
})
|> fn(r) {
case r {
Ok(opt) -> opt
Error(_) ->
case options {
[first, ..] -> first
_ -> #("", "", "")
}
}
}
let helper_text = case platform {
Linux -> g_(i18n_ctx, "Choose distribution")
_ -> format_arch_label(i18n_ctx, platform, default_arch_label, default_fmt)
}
let #(btn_class, chevron_class, helper_class) = get_desktop_btn_classes(style)
let container_class = case full_width {
True -> "flex w-full"
False -> "flex"
}
let width_modifier = case full_width {
True -> " flex-1 w-full min-w-0"
False -> ""
}
let button_class = btn_class <> width_modifier
let button_label = case compact {
True -> platform_name
False -> g_(i18n_ctx, "Download for ") <> platform_name
}
let overlay_items =
options
|> list.map(fn(opt) {
let #(arch, fmt, url) = opt
html.a(
[
attribute.class(
"download-overlay-link block px-4 py-3 text-sm text-gray-900 hover:bg-gray-100 transition-colors",
),
attribute.attribute("data-base-url", url),
attribute.attribute("data-arch", arch),
attribute.attribute("data-format", fmt),
attribute.attribute("data-platform", platform_id),
attribute.href(url),
],
[html.text(format_overlay_label(i18n_ctx, platform, arch, fmt))],
)
})
html.div([attribute.class("relative")], [
html.div(
[
attribute.class(container_class),
attribute.id(platform_id <> "-download-buttons"),
],
[
html.a(
[
attribute.class(button_class),
attribute.href(default_url),
attribute.attribute("data-base-url", default_url),
attribute.attribute("data-arch", default_arch_label),
attribute.attribute("data-format", default_fmt),
attribute.attribute("data-platform", platform_id),
],
[
html.div([attribute.class("flex items-center gap-3")], [
icon,
html.span(
[attribute.class("text-base md:text-lg font-semibold")],
[
html.text(button_label),
],
),
]),
html.span([attribute.class(helper_class)], [
html.text(helper_text),
]),
],
),
html.button(
[
attribute.class(chevron_class),
attribute.attribute(
"data-overlay-target",
platform_id <> "-overlay",
),
attribute.attribute(
"aria-label",
g_(i18n_ctx, "Show download options"),
),
attribute.attribute("type", "button"),
],
[icons.caret_down([attribute.class("h-5 w-5")])],
),
],
),
html.div(
[
attribute.id(platform_id <> "-overlay"),
attribute.class(
"download-overlay absolute left-0 z-20 mt-2 w-full min-w-[220px] rounded-xl bg-white shadow-xl border border-gray-200 hidden",
),
],
[html.div([attribute.class("py-1")], overlay_items)],
),
])
}
pub fn render_mobile_button(
ctx: Context,
platform: Platform,
style: ButtonStyle,
) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
let #(platform_name, icon, url, helper_text) = case platform {
IOS -> #(
g_(i18n_ctx, "iOS"),
icons.apple([attribute.class("h-6 w-6 shrink-0")]),
web.api_url(ctx, "/dl/ios/testflight"),
"TestFlight",
)
Android -> #(
g_(i18n_ctx, "Android"),
icons.android([attribute.class("h-6 w-6 shrink-0")]),
web.api_url(ctx, "/dl/android/arm64/apk"),
"APK",
)
_ -> #("", element.none(), "", "")
}
let #(btn_class, helper_class) = get_mobile_btn_classes(style)
html.a([attribute.class(btn_class), attribute.href(url)], [
html.div([attribute.class("flex items-center gap-3")], [
icon,
html.span([attribute.class("text-base md:text-lg font-semibold")], [
html.text(g_(i18n_ctx, "Download for ") <> platform_name),
]),
]),
html.span([attribute.class(helper_class)], [html.text(helper_text)]),
])
}

View File

@@ -1,86 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/i18n
import fluxer_marketing/icons
import fluxer_marketing/number_format
import fluxer_marketing/web.{type Context, href}
import gleam/string
import kielet.{gettext as g_}
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn render(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
let slots = ctx.visionary_slots
let remaining_text = number_format.format_number(slots.remaining)
let total_text = number_format.format_number(slots.total)
let headline = case slots.total {
0 ->
g_(
i18n_ctx,
"Lifetime Plutonium + Operator Pass with Visionary — limited slots",
)
_ ->
g_(
i18n_ctx,
"Only {0} of {1} Visionary lifetime slots left — lifetime Plutonium + Operator Pass",
)
|> string.replace("{0}", remaining_text)
|> string.replace("{1}", total_text)
}
html.div(
[
attribute.class(
"fixed top-0 left-0 right-0 z-30 bg-gradient-to-r from-black to-gray-900 text-white",
),
],
[
html.div([attribute.class("mx-auto max-w-7xl px-4 py-3 md:py-2.5")], [
html.div(
[
attribute.class(
"flex flex-col sm:flex-row items-center justify-center gap-2 sm:gap-3 text-xs sm:text-sm font-medium md:text-base text-center sm:text-left",
),
],
[
html.div([attribute.class("flex items-center gap-2 sm:gap-3")], [
icons.infinity([
attribute.class("hidden sm:block h-5 w-5 flex-shrink-0"),
]),
html.span([attribute.class("leading-tight")], [
html.text(headline),
]),
]),
html.a(
[
href(ctx, "/plutonium#visionary"),
attribute.class(
"rounded-lg bg-white px-3 py-1.5 sm:px-4 text-xs sm:text-sm font-semibold text-black hover:bg-gray-100 transition-colors whitespace-nowrap",
),
],
[html.text(g_(i18n_ctx, "Get Visionary"))],
),
],
),
]),
],
)
}

View File

@@ -1,201 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/i18n
import fluxer_marketing/icons
import fluxer_marketing/pricing
import fluxer_marketing/web.{type Context, href}
import kielet.{gettext as g_}
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn render(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
let monthly_price =
pricing.get_formatted_price(pricing.Monthly, ctx.country_code)
let yearly_price =
pricing.get_formatted_price(pricing.Yearly, ctx.country_code)
html.section(
[
attribute.class(
"bg-gradient-to-b from-white to-gray-50 px-6 py-24 md:py-40",
),
],
[
html.div([attribute.class("mx-auto max-w-7xl")], [
html.div([attribute.class("mb-16 md:mb-20 text-center")], [
html.h2(
[
attribute.class(
"display mb-8 md:mb-10 text-black text-4xl md:text-5xl lg:text-6xl",
),
],
[
html.text(g_(i18n_ctx, "Get more with Fluxer Plutonium")),
],
),
html.div(
[
attribute.class(
"flex flex-col sm:flex-row items-center justify-center gap-3 mb-6",
),
],
[
html.span(
[attribute.class("text-3xl md:text-4xl font-bold text-black")],
[
html.text(
monthly_price
<> g_(i18n_ctx, "/mo")
<> " "
<> g_(i18n_ctx, "or")
<> " "
<> yearly_price
<> g_(i18n_ctx, "/yr"),
),
],
),
html.span(
[
attribute.class(
"inline-flex items-center rounded-xl bg-[#4641D9] px-4 py-2 text-sm md:text-base font-semibold text-white",
),
],
[html.text(g_(i18n_ctx, "Save 17%"))],
),
],
),
html.p(
[
attribute.class("lead mx-auto max-w-2xl text-gray-700"),
],
[
html.text(g_(
i18n_ctx,
"Higher limits, exclusive features, and early access to new updates",
)),
],
),
]),
html.div(
[
attribute.class(
"mb-16 md:mb-20 grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12 max-w-4xl mx-auto",
),
],
[
html.div(
[attribute.class("flex flex-col items-center text-center")],
[
html.div(
[
attribute.class(
"inline-flex items-center justify-center w-20 h-20 md:w-24 md:h-24 rounded-3xl bg-gradient-to-br from-[#4641D9]/10 to-[#4641D9]/5 mb-4",
),
],
[
icons.hash([
attribute.class(
"h-10 w-10 md:h-12 md:w-12 text-[#4641D9]",
),
]),
],
),
html.h3(
[
attribute.class(
"title text-black text-lg md:text-xl whitespace-nowrap",
),
],
[html.text(g_(i18n_ctx, "Custom identity"))],
),
],
),
html.div(
[attribute.class("flex flex-col items-center text-center")],
[
html.div(
[
attribute.class(
"inline-flex items-center justify-center w-20 h-20 md:w-24 md:h-24 rounded-3xl bg-gradient-to-br from-[#4641D9]/10 to-[#4641D9]/5 mb-4",
),
],
[
icons.video_camera([
attribute.class(
"h-10 w-10 md:h-12 md:w-12 text-[#4641D9]",
),
]),
],
),
html.h3(
[
attribute.class(
"title text-black text-lg md:text-xl whitespace-nowrap",
),
],
[html.text(g_(i18n_ctx, "Premium quality"))],
),
],
),
html.div(
[attribute.class("flex flex-col items-center text-center")],
[
html.div(
[
attribute.class(
"inline-flex items-center justify-center w-20 h-20 md:w-24 md:h-24 rounded-3xl bg-gradient-to-br from-[#4641D9]/10 to-[#4641D9]/5 mb-4",
),
],
[
icons.sparkle([
attribute.class(
"h-10 w-10 md:h-12 md:w-12 text-[#4641D9]",
),
]),
],
),
html.h3(
[
attribute.class(
"title text-black text-lg md:text-xl whitespace-nowrap",
),
],
[html.text(g_(i18n_ctx, "Exclusive features"))],
),
],
),
],
),
html.div([attribute.class("text-center")], [
html.a(
[
href(ctx, "/plutonium"),
attribute.class(
"label inline-block rounded-xl bg-[#4641D9] px-10 py-5 md:px-12 md:py-6 text-lg md:text-xl text-white transition hover:bg-opacity-90 shadow-lg",
),
],
[html.text(g_(i18n_ctx, "Learn more"))],
),
]),
]),
],
)
}

View File

@@ -1,408 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/i18n
import fluxer_marketing/icons
import fluxer_marketing/web.{type Context}
import kielet.{gettext as g_}
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
import lustre/element/svg
pub fn render_trigger(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
html.button(
[
attribute.id("pwa-install-button"),
attribute.class(
"inline-flex items-center gap-2 rounded-xl px-5 py-3 transition-colors bg-white/10 hover:bg-white/20 text-white font-medium text-sm",
),
],
[
icons.devices([attribute.class("h-5 w-5")]),
html.text(g_(i18n_ctx, "How to install as an app")),
],
)
}
pub fn render_modal(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
html.div(
[
attribute.id("pwa-modal-backdrop"),
attribute.class("pwa-modal-backdrop"),
],
[
html.div([attribute.class("pwa-modal")], [
html.div([attribute.class("flex flex-col h-full")], [
html.div(
[attribute.class("flex items-center justify-between p-6 pb-4")],
[
html.h2([attribute.class("text-xl font-bold text-gray-900")], [
html.text(g_(i18n_ctx, "Install Fluxer as an app")),
]),
html.button(
[
attribute.class(
"p-2 hover:bg-gray-100 rounded-lg text-gray-600 hover:text-gray-900",
),
attribute.id("pwa-close"),
attribute.attribute("aria-label", "Close"),
],
[
svg.svg(
[
attribute.class("w-5 h-5"),
attribute.attribute("fill", "none"),
attribute.attribute("stroke", "currentColor"),
attribute.attribute("viewBox", "0 0 24 24"),
],
[
svg.path([
attribute.attribute("stroke-linecap", "round"),
attribute.attribute("stroke-linejoin", "round"),
attribute.attribute("stroke-width", "2"),
attribute.attribute("d", "M6 18L18 6M6 6l12 12"),
]),
],
),
],
),
],
),
html.div([attribute.class("px-6")], [
html.div(
[
attribute.class("flex gap-1 p-1 bg-gray-100 rounded-xl"),
attribute.id("pwa-tabs"),
],
[
tab_button("android", g_(i18n_ctx, "Android"), True),
tab_button("ios", g_(i18n_ctx, "iOS / iPadOS"), False),
tab_button("desktop", g_(i18n_ctx, "Desktop"), False),
],
),
]),
html.div([attribute.class("flex-1 overflow-y-auto p-6 pt-4")], [
html.div(
[
attribute.id("pwa-panel-android"),
attribute.class("pwa-panel"),
],
[render_android_steps(ctx)],
),
html.div(
[
attribute.id("pwa-panel-ios"),
attribute.class("pwa-panel hidden"),
],
[render_ios_steps(ctx)],
),
html.div(
[
attribute.id("pwa-panel-desktop"),
attribute.class("pwa-panel hidden"),
],
[render_desktop_steps(ctx)],
),
]),
html.div(
[
attribute.class("px-6 py-4 border-t border-gray-100 text-center"),
],
[
html.p([attribute.class("text-xs text-gray-400")], [
html.text(g_(i18n_ctx, "Screenshots courtesy of ")),
html.a(
[
attribute.href("https://installpwa.com/"),
attribute.target("_blank"),
attribute.rel("noopener noreferrer"),
attribute.class(
"text-blue-500 hover:text-blue-600 underline",
),
],
[html.text("installpwa.com")],
),
]),
],
),
]),
]),
],
)
}
fn tab_button(id: String, label: String, active: Bool) -> Element(a) {
html.button(
[
attribute.attribute("data-tab", id),
attribute.class(
"pwa-tab flex-1 px-4 py-2 text-sm font-medium rounded-lg transition-colors "
<> case active {
True -> "bg-white text-gray-900 shadow-sm"
False -> "text-gray-600 hover:text-gray-900"
},
),
],
[html.text(label)],
)
}
fn render_android_steps(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
html.div([attribute.class("flex flex-col md:flex-row gap-6")], [
html.div([attribute.class("md:w-1/3 flex justify-center")], [
render_image(ctx, "android", "240", "320", "480"),
]),
html.div([attribute.class("md:w-2/3")], [
html.ol([attribute.class("space-y-4")], [
step_item(
"1",
html.span([], [
html.a(
[
attribute.href("https://web.fluxer.app"),
attribute.target("_blank"),
attribute.rel("noopener noreferrer"),
attribute.class("text-blue-600 hover:text-blue-700 underline"),
],
[html.text(g_(i18n_ctx, "Open the web app"))],
),
html.text(g_(i18n_ctx, " in Chrome")),
]),
),
step_item(
"2",
html.text(g_(
i18n_ctx,
"Press the \"More\" (\u{22EE}) button in the top-right corner",
)),
),
step_item("3", html.text(g_(i18n_ctx, "Press \"Install app\""))),
step_item(
"4",
html.span([attribute.class("text-green-600 font-medium")], [
html.text(g_(
i18n_ctx,
"Done! You can open Fluxer from your home screen.",
)),
]),
),
]),
]),
])
}
fn render_ios_steps(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
html.div([attribute.class("flex flex-col md:flex-row gap-6")], [
html.div([attribute.class("md:w-1/2 flex justify-center")], [
render_image(ctx, "ios", "320", "480", "640"),
]),
html.div([attribute.class("md:w-1/2")], [
html.ol([attribute.class("space-y-4")], [
step_item(
"1",
html.span([], [
html.a(
[
attribute.href("https://web.fluxer.app"),
attribute.target("_blank"),
attribute.rel("noopener noreferrer"),
attribute.class("text-blue-600 hover:text-blue-700 underline"),
],
[html.text(g_(i18n_ctx, "Open the web app"))],
),
html.text(g_(i18n_ctx, " in Safari")),
]),
),
step_item(
"2",
html.text(g_(
i18n_ctx,
"Press the Share button (rectangle with upwards-pointing arrow)",
)),
),
step_item("3", html.text(g_(i18n_ctx, "Press \"Add to Home Screen\""))),
step_item(
"4",
html.text(g_(i18n_ctx, "Press \"Add\" in the upper-right corner")),
),
step_item(
"5",
html.span([attribute.class("text-green-600 font-medium")], [
html.text(g_(
i18n_ctx,
"Done! You can open Fluxer from your home screen.",
)),
]),
),
]),
]),
])
}
fn render_desktop_steps(ctx: Context) -> Element(a) {
let i18n_ctx = i18n.get_context(ctx.i18n_db, ctx.locale)
html.div([attribute.class("flex flex-col md:flex-row gap-6")], [
html.div([attribute.class("md:w-1/2 flex justify-center")], [
render_image(ctx, "desktop", "320", "480", "640"),
]),
html.div([attribute.class("md:w-1/2")], [
html.ol([attribute.class("space-y-4")], [
step_item(
"1",
html.span([], [
html.a(
[
attribute.href("https://web.fluxer.app"),
attribute.target("_blank"),
attribute.rel("noopener noreferrer"),
attribute.class("text-blue-600 hover:text-blue-700 underline"),
],
[html.text(g_(i18n_ctx, "Open the web app"))],
),
html.text(g_(
i18n_ctx,
" in Chrome or another browser with PWA support",
)),
]),
),
step_item(
"2",
html.text(g_(
i18n_ctx,
"Press the install button (downwards-pointing arrow on monitor) in the address bar",
)),
),
step_item(
"3",
html.text(g_(i18n_ctx, "Press \"Install\" in the popup that appears")),
),
step_item(
"4",
html.span([attribute.class("text-green-600 font-medium")], [
html.text(g_(
i18n_ctx,
"Done! You can now open Fluxer as if it were a regular program.",
)),
]),
),
]),
]),
])
}
fn step_item(number: String, content: Element(a)) -> Element(a) {
html.li([attribute.class("flex gap-4")], [
html.div(
[
attribute.class(
"flex-shrink-0 w-8 h-8 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center font-bold text-sm",
),
],
[html.text(number)],
),
html.div([attribute.class("pt-1 text-gray-700")], [content]),
])
}
fn render_image(
ctx: Context,
name: String,
small: String,
medium: String,
large: String,
) -> Element(a) {
let base_path = ctx.cdn_endpoint <> "/marketing/pwa-install/" <> name
element.element("picture", [], [
element.element(
"source",
[
attribute.type_("image/avif"),
attribute.attribute(
"srcset",
base_path
<> "-"
<> small
<> "w.avif 1x, "
<> base_path
<> "-"
<> medium
<> "w.avif 1.5x, "
<> base_path
<> "-"
<> large
<> "w.avif 2x",
),
],
[],
),
element.element(
"source",
[
attribute.type_("image/webp"),
attribute.attribute(
"srcset",
base_path
<> "-"
<> small
<> "w.webp 1x, "
<> base_path
<> "-"
<> medium
<> "w.webp 1.5x, "
<> base_path
<> "-"
<> large
<> "w.webp 2x",
),
],
[],
),
html.img([
attribute.src(base_path <> "-" <> medium <> "w.png"),
attribute.attribute(
"srcset",
base_path
<> "-"
<> small
<> "w.png 1x, "
<> base_path
<> "-"
<> medium
<> "w.png 1.5x, "
<> base_path
<> "-"
<> large
<> "w.png 2x",
),
attribute.alt("PWA installation guide for " <> name),
attribute.class(
"max-w-full h-auto rounded-lg shadow-lg border border-gray-200",
),
]),
])
}

View File

@@ -1,117 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/icons
import fluxer_marketing/web.{type Context}
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn render(
_ctx: Context,
icon: String,
title: String,
description: String,
button_text: String,
button_href: String,
) -> Element(a) {
html.div(
[
attribute.class(
"flex h-full flex-col rounded-3xl bg-white/95 backdrop-blur-sm p-10 md:p-12 shadow-lg border border-white/50",
),
],
[
html.div([attribute.class("mb-8 text-center")], [
html.div(
[
attribute.class(
"inline-flex items-center justify-center w-24 h-24 md:w-28 md:h-28 rounded-3xl bg-[#4641D9] mb-6",
),
],
[
case icon {
"rocket-launch" ->
icons.rocket_launch([
attribute.class("h-12 w-12 md:h-14 md:w-14 text-white"),
])
"fluxer-partner" ->
icons.fluxer_partner([
attribute.class("h-12 w-12 md:h-14 md:w-14 text-white"),
])
"chat-centered-text" ->
icons.chat_centered_text([
attribute.class("h-12 w-12 md:h-14 md:w-14 text-white"),
])
"bluesky" ->
icons.bluesky([
attribute.class("h-12 w-12 md:h-14 md:w-14 text-white"),
])
"bug" ->
icons.bug([
attribute.class("h-12 w-12 md:h-14 md:w-14 text-white"),
])
"code" ->
icons.code_icon([
attribute.class("h-12 w-12 md:h-14 md:w-14 text-white"),
])
"translate" ->
icons.translate([
attribute.class("h-12 w-12 md:h-14 md:w-14 text-white"),
])
"shield-check" ->
icons.shield_check([
attribute.class("h-12 w-12 md:h-14 md:w-14 text-white"),
])
_ -> html.div([], [])
},
],
),
html.h3(
[attribute.class("title text-gray-900 mb-4 text-xl md:text-2xl")],
[
html.text(title),
],
),
html.p([attribute.class("body-lg text-gray-700 leading-relaxed")], [
html.text(description),
]),
]),
html.div([attribute.class("mt-auto flex flex-col items-center")], [
html.a(
case button_href {
"https://" <> _ | "http://" <> _ | "mailto:" <> _ -> [
attribute.href(button_href),
attribute.class(
"label inline-block rounded-xl bg-[#4641D9] px-8 py-4 text-base md:text-lg text-white transition-colors hover:bg-opacity-90 shadow-md w-full text-center",
),
attribute.target("_blank"),
attribute.rel("noopener noreferrer"),
]
_ -> [
attribute.href(button_href),
attribute.class(
"label inline-block rounded-xl bg-[#4641D9] px-8 py-4 text-base md:text-lg text-white transition-colors hover:bg-opacity-90 shadow-md w-full text-center",
),
]
},
[html.text(button_text)],
),
]),
],
)
}

View File

@@ -1,131 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import envoy
import gleam/int
import gleam/list
import gleam/option
import gleam/result
import gleam/string
pub type Config {
Config(
secret_key_base: String,
api_endpoint: String,
api_host: String,
app_endpoint: String,
cdn_endpoint: String,
marketing_endpoint: String,
port: Int,
base_path: String,
build_timestamp: String,
release_channel: String,
gateway_rpc_secret: String,
metrics_endpoint: option.Option(String),
)
}
fn normalize_base_path(base_path: String) -> String {
let segments =
base_path
|> string.trim
|> string.split("/")
|> list.filter(fn(segment) { segment != "" })
case segments {
[] -> ""
_ -> "/" <> string.join(segments, "/")
}
}
fn normalize_endpoint(endpoint: String) -> String {
let len = string.length(endpoint)
case len > 0 && string.ends_with(endpoint, "/") {
True -> normalize_endpoint(string.slice(endpoint, 0, len - 1))
False -> endpoint
}
}
fn required_env(key: String) -> Result(String, String) {
case envoy.get(key) {
Ok(value) ->
case string.trim(value) {
"" -> Error("Missing required env: " <> key)
trimmed -> Ok(trimmed)
}
Error(_) -> Error("Missing required env: " <> key)
}
}
fn optional_env(key: String) -> option.Option(String) {
case envoy.get(key) {
Ok(value) ->
case string.trim(value) {
"" -> option.None
trimmed -> option.Some(trimmed)
}
Error(_) -> option.None
}
}
fn required_int_env(key: String) -> Result(Int, String) {
use raw <- result.try(required_env(key))
case int.parse(raw) {
Ok(n) -> Ok(n)
Error(_) -> Error("Invalid integer for env " <> key <> ": " <> raw)
}
}
pub fn load_config() -> Result(Config, String) {
use secret_key_base <- result.try(required_env("SECRET_KEY_BASE"))
use api_endpoint_raw <- result.try(required_env("FLUXER_API_PUBLIC_ENDPOINT"))
use api_host <- result.try(required_env("FLUXER_API_HOST"))
use app_endpoint_raw <- result.try(required_env("FLUXER_APP_ENDPOINT"))
use cdn_endpoint_raw <- result.try(required_env("FLUXER_CDN_ENDPOINT"))
use marketing_endpoint_raw <- result.try(required_env(
"FLUXER_MARKETING_ENDPOINT",
))
use base_path_raw <- result.try(required_env("FLUXER_PATH_MARKETING"))
use port <- result.try(required_int_env("FLUXER_MARKETING_PORT"))
use release_channel <- result.try(required_env("RELEASE_CHANNEL"))
use gateway_rpc_secret <- result.try(required_env("GATEWAY_RPC_SECRET"))
let api_endpoint = normalize_endpoint(api_endpoint_raw)
let app_endpoint = normalize_endpoint(app_endpoint_raw)
let cdn_endpoint = normalize_endpoint(cdn_endpoint_raw)
let marketing_endpoint = normalize_endpoint(marketing_endpoint_raw)
let base_path = normalize_base_path(base_path_raw)
let metrics_endpoint = case optional_env("FLUXER_METRICS_HOST") {
option.Some(host) -> option.Some("http://" <> host)
option.None -> option.None
}
Ok(Config(
secret_key_base: secret_key_base,
api_endpoint: api_endpoint,
api_host: api_host,
app_endpoint: app_endpoint,
cdn_endpoint: cdn_endpoint,
marketing_endpoint: marketing_endpoint,
port: port,
base_path: base_path,
build_timestamp: optional_env("BUILD_TIMESTAMP") |> option.unwrap(""),
release_channel: release_channel,
gateway_rpc_secret: gateway_rpc_secret,
metrics_endpoint: metrics_endpoint,
))
}

View File

@@ -1,42 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/locale.{type Locale}
import fluxer_marketing/web.{type Context}
import gleam/list
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html
pub fn flag_svg(
locale: Locale,
ctx: Context,
attributes: List(attribute.Attribute(a)),
) -> Element(a) {
let flag_code = locale.get_flag_code(locale)
html.img(
[
attribute.src(
ctx.cdn_endpoint <> "/marketing/flags/" <> flag_code <> ".svg",
),
attribute.alt("Flag"),
attribute.attribute("loading", "lazy"),
]
|> list.append(attributes),
)
}

View File

@@ -1,96 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import gleam/dict.{type Dict}
import gleam/int
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/string
pub type Frontmatter {
Frontmatter(data: Dict(String, String), content: String)
}
pub fn parse(markdown: String) -> Frontmatter {
case string.starts_with(markdown, "---\n") {
True -> parse_with_frontmatter(markdown)
False -> Frontmatter(data: dict.new(), content: markdown)
}
}
fn parse_with_frontmatter(markdown: String) -> Frontmatter {
let without_first = string.drop_start(markdown, 4)
case string.split_once(without_first, "\n---\n") {
Ok(#(frontmatter_text, content)) -> {
let metadata = parse_frontmatter_text(frontmatter_text)
Frontmatter(data: metadata, content: string.trim(content))
}
Error(_) -> Frontmatter(data: dict.new(), content: markdown)
}
}
fn parse_frontmatter_text(text: String) -> Dict(String, String) {
text
|> string.split("\n")
|> list.filter(fn(line) { !string.is_empty(string.trim(line)) })
|> list.filter_map(parse_frontmatter_line)
|> dict.from_list
}
fn parse_frontmatter_line(line: String) -> Result(#(String, String), Nil) {
case string.split_once(line, ":") {
Ok(#(key, value)) -> {
let clean_key = string.trim(key)
let clean_value = string.trim(value)
Ok(#(clean_key, clean_value))
}
Error(_) -> Error(Nil)
}
}
pub fn get_string(frontmatter: Frontmatter, key: String) -> Option(String) {
dict.get(frontmatter.data, key) |> option.from_result
}
pub fn get_string_or(
frontmatter: Frontmatter,
key: String,
default: String,
) -> String {
get_string(frontmatter, key) |> option.unwrap(default)
}
pub fn get_int(frontmatter: Frontmatter, key: String) -> Option(Int) {
case get_string(frontmatter, key) {
Some(value) -> {
case int.parse(value) {
Ok(num) -> Some(num)
Error(_) -> None
}
}
None -> None
}
}
pub fn get_int_or(frontmatter: Frontmatter, key: String, default: Int) -> Int {
get_int(frontmatter, key) |> option.unwrap(default)
}
pub fn get_content(frontmatter: Frontmatter) -> String {
frontmatter.content
}

View File

@@ -1,194 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import gleam/dynamic/decode
import gleam/http
import gleam/http/request
import gleam/httpc
import gleam/int
import gleam/json
import gleam/list
import gleam/result
import gleam/string
import wisp
pub type Settings {
Settings(api_host: String, rpc_secret: String)
}
const default_cc = "US"
const log_prefix = "[geoip]"
pub fn country_code(req: wisp.Request, settings: Settings) -> String {
case extract_client_ip(req) {
"" -> default_cc
ip ->
case fetch_country_code(settings, ip) {
Ok(code) -> code
Error(_) -> default_cc
}
}
}
fn fetch_country_code(settings: Settings, ip: String) -> Result(String, Nil) {
case rpc_url(settings.api_host) {
"" -> {
log_missing_api_host(settings.api_host)
Error(Nil)
}
url -> {
let body =
json.object([
#("type", json.string("geoip_lookup")),
#("ip", json.string(ip)),
])
|> json.to_string
let assert Ok(req) = request.to(url)
let req =
req
|> request.set_method(http.Post)
|> request.prepend_header("content-type", "application/json")
|> request.prepend_header(
"Authorization",
"Bearer " <> settings.rpc_secret,
)
|> request.set_body(body)
case httpc.send(req) {
Ok(resp) if resp.status >= 200 && resp.status < 300 ->
decode_country_code(resp.body)
Ok(resp) -> {
log_rpc_status(settings.api_host, resp.status, resp.body)
Error(Nil)
}
Error(error) -> {
log_rpc_error(settings.api_host, string.inspect(error))
Error(Nil)
}
}
}
}
}
fn decode_country_code(body: String) -> Result(String, Nil) {
let response_decoder = {
use data <- decode.field("data", {
use code <- decode.field("country_code", decode.string)
decode.success(code)
})
decode.success(data)
}
case json.parse(from: body, using: response_decoder) {
Ok(code) -> Ok(string.uppercase(string.trim(code)))
Error(_) -> Error(Nil)
}
}
fn extract_client_ip(req: wisp.Request) -> String {
case request.get_header(req, "x-forwarded-for") {
Ok(xff) ->
xff
|> string.split(",")
|> list.first
|> result.unwrap("")
|> string.trim
|> strip_brackets
Error(_) -> ""
}
}
pub fn strip_brackets(ip: String) -> String {
let len = string.length(ip)
case
len >= 2
&& string.first(ip) == Ok("[")
&& string.slice(ip, len - 1, 1) == "]"
{
True -> string.slice(ip, 1, len - 2)
False -> ip
}
}
fn log_missing_api_host(host: String) -> Nil {
wisp.log_warning(
string.concat([log_prefix, " missing api_host (", host, ")"]),
)
}
fn log_rpc_status(api_host: String, status: Int, body: String) -> Nil {
wisp.log_warning(
string.concat([
log_prefix,
" rpc returned status ",
int.to_string(status),
" from ",
host_display(api_host),
": ",
response_snippet(body),
]),
)
}
fn log_rpc_error(api_host: String, message: String) -> Nil {
wisp.log_warning(
string.concat([
log_prefix,
" rpc request to ",
host_display(api_host),
" failed: ",
message,
]),
)
}
fn host_display(api_host: String) -> String {
case string.contains(api_host, "://") {
True -> api_host
False -> "http://" <> api_host
}
}
fn rpc_url(api_host: String) -> String {
let host = string.trim(api_host)
case host {
"" -> ""
_ -> {
let base = case string.contains(host, "://") {
True -> host
False -> "http://" <> host
}
let normalized = case string.ends_with(base, "/") {
True -> string.slice(base, 0, string.length(base) - 1)
False -> base
}
normalized <> "/_rpc"
}
}
}
fn response_snippet(body: String) -> String {
let len = string.length(body)
case len <= 256 {
True -> body
False -> string.slice(body, 0, 256) <> "..."
}
}

View File

@@ -1,332 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/frontmatter
import fluxer_marketing/locale.{type Locale}
import gleam/dict
import gleam/int
import gleam/list
import gleam/regexp
import gleam/result
import gleam/string
import simplifile
const regex_options = regexp.Options(case_insensitive: False, multi_line: True)
pub type HelpArticle {
HelpArticle(
title: String,
description: String,
category: String,
category_title: String,
category_icon: String,
order: Int,
slug: String,
snowflake_id: String,
content: String,
)
}
pub type HelpCategory {
HelpCategory(
name: String,
title: String,
icon: String,
article_count: Int,
articles: List(HelpArticle),
)
}
pub type HelpCenterData {
HelpCenterData(
categories: List(HelpCategory),
all_articles: List(HelpArticle),
)
}
pub fn load_help_articles(locale: Locale) -> HelpCenterData {
let locale_code = locale.get_code_from_locale(locale)
let help_root = "priv/help"
case scan_help_directory(help_root, locale_code) {
Ok(articles) -> {
let categories = group_by_category(articles)
HelpCenterData(categories: categories, all_articles: articles)
}
Error(_) -> HelpCenterData(categories: [], all_articles: [])
}
}
fn scan_help_directory(
root: String,
locale_code: String,
) -> Result(List(HelpArticle), Nil) {
case list_directory_recursive(root) {
Ok(files) -> {
let locale_suffix = "/" <> locale_code <> ".md"
let fallback_suffix = "/en-US.md"
let locale_files =
files
|> list.filter(fn(path) { string.ends_with(path, locale_suffix) })
let fallback_files = case locale_code == "en-US" {
True -> []
False ->
files
|> list.filter(fn(path) { string.ends_with(path, fallback_suffix) })
|> list.filter(fn(fallback_path) {
let base_dir = get_article_base_dir(fallback_path)
let has_locale_version =
list.any(locale_files, fn(locale_path) {
get_article_base_dir(locale_path) == base_dir
})
!has_locale_version
})
}
let articles =
list.append(locale_files, fallback_files)
|> list.filter_map(fn(path) { load_article(path) })
Ok(articles)
}
Error(_) -> Error(Nil)
}
}
fn get_article_base_dir(path: String) -> String {
path
|> string.split("/")
|> list.reverse
|> list.drop(1)
|> list.reverse
|> string.join("/")
}
fn list_directory_recursive(dir: String) -> Result(List(String), Nil) {
case simplifile.read_directory(dir) {
Ok(entries) -> {
let results =
entries
|> list.map(fn(entry) {
let path = dir <> "/" <> entry
case simplifile.is_directory(path) {
Ok(True) -> list_directory_recursive(path)
Ok(False) -> Ok([path])
Error(_) -> Ok([])
}
})
let all_files =
results
|> list.filter_map(fn(r) { result.unwrap(r, []) |> Ok })
|> list.flatten
Ok(all_files)
}
Error(_) -> Error(Nil)
}
}
fn load_article(path: String) -> Result(HelpArticle, Nil) {
case simplifile.read(path) {
Ok(content) -> {
let fm = frontmatter.parse(content)
let slug = extract_slug_from_path(path)
let title = frontmatter.get_string_or(fm, "title", "Untitled")
let description =
frontmatter.get_string_or(fm, "description", "No description")
let category = frontmatter.get_string_or(fm, "category", "general")
let category_title =
frontmatter.get_string_or(fm, "category_title", "General")
let category_icon =
frontmatter.get_string_or(fm, "category_icon", "sparkle")
let order = frontmatter.get_int_or(fm, "order", 999)
let snowflake_id = frontmatter.get_string_or(fm, "snowflake_id", "")
Ok(HelpArticle(
title: title,
description: description,
category: category,
category_title: category_title,
category_icon: category_icon,
order: order,
slug: slug,
snowflake_id: snowflake_id,
content: frontmatter.get_content(fm),
))
}
Error(_) -> Error(Nil)
}
}
fn extract_slug_from_path(path: String) -> String {
path
|> string.split("/")
|> list.reverse
|> list.drop(1)
|> list.first
|> result.unwrap("unknown")
}
fn group_by_category(articles: List(HelpArticle)) -> List(HelpCategory) {
let grouped =
articles
|> list.group(fn(article) { article.category })
grouped
|> dict.to_list
|> list.map(fn(entry) {
let #(category_name, category_articles) = entry
let sorted_articles =
category_articles
|> list.sort(fn(a: HelpArticle, b: HelpArticle) {
int.compare(a.order, b.order)
})
let first = list.first(sorted_articles)
let category_title = case first {
Ok(article) -> article.category_title
Error(_) -> category_name
}
let category_icon = case first {
Ok(article) -> article.category_icon
Error(_) -> "sparkle"
}
HelpCategory(
name: category_name,
title: category_title,
icon: category_icon,
article_count: list.length(category_articles),
articles: sorted_articles,
)
})
|> list.sort(fn(a, b) { string.compare(a.title, b.title) })
}
pub fn get_category(
data: HelpCenterData,
category_name: String,
) -> Result(HelpCategory, Nil) {
data.categories
|> list.find(fn(cat) { cat.name == category_name })
}
pub fn get_article(
data: HelpCenterData,
category_name: String,
article_slug: String,
) -> Result(HelpArticle, Nil) {
data.all_articles
|> list.find(fn(article) {
article.category == category_name && article.slug == article_slug
})
}
pub fn get_article_by_snowflake(
data: HelpCenterData,
snowflake_id: String,
) -> Result(HelpArticle, Nil) {
data.all_articles
|> list.find(fn(article) { article.snowflake_id == snowflake_id })
}
pub fn create_slug(title: String) -> String {
let lower = string.lowercase(title)
let hyphened = case regexp.compile("\\s+", regex_options) {
Ok(regex) -> regexp.replace(regex, lower, "-")
Error(_) -> lower
}
let cleaned = case regexp.compile("[^\\p{L}\\p{N}\\-._~]+", regex_options) {
Ok(regex) -> regexp.replace(regex, hyphened, "-")
Error(_) -> hyphened
}
let collapsed = case regexp.compile("-+", regex_options) {
Ok(regex) -> regexp.replace(regex, cleaned, "-")
Error(_) -> cleaned
}
let trimmed = trim_hyphens(collapsed)
case string.is_empty(trimmed) {
True -> "article"
False -> trimmed
}
}
fn trim_hyphens(text: String) -> String {
let chars = string.to_graphemes(text)
let trimmed_start =
list.drop_while(chars, fn(c) { c == "-" })
|> list.reverse
|> list.drop_while(fn(c) { c == "-" })
|> list.reverse
case trimmed_start {
[] -> ""
_ -> string.join(trimmed_start, "")
}
}
pub fn search_articles(data: HelpCenterData, query: String) -> List(HelpArticle) {
let lower_query = string.lowercase(query)
data.all_articles
|> list.filter(fn(article) {
let title_match =
string.lowercase(article.title) |> string.contains(lower_query)
let desc_match =
string.lowercase(article.description) |> string.contains(lower_query)
title_match || desc_match
})
}
pub fn filter_by_category(
articles: List(HelpArticle),
category: String,
) -> List(HelpArticle) {
articles
|> list.filter(fn(article) { article.category == category })
}
pub fn article_href(
locale: Locale,
data: HelpCenterData,
snowflake_id: String,
) -> String {
let locale_code = locale.get_code_from_locale(locale) |> string.lowercase
case get_article_by_snowflake(data, snowflake_id) {
Ok(article) ->
"/help/"
<> locale_code
<> "/articles/"
<> article.snowflake_id
<> "-"
<> create_slug(article.title)
Error(_) -> "/help/articles/" <> snowflake_id
}
}

View File

@@ -1,56 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/locale.{type Locale}
import kielet/context.{type Context, Context}
import kielet/database
import kielet/language
import simplifile
pub fn setup_database() -> database.Database {
let db = database.new()
db
}
pub fn get_context(db: database.Database, locale: Locale) -> Context {
let locale_code = locale.get_code_from_locale(locale)
let db_with_fallback = case load_locale(db, locale_code) {
Ok(updated_db) -> updated_db
Error(_) -> db
}
Context(db_with_fallback, locale_code)
}
fn load_locale(
db: database.Database,
locale_code: String,
) -> Result(database.Database, Nil) {
let mo_path = "priv/locales/" <> locale_code <> "/LC_MESSAGES/messages.mo"
case simplifile.read_bits(mo_path) {
Ok(mo_data) -> {
case language.load(locale_code, mo_data) {
Ok(lang) -> Ok(database.add_language(db, lang))
Error(_) -> Error(Nil)
}
}
Error(_) -> Error(Nil)
}
}

View File

@@ -1,227 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_marketing/icons/android
import fluxer_marketing/icons/apple
import fluxer_marketing/icons/arrow_right
import fluxer_marketing/icons/arrow_up
import fluxer_marketing/icons/bluesky
import fluxer_marketing/icons/brain
import fluxer_marketing/icons/bug
import fluxer_marketing/icons/butterfly
import fluxer_marketing/icons/calendar_check
import fluxer_marketing/icons/caret_down
import fluxer_marketing/icons/chart_line
import fluxer_marketing/icons/chat_centered_text
import fluxer_marketing/icons/chats
import fluxer_marketing/icons/chats_circle
import fluxer_marketing/icons/check
import fluxer_marketing/icons/check_circle
import fluxer_marketing/icons/code_icon
import fluxer_marketing/icons/coins
import fluxer_marketing/icons/crown
import fluxer_marketing/icons/devices
import fluxer_marketing/icons/download
import fluxer_marketing/icons/flask
import fluxer_marketing/icons/fluxer_bug_hunter
import fluxer_marketing/icons/fluxer_ctp
import fluxer_marketing/icons/fluxer_logo_wordmark
import fluxer_marketing/icons/fluxer_partner
import fluxer_marketing/icons/fluxer_premium
import fluxer_marketing/icons/fluxer_staff
import fluxer_marketing/icons/game_controller
import fluxer_marketing/icons/gear
import fluxer_marketing/icons/gif
import fluxer_marketing/icons/gift
import fluxer_marketing/icons/github
import fluxer_marketing/icons/globe
import fluxer_marketing/icons/google_play
import fluxer_marketing/icons/graduation_cap
import fluxer_marketing/icons/hash
import fluxer_marketing/icons/heart
import fluxer_marketing/icons/infinity
import fluxer_marketing/icons/lightning
import fluxer_marketing/icons/link
import fluxer_marketing/icons/linux
import fluxer_marketing/icons/magnifying_glass
import fluxer_marketing/icons/medal
import fluxer_marketing/icons/menu
import fluxer_marketing/icons/microphone
import fluxer_marketing/icons/newspaper
import fluxer_marketing/icons/palette
import fluxer_marketing/icons/paperclip
import fluxer_marketing/icons/percent
import fluxer_marketing/icons/question
import fluxer_marketing/icons/rocket
import fluxer_marketing/icons/rocket_launch
import fluxer_marketing/icons/rss
import fluxer_marketing/icons/seal_check
import fluxer_marketing/icons/shield_check
import fluxer_marketing/icons/shopping_cart
import fluxer_marketing/icons/smiley
import fluxer_marketing/icons/sparkle
import fluxer_marketing/icons/speaker_high
import fluxer_marketing/icons/swish
import fluxer_marketing/icons/translate
import fluxer_marketing/icons/tshirt
import fluxer_marketing/icons/user_circle
import fluxer_marketing/icons/user_plus
import fluxer_marketing/icons/users_three
import fluxer_marketing/icons/video
import fluxer_marketing/icons/video_camera
import fluxer_marketing/icons/windows
import fluxer_marketing/icons/x
pub const android = android.android
pub const apple = apple.apple
pub const arrow_right = arrow_right.arrow_right
pub const arrow_up = arrow_up.arrow_up
pub const bluesky = bluesky.bluesky
pub const brain = brain.brain
pub const bug = bug.bug
pub const butterfly = butterfly.butterfly
pub const calendar_check = calendar_check.calendar_check
pub const caret_down = caret_down.caret_down
pub const chart_line = chart_line.chart_line
pub const chat_centered_text = chat_centered_text.chat_centered_text
pub const chats = chats.chats
pub const chats_circle = chats_circle.chats_circle
pub const check = check.check
pub const check_circle = check_circle.check_circle
pub const code_icon = code_icon.code_icon
pub const coins = coins.coins
pub const crown = crown.crown
pub const devices = devices.devices
pub const download = download.download
pub const flask = flask.flask
pub const fluxer_bug_hunter = fluxer_bug_hunter.fluxer_bug_hunter
pub const fluxer_ctp = fluxer_ctp.fluxer_ctp
pub const fluxer_logo_wordmark = fluxer_logo_wordmark.fluxer_logo_wordmark
pub const fluxer_partner = fluxer_partner.fluxer_partner
pub const fluxer_premium = fluxer_premium.fluxer_premium
pub const fluxer_staff = fluxer_staff.fluxer_staff
pub const game_controller = game_controller.game_controller
pub const gear = gear.gear
pub const gif = gif.gif
pub const github = github.github
pub const gift = gift.gift
pub const globe = globe.globe
pub const google_play = google_play.google_play
pub const graduation_cap = graduation_cap.graduation_cap
pub const hash = hash.hash
pub const heart = heart.heart
pub const infinity = infinity.infinity
pub const lightning = lightning.lightning
pub const link = link.link
pub const linux = linux.linux
pub const magnifying_glass = magnifying_glass.magnifying_glass
pub const medal = medal.medal
pub const menu = menu.menu
pub const microphone = microphone.microphone
pub const newspaper = newspaper.newspaper
pub const palette = palette.palette
pub const paperclip = paperclip.paperclip
pub const percent = percent.percent
pub const question = question.question
pub const rocket = rocket.rocket
pub const rocket_launch = rocket_launch.rocket_launch
pub const rss = rss.rss
pub const seal_check = seal_check.seal_check
pub const shield_check = shield_check.shield_check
pub const shopping_cart = shopping_cart.shopping_cart
pub const smiley = smiley.smiley
pub const speaker_high = speaker_high.speaker_high
pub const sparkle = sparkle.sparkle
pub const swish = swish.swish
pub const translate = translate.translate
pub const tshirt = tshirt.tshirt
pub const user_circle = user_circle.user_circle
pub const user_plus = user_plus.user_plus
pub const users_three = users_three.users_three
pub const video = video.video
pub const video_camera = video_camera.video_camera
pub const windows = windows.windows
pub const x = x.x

View File

@@ -1,39 +0,0 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import lustre/attribute.{type Attribute}
import lustre/element.{type Element}
import lustre/element/svg
pub fn android(attributes: List(Attribute(a))) -> Element(a) {
svg.svg(
[
attribute.attribute("xmlns", "http://www.w3.org/2000/svg"),
attribute.attribute("viewBox", "0 0 256 256"),
attribute.attribute("fill", "currentColor"),
..attributes
],
[
svg.path([
attribute.attribute(
"d",
"M207.06,80.67c-.74-.74-1.49-1.46-2.24-2.17l24.84-24.84a8,8,0,0,0-11.32-11.32l-26,26a111.43,111.43,0,0,0-128.55.19L37.66,42.34A8,8,0,0,0,26.34,53.66L51.4,78.72A113.38,113.38,0,0,0,16,161.13V184a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V160A111.25,111.25,0,0,0,207.06,80.67ZM92,160a12,12,0,1,1,12-12A12,12,0,0,1,92,160Zm72,0a12,12,0,1,1,12-12A12,12,0,0,1,164,160Z",
),
]),
],
)
}

Some files were not shown because too many files have changed in this diff Show More