refactor progress
This commit is contained in:
137
packages/marketing/src/App.tsx
Normal file
137
packages/marketing/src/App.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {resolve} from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {normalizeEndpointOrigin, validateOutboundEndpointUrl} from '@fluxer/hono/src/security/OutboundEndpoint';
|
||||
import type {MetricsCollector} from '@fluxer/hono_types/src/MetricsTypes';
|
||||
import type {TracingOptions} from '@fluxer/hono_types/src/TracingTypes';
|
||||
import type {LoggerInterface} from '@fluxer/logger/src/LoggerInterface';
|
||||
import {createMarketingContextFactory} from '@fluxer/marketing/src/app/MarketingContextFactory';
|
||||
import {applyMarketingMiddlewareStack} from '@fluxer/marketing/src/app/MarketingMiddlewareStack';
|
||||
import {registerMarketingRoutes} from '@fluxer/marketing/src/app/MarketingRouteRegistrar';
|
||||
import {applyMarketingStaticAssets} from '@fluxer/marketing/src/app/MarketingStaticAssets';
|
||||
import {createBadgeCache, productHuntFeaturedUrl, productHuntTopPostUrl} from '@fluxer/marketing/src/BadgeProxy';
|
||||
import type {MarketingConfig} from '@fluxer/marketing/src/MarketingConfig';
|
||||
import {createMarketingMetricsMiddleware} from '@fluxer/marketing/src/MarketingTelemetry';
|
||||
import {initializeMarketingCsrf} from '@fluxer/marketing/src/middleware/Csrf';
|
||||
import {normalizeBasePath} from '@fluxer/marketing/src/UrlUtils';
|
||||
import type {IRateLimitService} from '@fluxer/rate_limit/src/IRateLimitService';
|
||||
import {Hono} from 'hono';
|
||||
|
||||
export interface CreateMarketingAppOptions {
|
||||
config: MarketingConfig;
|
||||
logger: LoggerInterface;
|
||||
publicDir?: string;
|
||||
rateLimitService?: IRateLimitService | null;
|
||||
metricsCollector?: MetricsCollector;
|
||||
tracing?: TracingOptions;
|
||||
}
|
||||
|
||||
export interface MarketingAppResult {
|
||||
app: Hono;
|
||||
shutdown: () => void;
|
||||
}
|
||||
|
||||
export function createMarketingApp(options: CreateMarketingAppOptions): MarketingAppResult {
|
||||
const {logger, publicDir: publicDirOption, rateLimitService = null, metricsCollector, tracing} = options;
|
||||
|
||||
const config = normalizeMarketingSecurityConfig(options.config);
|
||||
const publicDir = resolve(publicDirOption ?? fileURLToPath(new URL('../public', import.meta.url)));
|
||||
const app = new Hono();
|
||||
|
||||
const badgeFeaturedCache = createBadgeCache(productHuntFeaturedUrl);
|
||||
const badgeTopPostCache = createBadgeCache(productHuntTopPostUrl);
|
||||
|
||||
const contextFactory = createMarketingContextFactory({
|
||||
config,
|
||||
publicDir,
|
||||
badgeFeaturedCache,
|
||||
badgeTopPostCache,
|
||||
});
|
||||
|
||||
initializeMarketingCsrf(config.secretKeyBase, config.env === 'production');
|
||||
|
||||
app.use('*', createMarketingMetricsMiddleware(config));
|
||||
|
||||
applyMarketingStaticAssets({
|
||||
app,
|
||||
publicDir,
|
||||
basePath: config.basePath,
|
||||
logger,
|
||||
});
|
||||
|
||||
applyMarketingMiddlewareStack({
|
||||
app,
|
||||
config,
|
||||
logger,
|
||||
rateLimitService,
|
||||
metricsCollector,
|
||||
tracing,
|
||||
});
|
||||
|
||||
registerMarketingRoutes({
|
||||
app,
|
||||
config,
|
||||
contextFactory,
|
||||
badgeFeaturedCache,
|
||||
badgeTopPostCache,
|
||||
});
|
||||
|
||||
const shutdown = (): void => {
|
||||
logger.info('Marketing app shutting down');
|
||||
};
|
||||
|
||||
return {app, shutdown};
|
||||
}
|
||||
|
||||
function normalizeMarketingSecurityConfig(rawConfig: MarketingConfig): MarketingConfig {
|
||||
const basePath = normalizeBasePath(rawConfig.basePath);
|
||||
const isProduction = rawConfig.env === 'production';
|
||||
const apiEndpoint = validateOutboundEndpointUrl(rawConfig.apiEndpoint, {
|
||||
name: 'marketing.apiEndpoint',
|
||||
allowHttp: !isProduction,
|
||||
allowLocalhost: !isProduction,
|
||||
allowPrivateIpLiterals: !isProduction,
|
||||
});
|
||||
const apiRpcHost = validateOutboundEndpointUrl(resolveApiRpcHost(rawConfig.apiRpcHost), {
|
||||
name: 'marketing.apiRpcHost',
|
||||
allowHttp: true,
|
||||
allowLocalhost: !isProduction,
|
||||
allowPrivateIpLiterals: !isProduction,
|
||||
});
|
||||
|
||||
return {
|
||||
...rawConfig,
|
||||
basePath,
|
||||
apiEndpoint: normalizeEndpointOrigin(apiEndpoint),
|
||||
apiRpcHost: normalizeEndpointOrigin(apiRpcHost),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveApiRpcHost(apiRpcHost: string): string {
|
||||
const trimmed = apiRpcHost.trim();
|
||||
if (!trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
return trimmed.includes('://') ? trimmed : `http://${trimmed}`;
|
||||
}
|
||||
110
packages/marketing/src/BadgeProxy.tsx
Normal file
110
packages/marketing/src/BadgeProxy.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {readMarketingResponseAsText, sendMarketingRequest} from '@fluxer/marketing/src/MarketingHttpClient';
|
||||
import type {Context} from 'hono';
|
||||
|
||||
export const productHuntFeaturedUrl =
|
||||
'https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1057558&theme=light';
|
||||
export const productHuntTopPostUrl =
|
||||
'https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=1057558&theme=light&period=daily&t=1767529639613';
|
||||
|
||||
const staleAfterMs = 300_000;
|
||||
const fetchTimeoutMs = 4_500;
|
||||
|
||||
export interface BadgeCache {
|
||||
getBadge(): Promise<string | null>;
|
||||
}
|
||||
|
||||
interface CacheEntry {
|
||||
svg: string;
|
||||
fetchedAt: number;
|
||||
}
|
||||
|
||||
export function createBadgeCache(url: string): BadgeCache {
|
||||
let cache: CacheEntry | null = null;
|
||||
let isRefreshing = false;
|
||||
|
||||
async function refreshBadge(): Promise<void> {
|
||||
if (isRefreshing) return;
|
||||
isRefreshing = true;
|
||||
try {
|
||||
const svg = await fetchBadgeSvg(url);
|
||||
if (svg) {
|
||||
cache = {svg, fetchedAt: Date.now()};
|
||||
}
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
async getBadge() {
|
||||
const now = Date.now();
|
||||
if (!cache) {
|
||||
const svg = await fetchBadgeSvg(url);
|
||||
if (svg) {
|
||||
cache = {svg, fetchedAt: now};
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
|
||||
const isStale = now - cache.fetchedAt > staleAfterMs;
|
||||
if (isStale) {
|
||||
void refreshBadge();
|
||||
}
|
||||
return cache.svg;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function createBadgeResponse(cache: BadgeCache, c: Context): Promise<Response> {
|
||||
const svg = await cache.getBadge();
|
||||
if (!svg) {
|
||||
c.header('content-type', 'text/plain');
|
||||
c.header('retry-after', '60');
|
||||
return c.text('Badge temporarily unavailable', 503);
|
||||
}
|
||||
|
||||
c.header('content-type', 'image/svg+xml');
|
||||
c.header('cache-control', 'public, max-age=300, stale-while-revalidate=600');
|
||||
c.header('vary', 'Accept');
|
||||
return c.body(svg, 200);
|
||||
}
|
||||
|
||||
async function fetchBadgeSvg(url: string): Promise<string | null> {
|
||||
try {
|
||||
const response = await sendMarketingRequest({
|
||||
url,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'image/svg+xml',
|
||||
},
|
||||
timeout: fetchTimeoutMs,
|
||||
serviceName: 'marketing_badges',
|
||||
});
|
||||
if (response.status < 200 || response.status >= 300) return null;
|
||||
return await readMarketingResponseAsText(response.stream);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
63
packages/marketing/src/DeviceUtils.tsx
Normal file
63
packages/marketing/src/DeviceUtils.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {MarketingArchitecture, MarketingPlatform} from '@fluxer/marketing/src/MarketingContext';
|
||||
|
||||
export function detectPlatform(userAgent: string): MarketingPlatform {
|
||||
const ua = userAgent.toLowerCase();
|
||||
|
||||
if (ua.includes('iphone') || ua.includes('ipad')) return 'ios';
|
||||
if (ua.includes('android')) return 'android';
|
||||
if (ua.includes('windows')) return 'windows';
|
||||
if (ua.includes('macintosh') || ua.includes('mac os x')) return 'macos';
|
||||
if (ua.includes('linux') && !ua.includes('android')) return 'linux';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
export function detectArchitecture(userAgent: string, platform: MarketingPlatform): MarketingArchitecture {
|
||||
const ua = userAgent.toLowerCase();
|
||||
|
||||
if (platform === 'windows') {
|
||||
return ua.includes('arm64') || ua.includes('aarch64') ? 'arm64' : 'x64';
|
||||
}
|
||||
|
||||
if (platform === 'linux') {
|
||||
return ua.includes('arm64') || ua.includes('aarch64') || ua.includes('armv8') ? 'arm64' : 'x64';
|
||||
}
|
||||
|
||||
if (platform === 'macos') {
|
||||
if (
|
||||
ua.includes('arm64') ||
|
||||
ua.includes('aarch64') ||
|
||||
ua.includes('apple silicon') ||
|
||||
ua.includes('apple m1') ||
|
||||
ua.includes('apple m2') ||
|
||||
ua.includes('apple m3')
|
||||
) {
|
||||
return 'arm64';
|
||||
}
|
||||
return ua.includes('x86_64') ? 'x64' : 'arm64';
|
||||
}
|
||||
|
||||
if (platform === 'android') return 'arm64';
|
||||
return 'unknown';
|
||||
}
|
||||
80
packages/marketing/src/GeoIp.tsx
Normal file
80
packages/marketing/src/GeoIp.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {extractClientIp} from '@fluxer/ip_utils/src/ClientIp';
|
||||
import {readMarketingResponseAsText, sendMarketingRequest} from '@fluxer/marketing/src/MarketingHttpClient';
|
||||
import {ms} from 'itty-time';
|
||||
|
||||
export interface GeoIpSettings {
|
||||
apiHost: string;
|
||||
rpcSecret: string;
|
||||
}
|
||||
|
||||
const defaultCountryCode = 'US';
|
||||
|
||||
export async function getCountryCode(req: Request, settings: GeoIpSettings): Promise<string> {
|
||||
const ip = extractClientIp(req);
|
||||
if (!ip) return defaultCountryCode;
|
||||
|
||||
const url = rpcUrl(settings.apiHost);
|
||||
if (!url) return defaultCountryCode;
|
||||
|
||||
try {
|
||||
const response = await sendMarketingRequest({
|
||||
url,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${settings.rpcSecret}`,
|
||||
},
|
||||
body: JSON.stringify({type: 'geoip_lookup', ip}),
|
||||
timeout: ms('5 seconds'),
|
||||
serviceName: 'marketing_geoip',
|
||||
});
|
||||
|
||||
if (response.status < 200 || response.status >= 300) return defaultCountryCode;
|
||||
const body = await readMarketingResponseAsText(response.stream);
|
||||
const code = decodeCountryCode(body);
|
||||
return code ?? defaultCountryCode;
|
||||
} catch {
|
||||
return defaultCountryCode;
|
||||
}
|
||||
}
|
||||
|
||||
function decodeCountryCode(body: string): string | null {
|
||||
try {
|
||||
const parsed = JSON.parse(body) as {data?: {country_code?: string}};
|
||||
const code = parsed.data?.country_code;
|
||||
if (!code) return null;
|
||||
return code.trim().toUpperCase();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function rpcUrl(apiHost: string): string {
|
||||
const host = apiHost.trim();
|
||||
if (!host) return '';
|
||||
const base = host.includes('://') ? host : `http://${host}`;
|
||||
const normalized = base.endsWith('/') ? base.slice(0, -1) : base;
|
||||
return `${normalized}/_rpc`;
|
||||
}
|
||||
28
packages/marketing/src/HonoJsx.d.ts
vendored
Normal file
28
packages/marketing/src/HonoJsx.d.ts
vendored
Normal 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 {}
|
||||
}
|
||||
}
|
||||
24
packages/marketing/src/I18n.tsx
Normal file
24
packages/marketing/src/I18n.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 {MarketingI18nService} from '@fluxer/marketing/src/marketing_i18n/MarketingI18nService';
|
||||
|
||||
export function createI18n(): MarketingI18nService {
|
||||
return new MarketingI18nService();
|
||||
}
|
||||
29
packages/marketing/src/InstallScripts.tsx
Normal file
29
packages/marketing/src/InstallScripts.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {Context} from 'hono';
|
||||
|
||||
const githubRawBase = 'https://raw.githubusercontent.com/fluxerapp/fluxer/main';
|
||||
|
||||
export function livekitctl(c: Context): Response {
|
||||
return c.redirect(`${githubRawBase}/fluxer_devops/livekitctl/scripts/install.sh`, 302);
|
||||
}
|
||||
43
packages/marketing/src/MarketingConfig.tsx
Normal file
43
packages/marketing/src/MarketingConfig.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
export interface RateLimitConfig {
|
||||
limit: number;
|
||||
windowMs: number;
|
||||
}
|
||||
|
||||
export interface MarketingConfig {
|
||||
env: 'development' | 'production' | 'test';
|
||||
port: number;
|
||||
host: string;
|
||||
secretKeyBase: string;
|
||||
basePath: string;
|
||||
apiEndpoint: string;
|
||||
appEndpoint: string;
|
||||
staticCdnEndpoint: string;
|
||||
marketingEndpoint: string;
|
||||
apiRpcHost: string;
|
||||
gatewayRpcSecret: string;
|
||||
releaseChannel: string;
|
||||
buildTimestamp: string;
|
||||
rateLimit: RateLimitConfig | null;
|
||||
}
|
||||
47
packages/marketing/src/MarketingContext.tsx
Normal file
47
packages/marketing/src/MarketingContext.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {LocaleCode} from '@fluxer/constants/src/Locales';
|
||||
import type {BadgeCache} from '@fluxer/marketing/src/BadgeProxy';
|
||||
import type {MarketingI18nService} from '@fluxer/marketing/src/marketing_i18n/MarketingI18nService';
|
||||
|
||||
export type MarketingPlatform = 'windows' | 'macos' | 'linux' | 'ios' | 'android' | 'unknown';
|
||||
export type MarketingArchitecture = 'x64' | 'arm64' | 'unknown';
|
||||
export interface MarketingContext {
|
||||
locale: LocaleCode;
|
||||
i18n: MarketingI18nService;
|
||||
staticDirectory: string;
|
||||
baseUrl: string;
|
||||
countryCode: string;
|
||||
apiEndpoint: string;
|
||||
appEndpoint: string;
|
||||
staticCdnEndpoint: string;
|
||||
assetVersion: string;
|
||||
basePath: string;
|
||||
platform: MarketingPlatform;
|
||||
architecture: MarketingArchitecture;
|
||||
releaseChannel: string;
|
||||
badgeFeaturedCache: BadgeCache;
|
||||
badgeTopPostCache: BadgeCache;
|
||||
csrfToken: string;
|
||||
isDev: boolean;
|
||||
}
|
||||
35
packages/marketing/src/MarketingHttpClient.tsx
Normal file
35
packages/marketing/src/MarketingHttpClient.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 {createHttpClient} from '@fluxer/http_client/src/HttpClient';
|
||||
import type {HttpClient, RequestOptions, StreamResponse} from '@fluxer/http_client/src/HttpClientTypes';
|
||||
import {createPublicInternetRequestUrlPolicy} from '@fluxer/http_client/src/PublicInternetRequestUrlPolicy';
|
||||
|
||||
const client: HttpClient = createHttpClient({
|
||||
userAgent: 'fluxer-marketing',
|
||||
requestUrlPolicy: createPublicInternetRequestUrlPolicy(),
|
||||
});
|
||||
|
||||
export async function sendMarketingRequest(options: RequestOptions): Promise<StreamResponse> {
|
||||
return await client.sendRequest(options);
|
||||
}
|
||||
|
||||
export async function readMarketingResponseAsText(stream: StreamResponse['stream']): Promise<string> {
|
||||
return await client.streamToString(stream);
|
||||
}
|
||||
166
packages/marketing/src/MarketingTelemetry.tsx
Normal file
166
packages/marketing/src/MarketingTelemetry.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {LocaleCode} from '@fluxer/constants/src/Locales';
|
||||
import {parseSession} from '@fluxer/hono/src/Session';
|
||||
import {getLocaleFromCode, parseAcceptLanguage} from '@fluxer/locale/src/LocaleService';
|
||||
import {detectArchitecture, detectPlatform} from '@fluxer/marketing/src/DeviceUtils';
|
||||
import {getCountryCode} from '@fluxer/marketing/src/GeoIp';
|
||||
import type {MarketingConfig} from '@fluxer/marketing/src/MarketingConfig';
|
||||
import type {MarketingArchitecture, MarketingPlatform} from '@fluxer/marketing/src/MarketingContext';
|
||||
import {recordCounter, recordHistogram} from '@fluxer/telemetry/src/Metrics';
|
||||
import type {Context, MiddlewareHandler} from 'hono';
|
||||
import {getCookie} from 'hono/cookie';
|
||||
import {createMiddleware} from 'hono/factory';
|
||||
|
||||
export interface MarketingRequestInfo {
|
||||
locale: LocaleCode;
|
||||
platform: MarketingPlatform;
|
||||
architecture: MarketingArchitecture;
|
||||
countryCode: string;
|
||||
referrerDomain: string;
|
||||
}
|
||||
|
||||
const LOCALE_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;
|
||||
const REQUEST_INFO_KEY = 'marketing.requestInfo';
|
||||
|
||||
interface LocaleCookieSession {
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export async function getMarketingRequestInfo(c: Context, config: MarketingConfig): Promise<MarketingRequestInfo> {
|
||||
const existing = c.get(REQUEST_INFO_KEY);
|
||||
if (isMarketingRequestInfo(existing)) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const locale = getRequestLocale(c, config);
|
||||
const userAgent = c.req.header('user-agent') ?? '';
|
||||
const platform = detectPlatform(userAgent);
|
||||
const architecture = detectArchitecture(userAgent, platform);
|
||||
const countryCode = await getCountryCode(c.req.raw, {
|
||||
apiHost: config.apiRpcHost,
|
||||
rpcSecret: config.gatewayRpcSecret,
|
||||
});
|
||||
const referrerDomain = extractReferrerDomain(c.req.header('referer') ?? c.req.header('referrer'));
|
||||
|
||||
const info = {
|
||||
locale,
|
||||
platform,
|
||||
architecture,
|
||||
countryCode,
|
||||
referrerDomain,
|
||||
};
|
||||
c.set(REQUEST_INFO_KEY, info);
|
||||
return info;
|
||||
}
|
||||
|
||||
export function createMarketingMetricsMiddleware(config: MarketingConfig): MiddlewareHandler {
|
||||
return createMiddleware(async (c, next) => {
|
||||
const startTime = Date.now();
|
||||
const method = c.req.method;
|
||||
const path = c.req.path;
|
||||
|
||||
await next();
|
||||
|
||||
const durationMs = Date.now() - startTime;
|
||||
const status = c.res.status;
|
||||
const requestInfo = await getMarketingRequestInfo(c, config);
|
||||
|
||||
const dimensions: Record<string, string> = {
|
||||
method,
|
||||
path,
|
||||
status: status.toString(),
|
||||
release_channel: config.releaseChannel,
|
||||
base_path: config.basePath,
|
||||
platform: requestInfo.platform,
|
||||
architecture: requestInfo.architecture,
|
||||
country_code: requestInfo.countryCode,
|
||||
locale: requestInfo.locale,
|
||||
referrer_domain: requestInfo.referrerDomain,
|
||||
};
|
||||
|
||||
recordHistogram({
|
||||
name: 'marketing.request.latency',
|
||||
valueMs: durationMs,
|
||||
dimensions,
|
||||
});
|
||||
|
||||
recordCounter({
|
||||
name: 'marketing.request.count',
|
||||
value: 1,
|
||||
dimensions,
|
||||
});
|
||||
|
||||
recordCounter({
|
||||
name: 'marketing.request.outcome',
|
||||
value: 1,
|
||||
dimensions: {
|
||||
...dimensions,
|
||||
outcome: status >= 400 ? 'failure' : 'success',
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getRequestLocale(c: Context, config: MarketingConfig): LocaleCode {
|
||||
const localeCookie = getCookie(c, 'locale');
|
||||
if (localeCookie) {
|
||||
const session = parseLocaleCookie(localeCookie, config.secretKeyBase) ?? localeCookie;
|
||||
const locale = getLocaleFromCode(session);
|
||||
if (locale) return locale;
|
||||
}
|
||||
|
||||
const header = c.req.header('accept-language');
|
||||
if (header) {
|
||||
return parseAcceptLanguage(header);
|
||||
}
|
||||
|
||||
return 'en-US';
|
||||
}
|
||||
|
||||
function parseLocaleCookie(cookieValue: string, secretKeyBase: string): string | null {
|
||||
const session = parseSession<LocaleCookieSession>(cookieValue, secretKeyBase, LOCALE_COOKIE_MAX_AGE_SECONDS);
|
||||
return session?.locale ?? null;
|
||||
}
|
||||
|
||||
function extractReferrerDomain(value?: string): string {
|
||||
if (!value) return 'direct';
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.hostname || 'unknown';
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function isMarketingRequestInfo(value: unknown): value is MarketingRequestInfo {
|
||||
if (!value || typeof value !== 'object') return false;
|
||||
const record = value as Record<string, unknown>;
|
||||
return (
|
||||
typeof record['locale'] === 'string' &&
|
||||
typeof record['platform'] === 'string' &&
|
||||
typeof record['architecture'] === 'string' &&
|
||||
typeof record['countryCode'] === 'string' &&
|
||||
typeof record['referrerDomain'] === 'string'
|
||||
);
|
||||
}
|
||||
26
packages/marketing/src/PathUtils.tsx
Normal file
26
packages/marketing/src/PathUtils.tsx
Normal 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
export function getCurrentPath(path: string): string {
|
||||
if (!path.startsWith('/')) return `/${path}`;
|
||||
return path;
|
||||
}
|
||||
103
packages/marketing/src/PricingUtils.tsx
Normal file
103
packages/marketing/src/PricingUtils.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {ValueOf} from '@fluxer/constants/src/ValueOf';
|
||||
|
||||
export const PricingTier = {
|
||||
Monthly: 'monthly',
|
||||
Yearly: 'yearly',
|
||||
} as const;
|
||||
|
||||
export type PricingTier = ValueOf<typeof PricingTier>;
|
||||
|
||||
export const Currency = {
|
||||
USD: 'USD',
|
||||
EUR: 'EUR',
|
||||
} as const;
|
||||
|
||||
export type Currency = ValueOf<typeof Currency>;
|
||||
|
||||
const EEA_COUNTRIES = [
|
||||
'AT',
|
||||
'BE',
|
||||
'BG',
|
||||
'HR',
|
||||
'CY',
|
||||
'CZ',
|
||||
'DK',
|
||||
'EE',
|
||||
'FI',
|
||||
'FR',
|
||||
'DE',
|
||||
'GR',
|
||||
'HU',
|
||||
'IE',
|
||||
'IT',
|
||||
'LV',
|
||||
'LT',
|
||||
'LU',
|
||||
'MT',
|
||||
'NL',
|
||||
'PL',
|
||||
'PT',
|
||||
'RO',
|
||||
'SK',
|
||||
'SI',
|
||||
'ES',
|
||||
'SE',
|
||||
'IS',
|
||||
'LI',
|
||||
'NO',
|
||||
];
|
||||
|
||||
export function getCurrency(countryCode: string): Currency {
|
||||
return isEeaCountry(countryCode) ? Currency.EUR : Currency.USD;
|
||||
}
|
||||
|
||||
export function isEeaCountry(countryCode: string): boolean {
|
||||
const upperCode = countryCode.toUpperCase();
|
||||
return EEA_COUNTRIES.includes(upperCode);
|
||||
}
|
||||
|
||||
export function getPriceCents(tier: PricingTier, currency: Currency): number {
|
||||
if (tier === PricingTier.Monthly) return currency === Currency.USD ? 499 : 499;
|
||||
if (tier === PricingTier.Yearly) return currency === Currency.USD ? 4999 : 4999;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function formatPriceCents(priceCents: number, currency: Currency): string {
|
||||
const amount = formatAmountCents(priceCents);
|
||||
return currency === Currency.EUR ? `€${amount}` : `$${amount}`;
|
||||
}
|
||||
|
||||
function formatAmountCents(priceCents: number): string {
|
||||
const dollars = Math.floor(priceCents / 100);
|
||||
const cents = priceCents % 100;
|
||||
const centsText = cents < 10 ? `0${cents}` : `${cents}`;
|
||||
return `${dollars}.${centsText}`;
|
||||
}
|
||||
|
||||
export function getFormattedPrice(tier: PricingTier, countryCode: string): string {
|
||||
const currency = getCurrency(countryCode);
|
||||
const priceCents = getPriceCents(tier, currency);
|
||||
return formatPriceCents(priceCents, currency);
|
||||
}
|
||||
24
packages/marketing/src/PublicDir.tsx
Normal file
24
packages/marketing/src/PublicDir.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 {fileURLToPath} from 'node:url';
|
||||
|
||||
export function resolveMarketingPublicDir(): string {
|
||||
return fileURLToPath(new URL('../public', import.meta.url));
|
||||
}
|
||||
42
packages/marketing/src/RedirectPathUtils.tsx
Normal file
42
packages/marketing/src/RedirectPathUtils.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
const SAFE_REDIRECT_BASE_URL = 'https://marketing.fluxer.invalid';
|
||||
|
||||
export function sanitizeInternalRedirectPath(rawPath: string): string {
|
||||
const trimmedPath = rawPath.trim();
|
||||
if (!trimmedPath) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
try {
|
||||
const resolvedUrl = new URL(trimmedPath, SAFE_REDIRECT_BASE_URL);
|
||||
if (resolvedUrl.origin !== SAFE_REDIRECT_BASE_URL) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
if (!resolvedUrl.pathname.startsWith('/')) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
return `${resolvedUrl.pathname}${resolvedUrl.search}${resolvedUrl.hash}`;
|
||||
} catch {
|
||||
return '/';
|
||||
}
|
||||
}
|
||||
90
packages/marketing/src/Sitemap.tsx
Normal file
90
packages/marketing/src/Sitemap.tsx
Normal 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {getHelpArticles} from '@fluxer/marketing/src/help/HelpContentLoader';
|
||||
import {getPolicies} from '@fluxer/marketing/src/policies/PolicyContentLoader';
|
||||
|
||||
interface UrlEntry {
|
||||
loc: string;
|
||||
changefreq: string;
|
||||
priority: string;
|
||||
}
|
||||
|
||||
export function generateSitemap(baseUrl: string): string {
|
||||
const urls = generateUrls(baseUrl);
|
||||
const urlEntries = urls
|
||||
.map(
|
||||
(entry) => ` <url>
|
||||
<loc>${entry.loc}</loc>
|
||||
<changefreq>${entry.changefreq}</changefreq>
|
||||
<priority>${entry.priority}</priority>
|
||||
</url>`,
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${urlEntries}
|
||||
</urlset>`;
|
||||
}
|
||||
|
||||
function generateUrls(baseUrl: string): ReadonlyArray<UrlEntry> {
|
||||
const staticPages: ReadonlyArray<UrlEntry> = [
|
||||
{loc: baseUrl, changefreq: 'weekly', priority: '1.0'},
|
||||
{loc: `${baseUrl}/policies`, changefreq: 'monthly', priority: '0.6'},
|
||||
{loc: `${baseUrl}/terms`, changefreq: 'monthly', priority: '0.5'},
|
||||
{loc: `${baseUrl}/privacy`, changefreq: 'monthly', priority: '0.5'},
|
||||
{loc: `${baseUrl}/security`, changefreq: 'monthly', priority: '0.5'},
|
||||
{loc: `${baseUrl}/guidelines`, changefreq: 'monthly', priority: '0.7'},
|
||||
{loc: `${baseUrl}/company-information`, changefreq: 'monthly', priority: '0.4'},
|
||||
{loc: `${baseUrl}/careers`, changefreq: 'weekly', priority: '0.6'},
|
||||
{loc: `${baseUrl}/download`, changefreq: 'weekly', priority: '0.9'},
|
||||
{loc: `${baseUrl}/plutonium`, changefreq: 'weekly', priority: '0.8'},
|
||||
{loc: `${baseUrl}/partners`, changefreq: 'monthly', priority: '0.6'},
|
||||
{loc: `${baseUrl}/press`, changefreq: 'monthly', priority: '0.5'},
|
||||
];
|
||||
|
||||
const policyUrls = generatePolicyUrls(baseUrl);
|
||||
const helpUrls = generateHelpUrls(baseUrl);
|
||||
return [...staticPages, ...policyUrls, ...helpUrls];
|
||||
}
|
||||
|
||||
function generatePolicyUrls(baseUrl: string): ReadonlyArray<UrlEntry> {
|
||||
const policies = getPolicies();
|
||||
return policies.map((policy) => ({
|
||||
loc: `${baseUrl}/${policy.slug}`,
|
||||
changefreq: 'monthly',
|
||||
priority: '0.5',
|
||||
}));
|
||||
}
|
||||
|
||||
function generateHelpUrls(baseUrl: string): ReadonlyArray<UrlEntry> {
|
||||
const articles = getHelpArticles();
|
||||
return [
|
||||
{loc: `${baseUrl}/help`, changefreq: 'weekly', priority: '0.7'},
|
||||
...articles.map((article) => ({
|
||||
loc: `${baseUrl}/help/${article.slug}`,
|
||||
changefreq: 'monthly',
|
||||
priority: '0.5',
|
||||
})),
|
||||
];
|
||||
}
|
||||
49
packages/marketing/src/SlugUtils.tsx
Normal file
49
packages/marketing/src/SlugUtils.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
const whitespaceRegex = /\s+/g;
|
||||
const invalidSlugRegex = /[^\p{L}\p{N}\-._~]+/gu;
|
||||
const collapseHyphenRegex = /-+/g;
|
||||
|
||||
export function createSlug(title: string): string {
|
||||
const lower = title.toLowerCase();
|
||||
const hyphened = lower.replace(whitespaceRegex, '-');
|
||||
const cleaned = hyphened.replace(invalidSlugRegex, '-');
|
||||
const collapsed = cleaned.replace(collapseHyphenRegex, '-');
|
||||
const trimmed = trimHyphens(collapsed);
|
||||
return trimmed.length === 0 ? 'article' : trimmed;
|
||||
}
|
||||
|
||||
function trimHyphens(text: string): string {
|
||||
let start = 0;
|
||||
let end = text.length;
|
||||
|
||||
while (start < end && text[start] === '-') {
|
||||
start += 1;
|
||||
}
|
||||
|
||||
while (end > start && text[end - 1] === '-') {
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
return text.slice(start, end);
|
||||
}
|
||||
59
packages/marketing/src/UrlUtils.tsx
Normal file
59
packages/marketing/src/UrlUtils.tsx
Normal 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
|
||||
export function cacheBustedWithVersion(path: string, version: string): string {
|
||||
const separator = path.includes('?') ? '&' : '?';
|
||||
return `${path}${separator}t=${version}`;
|
||||
}
|
||||
|
||||
export function cacheBustedAsset(ctx: MarketingContext, path: string): string {
|
||||
return prependBasePath(ctx.basePath, cacheBustedWithVersion(path, ctx.assetVersion));
|
||||
}
|
||||
|
||||
export function prependBasePath(basePath: string, path: string): string {
|
||||
if (!basePath) return path;
|
||||
return `${basePath}${path}`;
|
||||
}
|
||||
|
||||
export function href(ctx: MarketingContext, path: string): string {
|
||||
return prependBasePath(ctx.basePath, path);
|
||||
}
|
||||
|
||||
export function apiUrl(ctx: MarketingContext, path: string): string {
|
||||
return `${ctx.apiEndpoint}${path}`;
|
||||
}
|
||||
|
||||
export function isCanary(ctx: MarketingContext): boolean {
|
||||
return ctx.releaseChannel === 'canary';
|
||||
}
|
||||
|
||||
export function normalizeBasePath(basePath: string): string {
|
||||
const segments = basePath
|
||||
.trim()
|
||||
.split('/')
|
||||
.filter((segment) => segment.length > 0);
|
||||
|
||||
if (segments.length === 0) return '';
|
||||
return `/${segments.join('/')}`;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 {escapeInlineScriptValue} from '@fluxer/marketing/src/pages/InlineScriptEscaping';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
describe('escapeInlineScriptValue', () => {
|
||||
it('escapes backslash for safe embedding in single-quoted JS strings', () => {
|
||||
expect(escapeInlineScriptValue('path\\to\\file')).toBe('path\\\\to\\\\file');
|
||||
expect(escapeInlineScriptValue('\\\\')).toBe('\\\\\\\\');
|
||||
});
|
||||
|
||||
it('escapes newline for safe embedding in single-quoted JS strings', () => {
|
||||
expect(escapeInlineScriptValue('line1\nline2')).toBe('line1\\nline2');
|
||||
expect(escapeInlineScriptValue('\n')).toBe('\\n');
|
||||
});
|
||||
|
||||
it('escapes carriage return for safe embedding in single-quoted JS strings', () => {
|
||||
expect(escapeInlineScriptValue('a\rb')).toBe('a\\rb');
|
||||
expect(escapeInlineScriptValue('\r\n')).toBe('\\r\\n');
|
||||
});
|
||||
|
||||
it('escapes single quote to prevent string breakout', () => {
|
||||
expect(escapeInlineScriptValue("don't")).toBe("don\\'t");
|
||||
expect(escapeInlineScriptValue("it's")).toBe("it\\'s");
|
||||
expect(escapeInlineScriptValue("'")).toBe("\\'");
|
||||
});
|
||||
|
||||
it('escapes combined XSS-payload characters that could break out of JS strings', () => {
|
||||
const payload = "'; alert('xss'); //";
|
||||
expect(escapeInlineScriptValue(payload)).toBe("\\'; alert(\\'xss\\'); //");
|
||||
});
|
||||
|
||||
it('returns plain text unchanged when no special characters', () => {
|
||||
expect(escapeInlineScriptValue('Hello world')).toBe('Hello world');
|
||||
expect(escapeInlineScriptValue('Error: invalid amount')).toBe('Error: invalid amount');
|
||||
});
|
||||
});
|
||||
41
packages/marketing/src/__tests__/RedirectPathUtils.test.tsx
Normal file
41
packages/marketing/src/__tests__/RedirectPathUtils.test.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 {sanitizeInternalRedirectPath} from '@fluxer/marketing/src/RedirectPathUtils';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
describe('sanitizeInternalRedirectPath', () => {
|
||||
test('allows same-site relative paths', () => {
|
||||
expect(sanitizeInternalRedirectPath('/partners')).toBe('/partners');
|
||||
expect(sanitizeInternalRedirectPath('/download?channel=canary')).toBe('/download?channel=canary');
|
||||
expect(sanitizeInternalRedirectPath('/docs#faq')).toBe('/docs#faq');
|
||||
});
|
||||
|
||||
test('rejects absolute and protocol-relative external redirects', () => {
|
||||
expect(sanitizeInternalRedirectPath('https://evil.example/path')).toBe('/');
|
||||
expect(sanitizeInternalRedirectPath('//evil.example/path')).toBe('/');
|
||||
expect(sanitizeInternalRedirectPath('javascript:alert(1)')).toBe('/');
|
||||
});
|
||||
|
||||
test('normalises blank and relative values to internal paths', () => {
|
||||
expect(sanitizeInternalRedirectPath('')).toBe('/');
|
||||
expect(sanitizeInternalRedirectPath(' ')).toBe('/');
|
||||
expect(sanitizeInternalRedirectPath('download')).toBe('/download');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
# Terms of Service
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Welcome to Fluxer](#welcome-to-fluxer)
|
||||
|
||||
## Welcome to Fluxer {#welcome-to-fluxer}
|
||||
|
||||
we will automatically attempt to retry charging your payment method a reasonable number of times;
|
||||
|
||||
See [How to delete or disable your account](/help/delete-account) for details.
|
||||
78
packages/marketing/src/app/MarketingContextFactory.tsx
Normal file
78
packages/marketing/src/app/MarketingContextFactory.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {CdnEndpoints} from '@fluxer/constants/src/CdnEndpoints';
|
||||
import type {BadgeCache} from '@fluxer/marketing/src/BadgeProxy';
|
||||
import {createI18n} from '@fluxer/marketing/src/I18n';
|
||||
import type {MarketingConfig} from '@fluxer/marketing/src/MarketingConfig';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
import {getMarketingRequestInfo} from '@fluxer/marketing/src/MarketingTelemetry';
|
||||
import {getMarketingCsrfToken} from '@fluxer/marketing/src/middleware/Csrf';
|
||||
import {normalizeBasePath} from '@fluxer/marketing/src/UrlUtils';
|
||||
import type {Context as HonoContext} from 'hono';
|
||||
|
||||
export interface CreateMarketingContextFactoryOptions {
|
||||
config: MarketingConfig;
|
||||
publicDir: string;
|
||||
badgeFeaturedCache: BadgeCache;
|
||||
badgeTopPostCache: BadgeCache;
|
||||
}
|
||||
|
||||
export type MarketingContextFactory = (c: HonoContext) => Promise<MarketingContext>;
|
||||
|
||||
export function createMarketingContextFactory(options: CreateMarketingContextFactoryOptions): MarketingContextFactory {
|
||||
const i18n = createI18n();
|
||||
const basePath = normalizeBasePath(options.config.basePath);
|
||||
const baseUrl = buildMarketingBaseUrl(options.config.marketingEndpoint, basePath);
|
||||
|
||||
return async function buildContext(c: HonoContext): Promise<MarketingContext> {
|
||||
const requestInfo = await getMarketingRequestInfo(c, options.config);
|
||||
const csrfToken = getMarketingCsrfToken(c);
|
||||
|
||||
return {
|
||||
locale: requestInfo.locale,
|
||||
i18n,
|
||||
staticDirectory: `${options.publicDir}/static`,
|
||||
baseUrl,
|
||||
countryCode: requestInfo.countryCode,
|
||||
apiEndpoint: options.config.apiEndpoint,
|
||||
appEndpoint: options.config.appEndpoint,
|
||||
staticCdnEndpoint: CdnEndpoints.STATIC,
|
||||
assetVersion: options.config.buildTimestamp,
|
||||
basePath,
|
||||
platform: requestInfo.platform,
|
||||
architecture: requestInfo.architecture,
|
||||
releaseChannel: options.config.releaseChannel,
|
||||
badgeFeaturedCache: options.badgeFeaturedCache,
|
||||
badgeTopPostCache: options.badgeTopPostCache,
|
||||
csrfToken,
|
||||
isDev: options.config.env === 'development',
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function buildMarketingBaseUrl(marketingEndpoint: string, basePath: string): string {
|
||||
const trimmedEndpoint = marketingEndpoint.endsWith('/') ? marketingEndpoint.slice(0, -1) : marketingEndpoint;
|
||||
if (!basePath) return trimmedEndpoint;
|
||||
if (trimmedEndpoint.endsWith(basePath)) return trimmedEndpoint;
|
||||
return `${trimmedEndpoint}${basePath}`;
|
||||
}
|
||||
94
packages/marketing/src/app/MarketingMiddlewareStack.tsx
Normal file
94
packages/marketing/src/app/MarketingMiddlewareStack.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {applyMiddlewareStack} from '@fluxer/hono/src/middleware/MiddlewareStack';
|
||||
import type {MetricsCollector} from '@fluxer/hono_types/src/MetricsTypes';
|
||||
import type {TracingOptions} from '@fluxer/hono_types/src/TracingTypes';
|
||||
import {extractClientIp} from '@fluxer/ip_utils/src/ClientIp';
|
||||
import type {LoggerInterface} from '@fluxer/logger/src/LoggerInterface';
|
||||
import type {MarketingConfig} from '@fluxer/marketing/src/MarketingConfig';
|
||||
import {cacheHeadersMiddleware} from '@fluxer/marketing/src/middleware/CacheHeadersMiddleware';
|
||||
import {marketingCsrfMiddleware} from '@fluxer/marketing/src/middleware/Csrf';
|
||||
import type {IRateLimitService} from '@fluxer/rate_limit/src/IRateLimitService';
|
||||
import type {Hono} from 'hono';
|
||||
|
||||
export interface ApplyMarketingMiddlewareStackOptions {
|
||||
app: Hono;
|
||||
config: MarketingConfig;
|
||||
logger: LoggerInterface;
|
||||
rateLimitService?: IRateLimitService | null;
|
||||
metricsCollector?: MetricsCollector;
|
||||
tracing?: TracingOptions;
|
||||
}
|
||||
|
||||
export function applyMarketingMiddlewareStack(options: ApplyMarketingMiddlewareStackOptions): void {
|
||||
applyMiddlewareStack(options.app, {
|
||||
requestId: {},
|
||||
tracing: options.tracing,
|
||||
metrics: options.metricsCollector
|
||||
? {
|
||||
enabled: true,
|
||||
collector: options.metricsCollector,
|
||||
skipPaths: ['/_health', '/static'],
|
||||
}
|
||||
: undefined,
|
||||
logger: {
|
||||
log: (data) => {
|
||||
options.logger.debug(
|
||||
{
|
||||
method: data.method,
|
||||
path: data.path,
|
||||
status: data.status,
|
||||
durationMs: data.durationMs,
|
||||
},
|
||||
'Request completed',
|
||||
);
|
||||
},
|
||||
skip: ['/_health', '/static'],
|
||||
},
|
||||
rateLimit: options.config.rateLimit
|
||||
? {
|
||||
enabled: true,
|
||||
service: options.rateLimitService ?? undefined,
|
||||
maxAttempts: options.config.rateLimit.limit,
|
||||
windowMs: options.config.rateLimit.windowMs,
|
||||
skipPaths: ['/_health', '/static'],
|
||||
keyGenerator: (req) => extractClientIp(req) ?? 'unknown',
|
||||
}
|
||||
: undefined,
|
||||
customMiddleware: [cacheHeadersMiddleware(), marketingCsrfMiddleware],
|
||||
errorHandler: {
|
||||
includeStack: options.config.env === 'development',
|
||||
logger: (err, ctx) => {
|
||||
options.logger.error(
|
||||
{
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
path: ctx.req.path,
|
||||
method: ctx.req.method,
|
||||
},
|
||||
'Request error',
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
252
packages/marketing/src/app/MarketingRouteRegistrar.tsx
Normal file
252
packages/marketing/src/app/MarketingRouteRegistrar.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {CdnEndpoints} from '@fluxer/constants/src/CdnEndpoints';
|
||||
import {Headers, HeaderValues} from '@fluxer/constants/src/Headers';
|
||||
import {HttpStatus, MimeType} from '@fluxer/constants/src/HttpConstants';
|
||||
import {isPressAssetId, PressAssets} from '@fluxer/constants/src/PressAssets';
|
||||
import {createSession} from '@fluxer/hono/src/Session';
|
||||
import {getLocaleFromCode} from '@fluxer/locale/src/LocaleService';
|
||||
import {type BadgeCache, createBadgeResponse} from '@fluxer/marketing/src/BadgeProxy';
|
||||
import type {MarketingConfig} from '@fluxer/marketing/src/MarketingConfig';
|
||||
import {sendMarketingRequest} from '@fluxer/marketing/src/MarketingHttpClient';
|
||||
import {renderCareersPage} from '@fluxer/marketing/src/pages/CareersPage';
|
||||
import {renderDonateManagePage} from '@fluxer/marketing/src/pages/DonateManagePage';
|
||||
import {renderDonatePage} from '@fluxer/marketing/src/pages/DonatePage';
|
||||
import {renderDonateSuccessPage} from '@fluxer/marketing/src/pages/DonateSuccessPage';
|
||||
import {renderDownloadPage} from '@fluxer/marketing/src/pages/DownloadPage';
|
||||
import {renderHelpArticlePage} from '@fluxer/marketing/src/pages/HelpArticlePage';
|
||||
import {renderHelpIndexPage} from '@fluxer/marketing/src/pages/HelpIndexPage';
|
||||
import {renderHomePage} from '@fluxer/marketing/src/pages/HomePage';
|
||||
import {renderNotFoundPage} from '@fluxer/marketing/src/pages/NotFoundPage';
|
||||
import {renderPartnersPage} from '@fluxer/marketing/src/pages/PartnersPage';
|
||||
import {renderPlutoniumPage} from '@fluxer/marketing/src/pages/PlutoniumPage';
|
||||
import {renderPolicyPage} from '@fluxer/marketing/src/pages/PolicyPage';
|
||||
import {renderPressPage} from '@fluxer/marketing/src/pages/PressPage';
|
||||
import {sanitizeInternalRedirectPath} from '@fluxer/marketing/src/RedirectPathUtils';
|
||||
import type {MarketingRouteHandler} from '@fluxer/marketing/src/routes/RouteTypes';
|
||||
import {generateSitemap} from '@fluxer/marketing/src/Sitemap';
|
||||
import {prependBasePath} from '@fluxer/marketing/src/UrlUtils';
|
||||
import type {Hono} from 'hono';
|
||||
import {setCookie} from 'hono/cookie';
|
||||
import type {MarketingContextFactory} from './MarketingContextFactory';
|
||||
|
||||
export interface RegisterMarketingRoutesOptions {
|
||||
app: Hono;
|
||||
config: MarketingConfig;
|
||||
contextFactory: MarketingContextFactory;
|
||||
badgeFeaturedCache: BadgeCache;
|
||||
badgeTopPostCache: BadgeCache;
|
||||
}
|
||||
|
||||
interface LocaleCookieSession {
|
||||
locale: string;
|
||||
}
|
||||
|
||||
const LOCALE_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;
|
||||
|
||||
const POLICY_ROUTE_DEFINITIONS = [
|
||||
{path: '/terms', slug: 'terms'},
|
||||
{path: '/privacy', slug: 'privacy'},
|
||||
{path: '/security', slug: 'security'},
|
||||
{path: '/guidelines', slug: 'guidelines'},
|
||||
{path: '/company-information', slug: 'company-information'},
|
||||
] as const;
|
||||
|
||||
const PAGE_ROUTE_DEFINITIONS: ReadonlyArray<{
|
||||
path: string;
|
||||
handler: MarketingRouteHandler;
|
||||
}> = [
|
||||
{path: '/', handler: renderHomePage},
|
||||
{path: '/careers', handler: renderCareersPage},
|
||||
{path: '/download', handler: renderDownloadPage},
|
||||
{path: '/donate', handler: renderDonatePage},
|
||||
{path: '/donate/manage', handler: renderDonateManagePage},
|
||||
{path: '/donate/success', handler: renderDonateSuccessPage},
|
||||
{path: '/plutonium', handler: renderPlutoniumPage},
|
||||
{path: '/partners', handler: renderPartnersPage},
|
||||
{path: '/press', handler: renderPressPage},
|
||||
];
|
||||
|
||||
export function registerMarketingRoutes(options: RegisterMarketingRoutesOptions): void {
|
||||
registerBadgeRoutes(options.app, options.badgeFeaturedCache, options.badgeTopPostCache);
|
||||
registerLocaleRoute(options.app, options.config);
|
||||
registerExternalRedirects(options.app);
|
||||
registerSystemContentRoutes(options.app, options.contextFactory);
|
||||
registerHelpRoutes(options.app, options.contextFactory);
|
||||
registerPolicyRoutes(options.app, options.contextFactory);
|
||||
registerPageRoutes(options.app, options.contextFactory);
|
||||
registerPressDownloadRoute(options.app);
|
||||
registerNotFoundRoute(options.app, options.contextFactory);
|
||||
}
|
||||
|
||||
function registerBadgeRoutes(app: Hono, badgeFeaturedCache: BadgeCache, badgeTopPostCache: BadgeCache): void {
|
||||
app.get('/api/badges/product-hunt', async (c) => {
|
||||
return await createBadgeResponse(badgeFeaturedCache, c);
|
||||
});
|
||||
|
||||
app.get('/api/badges/product-hunt-top-post', async (c) => {
|
||||
return await createBadgeResponse(badgeTopPostCache, c);
|
||||
});
|
||||
}
|
||||
|
||||
function registerLocaleRoute(app: Hono, config: MarketingConfig): void {
|
||||
app.post('/_locale', async (c) => {
|
||||
const body = await c.req.parseBody();
|
||||
const localeCode = typeof body['locale'] === 'string' ? body['locale'] : '';
|
||||
const redirectPath = sanitizeInternalRedirectPath(typeof body['redirect'] === 'string' ? body['redirect'] : '/');
|
||||
const locale = getLocaleFromCode(localeCode);
|
||||
if (!locale) return c.text('Bad Request', HttpStatus.BAD_REQUEST);
|
||||
|
||||
const cookieValue = createLocaleCookieValue(locale, config.secretKeyBase);
|
||||
setCookie(c, 'locale', cookieValue, {path: '/', maxAge: LOCALE_COOKIE_MAX_AGE_SECONDS});
|
||||
return c.redirect(prependBasePath(config.basePath, redirectPath), HttpStatus.FOUND);
|
||||
});
|
||||
}
|
||||
|
||||
function registerExternalRedirects(app: Hono): void {
|
||||
app.get('/get/livekitctl', (c) => {
|
||||
return c.redirect(
|
||||
'https://raw.githubusercontent.com/fluxerapp/fluxer/main/fluxer_devops/livekitctl/scripts/install.sh',
|
||||
HttpStatus.FOUND,
|
||||
);
|
||||
});
|
||||
|
||||
app.get('/regional-restrictions', (c) => c.redirect('/help/regional-restrictions', HttpStatus.MOVED_PERMANENTLY));
|
||||
app.get('/blog', (c) => c.redirect('https://blog.fluxer.app', HttpStatus.FOUND));
|
||||
app.get('/blog/*', (c) => c.redirect('https://blog.fluxer.app', HttpStatus.FOUND));
|
||||
}
|
||||
|
||||
function registerSystemContentRoutes(app: Hono, contextFactory: MarketingContextFactory): void {
|
||||
app.get('/_health', (c) => c.json({status: 'ok'}));
|
||||
|
||||
app.get('/robots.txt', (c) => {
|
||||
return c.text('User-agent: *\nAllow: /\n');
|
||||
});
|
||||
|
||||
registerContextRoute(app, '/security.txt', contextFactory, (_c, ctx) => {
|
||||
const securityUrl = `${ctx.baseUrl}/security`;
|
||||
const expires = `${new Date().getUTCFullYear() + 1}-01-05T13:37:00.000Z`;
|
||||
const body = [
|
||||
`Contact: ${securityUrl}`,
|
||||
'Contact: mailto:security@fluxer.app',
|
||||
`Expires: ${expires}`,
|
||||
'Preferred-Languages: en',
|
||||
`Policy: ${securityUrl}`,
|
||||
].join('\n');
|
||||
return _c.text(`${body}\n`);
|
||||
});
|
||||
|
||||
registerContextRoute(app, '/sitemap.xml', contextFactory, (c, ctx) => {
|
||||
const xml = generateSitemap(ctx.baseUrl);
|
||||
c.header(Headers.CONTENT_TYPE, `${MimeType.XML}; charset=utf-8`);
|
||||
return c.text(xml);
|
||||
});
|
||||
}
|
||||
|
||||
function registerHelpRoutes(app: Hono, contextFactory: MarketingContextFactory): void {
|
||||
registerContextRoute(app, '/help', contextFactory, async (c, ctx) => {
|
||||
return await renderHelpIndexPage(c, ctx);
|
||||
});
|
||||
|
||||
registerContextRoute(app, '/help/:slug', contextFactory, async (c, ctx) => {
|
||||
const slug = c.req.param('slug');
|
||||
return await renderHelpArticlePage(c, ctx, slug);
|
||||
});
|
||||
}
|
||||
|
||||
function registerPolicyRoutes(app: Hono, contextFactory: MarketingContextFactory): void {
|
||||
for (const route of POLICY_ROUTE_DEFINITIONS) {
|
||||
registerContextRoute(app, route.path, contextFactory, async (c, ctx) => {
|
||||
return await renderPolicyPage(c, ctx, route.slug);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerPageRoutes(app: Hono, contextFactory: MarketingContextFactory): void {
|
||||
for (const route of PAGE_ROUTE_DEFINITIONS) {
|
||||
registerContextRoute(app, route.path, contextFactory, async (c, ctx) => {
|
||||
return await route.handler(c, ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerPressDownloadRoute(app: Hono): void {
|
||||
app.get('/press/download/:assetId', async (c) => {
|
||||
const assetId = c.req.param('assetId');
|
||||
if (!isPressAssetId(assetId)) {
|
||||
return c.text('', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
const asset = PressAssets[assetId];
|
||||
const response = await sendMarketingRequest({
|
||||
url: `${CdnEndpoints.STATIC}${asset.path}`,
|
||||
method: 'GET',
|
||||
serviceName: 'marketing_press_download',
|
||||
});
|
||||
if (response.status < 200 || response.status >= 300 || !response.stream) {
|
||||
return c.text('', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get(Headers.CONTENT_TYPE);
|
||||
if (contentType) {
|
||||
c.header(Headers.CONTENT_TYPE, contentType);
|
||||
} else {
|
||||
c.header(Headers.CONTENT_TYPE, MimeType.OCTET_STREAM);
|
||||
}
|
||||
|
||||
const contentLength = response.headers.get(Headers.CONTENT_LENGTH);
|
||||
if (contentLength) {
|
||||
c.header(Headers.CONTENT_LENGTH, contentLength);
|
||||
}
|
||||
|
||||
const cacheControl = response.headers.get(Headers.CACHE_CONTROL);
|
||||
if (cacheControl) {
|
||||
c.header(Headers.CACHE_CONTROL, cacheControl);
|
||||
}
|
||||
|
||||
c.header(Headers.CONTENT_DISPOSITION, `${HeaderValues.ATTACHMENT}; filename="${asset.filename}"`);
|
||||
return c.body(response.stream, HttpStatus.OK);
|
||||
});
|
||||
}
|
||||
|
||||
function registerNotFoundRoute(app: Hono, contextFactory: MarketingContextFactory): void {
|
||||
registerContextRoute(app, '*', contextFactory, async (c, ctx) => {
|
||||
return await renderNotFoundPage(c, ctx);
|
||||
});
|
||||
}
|
||||
|
||||
function registerContextRoute(
|
||||
app: Hono,
|
||||
path: string,
|
||||
contextFactory: MarketingContextFactory,
|
||||
handler: MarketingRouteHandler,
|
||||
): void {
|
||||
app.get(path, async (c) => {
|
||||
const ctx = await contextFactory(c);
|
||||
return await handler(c, ctx);
|
||||
});
|
||||
}
|
||||
|
||||
function createLocaleCookieValue(locale: string, secretKeyBase: string): string {
|
||||
return createSession<LocaleCookieSession>({locale}, secretKeyBase);
|
||||
}
|
||||
63
packages/marketing/src/app/MarketingStaticAssets.tsx
Normal file
63
packages/marketing/src/app/MarketingStaticAssets.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {LoggerInterface} from '@fluxer/logger/src/LoggerInterface';
|
||||
import {serveStatic} from '@hono/node-server/serve-static';
|
||||
import type {Hono} from 'hono';
|
||||
|
||||
export interface ApplyMarketingStaticAssetsOptions {
|
||||
app: Hono;
|
||||
publicDir: string;
|
||||
basePath: string;
|
||||
logger: LoggerInterface;
|
||||
}
|
||||
|
||||
export function applyMarketingStaticAssets(options: ApplyMarketingStaticAssetsOptions): void {
|
||||
options.app.use(
|
||||
'/static/*',
|
||||
serveStatic({
|
||||
root: options.publicDir,
|
||||
rewriteRequestPath: (path: string) => toRelativeStaticPath(stripLeadingBasePath(path, options.basePath)),
|
||||
onNotFound: (_path) => {
|
||||
options.logger.error(
|
||||
{
|
||||
publicDir: options.publicDir,
|
||||
cwd: process.cwd(),
|
||||
},
|
||||
'Marketing static asset not found (expected packages/marketing/public/static/app.css to exist)',
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function stripLeadingBasePath(path: string, basePath: string): string {
|
||||
if (!basePath) return path;
|
||||
if (path === basePath) return '';
|
||||
if (path.startsWith(`${basePath}/`)) return path.slice(basePath.length);
|
||||
return path;
|
||||
}
|
||||
|
||||
function toRelativeStaticPath(path: string): string {
|
||||
if (!path) return path;
|
||||
return path.startsWith('/') ? path.slice(1) : path;
|
||||
}
|
||||
61
packages/marketing/src/components/ContentToc.tsx
Normal file
61
packages/marketing/src/components/ContentToc.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
export interface HeadingEntry {
|
||||
id: string;
|
||||
title: string;
|
||||
level: number;
|
||||
}
|
||||
|
||||
export interface ContentTocProps {
|
||||
title: string;
|
||||
headings: ReadonlyArray<HeadingEntry>;
|
||||
}
|
||||
|
||||
export function ContentToc(props: ContentTocProps): JSX.Element | null {
|
||||
if (props.headings.length === 0) return null;
|
||||
|
||||
return (
|
||||
<nav class="space-y-3 text-sm">
|
||||
<p class="font-semibold text-foreground">{props.title}</p>
|
||||
<ul class="space-y-2 border-border border-l pl-4">
|
||||
{props.headings.map((heading) => (
|
||||
<li>
|
||||
<a href={`#${heading.id}`} data-toc-link={heading.id} class={tocClassName(heading.level)}>
|
||||
{heading.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function tocClassName(level: number): string {
|
||||
if (level <= 2) {
|
||||
return 'block text-muted-foreground hover:text-foreground transition';
|
||||
}
|
||||
if (level === 3) {
|
||||
return 'block text-muted-foreground hover:text-foreground transition ml-3';
|
||||
}
|
||||
return 'block text-muted-foreground hover:text-foreground transition ml-5';
|
||||
}
|
||||
117
packages/marketing/src/components/CurrentFeaturesSection.tsx
Normal file
117
packages/marketing/src/components/CurrentFeaturesSection.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {FeatureCard} from '@fluxer/marketing/src/components/FeatureCard';
|
||||
import {Section} from '@fluxer/marketing/src/components/Section';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
|
||||
interface CurrentFeaturesSectionProps {
|
||||
ctx: MarketingContext;
|
||||
}
|
||||
|
||||
export function CurrentFeaturesSection(props: CurrentFeaturesSectionProps): JSX.Element {
|
||||
const {ctx} = props;
|
||||
|
||||
return (
|
||||
<Section
|
||||
variant="dark"
|
||||
title={ctx.i18n.getMessage('general.coming_soon.whats_available_today', ctx.locale)}
|
||||
description={ctx.i18n.getMessage('beta_and_access.featured_benefit_line', ctx.locale)}
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-6 md:gap-8 lg:grid-cols-2">
|
||||
<FeatureCard
|
||||
ctx={ctx}
|
||||
icon="chats"
|
||||
title={ctx.i18n.getMessage('app.messaging.title', ctx.locale)}
|
||||
description={ctx.i18n.getMessage('app.messaging.description', ctx.locale)}
|
||||
features={[
|
||||
ctx.i18n.getMessage('app.messaging.features.full_markdown_support', ctx.locale),
|
||||
ctx.i18n.getMessage('app.messaging.features.private_dms_and_group_chats', ctx.locale),
|
||||
ctx.i18n.getMessage('app.messaging.features.organised_channels_for_communities', ctx.locale),
|
||||
ctx.i18n.getMessage('app.messaging.features.file_sharing', ctx.locale),
|
||||
]}
|
||||
/>
|
||||
<FeatureCard
|
||||
ctx={ctx}
|
||||
icon="microphone"
|
||||
title={ctx.i18n.getMessage('app.voice_and_video.title', ctx.locale)}
|
||||
description={ctx.i18n.getMessage('app.voice_and_video.hop_in_a_call', ctx.locale)}
|
||||
features={[
|
||||
ctx.i18n.getMessage('misc_labels.join_multiple_devices', ctx.locale),
|
||||
ctx.i18n.getMessage('app.voice_and_video.features.screen_sharing', ctx.locale),
|
||||
ctx.i18n.getMessage('app.voice_and_video.features.noise_suppression', ctx.locale),
|
||||
ctx.i18n.getMessage('app.voice_and_video.features.mute_controls', ctx.locale),
|
||||
]}
|
||||
/>
|
||||
<FeatureCard
|
||||
ctx={ctx}
|
||||
icon="gear"
|
||||
title={ctx.i18n.getMessage('app.communities.moderation.tools', ctx.locale)}
|
||||
description={ctx.i18n.getMessage('app.communities.roles_permissions_audit.keep_running_smoothly', ctx.locale)}
|
||||
features={[
|
||||
ctx.i18n.getMessage('app.communities.roles_permissions_audit.granular_roles_and_permissions', ctx.locale),
|
||||
ctx.i18n.getMessage('app.communities.moderation.actions_and_tools', ctx.locale),
|
||||
ctx.i18n.getMessage('app.communities.roles_permissions_audit.audit_logs', ctx.locale),
|
||||
ctx.i18n.getMessage('pricing_and_tiers.plutonium.features.webhooks_and_bot_support', ctx.locale),
|
||||
]}
|
||||
/>
|
||||
<FeatureCard
|
||||
ctx={ctx}
|
||||
icon="magnifying_glass"
|
||||
title={ctx.i18n.getMessage('app.messaging.features.search.search_and_quick_switcher', ctx.locale)}
|
||||
description={ctx.i18n.getMessage('app.messaging.features.search.find_old_messages', ctx.locale)}
|
||||
features={[
|
||||
ctx.i18n.getMessage('app.messaging.features.search.label', ctx.locale),
|
||||
ctx.i18n.getMessage('app.messaging.features.search.filter_options', ctx.locale),
|
||||
ctx.i18n.getMessage('app.messaging.features.search.quick_switcher_shortcuts', ctx.locale),
|
||||
ctx.i18n.getMessage('app.profiles_identity.manage_friends_and_block_users', ctx.locale),
|
||||
]}
|
||||
/>
|
||||
<FeatureCard
|
||||
ctx={ctx}
|
||||
icon="palette"
|
||||
title={ctx.i18n.getMessage('app.customization.title', ctx.locale)}
|
||||
description={ctx.i18n.getMessage('app.customization.saved_media_and_css', ctx.locale)}
|
||||
features={[
|
||||
ctx.i18n.getMessage('app.customization.upload_custom_emojis_and_stickers', ctx.locale),
|
||||
ctx.i18n.getMessage('app.messaging.features.save_media', ctx.locale),
|
||||
ctx.i18n.getMessage('app.customization.custom_css_themes', ctx.locale),
|
||||
ctx.i18n.getMessage('app.customization.compact_mode', ctx.locale),
|
||||
]}
|
||||
/>
|
||||
<FeatureCard
|
||||
ctx={ctx}
|
||||
icon="server"
|
||||
title={ctx.i18n.getMessage('product_positioning.self_hosting.label', ctx.locale)}
|
||||
description={ctx.i18n.getMessage('product_positioning.self_hosting.run_backend_on_your_hardware', ctx.locale)}
|
||||
features={[
|
||||
ctx.i18n.getMessage('product_positioning.open_source.fully_open_source_agplv3', ctx.locale),
|
||||
ctx.i18n.getMessage('product_positioning.self_hosting.host_your_own_instance', ctx.locale),
|
||||
ctx.i18n.getMessage('platform_support.desktop.use_desktop_client_mobile_soon', ctx.locale),
|
||||
ctx.i18n.getMessage('product_positioning.self_hosting.switch_between_instances', ctx.locale),
|
||||
]}
|
||||
learnMoreLink="https://docs.fluxer.app/self-hosting"
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
87
packages/marketing/src/components/FeatureCard.tsx
Normal file
87
packages/marketing/src/components/FeatureCard.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ArrowRightIcon} from '@fluxer/marketing/src/components/icons/ArrowRightIcon';
|
||||
import {Icon} from '@fluxer/marketing/src/components/icons/IconRegistry';
|
||||
import {MarketingButton} from '@fluxer/marketing/src/components/MarketingButton';
|
||||
import {MarketingCard} from '@fluxer/marketing/src/components/MarketingCard';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
|
||||
type FeatureIcon =
|
||||
| 'chats'
|
||||
| 'microphone'
|
||||
| 'palette'
|
||||
| 'magnifying_glass'
|
||||
| 'devices'
|
||||
| 'gear'
|
||||
| 'heart'
|
||||
| 'globe'
|
||||
| 'server'
|
||||
| 'newspaper';
|
||||
|
||||
interface FeatureCardProps {
|
||||
ctx: MarketingContext;
|
||||
icon: FeatureIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
features: ReadonlyArray<string>;
|
||||
learnMoreLink?: string;
|
||||
}
|
||||
|
||||
export function FeatureCard(props: FeatureCardProps): JSX.Element {
|
||||
const textColor = 'text-gray-900';
|
||||
const descriptionColor = 'text-gray-600';
|
||||
|
||||
return (
|
||||
<MarketingCard theme="light" padding="md" class="relative rounded-2xl">
|
||||
{props.learnMoreLink && (
|
||||
<MarketingButton
|
||||
href={props.learnMoreLink}
|
||||
size="small"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="absolute top-4 right-4 inline-flex items-center gap-1.5 px-3 py-1.5 md:top-6 md:right-6"
|
||||
>
|
||||
{props.ctx.i18n.getMessage('misc_labels.learn_more', props.ctx.locale)}
|
||||
<ArrowRightIcon class="h-3.5 w-3.5" />
|
||||
</MarketingButton>
|
||||
)}
|
||||
<div class="mb-6">
|
||||
<div class="mb-5 inline-flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-br from-[#4641D9]/10 to-[#4641D9]/5 md:h-16 md:w-16">
|
||||
<Icon name={props.icon} class="h-7 w-7 text-[#4641D9] md:h-8 md:w-8" />
|
||||
</div>
|
||||
<h3 class={`title ${textColor} mb-3`}>{props.title}</h3>
|
||||
<p class={`body-lg ${descriptionColor}`}>{props.description}</p>
|
||||
</div>
|
||||
<div class="mt-2 flex-1">
|
||||
<ul class="space-y-3">
|
||||
{props.features.map((feature, index) => (
|
||||
<li key={`${index}-${feature}`} class="flex items-start gap-3">
|
||||
<span class="mt-[.7em] h-1.5 w-1.5 shrink-0 rounded-full bg-[#4641D9]" />
|
||||
<span class={`body-lg ${textColor}`}>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</MarketingCard>
|
||||
);
|
||||
}
|
||||
52
packages/marketing/src/components/FeaturePill.tsx
Normal file
52
packages/marketing/src/components/FeaturePill.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
export type FeatureStatus = 'live' | 'comingSoon';
|
||||
export type FeatureTheme = 'light' | 'dark';
|
||||
|
||||
interface FeaturePillProps {
|
||||
text: string;
|
||||
status: FeatureStatus;
|
||||
}
|
||||
|
||||
interface FeaturePillThemeProps {
|
||||
text: string;
|
||||
status: FeatureStatus;
|
||||
theme: FeatureTheme;
|
||||
}
|
||||
|
||||
export function FeaturePill(props: FeaturePillProps): JSX.Element {
|
||||
return (
|
||||
<span class="inline-block rounded-lg border border-white/20 bg-white px-3 py-2 font-medium text-[#4641D9] text-sm shadow-sm sm:px-4 sm:py-3 sm:text-base">
|
||||
{props.text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function FeaturePillWithTheme(props: FeaturePillThemeProps): JSX.Element {
|
||||
const pillClass =
|
||||
props.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'
|
||||
: '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';
|
||||
|
||||
return <span class={pillClass}>{props.text}</span>;
|
||||
}
|
||||
68
packages/marketing/src/components/FeaturesGrid.tsx
Normal file
68
packages/marketing/src/components/FeaturesGrid.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {
|
||||
FeaturePillWithTheme,
|
||||
type FeatureStatus,
|
||||
type FeatureTheme,
|
||||
} from '@fluxer/marketing/src/components/FeaturePill';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
|
||||
export interface FeaturePillItem {
|
||||
text: string;
|
||||
status: FeatureStatus;
|
||||
}
|
||||
|
||||
interface FeaturesGridProps {
|
||||
ctx: MarketingContext;
|
||||
title: string;
|
||||
description: string;
|
||||
features: ReadonlyArray<FeaturePillItem>;
|
||||
theme: FeatureTheme;
|
||||
}
|
||||
|
||||
export function FeaturesGrid(props: FeaturesGridProps): JSX.Element {
|
||||
const classSet = getThemeClasses(props.theme);
|
||||
|
||||
return (
|
||||
<section class={`${classSet.bgClass} px-6 py-20 md:py-32`}>
|
||||
<div class="mx-auto max-w-7xl">
|
||||
<div class="mb-16 text-center">
|
||||
<h2 class={`mb-6 font-bold text-3xl ${classSet.textClass} md:text-4xl`}>{props.title}</h2>
|
||||
<p class={`mx-auto max-w-3xl text-lg ${classSet.descClass} md:text-xl`}>{props.description}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-center gap-3">
|
||||
{props.features.map((feature) => (
|
||||
<FeaturePillWithTheme text={feature.text} status={feature.status} theme={props.theme} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function getThemeClasses(theme: FeatureTheme): {bgClass: string; textClass: string; descClass: string} {
|
||||
if (theme === 'dark') {
|
||||
return {bgClass: 'bg-[#4641D9]', textClass: 'text-white', descClass: 'text-white/80'};
|
||||
}
|
||||
return {bgClass: 'bg-white', textClass: 'text-black', descClass: 'text-gray-600'};
|
||||
}
|
||||
57
packages/marketing/src/components/FinalCtaSection.tsx
Normal file
57
packages/marketing/src/components/FinalCtaSection.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {renderSecondaryButton, renderWithOverlay} from '@fluxer/marketing/src/components/PlatformDownloadButton';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
import {GRADIENTS} from '@fluxer/ui/src/styles/Gradients';
|
||||
import {SPACING} from '@fluxer/ui/src/styles/Spacing';
|
||||
|
||||
interface FinalCtaSectionProps {
|
||||
ctx: MarketingContext;
|
||||
}
|
||||
|
||||
export function FinalCtaSection(props: FinalCtaSectionProps): JSX.Element {
|
||||
const {ctx} = props;
|
||||
|
||||
return (
|
||||
<section class={GRADIENTS.light}>
|
||||
<div class={`${GRADIENTS.cta} rounded-t-3xl`}>
|
||||
<div class={`${SPACING.large} text-center lg:py-40`}>
|
||||
<h2 class="display mb-8 font-bold text-5xl text-white md:mb-10 md:text-6xl lg:text-7xl">
|
||||
{ctx.i18n.getMessage('misc_labels.ready_to_get_started', ctx.locale)}
|
||||
</h2>
|
||||
<p class="lead mx-auto mb-12 max-w-3xl text-white/90 text-xl md:mb-14 md:text-2xl">
|
||||
{ctx.i18n.getMessage('download.download_app_or_open_in_browser', ctx.locale)}
|
||||
</p>
|
||||
<div class="flex flex-col items-center justify-center gap-4 sm:flex-row sm:items-stretch">
|
||||
{renderWithOverlay(ctx, 'cta')}
|
||||
{renderSecondaryButton(
|
||||
ctx,
|
||||
`${ctx.appEndpoint}/channels/@me`,
|
||||
ctx.i18n.getMessage('download.open_in_browser', ctx.locale),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
43
packages/marketing/src/components/Flags.tsx
Normal file
43
packages/marketing/src/components/Flags.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {LocaleCode} from '@fluxer/constants/src/Locales';
|
||||
import {getFlagCode} from '@fluxer/locale/src/LocaleService';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
|
||||
interface FlagSvgProps {
|
||||
locale: LocaleCode;
|
||||
ctx: MarketingContext;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function FlagSvg(props: FlagSvgProps): JSX.Element {
|
||||
const flagCode = getFlagCode(props.locale);
|
||||
return (
|
||||
<img
|
||||
src={`${props.ctx.staticCdnEndpoint}/marketing/flags/${flagCode}.svg`}
|
||||
alt={`${flagCode} flag`}
|
||||
loading="lazy"
|
||||
class={props.class}
|
||||
/>
|
||||
);
|
||||
}
|
||||
181
packages/marketing/src/components/Footer.tsx
Normal file
181
packages/marketing/src/components/Footer.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ArrowRightIcon} from '@fluxer/marketing/src/components/icons/ArrowRightIcon';
|
||||
import {FluxerLogoWordmarkIcon} from '@fluxer/marketing/src/components/icons/FluxerLogoWordmarkIcon';
|
||||
import {HeartIcon} from '@fluxer/marketing/src/components/icons/HeartIcon';
|
||||
import {RssIcon} from '@fluxer/marketing/src/components/icons/RssIcon';
|
||||
import {MarketingButtonInverted} from '@fluxer/marketing/src/components/MarketingButton';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
import {href} from '@fluxer/marketing/src/UrlUtils';
|
||||
import {GRADIENTS} from '@fluxer/ui/src/styles/Gradients';
|
||||
|
||||
const linkClass = 'body-lg text-white/90 transition-colors hover:text-white hover:underline';
|
||||
|
||||
interface FooterLinkProps {
|
||||
href: string;
|
||||
children: JSX.Element | string;
|
||||
}
|
||||
|
||||
function FooterLink(props: FooterLinkProps): JSX.Element {
|
||||
return (
|
||||
<li>
|
||||
<a href={props.href} class={linkClass}>
|
||||
{props.children}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
interface FooterSectionProps {
|
||||
title: string;
|
||||
children: JSX.Element | Array<JSX.Element>;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
function FooterSection(props: FooterSectionProps): JSX.Element {
|
||||
return (
|
||||
<div class={props.class}>
|
||||
<h3 class="title mb-4 text-white md:mb-6">{props.title}</h3>
|
||||
<ul class="space-y-3">{props.children}</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface FooterProps {
|
||||
ctx: MarketingContext;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Footer(props: FooterProps): JSX.Element {
|
||||
const {ctx, className = ''} = props;
|
||||
const link = (path: string) => href(ctx, path);
|
||||
|
||||
return (
|
||||
<footer
|
||||
class={`${GRADIENTS.purple} px-6 py-20 text-white sm:px-8 md:px-12 md:py-24 lg:px-16 xl:px-20 ${className}`}
|
||||
>
|
||||
<div class="mx-auto max-w-7xl">
|
||||
<div class="mb-10 md:mb-12">
|
||||
<div class="flex flex-col items-start gap-4 lg:flex-row lg:items-center lg:justify-between lg:gap-8">
|
||||
<div class="flex flex-col items-start gap-3">
|
||||
<div class="flex shrink-0 items-center justify-center sm:h-12 sm:w-12 sm:rounded-full sm:bg-white/10">
|
||||
<HeartIcon class="h-8 w-8 text-white" />
|
||||
</div>
|
||||
<p class="body-lg max-w-xl text-white/90">
|
||||
{ctx.i18n.getMessage('footer.help_support_an_independent_communication', ctx.locale)}
|
||||
</p>
|
||||
</div>
|
||||
<MarketingButtonInverted
|
||||
href={link('/donate')}
|
||||
size="medium"
|
||||
class="mt-2 inline-flex w-fit shrink-0 items-center justify-center gap-2 rounded-full transition-opacity hover:opacity-90 lg:mt-0"
|
||||
>
|
||||
{ctx.i18n.getMessage('footer.donate', ctx.locale)}
|
||||
<ArrowRightIcon class="h-4 w-4 shrink-0" />
|
||||
</MarketingButtonInverted>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-10">
|
||||
<FluxerLogoWordmarkIcon class="h-8" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-8 sm:grid-cols-3 sm:gap-10 md:gap-x-12 md:gap-y-10 min-[480px]:grid-cols-2 min-[480px]:gap-x-6 min-[480px]:gap-y-8">
|
||||
<FooterSection title={ctx.i18n.getMessage('footer.fluxer', ctx.locale)}>
|
||||
<FooterLink href={link('/plutonium')}>
|
||||
{ctx.i18n.getMessage('footer.plutonium_tier', ctx.locale)}
|
||||
</FooterLink>
|
||||
<FooterLink href={link('/partners')}>{ctx.i18n.getMessage('footer.partners', ctx.locale)}</FooterLink>
|
||||
<FooterLink href={link('/download')}>{ctx.i18n.getMessage('footer.download', ctx.locale)}</FooterLink>
|
||||
<FooterLink href="https://github.com/fluxerapp/fluxer">
|
||||
{ctx.i18n.getMessage('footer.source_code', ctx.locale)}
|
||||
</FooterLink>
|
||||
<FooterLink href="https://bsky.app/profile/fluxer.app">
|
||||
{ctx.i18n.getMessage('footer.bluesky_social_media', ctx.locale)}
|
||||
</FooterLink>
|
||||
<li>
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="https://blog.fluxer.app" class={linkClass}>
|
||||
{ctx.i18n.getMessage('company_and_resources.blog', ctx.locale)}
|
||||
</a>
|
||||
<a
|
||||
href="https://blog.fluxer.app/rss/"
|
||||
title={ctx.i18n.getMessage('footer.rss_feed', ctx.locale)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-white/90 transition-colors hover:text-white"
|
||||
>
|
||||
<RssIcon class="h-[1em] w-[1em]" />
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<FooterLink href="https://blog.fluxer.app/roadmap-2026">
|
||||
{ctx.i18n.getMessage('footer.roadmap', ctx.locale)}
|
||||
</FooterLink>
|
||||
<FooterLink href={link('/help')}>
|
||||
{ctx.i18n.getMessage('company_and_resources.help.help_center', ctx.locale)}
|
||||
</FooterLink>
|
||||
<FooterLink href={link('/press')}>{ctx.i18n.getMessage('footer.press', ctx.locale)}</FooterLink>
|
||||
<FooterLink href="https://docs.fluxer.app">
|
||||
{ctx.i18n.getMessage('company_and_resources.docs', ctx.locale)}
|
||||
</FooterLink>
|
||||
<FooterLink href={link('/careers')}>
|
||||
{ctx.i18n.getMessage('company_and_resources.careers.label', ctx.locale)}
|
||||
</FooterLink>
|
||||
</FooterSection>
|
||||
|
||||
<FooterSection title={ctx.i18n.getMessage('footer.policies', ctx.locale)}>
|
||||
<FooterLink href={link('/terms')}>{ctx.i18n.getMessage('footer.terms_of_service', ctx.locale)}</FooterLink>
|
||||
<FooterLink href={link('/privacy')}>{ctx.i18n.getMessage('footer.privacy_policy', ctx.locale)}</FooterLink>
|
||||
<FooterLink href={link('/guidelines')}>
|
||||
{ctx.i18n.getMessage('footer.community_guidelines', ctx.locale)}
|
||||
</FooterLink>
|
||||
<FooterLink href={link('/security')}>
|
||||
{ctx.i18n.getMessage('footer.security_bug_bounty', ctx.locale)}
|
||||
</FooterLink>
|
||||
<FooterLink href={link('/company-information')}>
|
||||
{ctx.i18n.getMessage('footer.company_information', ctx.locale)}
|
||||
</FooterLink>
|
||||
</FooterSection>
|
||||
|
||||
<FooterSection
|
||||
title={ctx.i18n.getMessage('footer.connect', ctx.locale)}
|
||||
class="sm:col-span-1 min-[480px]:col-span-2"
|
||||
>
|
||||
<FooterLink href="mailto:press@fluxer.app">press@fluxer.app</FooterLink>
|
||||
<FooterLink href="mailto:support@fluxer.app">support@fluxer.app</FooterLink>
|
||||
<FooterLink href={link('/help/report-bug')}>
|
||||
{ctx.i18n.getMessage('footer.report_a_bug', ctx.locale)}
|
||||
</FooterLink>
|
||||
</FooterSection>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 pt-8">
|
||||
<p class="body-sm text-white/80">
|
||||
{ctx.i18n.getMessage('footer.fluxer_platform_ab_swedish_limited', ctx.locale)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
143
packages/marketing/src/components/GetInvolvedSection.tsx
Normal file
143
packages/marketing/src/components/GetInvolvedSection.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {BlueskyIcon} from '@fluxer/marketing/src/components/icons/BlueskyIcon';
|
||||
import {MarketingButton} from '@fluxer/marketing/src/components/MarketingButton';
|
||||
import {MarketingCard} from '@fluxer/marketing/src/components/MarketingCard';
|
||||
import {Section} from '@fluxer/marketing/src/components/Section';
|
||||
import {SupportCard} from '@fluxer/marketing/src/components/SupportCard';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
|
||||
interface GetInvolvedSectionProps {
|
||||
ctx: MarketingContext;
|
||||
}
|
||||
|
||||
export function GetInvolvedSection(props: GetInvolvedSectionProps): JSX.Element {
|
||||
const {ctx} = props;
|
||||
|
||||
return (
|
||||
<Section
|
||||
variant="light"
|
||||
title={ctx.i18n.getMessage('company_and_resources.source_and_contribution.get_involved', ctx.locale)}
|
||||
description={ctx.i18n.getMessage(
|
||||
'company_and_resources.source_and_contribution.fluxer_built_in_open',
|
||||
ctx.locale,
|
||||
)}
|
||||
className="md:py-28"
|
||||
id="get-involved"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-8 md:grid-cols-2 md:gap-10">
|
||||
<SupportCard
|
||||
ctx={ctx}
|
||||
icon="rocket_launch"
|
||||
title={ctx.i18n.getMessage('misc_labels.join_and_spread_word', ctx.locale)}
|
||||
description={ctx.i18n.getMessage('beta_and_access.registration_limited_during_beta', ctx.locale)}
|
||||
buttonText={ctx.i18n.getMessage('misc_labels.register_now', ctx.locale)}
|
||||
buttonHref={`${ctx.appEndpoint}/register`}
|
||||
theme="light"
|
||||
/>
|
||||
<SupportCard
|
||||
ctx={ctx}
|
||||
icon="chat_centered_text"
|
||||
title={ctx.i18n.getMessage('misc_labels.join_fluxer_hq', ctx.locale)}
|
||||
description={ctx.i18n.getMessage('misc_labels.get_updates', ctx.locale)}
|
||||
buttonText={ctx.i18n.getMessage('misc_labels.join_fluxer_hq', ctx.locale)}
|
||||
buttonHref="https://fluxer.gg/fluxer-hq"
|
||||
theme="light"
|
||||
/>
|
||||
<MarketingCard
|
||||
theme="light"
|
||||
padding="md"
|
||||
style="box-shadow: 0 0 0 1px rgba(0,0,0,0.03), 0 4px 12px rgba(0,0,0,0.08), 0 2px 6px rgba(0,0,0,0.05);"
|
||||
>
|
||||
<div class="mb-8 text-center">
|
||||
<div class="mb-6 inline-flex h-20 w-20 items-center justify-center rounded-3xl bg-[#4641D9] md:h-24 md:w-24">
|
||||
<BlueskyIcon class="h-10 w-10 text-white md:h-12 md:w-12" />
|
||||
</div>
|
||||
<h3 class="title mb-4 text-black text-xl md:text-2xl">
|
||||
{ctx.i18n.getMessage('social_and_feeds.bluesky.follow_us', ctx.locale)}
|
||||
</h3>
|
||||
<p class="body-lg text-gray-700 leading-relaxed">
|
||||
{ctx.i18n.getMessage('social_and_feeds.stay_updated_cta', ctx.locale)}{' '}
|
||||
<a
|
||||
href="https://bsky.app/profile/fluxer.app/rss"
|
||||
class="underline hover:text-[#4641D9]"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{ctx.i18n.getMessage('social_and_feeds.bluesky.rss_feed', ctx.locale)}
|
||||
</a>{' '}
|
||||
{ctx.i18n.getMessage('general.or', ctx.locale)}{' '}
|
||||
<a
|
||||
href="https://blog.fluxer.app/rss/"
|
||||
class="underline hover:text-[#4641D9]"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{ctx.i18n.getMessage('social_and_feeds.rss.blog_rss_feed', ctx.locale)}
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-auto flex flex-col items-center">
|
||||
<MarketingButton
|
||||
href="https://bsky.app/profile/fluxer.app"
|
||||
size="medium"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="label w-full text-center md:text-lg"
|
||||
>
|
||||
{ctx.i18n.getMessage('social_and_feeds.follow_fluxer', ctx.locale)}
|
||||
</MarketingButton>
|
||||
</div>
|
||||
</MarketingCard>
|
||||
<SupportCard
|
||||
ctx={ctx}
|
||||
icon="bug"
|
||||
title={ctx.i18n.getMessage('misc_labels.report_bugs', ctx.locale)}
|
||||
description={ctx.i18n.getMessage('security.testers_access_from_reports', ctx.locale)}
|
||||
buttonText={ctx.i18n.getMessage('misc_labels.read_the_guide', ctx.locale)}
|
||||
buttonHref="/help/report-bug"
|
||||
theme="light"
|
||||
/>
|
||||
<SupportCard
|
||||
ctx={ctx}
|
||||
icon="code"
|
||||
title={ctx.i18n.getMessage('company_and_resources.source_and_contribution.contribute_code', ctx.locale)}
|
||||
description={ctx.i18n.getMessage('product_positioning.open_source.fully_open_source_agplv3', ctx.locale)}
|
||||
buttonText={ctx.i18n.getMessage('company_and_resources.source_and_contribution.view_repository', ctx.locale)}
|
||||
buttonHref="https://github.com/fluxerapp/fluxer"
|
||||
theme="light"
|
||||
/>
|
||||
<SupportCard
|
||||
ctx={ctx}
|
||||
icon="shield_check"
|
||||
title={ctx.i18n.getMessage('security.found_security_issue', ctx.locale)}
|
||||
description={ctx.i18n.getMessage('security.responsible_disclosure_note', ctx.locale)}
|
||||
buttonText={ctx.i18n.getMessage('security.security_bug_bounty', ctx.locale)}
|
||||
buttonHref="/security"
|
||||
theme="light"
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
42
packages/marketing/src/components/HackernewsBanner.tsx
Normal file
42
packages/marketing/src/components/HackernewsBanner.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ArrowRightIcon} from '@fluxer/marketing/src/components/icons/ArrowRightIcon';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
|
||||
interface HackernewsBannerProps {
|
||||
ctx: MarketingContext;
|
||||
}
|
||||
|
||||
export function HackernewsBanner(props: HackernewsBannerProps): JSX.Element {
|
||||
const {ctx} = props;
|
||||
|
||||
return (
|
||||
<a
|
||||
href="https://fluxer.gg/fluxer-hq"
|
||||
class="mt-6 inline-flex items-center gap-2 rounded-full bg-white px-4 py-1.5 font-medium text-[#4641D9] text-sm transition-opacity hover:opacity-90"
|
||||
>
|
||||
<span>{ctx.i18n.getMessage('beta_and_access.try_without_email', ctx.locale)}</span>
|
||||
<ArrowRightIcon class="h-4 w-4" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
136
packages/marketing/src/components/Hero.tsx
Normal file
136
packages/marketing/src/components/Hero.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {HackernewsBanner} from '@fluxer/marketing/src/components/HackernewsBanner';
|
||||
import {MadeInSwedenBadge} from '@fluxer/marketing/src/components/MadeInSwedenBadge';
|
||||
import {renderSecondaryButton, renderWithOverlay} from '@fluxer/marketing/src/components/PlatformDownloadButton';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
import {href} from '@fluxer/marketing/src/UrlUtils';
|
||||
|
||||
interface HeroProps {
|
||||
ctx: MarketingContext;
|
||||
}
|
||||
|
||||
export function Hero(props: HeroProps): JSX.Element {
|
||||
const {ctx} = props;
|
||||
|
||||
return (
|
||||
<main class="flex flex-col items-center justify-center px-6 pt-44 pb-16 sm:px-8 md:px-12 md:pt-52 md:pb-20 lg:px-16 lg:pb-24 xl:px-20">
|
||||
<div class="max-w-4xl space-y-8 text-center">
|
||||
{ctx.locale === 'ja' ? (
|
||||
<div class="flex justify-center">
|
||||
<span class="font-bold text-3xl text-white">Fluxer(フラクサー)</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div class="flex flex-wrap justify-center gap-3 pb-2">
|
||||
<span class="rounded-full bg-white px-4 py-1.5 font-medium text-[#4641D9] text-sm">
|
||||
{ctx.i18n.getMessage('beta_and_access.public_beta', ctx.locale)}
|
||||
</span>
|
||||
<MadeInSwedenBadge ctx={ctx} />
|
||||
</div>
|
||||
<h1 class="hero">{ctx.i18n.getMessage('general.tagline', ctx.locale)}</h1>
|
||||
<p class="lead mx-auto max-w-2xl text-white/90">
|
||||
{ctx.i18n.getMessage('product_positioning.intro', ctx.locale)}
|
||||
</p>
|
||||
<div class="flex flex-col items-center justify-center gap-4 pt-4 sm:flex-row sm:items-stretch">
|
||||
{renderWithOverlay(ctx)}
|
||||
{renderSecondaryButton(
|
||||
ctx,
|
||||
`${ctx.appEndpoint}/channels/@me`,
|
||||
ctx.i18n.getMessage('download.open_in_browser', ctx.locale),
|
||||
)}
|
||||
</div>
|
||||
<HackernewsBanner ctx={ctx} />
|
||||
</div>
|
||||
<div class="mt-16 flex w-full max-w-6xl items-end justify-center gap-4 px-6 md:mt-24 md:gap-8">
|
||||
<div class="hidden w-full md:block md:w-4/5 lg:w-3/4">
|
||||
<picture>
|
||||
<source
|
||||
type="image/avif"
|
||||
srcset={`${ctx.staticCdnEndpoint}/marketing/screenshots/desktop-480w.avif?v=4 480w, ${ctx.staticCdnEndpoint}/marketing/screenshots/desktop-768w.avif?v=4 768w, ${ctx.staticCdnEndpoint}/marketing/screenshots/desktop-1024w.avif?v=4 1024w, ${ctx.staticCdnEndpoint}/marketing/screenshots/desktop-1920w.avif?v=4 1920w, ${ctx.staticCdnEndpoint}/marketing/screenshots/desktop-2560w.avif?v=4 2560w`}
|
||||
sizes="(max-width: 768px) 100vw, 80vw"
|
||||
/>
|
||||
<source
|
||||
type="image/webp"
|
||||
srcset={`${ctx.staticCdnEndpoint}/marketing/screenshots/desktop-480w.webp?v=4 480w, ${ctx.staticCdnEndpoint}/marketing/screenshots/desktop-768w.webp?v=4 768w, ${ctx.staticCdnEndpoint}/marketing/screenshots/desktop-1024w.webp?v=4 1024w, ${ctx.staticCdnEndpoint}/marketing/screenshots/desktop-1920w.webp?v=4 1920w, ${ctx.staticCdnEndpoint}/marketing/screenshots/desktop-2560w.webp?v=4 2560w`}
|
||||
sizes="(max-width: 768px) 100vw, 80vw"
|
||||
/>
|
||||
<img
|
||||
src={`${ctx.staticCdnEndpoint}/marketing/screenshots/desktop-1920w.png?v=4`}
|
||||
srcset={`${ctx.staticCdnEndpoint}/marketing/screenshots/desktop-480w.png?v=4 480w, ${ctx.staticCdnEndpoint}/marketing/screenshots/desktop-768w.png?v=4 768w, ${ctx.staticCdnEndpoint}/marketing/screenshots/desktop-1024w.png?v=4 1024w, ${ctx.staticCdnEndpoint}/marketing/screenshots/desktop-1920w.png?v=4 1920w, ${ctx.staticCdnEndpoint}/marketing/screenshots/desktop-2560w.png?v=4 2560w`}
|
||||
sizes="(max-width: 768px) 100vw, 80vw"
|
||||
alt={ctx.i18n.getMessage('platform_support.desktop.interface_label', ctx.locale)}
|
||||
class="aspect-video w-full rounded-lg border-2 border-white/50"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
<div class="w-full max-w-[240px] md:w-1/6 md:max-w-none lg:w-1/6">
|
||||
<picture>
|
||||
<source
|
||||
type="image/avif"
|
||||
srcset={`${ctx.staticCdnEndpoint}/marketing/screenshots/mobile-480w.avif?v=4 480w, ${ctx.staticCdnEndpoint}/marketing/screenshots/mobile-768w.avif?v=4 768w`}
|
||||
sizes="(max-width: 768px) 240px, 17vw"
|
||||
/>
|
||||
<source
|
||||
type="image/webp"
|
||||
srcset={`${ctx.staticCdnEndpoint}/marketing/screenshots/mobile-480w.webp?v=4 480w, ${ctx.staticCdnEndpoint}/marketing/screenshots/mobile-768w.webp?v=4 768w`}
|
||||
sizes="(max-width: 768px) 240px, 17vw"
|
||||
/>
|
||||
<img
|
||||
src={`${ctx.staticCdnEndpoint}/marketing/screenshots/mobile-768w.png?v=4`}
|
||||
srcset={`${ctx.staticCdnEndpoint}/marketing/screenshots/mobile-480w.png?v=4 480w, ${ctx.staticCdnEndpoint}/marketing/screenshots/mobile-768w.png?v=4 768w`}
|
||||
sizes="(max-width: 768px) 240px, 17vw"
|
||||
alt={ctx.i18n.getMessage('platform_support.mobile.interface_label', ctx.locale)}
|
||||
class="aspect-[9/19] w-full rounded-xl border-2 border-white/50"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-10 flex flex-wrap items-center justify-center gap-4 md:mt-12">
|
||||
<a
|
||||
href="https://www.producthunt.com/products/fluxer?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-fluxer"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
alt="Fluxer - Open-source Discord-like instant messaging & VoIP platform | Product Hunt"
|
||||
width="250"
|
||||
height="54"
|
||||
src={href(ctx, '/api/badges/product-hunt')}
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.producthunt.com/products/fluxer?embed=true&utm_source=badge-top-post-badge&utm_medium=badge&utm_campaign=badge-fluxer"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
alt={ctx.i18n.getMessage('misc_labels.product_hunt_badge_title', ctx.locale)}
|
||||
width="250"
|
||||
height="54"
|
||||
src={href(ctx, '/api/badges/product-hunt-top-post')}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
50
packages/marketing/src/components/HeroBase.tsx
Normal file
50
packages/marketing/src/components/HeroBase.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
export interface HeroConfig {
|
||||
icon: JSX.Element;
|
||||
title: string;
|
||||
description: string;
|
||||
extraContent: JSX.Element;
|
||||
customPadding: string;
|
||||
}
|
||||
|
||||
export function defaultHeroPadding(): string {
|
||||
return 'px-6 sm:px-8 md:px-12 lg:px-16 xl:px-20 pt-48 md:pt-60 pb-16 md:pb-20 lg:pb-24 text-white';
|
||||
}
|
||||
|
||||
export function HeroBase(config: HeroConfig): JSX.Element {
|
||||
return (
|
||||
<section class={config.customPadding}>
|
||||
<div class="mx-auto max-w-5xl text-center">
|
||||
<div class="mb-8 flex justify-center">
|
||||
<div class="inline-flex h-28 w-28 items-center justify-center rounded-3xl bg-white/10 backdrop-blur-sm md:h-36 md:w-36">
|
||||
{config.icon}
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="hero mb-8 font-bold text-5xl md:mb-10 md:text-6xl lg:text-7xl">{config.title}</h1>
|
||||
<p class="lead mx-auto max-w-4xl text-white/90 text-xl md:text-2xl">{config.description}</p>
|
||||
{config.extraContent}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
67
packages/marketing/src/components/LaunchBlogSection.tsx
Normal file
67
packages/marketing/src/components/LaunchBlogSection.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ArrowRightIcon} from '@fluxer/marketing/src/components/icons/ArrowRightIcon';
|
||||
import {MarketingButton, MarketingButtonSecondary} from '@fluxer/marketing/src/components/MarketingButton';
|
||||
import {Section} from '@fluxer/marketing/src/components/Section';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
|
||||
interface LaunchBlogSectionProps {
|
||||
ctx: MarketingContext;
|
||||
}
|
||||
|
||||
export function LaunchBlogSection(props: LaunchBlogSectionProps): JSX.Element {
|
||||
const {ctx} = props;
|
||||
|
||||
return (
|
||||
<Section
|
||||
variant="light"
|
||||
title={ctx.i18n.getMessage('launch.heading', ctx.locale)}
|
||||
description={ctx.i18n.getMessage('launch.description', ctx.locale)}
|
||||
>
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<div class="flex flex-col items-center justify-center gap-4 sm:flex-row sm:items-stretch">
|
||||
<MarketingButton
|
||||
href="https://blog.fluxer.app/how-i-built-fluxer-a-discord-like-chat-app/"
|
||||
size="large"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-2 md:text-xl"
|
||||
>
|
||||
{ctx.i18n.getMessage('launch.read_more', ctx.locale)}
|
||||
<ArrowRightIcon class="h-5 w-5 md:h-6 md:w-6" />
|
||||
</MarketingButton>
|
||||
<MarketingButtonSecondary
|
||||
href="https://blog.fluxer.app/roadmap-2026"
|
||||
size="large"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="md:text-xl"
|
||||
>
|
||||
{ctx.i18n.getMessage('launch.view_full_roadmap', ctx.locale)}
|
||||
<ArrowRightIcon class="h-5 w-5 md:h-6 md:w-6" />
|
||||
</MarketingButtonSecondary>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
212
packages/marketing/src/components/LocaleSelector.tsx
Normal file
212
packages/marketing/src/components/LocaleSelector.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {AllLocales} from '@fluxer/constants/src/Locales';
|
||||
import {getLocaleName} from '@fluxer/locale/src/LocaleService';
|
||||
import {FlagSvg} from '@fluxer/marketing/src/components/Flags';
|
||||
import {TranslateIcon} from '@fluxer/marketing/src/components/icons/TranslateIcon';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
import {href, prependBasePath} from '@fluxer/marketing/src/UrlUtils';
|
||||
import {CsrfInput} from '@fluxer/ui/src/components/CsrfInput';
|
||||
|
||||
interface LocaleSelectorProps {
|
||||
ctx: MarketingContext;
|
||||
currentPath: string;
|
||||
}
|
||||
|
||||
export function LocaleSelector(props: LocaleSelectorProps): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<LocaleSelectorTrigger ctx={props.ctx} />
|
||||
<LocaleSelectorModal ctx={props.ctx} currentPath={props.currentPath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface LocaleSelectorTriggerProps {
|
||||
ctx: MarketingContext;
|
||||
className?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export function LocaleSelectorTrigger(props: LocaleSelectorTriggerProps): JSX.Element {
|
||||
const {ctx, className, text} = props;
|
||||
const label = ctx.i18n.getMessage('languages.change_language', ctx.locale);
|
||||
const baseClass = 'locale-toggle flex items-center justify-center rounded-lg p-2 transition-colors hover:bg-gray-100';
|
||||
const classes = [baseClass, className].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<a class={classes} aria-label={label} id="locale-button" href={href(ctx, '#locale-modal-backdrop')}>
|
||||
<TranslateIcon class="h-5 w-5" />
|
||||
{text ? <span class="ml-2 font-semibold text-base text-gray-900">{text}</span> : null}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function LocaleSelectorModal(props: LocaleSelectorProps): JSX.Element {
|
||||
const ctx = props.ctx;
|
||||
const currentPath = props.currentPath;
|
||||
const title = ctx.i18n.getMessage('languages.choose_your_language', ctx.locale);
|
||||
const notice = ctx.i18n.getMessage(
|
||||
'company_and_resources.source_and_contribution.translation.llm_translation_note',
|
||||
ctx.locale,
|
||||
);
|
||||
|
||||
return (
|
||||
<div id="locale-modal-backdrop" class="locale-modal-backdrop">
|
||||
<div class="locale-modal">
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="flex items-center justify-between p-6 pb-0">
|
||||
<h2 class="font-bold text-gray-900 text-xl">{title}</h2>
|
||||
<a
|
||||
class="rounded-lg p-2 text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||
id="locale-close"
|
||||
aria-label={ctx.i18n.getMessage('navigation.close', ctx.locale)}
|
||||
href={href(ctx, '#')}
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<p class="px-6 pb-2 text-gray-500 text-xs leading-relaxed">{notice}</p>
|
||||
<div class="flex-1 overflow-y-auto p-6 pt-4">
|
||||
<div class="grid grid-cols-2 gap-3 md:grid-cols-4 lg:grid-cols-5">
|
||||
{AllLocales.map((locale) => {
|
||||
const isCurrent = locale === ctx.locale;
|
||||
const nativeName = getLocaleName(locale);
|
||||
const localizedName = getLocalizedLocaleName(ctx, locale);
|
||||
const localeCode = locale;
|
||||
|
||||
return (
|
||||
<form action={prependBasePath(ctx.basePath, '/_locale')} method="post" class="locale-form contents">
|
||||
<CsrfInput token={ctx.csrfToken} />
|
||||
<input type="hidden" name="locale" value={localeCode} />
|
||||
<input type="hidden" name="redirect" value={currentPath} />
|
||||
<button
|
||||
type="submit"
|
||||
class={`relative flex min-h-[120px] flex-col items-center justify-center gap-3 rounded-xl border-2 p-4 text-center transition-colors hover:bg-gray-50 ${
|
||||
isCurrent ? 'border-blue-500 bg-blue-50' : 'border-gray-200'
|
||||
}`}
|
||||
>
|
||||
{isCurrent ? (
|
||||
<div class="absolute top-2 right-2 flex h-6 w-6 items-center justify-center rounded-full bg-blue-600">
|
||||
<svg
|
||||
class="h-4 w-4 text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
) : null}
|
||||
<FlagSvg locale={locale} ctx={ctx} class="h-8 w-8 rounded" />
|
||||
<div class="font-semibold text-gray-900 text-sm">{nativeName}</div>
|
||||
<div class="text-gray-500 text-xs">{localizedName}</div>
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getLocalizedLocaleName(ctx: MarketingContext, locale: string): string {
|
||||
switch (locale) {
|
||||
case 'ar':
|
||||
return ctx.i18n.getMessage('languages.list.arabic', ctx.locale);
|
||||
case 'bg':
|
||||
return ctx.i18n.getMessage('languages.list.bulgarian', ctx.locale);
|
||||
case 'cs':
|
||||
return ctx.i18n.getMessage('languages.list.czech', ctx.locale);
|
||||
case 'da':
|
||||
return ctx.i18n.getMessage('languages.list.danish', ctx.locale);
|
||||
case 'de':
|
||||
return ctx.i18n.getMessage('languages.list.german', ctx.locale);
|
||||
case 'el':
|
||||
return ctx.i18n.getMessage('languages.list.greek', ctx.locale);
|
||||
case 'en-GB':
|
||||
return ctx.i18n.getMessage('languages.list.english', ctx.locale);
|
||||
case 'en-US':
|
||||
return ctx.i18n.getMessage('languages.list.english_us', ctx.locale);
|
||||
case 'es-ES':
|
||||
return ctx.i18n.getMessage('languages.list.spanish_spain', ctx.locale);
|
||||
case 'es-419':
|
||||
return ctx.i18n.getMessage('languages.list.spanish_latin_america', ctx.locale);
|
||||
case 'fi':
|
||||
return ctx.i18n.getMessage('languages.list.finnish', ctx.locale);
|
||||
case 'fr':
|
||||
return ctx.i18n.getMessage('languages.list.french', ctx.locale);
|
||||
case 'he':
|
||||
return ctx.i18n.getMessage('languages.list.hebrew', ctx.locale);
|
||||
case 'hi':
|
||||
return ctx.i18n.getMessage('languages.list.hindi', ctx.locale);
|
||||
case 'hr':
|
||||
return ctx.i18n.getMessage('languages.list.croatian', ctx.locale);
|
||||
case 'hu':
|
||||
return ctx.i18n.getMessage('languages.list.hungarian', ctx.locale);
|
||||
case 'id':
|
||||
return ctx.i18n.getMessage('languages.list.indonesian', ctx.locale);
|
||||
case 'it':
|
||||
return ctx.i18n.getMessage('languages.list.italian', ctx.locale);
|
||||
case 'ja':
|
||||
return ctx.i18n.getMessage('languages.list.japanese', ctx.locale);
|
||||
case 'ko':
|
||||
return ctx.i18n.getMessage('languages.list.korean', ctx.locale);
|
||||
case 'lt':
|
||||
return ctx.i18n.getMessage('languages.list.lithuanian', ctx.locale);
|
||||
case 'nl':
|
||||
return ctx.i18n.getMessage('languages.list.dutch', ctx.locale);
|
||||
case 'no':
|
||||
return ctx.i18n.getMessage('languages.list.norwegian', ctx.locale);
|
||||
case 'pl':
|
||||
return ctx.i18n.getMessage('languages.list.polish', ctx.locale);
|
||||
case 'pt-BR':
|
||||
return ctx.i18n.getMessage('languages.list.portuguese_brazil', ctx.locale);
|
||||
case 'ro':
|
||||
return ctx.i18n.getMessage('languages.list.romanian', ctx.locale);
|
||||
case 'ru':
|
||||
return ctx.i18n.getMessage('languages.list.russian', ctx.locale);
|
||||
case 'sv-SE':
|
||||
return ctx.i18n.getMessage('languages.list.swedish', ctx.locale);
|
||||
case 'th':
|
||||
return ctx.i18n.getMessage('languages.list.thai', ctx.locale);
|
||||
case 'tr':
|
||||
return ctx.i18n.getMessage('languages.list.turkish', ctx.locale);
|
||||
case 'uk':
|
||||
return ctx.i18n.getMessage('languages.list.ukrainian', ctx.locale);
|
||||
case 'vi':
|
||||
return ctx.i18n.getMessage('languages.list.vietnamese', ctx.locale);
|
||||
case 'zh-CN':
|
||||
return ctx.i18n.getMessage('languages.list.chinese_simplified', ctx.locale);
|
||||
case 'zh-TW':
|
||||
return ctx.i18n.getMessage('languages.list.chinese_traditional', ctx.locale);
|
||||
default:
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
40
packages/marketing/src/components/MadeInSwedenBadge.tsx
Normal file
40
packages/marketing/src/components/MadeInSwedenBadge.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {Locales} from '@fluxer/constants/src/Locales';
|
||||
import {FlagSvg} from '@fluxer/marketing/src/components/Flags';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
|
||||
interface MadeInSwedenBadgeProps {
|
||||
ctx: MarketingContext;
|
||||
}
|
||||
|
||||
export function MadeInSwedenBadge(props: MadeInSwedenBadgeProps): JSX.Element {
|
||||
const {ctx} = props;
|
||||
|
||||
return (
|
||||
<span class="inline-flex items-center gap-2 rounded-full bg-white px-4 py-1.5 font-medium text-[#4641D9] text-sm">
|
||||
<FlagSvg locale={Locales.SV_SE} ctx={ctx} class="h-4 w-4 rounded-sm" />
|
||||
<span>{ctx.i18n.getMessage('general.made_in_sweden', ctx.locale)}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
107
packages/marketing/src/components/MarketingButton.tsx
Normal file
107
packages/marketing/src/components/MarketingButton.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {ButtonProps} from '@fluxer/ui/src/components/Button';
|
||||
import type {Child, PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
type MarketingButtonSize = 'small' | 'medium' | 'large' | 'xl';
|
||||
|
||||
interface MarketingButtonProps extends Omit<ButtonProps, 'size' | 'variant' | 'target'> {
|
||||
size?: MarketingButtonSize;
|
||||
href?: string;
|
||||
target?: '_blank' | '_self' | '_parent' | '_top';
|
||||
rel?: string;
|
||||
children?: Child;
|
||||
}
|
||||
|
||||
const marketingButtonSizeClasses: Record<MarketingButtonSize, string> = {
|
||||
small: 'px-4 py-2 text-sm',
|
||||
medium: 'px-6 py-3 text-base',
|
||||
large: 'px-8 py-4 text-lg',
|
||||
xl: 'px-10 py-5 text-xl',
|
||||
};
|
||||
|
||||
export function MarketingButton({
|
||||
size = 'large',
|
||||
href,
|
||||
target,
|
||||
rel,
|
||||
children,
|
||||
class: className = '',
|
||||
...props
|
||||
}: PropsWithChildren<MarketingButtonProps>): JSX.Element {
|
||||
const sizeClass = marketingButtonSizeClasses[size];
|
||||
const marketingClasses =
|
||||
`rounded-xl bg-[#4641D9] font-semibold text-white shadow-lg transition hover:bg-[#3832B8] ${sizeClass} ${className}`.trim();
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<a href={href} class={marketingClasses} target={target} rel={rel}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button {...props} class={marketingClasses}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function MarketingButtonSecondary({
|
||||
size = 'large',
|
||||
href,
|
||||
target,
|
||||
rel,
|
||||
children,
|
||||
class: className = '',
|
||||
}: PropsWithChildren<MarketingButtonProps>): JSX.Element {
|
||||
const sizeClass = marketingButtonSizeClasses[size];
|
||||
const combinedClasses =
|
||||
`inline-flex items-center gap-2 rounded-xl border-2 border-[#4641D9] font-semibold text-[#4641D9] shadow-lg transition hover:bg-[#4641D9]/10 ${sizeClass} ${className}`.trim();
|
||||
|
||||
return (
|
||||
<a href={href} class={combinedClasses} target={target} rel={rel}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function MarketingButtonInverted({
|
||||
size = 'large',
|
||||
href,
|
||||
target,
|
||||
rel,
|
||||
children,
|
||||
class: className = '',
|
||||
}: PropsWithChildren<MarketingButtonProps>): JSX.Element {
|
||||
const sizeClass = marketingButtonSizeClasses[size];
|
||||
const combinedClasses =
|
||||
`inline-flex items-center justify-center gap-2 rounded-2xl bg-white font-semibold text-[#4641D9] shadow-lg transition hover:bg-white/90 ${sizeClass} ${className}`.trim();
|
||||
|
||||
return (
|
||||
<a href={href} class={combinedClasses} target={target} rel={rel}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
61
packages/marketing/src/components/MarketingCard.tsx
Normal file
61
packages/marketing/src/components/MarketingCard.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import type {PropsWithChildren} from 'hono/jsx';
|
||||
|
||||
type MarketingCardTheme = 'light' | 'dark';
|
||||
type MarketingCardPadding = 'sm' | 'md' | 'lg';
|
||||
|
||||
interface MarketingCardProps {
|
||||
theme?: MarketingCardTheme;
|
||||
padding?: MarketingCardPadding;
|
||||
class?: string;
|
||||
style?: string;
|
||||
children: JSX.Element | Array<JSX.Element> | string;
|
||||
}
|
||||
|
||||
const marketingCardPaddingClasses: Record<MarketingCardPadding, string> = {
|
||||
sm: 'p-6 md:p-8',
|
||||
md: 'p-8 md:p-10',
|
||||
lg: 'p-10 md:p-12',
|
||||
};
|
||||
|
||||
export function MarketingCard({
|
||||
theme = 'light',
|
||||
padding = 'md',
|
||||
class: className = '',
|
||||
style,
|
||||
children,
|
||||
}: PropsWithChildren<MarketingCardProps>): JSX.Element {
|
||||
const paddingClass = marketingCardPaddingClasses[padding];
|
||||
const themeClasses =
|
||||
theme === 'dark' ? 'border-white/20 bg-white/10 shadow-lg backdrop-blur-sm' : 'border-gray-200 bg-white shadow-md';
|
||||
const baseClasses = 'flex h-full flex-col rounded-3xl border';
|
||||
|
||||
const combinedClasses = `${baseClasses} ${themeClasses} ${paddingClass} ${className}`.trim();
|
||||
|
||||
return (
|
||||
<div class={combinedClasses} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
333
packages/marketing/src/components/Navigation.tsx
Normal file
333
packages/marketing/src/components/Navigation.tsx
Normal file
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {BlueskyIcon} from '@fluxer/marketing/src/components/icons/BlueskyIcon';
|
||||
import {DownloadIcon} from '@fluxer/marketing/src/components/icons/DownloadIcon';
|
||||
import {FluxerLogoWordmarkIcon} from '@fluxer/marketing/src/components/icons/FluxerLogoWordmarkIcon';
|
||||
import {GithubIcon} from '@fluxer/marketing/src/components/icons/GithubIcon';
|
||||
import {MenuIcon} from '@fluxer/marketing/src/components/icons/MenuIcon';
|
||||
import {RssIcon} from '@fluxer/marketing/src/components/icons/RssIcon';
|
||||
import {TranslateIcon} from '@fluxer/marketing/src/components/icons/TranslateIcon';
|
||||
import {XIcon} from '@fluxer/marketing/src/components/icons/XIcon';
|
||||
import {LocaleSelectorTrigger} from '@fluxer/marketing/src/components/LocaleSelector';
|
||||
import {MarketingButton} from '@fluxer/marketing/src/components/MarketingButton';
|
||||
import {getPlatformDownloadInfo} from '@fluxer/marketing/src/components/PlatformDownloadButton';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
import {href} from '@fluxer/marketing/src/UrlUtils';
|
||||
import type {Context} from 'hono';
|
||||
|
||||
interface NavigationProps {
|
||||
ctx: MarketingContext;
|
||||
request: Context;
|
||||
}
|
||||
|
||||
export function Navigation(props: NavigationProps): JSX.Element {
|
||||
const {ctx} = props;
|
||||
const drawer = getPlatformDownloadInfo(ctx);
|
||||
|
||||
return (
|
||||
<nav id="navbar" class="fixed top-0 right-0 left-0 z-40">
|
||||
<input type="checkbox" id="nav-toggle" class="peer hidden" />
|
||||
<div class="px-6 py-4 sm:px-8 md:px-12 md:py-5 lg:px-16 xl:px-20">
|
||||
<div class="mx-auto max-w-7xl rounded-2xl border border-gray-200/60 bg-white/95 px-3 py-2 shadow-lg backdrop-blur-lg md:px-5 md:py-2.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-6 xl:gap-8">
|
||||
<a
|
||||
href={href(ctx, '/')}
|
||||
class="relative z-10 flex items-center transition-opacity hover:opacity-80"
|
||||
aria-label={ctx.i18n.getMessage('navigation.go_home', ctx.locale)}
|
||||
>
|
||||
<FluxerLogoWordmarkIcon class="h-8 text-[#4641D9] md:h-9" />
|
||||
</a>
|
||||
<div class="marketing-nav-links hidden items-center gap-6 lg:flex xl:gap-8">
|
||||
<a
|
||||
href={href(ctx, '/download')}
|
||||
class="body-lg font-semibold text-gray-900/90 transition-colors hover:text-gray-900"
|
||||
>
|
||||
{ctx.i18n.getMessage('download.download', ctx.locale)}
|
||||
</a>
|
||||
<a
|
||||
href={href(ctx, '/plutonium')}
|
||||
class="body-lg font-semibold text-gray-900/90 transition-colors hover:text-gray-900"
|
||||
>
|
||||
{ctx.i18n.getMessage('pricing_and_tiers.plutonium.tier_name', ctx.locale)}
|
||||
</a>
|
||||
<a
|
||||
href={href(ctx, '/help')}
|
||||
class="body-lg font-semibold text-gray-900/90 transition-colors hover:text-gray-900"
|
||||
>
|
||||
{ctx.i18n.getMessage('company_and_resources.help.label', ctx.locale)}
|
||||
</a>
|
||||
<a
|
||||
href="https://docs.fluxer.app"
|
||||
class="body-lg font-semibold text-gray-900/90 transition-colors hover:text-gray-900"
|
||||
>
|
||||
{ctx.i18n.getMessage('company_and_resources.docs', ctx.locale)}
|
||||
</a>
|
||||
<a
|
||||
href="https://blog.fluxer.app"
|
||||
class="body-lg font-semibold text-gray-900/90 transition-colors hover:text-gray-900"
|
||||
>
|
||||
{ctx.i18n.getMessage('company_and_resources.blog', ctx.locale)}
|
||||
</a>
|
||||
<a
|
||||
href={href(ctx, '/donate')}
|
||||
class="body-lg font-semibold text-gray-900/90 transition-colors hover:text-gray-900"
|
||||
>
|
||||
{ctx.i18n.getMessage('donations.donate.action', ctx.locale)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<a
|
||||
href="https://bsky.app/profile/fluxer.app"
|
||||
class="hidden items-center rounded-lg p-2 text-[#4641D9] transition-colors hover:bg-gray-100 hover:text-[#3d38c7] lg:flex"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={ctx.i18n.getMessage('social_and_feeds.bluesky.label', ctx.locale)}
|
||||
>
|
||||
<BlueskyIcon class="h-5 w-5" />
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/fluxerapp/fluxer"
|
||||
class="hidden items-center rounded-lg p-2 text-[#4641D9] transition-colors hover:bg-gray-100 hover:text-[#3d38c7] lg:flex"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={ctx.i18n.getMessage('social_and_feeds.github', ctx.locale)}
|
||||
>
|
||||
<GithubIcon class="h-5 w-5" />
|
||||
</a>
|
||||
<a
|
||||
href="https://blog.fluxer.app/rss/"
|
||||
class="marketing-nav-rss hidden items-center rounded-lg p-2 text-[#4641D9] transition-colors hover:bg-gray-100 hover:text-[#3d38c7] lg:flex"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={ctx.i18n.getMessage('social_and_feeds.rss.label', ctx.locale)}
|
||||
>
|
||||
<RssIcon class="h-5 w-5" />
|
||||
</a>
|
||||
<LocaleSelectorTrigger
|
||||
ctx={ctx}
|
||||
className="hidden text-[#4641D9] transition-colors hover:bg-gray-100 hover:text-[#3d38c7] lg:flex"
|
||||
/>
|
||||
<MarketingButton
|
||||
href={`${ctx.appEndpoint}/channels/@me`}
|
||||
size="medium"
|
||||
class="hidden whitespace-nowrap lg:inline-flex"
|
||||
>
|
||||
{ctx.i18n.getMessage('app.open.open_fluxer', ctx.locale)}
|
||||
</MarketingButton>
|
||||
<label
|
||||
for="nav-toggle"
|
||||
class="relative z-10 flex cursor-pointer items-center justify-center rounded-lg p-2 transition-colors hover:bg-gray-100 lg:hidden"
|
||||
>
|
||||
<MenuIcon class="h-6 w-6 text-gray-900 peer-checked:hidden" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pointer-events-none fixed inset-0 z-50 bg-black/50 opacity-0 backdrop-blur-sm transition-opacity peer-checked:pointer-events-auto peer-checked:opacity-100 lg:hidden">
|
||||
<button
|
||||
type="button"
|
||||
onclick="document.getElementById('nav-toggle').checked = false"
|
||||
class="absolute inset-0"
|
||||
aria-label={ctx.i18n.getMessage('navigation.close_navigation_menu', ctx.locale)}
|
||||
></button>
|
||||
</div>
|
||||
<div class="fixed top-0 right-0 bottom-0 z-50 w-full translate-x-full transform overflow-y-auto rounded-none bg-white shadow-2xl transition-transform peer-checked:translate-x-0 sm:w-[420px] sm:max-w-[90vw] sm:rounded-l-3xl lg:hidden">
|
||||
<div class="flex h-full flex-col p-6">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<a
|
||||
href={href(ctx, '/')}
|
||||
class="flex items-center gap-3 rounded-xl px-2 py-1 transition-colors hover:bg-gray-50"
|
||||
aria-label={ctx.i18n.getMessage('navigation.go_home', ctx.locale)}
|
||||
>
|
||||
<FluxerLogoWordmarkIcon class="h-7 text-[#4641D9]" />
|
||||
</a>
|
||||
<label for="nav-toggle" class="cursor-pointer rounded-lg p-2 transition-colors hover:bg-gray-100">
|
||||
<XIcon class="h-6 w-6 text-gray-900" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="-mx-2 flex-1 overflow-y-auto px-2">
|
||||
<div class="flex flex-col gap-6">
|
||||
<div>
|
||||
<p class="mb-2 font-semibold text-gray-500 text-xs uppercase tracking-wide">
|
||||
{ctx.i18n.getMessage('company_and_resources.product', ctx.locale)}
|
||||
</p>
|
||||
<div class="flex flex-col gap-1">
|
||||
<a
|
||||
href={href(ctx, '/download')}
|
||||
class="rounded-lg py-2.5 pr-3 pl-0 font-semibold text-base text-gray-900 transition-colors hover:bg-gray-100"
|
||||
>
|
||||
{ctx.i18n.getMessage('download.download', ctx.locale)}
|
||||
</a>
|
||||
<a
|
||||
href={href(ctx, '/plutonium')}
|
||||
class="rounded-lg py-2.5 pr-3 pl-0 font-semibold text-base text-gray-900 transition-colors hover:bg-gray-100"
|
||||
>
|
||||
{ctx.i18n.getMessage('pricing_and_tiers.plutonium.tier_name', ctx.locale)}
|
||||
</a>
|
||||
<a
|
||||
href={href(ctx, '/partners')}
|
||||
class="rounded-lg py-2.5 pr-3 pl-0 font-semibold text-base text-gray-900 transition-colors hover:bg-gray-100"
|
||||
>
|
||||
{ctx.i18n.getMessage('partner_program.label', ctx.locale)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 font-semibold text-gray-500 text-xs uppercase tracking-wide">
|
||||
{ctx.i18n.getMessage('company_and_resources.resources', ctx.locale)}
|
||||
</p>
|
||||
<div class="flex flex-col gap-1">
|
||||
<a
|
||||
href={href(ctx, '/help')}
|
||||
class="rounded-lg py-2.5 pr-3 pl-0 font-semibold text-base text-gray-900 transition-colors hover:bg-gray-100"
|
||||
>
|
||||
{ctx.i18n.getMessage('company_and_resources.help.help_center', ctx.locale)}
|
||||
</a>
|
||||
<a
|
||||
href="https://docs.fluxer.app"
|
||||
class="rounded-lg py-2.5 pr-3 pl-0 font-semibold text-base text-gray-900 transition-colors hover:bg-gray-100"
|
||||
>
|
||||
{ctx.i18n.getMessage('company_and_resources.docs', ctx.locale)}
|
||||
</a>
|
||||
<a
|
||||
href="https://blog.fluxer.app"
|
||||
class="flex items-center gap-2 rounded-lg py-2.5 pr-3 pl-0 font-semibold text-base text-gray-900 transition-colors hover:bg-gray-100"
|
||||
>
|
||||
{ctx.i18n.getMessage('company_and_resources.blog', ctx.locale)}
|
||||
<RssIcon class="h-4 w-4 text-gray-500" />
|
||||
</a>
|
||||
<a
|
||||
href={href(ctx, '/press')}
|
||||
class="rounded-lg py-2.5 pr-3 pl-0 font-semibold text-base text-gray-900 transition-colors hover:bg-gray-100"
|
||||
>
|
||||
{ctx.i18n.getMessage('company_and_resources.press.label', ctx.locale)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 font-semibold text-gray-500 text-xs uppercase tracking-wide">
|
||||
{ctx.i18n.getMessage('company_and_resources.connect', ctx.locale)}
|
||||
</p>
|
||||
<div class="flex flex-col gap-1">
|
||||
<a
|
||||
href="https://bsky.app/profile/fluxer.app"
|
||||
class="rounded-lg py-2.5 pr-3 pl-0 font-semibold text-base text-gray-900 transition-colors hover:bg-gray-100"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{ctx.i18n.getMessage('social_and_feeds.bluesky.label', ctx.locale)}
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/fluxerapp/fluxer"
|
||||
class="rounded-lg py-2.5 pr-3 pl-0 font-semibold text-base text-gray-900 transition-colors hover:bg-gray-100"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{ctx.i18n.getMessage('company_and_resources.source_and_contribution.source_code', ctx.locale)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 font-semibold text-gray-500 text-xs uppercase tracking-wide">
|
||||
{ctx.i18n.getMessage('company_and_resources.company', ctx.locale)}
|
||||
</p>
|
||||
<div class="flex flex-col gap-1">
|
||||
<a
|
||||
href={href(ctx, '/careers')}
|
||||
class="rounded-lg py-2.5 pr-3 pl-0 font-semibold text-base text-gray-900 transition-colors hover:bg-gray-100"
|
||||
>
|
||||
{ctx.i18n.getMessage('company_and_resources.careers.label', ctx.locale)}
|
||||
</a>
|
||||
<a
|
||||
href={href(ctx, '/donate')}
|
||||
class="rounded-lg py-2.5 pr-3 pl-0 font-semibold text-base text-gray-900 transition-colors hover:bg-gray-100"
|
||||
>
|
||||
{ctx.i18n.getMessage('donations.donate.action', ctx.locale)}
|
||||
</a>
|
||||
<a
|
||||
href={href(ctx, '/company-information')}
|
||||
class="rounded-lg py-2.5 pr-3 pl-0 font-semibold text-base text-gray-900 transition-colors hover:bg-gray-100"
|
||||
>
|
||||
{ctx.i18n.getMessage('company_and_resources.company_info', ctx.locale)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex flex-col gap-3 border-gray-200 border-t pt-4">
|
||||
<MobileDrawerButton
|
||||
href={href(ctx, '#locale-modal-backdrop')}
|
||||
id="locale-button"
|
||||
ariaLabel={ctx.i18n.getMessage('languages.change_language', ctx.locale)}
|
||||
className="locale-toggle"
|
||||
icon={<TranslateIcon class="h-5 w-5" />}
|
||||
label={ctx.i18n.getMessage('languages.language_label', ctx.locale)}
|
||||
/>
|
||||
<MobileDrawerButton
|
||||
href={drawer.url}
|
||||
icon={<DownloadIcon class="h-5 w-5" />}
|
||||
label={ctx.i18n.getMessage('download.download', ctx.locale)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-6">
|
||||
<MarketingButton
|
||||
href={`${ctx.appEndpoint}/channels/@me`}
|
||||
size="medium"
|
||||
class="flex w-full items-center justify-center px-5 py-3"
|
||||
>
|
||||
{ctx.i18n.getMessage('app.open.open_fluxer', ctx.locale)}
|
||||
</MarketingButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
interface MobileDrawerButtonProps {
|
||||
href: string;
|
||||
icon?: JSX.Element;
|
||||
label: string;
|
||||
ariaLabel?: string;
|
||||
id?: string;
|
||||
className?: string;
|
||||
target?: string;
|
||||
rel?: string;
|
||||
}
|
||||
|
||||
function MobileDrawerButton(props: MobileDrawerButtonProps): JSX.Element {
|
||||
const {href, icon, label, ariaLabel, id, className, target, rel} = props;
|
||||
const baseClass =
|
||||
'flex w-full items-center justify-start gap-3 rounded-lg px-3 py-2.5 font-semibold text-base text-gray-900 transition-colors hover:bg-gray-100 lg:hidden';
|
||||
const classes = [baseClass, className].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<a href={href} id={id} aria-label={ariaLabel} class={classes} target={target} rel={rel}>
|
||||
{icon}
|
||||
<span class="flex-1 text-left">{label}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
88
packages/marketing/src/components/PartnerSection.tsx
Normal file
88
packages/marketing/src/components/PartnerSection.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ChatsCircleIcon} from '@fluxer/marketing/src/components/icons/ChatsCircleIcon';
|
||||
import {FluxerPartnerIcon} from '@fluxer/marketing/src/components/icons/FluxerPartnerIcon';
|
||||
import {FluxerPremiumIcon} from '@fluxer/marketing/src/components/icons/FluxerPremiumIcon';
|
||||
import {SealCheckIcon} from '@fluxer/marketing/src/components/icons/SealCheckIcon';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
import {href} from '@fluxer/marketing/src/UrlUtils';
|
||||
|
||||
interface PartnerSectionProps {
|
||||
ctx: MarketingContext;
|
||||
}
|
||||
|
||||
export function PartnerSection(props: PartnerSectionProps): JSX.Element {
|
||||
const {ctx} = props;
|
||||
|
||||
return (
|
||||
<section class="bg-gradient-to-b from-gray-50 to-white px-6 pb-24 sm:px-8 md:px-12 md:pb-40 lg:px-16 xl:px-20">
|
||||
<div class="mx-auto max-w-6xl rounded-3xl bg-gradient-to-br from-black to-gray-900 p-10 text-white shadow-xl md:p-16 lg:p-20">
|
||||
<div class="mb-10 text-center md:mb-12">
|
||||
<div class="mb-6 inline-flex h-20 w-20 items-center justify-center rounded-2xl bg-white/10 backdrop-blur-sm md:mb-8 md:h-24 md:w-24">
|
||||
<FluxerPartnerIcon class="h-10 w-10 md:h-12 md:w-12" />
|
||||
</div>
|
||||
<h2 class="display mb-6 text-3xl text-white md:mb-8 md:text-4xl lg:text-5xl">
|
||||
{ctx.i18n.getMessage('partner_program.become_partner.heading', ctx.locale)}
|
||||
</h2>
|
||||
<p class="lead mx-auto max-w-3xl text-white/90">
|
||||
{ctx.i18n.getMessage('partner_program.who_its_for', ctx.locale)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mx-auto mb-10 grid max-w-3xl grid-cols-1 gap-6 sm:grid-cols-3 md:mb-12 md:gap-8">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-xl bg-white/10 backdrop-blur-sm md:h-20 md:w-20">
|
||||
<FluxerPremiumIcon class="h-8 w-8 md:h-10 md:w-10" />
|
||||
</div>
|
||||
<p class="body-lg font-semibold text-white">
|
||||
{ctx.i18n.getMessage('partner_program.perks.free_plutonium.label', ctx.locale)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-xl bg-white/10 backdrop-blur-sm md:h-20 md:w-20">
|
||||
<SealCheckIcon class="h-8 w-8 md:h-10 md:w-10" />
|
||||
</div>
|
||||
<p class="body-lg font-semibold text-white">
|
||||
{ctx.i18n.getMessage('app.communities.verification.verified_community', ctx.locale)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-xl bg-white/10 backdrop-blur-sm md:h-20 md:w-20">
|
||||
<ChatsCircleIcon class="h-8 w-8 md:h-10 md:w-10" />
|
||||
</div>
|
||||
<p class="body-lg font-semibold text-white">
|
||||
{ctx.i18n.getMessage('partner_program.perks.direct_team_access.label', ctx.locale)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a
|
||||
href={href(ctx, '/partners')}
|
||||
class="label inline-block rounded-lg bg-white px-8 py-4 text-black transition-colors hover:bg-opacity-90"
|
||||
>
|
||||
{ctx.i18n.getMessage('partner_program.become_partner.call_to_action', ctx.locale)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
70
packages/marketing/src/components/Picture.tsx
Normal file
70
packages/marketing/src/components/Picture.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface PictureProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
class: string;
|
||||
sizes: string;
|
||||
width1x: number;
|
||||
width2x: number;
|
||||
priority?: boolean;
|
||||
}
|
||||
|
||||
export function Picture(props: PictureProps): JSX.Element {
|
||||
return renderWithPriority(props, false);
|
||||
}
|
||||
|
||||
export function PicturePriority(props: PictureProps): JSX.Element {
|
||||
return renderWithPriority(props, true);
|
||||
}
|
||||
|
||||
function renderWithPriority(props: PictureProps, highPriority: boolean): JSX.Element {
|
||||
const basePath = getBasePath(props.src);
|
||||
const width1x = props.width1x.toString();
|
||||
const width2x = props.width2x.toString();
|
||||
|
||||
const srcsetAvif = `${basePath}-1x.avif ${width1x}w, ${basePath}-2x.avif ${width2x}w`;
|
||||
const srcsetWebp = `${basePath}-1x.webp ${width1x}w, ${basePath}-2x.webp ${width2x}w`;
|
||||
const srcsetPng = `${basePath}-1x.png ${width1x}w, ${basePath}-2x.png ${width2x}w`;
|
||||
|
||||
return (
|
||||
<picture>
|
||||
<source srcset={srcsetAvif} sizes={props.sizes} type="image/avif" />
|
||||
<source srcset={srcsetWebp} sizes={props.sizes} type="image/webp" />
|
||||
<img
|
||||
srcset={srcsetPng}
|
||||
sizes={props.sizes}
|
||||
fetchpriority={highPriority ? 'high' : undefined}
|
||||
src={`${basePath}-1x.png`}
|
||||
alt={props.alt}
|
||||
class={props.class}
|
||||
/>
|
||||
</picture>
|
||||
);
|
||||
}
|
||||
|
||||
function getBasePath(src: string): string {
|
||||
const parts = src.split('.');
|
||||
if (parts.length <= 1) return src;
|
||||
return parts.slice(0, -1).join('.');
|
||||
}
|
||||
384
packages/marketing/src/components/PlatformDownloadButton.tsx
Normal file
384
packages/marketing/src/components/PlatformDownloadButton.tsx
Normal file
@@ -0,0 +1,384 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {AndroidIcon} from '@fluxer/marketing/src/components/icons/AndroidIcon';
|
||||
import {AppleIcon} from '@fluxer/marketing/src/components/icons/AppleIcon';
|
||||
import {CaretDownIcon} from '@fluxer/marketing/src/components/icons/CaretDownIcon';
|
||||
import {DownloadIcon} from '@fluxer/marketing/src/components/icons/DownloadIcon';
|
||||
import {LinuxIcon} from '@fluxer/marketing/src/components/icons/LinuxIcon';
|
||||
import {WindowsIcon} from '@fluxer/marketing/src/components/icons/WindowsIcon';
|
||||
import type {MarketingContext, MarketingPlatform} from '@fluxer/marketing/src/MarketingContext';
|
||||
import {apiUrl, href, isCanary} from '@fluxer/marketing/src/UrlUtils';
|
||||
|
||||
export type ButtonStyle = 'light' | 'dark';
|
||||
|
||||
const lightBg = 'bg-white';
|
||||
const lightText = 'text-[#4641D9]';
|
||||
const lightHover = 'hover:bg-gray-50';
|
||||
const darkBg = 'bg-[#4641D9]';
|
||||
const darkText = 'text-white';
|
||||
const darkHover = 'hover:bg-[#3a36b0]';
|
||||
const btnSizing = 'px-5 py-3 md:px-6 md:py-3.5';
|
||||
const btnBase = `download-link flex items-center justify-center rounded-l-2xl ${btnSizing} transition-colors shadow-lg`;
|
||||
const chevronBase = 'overlay-toggle flex items-center self-stretch rounded-r-2xl px-3 transition-colors shadow-lg';
|
||||
const mobileBtnBase = `inline-flex items-center justify-center rounded-2xl ${btnSizing} transition-colors shadow-lg`;
|
||||
const secondaryBtnBase = `hidden items-center justify-center gap-2 rounded-2xl ${btnSizing} font-semibold text-sm text-white shadow-lg ring-1 ring-inset ring-white/30 bg-white/10 backdrop-blur-sm transition-colors hover:bg-white/20 sm:inline-flex md:text-base`;
|
||||
|
||||
interface PlatformDownloadInfo {
|
||||
url: string;
|
||||
label: string;
|
||||
icon: JSX.Element;
|
||||
}
|
||||
|
||||
export function getPlatformDownloadInfo(ctx: MarketingContext): PlatformDownloadInfo {
|
||||
switch (ctx.platform) {
|
||||
case 'windows': {
|
||||
const arch = defaultArchitecture(ctx, 'windows');
|
||||
return {
|
||||
url: desktopRedirectUrl(ctx, 'win32', arch, 'setup'),
|
||||
label: ctx.i18n.getMessage('platform_support.platforms.windows.download_label', ctx.locale),
|
||||
icon: <WindowsIcon class="h-5 w-5" />,
|
||||
};
|
||||
}
|
||||
case 'macos': {
|
||||
const arch = defaultArchitecture(ctx, 'macos');
|
||||
return {
|
||||
url: desktopRedirectUrl(ctx, 'darwin', arch, 'dmg'),
|
||||
label: ctx.i18n.getMessage('platform_support.platforms.macos.download_label', ctx.locale),
|
||||
icon: <AppleIcon class="h-5 w-5" />,
|
||||
};
|
||||
}
|
||||
case 'linux': {
|
||||
const arch = defaultArchitecture(ctx, 'linux');
|
||||
return {
|
||||
url: desktopRedirectUrl(ctx, 'linux', arch, 'deb'),
|
||||
label: ctx.i18n.getMessage('platform_support.platforms.linux.choose_distribution', ctx.locale),
|
||||
icon: <LinuxIcon class="h-5 w-5" />,
|
||||
};
|
||||
}
|
||||
case 'ios':
|
||||
case 'android':
|
||||
return {
|
||||
url: href(ctx, '/download'),
|
||||
label: ctx.i18n.getMessage('platform_support.mobile.mobile_apps_underway', ctx.locale),
|
||||
icon: <DownloadIcon class="h-5 w-5" />,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
url: href(ctx, '/download'),
|
||||
label: ctx.i18n.getMessage('download.download', ctx.locale),
|
||||
icon: <DownloadIcon class="h-5 w-5" />,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getSystemRequirements(ctx: MarketingContext, platform: MarketingPlatform): string {
|
||||
switch (platform) {
|
||||
case 'windows':
|
||||
return ctx.i18n.getMessage('platform_support.platforms.windows.min_version', ctx.locale);
|
||||
case 'macos':
|
||||
return ctx.i18n.getMessage('platform_support.platforms.macos.min_version', ctx.locale);
|
||||
case 'linux':
|
||||
return '';
|
||||
case 'ios':
|
||||
return ctx.i18n.getMessage('platform_support.platforms.ios.min_version', ctx.locale);
|
||||
case 'android':
|
||||
return ctx.i18n.getMessage('platform_support.platforms.android.min_version', ctx.locale);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function renderSecondaryButton(_ctx: MarketingContext, href: string, label: string): JSX.Element {
|
||||
return (
|
||||
<a href={href} class={secondaryBtnBase}>
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderWithOverlay(ctx: MarketingContext, idPrefix: string | null = null): JSX.Element {
|
||||
const requirements = getSystemRequirements(ctx, ctx.platform);
|
||||
let button: JSX.Element;
|
||||
|
||||
switch (ctx.platform) {
|
||||
case 'windows':
|
||||
button = renderDesktopButton(ctx, 'windows', 'light', idPrefix, false, false);
|
||||
break;
|
||||
case 'macos':
|
||||
button = renderDesktopButton(ctx, 'macos', 'light', idPrefix, false, false);
|
||||
break;
|
||||
case 'linux':
|
||||
button = renderDesktopButton(ctx, 'linux', 'light', idPrefix, false, false);
|
||||
break;
|
||||
case 'ios':
|
||||
case 'android':
|
||||
button = renderMobileRedirectButton(ctx, 'light');
|
||||
break;
|
||||
default:
|
||||
button = (
|
||||
<a
|
||||
href={href(ctx, '/download')}
|
||||
class={`inline-flex items-center justify-center gap-2 rounded-2xl ${lightBg} px-5 py-3 font-semibold text-base md:px-6 md:py-3.5 md:text-lg ${lightText} shadow-lg transition-colors hover:bg-white/90`}
|
||||
>
|
||||
<DownloadIcon class="h-5 w-5 shrink-0" />
|
||||
<span>{ctx.i18n.getMessage('download.download_fluxer', ctx.locale)}</span>
|
||||
</a>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!requirements) return button;
|
||||
return (
|
||||
<div class="relative">
|
||||
{button}
|
||||
<p class="absolute top-full left-1/2 mt-2 -translate-x-1/2 whitespace-nowrap text-center text-white/50 text-xs">
|
||||
{requirements}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderMobileButton(
|
||||
ctx: MarketingContext,
|
||||
platform: MarketingPlatform,
|
||||
style: ButtonStyle,
|
||||
): JSX.Element {
|
||||
const config = getMobileConfig(ctx, platform);
|
||||
if (!config) return <span />;
|
||||
const [btnClass] = getMobileButtonClasses(style);
|
||||
const downloadFor = ctx.i18n.getMessage('download.download_for_prefix', ctx.locale);
|
||||
|
||||
return (
|
||||
<a class={btnClass} href={config.url}>
|
||||
{config.icon}
|
||||
<span class="font-semibold text-sm md:text-base">
|
||||
{downloadFor} {config.platformName}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function renderMobileRedirectButton(ctx: MarketingContext, style: ButtonStyle): JSX.Element {
|
||||
const [btnClass] = getMobileButtonClasses(style);
|
||||
return (
|
||||
<a class={btnClass} href={href(ctx, '/download')}>
|
||||
<DownloadIcon class="h-5 w-5 shrink-0" />
|
||||
<span class="font-semibold text-sm md:text-base">
|
||||
{ctx.i18n.getMessage('platform_support.mobile.mobile_apps_underway', ctx.locale)}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function getMobileButtonClasses(style: ButtonStyle): [string] {
|
||||
if (style === 'light') {
|
||||
return [`${mobileBtnBase} gap-2 ${lightBg} ${lightText} hover:bg-white/90`];
|
||||
}
|
||||
return [`${mobileBtnBase} gap-2 ${darkBg} ${darkText} ${darkHover}`];
|
||||
}
|
||||
|
||||
interface MobileButtonConfig {
|
||||
platformName: string;
|
||||
icon: JSX.Element;
|
||||
url: string;
|
||||
helperText: string;
|
||||
}
|
||||
|
||||
function getMobileConfig(ctx: MarketingContext, platform: MarketingPlatform): MobileButtonConfig | null {
|
||||
switch (platform) {
|
||||
case 'ios':
|
||||
return {
|
||||
platformName: ctx.i18n.getMessage('platform_support.platforms.ios.name', ctx.locale),
|
||||
icon: <AppleIcon class="h-6 w-6 shrink-0" />,
|
||||
url: apiUrl(ctx, '/dl/ios/testflight'),
|
||||
helperText: ctx.i18n.getMessage('platform_support.platforms.ios.testflight', ctx.locale),
|
||||
};
|
||||
case 'android':
|
||||
return {
|
||||
platformName: ctx.i18n.getMessage('platform_support.platforms.android.name', ctx.locale),
|
||||
icon: <AndroidIcon class="h-6 w-6 shrink-0" />,
|
||||
url: apiUrl(ctx, '/dl/android/arm64/apk'),
|
||||
helperText: ctx.i18n.getMessage('platform_support.platforms.android.apk', ctx.locale),
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function renderDesktopButton(
|
||||
ctx: MarketingContext,
|
||||
platform: MarketingPlatform,
|
||||
style: ButtonStyle,
|
||||
idPrefix: string | null,
|
||||
compact: boolean,
|
||||
fullWidth: boolean,
|
||||
): JSX.Element {
|
||||
const {platformId, platformName, icon, options} = getPlatformConfig(ctx, platform);
|
||||
const finalId = idPrefix ? `${idPrefix}-${platformId}` : platformId;
|
||||
const defaultArch = defaultArchitecture(ctx, platform);
|
||||
const selected = options.find((opt) => opt.arch === defaultArch) ?? options[0];
|
||||
const [btnClass, chevronClass] = getDesktopButtonClasses(style);
|
||||
const containerClass = fullWidth ? 'flex w-full' : 'flex';
|
||||
const widthModifier = fullWidth ? ' flex-1 w-full min-w-0' : '';
|
||||
const buttonClass = `${btnClass}${widthModifier}`;
|
||||
const buttonLabel = compact
|
||||
? platformName
|
||||
: `${ctx.i18n.getMessage('download.download_for_prefix', ctx.locale)}${platformName}`;
|
||||
|
||||
return (
|
||||
<div class={`${containerClass} relative`} id={`${finalId}-download-buttons`}>
|
||||
<a
|
||||
class={buttonClass}
|
||||
href={selected.url}
|
||||
data-base-url={selected.url}
|
||||
data-arch={selected.arch}
|
||||
data-format={selected.format}
|
||||
data-platform={platformId}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
{icon}
|
||||
<span class="font-semibold text-sm md:text-base">{buttonLabel}</span>
|
||||
</div>
|
||||
</a>
|
||||
<button type="button" class={chevronClass} data-overlay-target={`${finalId}-download-overlay`}>
|
||||
<CaretDownIcon class="h-4 w-4" />
|
||||
</button>
|
||||
<div
|
||||
class="download-overlay absolute top-full left-0 z-50 mt-1 hidden w-full min-w-48 rounded-xl border border-gray-200 bg-white shadow-xl"
|
||||
id={`${finalId}-download-overlay`}
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<a
|
||||
class="download-overlay-link block px-4 py-3 text-gray-900 text-sm transition-colors first:rounded-t-xl last:rounded-b-xl hover:bg-gray-100"
|
||||
href={opt.url}
|
||||
data-arch={opt.arch}
|
||||
data-format={opt.format}
|
||||
data-base-url={opt.url}
|
||||
>
|
||||
{formatOverlayLabel(ctx, platform, opt.arch, opt.format)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PlatformOption {
|
||||
arch: string;
|
||||
format: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface PlatformConfig {
|
||||
platformId: string;
|
||||
platformName: string;
|
||||
icon: JSX.Element;
|
||||
options: ReadonlyArray<PlatformOption>;
|
||||
}
|
||||
|
||||
function getPlatformConfig(ctx: MarketingContext, platform: MarketingPlatform): PlatformConfig {
|
||||
switch (platform) {
|
||||
case 'windows':
|
||||
return {
|
||||
platformId: 'windows',
|
||||
platformName: ctx.i18n.getMessage('platform_support.platforms.windows.name', ctx.locale),
|
||||
icon: <WindowsIcon class="h-6 w-6 shrink-0" />,
|
||||
options: [
|
||||
{arch: 'x64', format: 'EXE', url: desktopRedirectUrl(ctx, 'win32', 'x64', 'setup')},
|
||||
{arch: 'arm64', format: 'EXE', url: desktopRedirectUrl(ctx, 'win32', 'arm64', 'setup')},
|
||||
],
|
||||
};
|
||||
case 'macos':
|
||||
return {
|
||||
platformId: 'macos',
|
||||
platformName: ctx.i18n.getMessage('platform_support.platforms.macos.name', ctx.locale),
|
||||
icon: <AppleIcon class="h-6 w-6 shrink-0" />,
|
||||
options: [
|
||||
{arch: 'arm64', format: 'DMG', url: desktopRedirectUrl(ctx, 'darwin', 'arm64', 'dmg')},
|
||||
{arch: 'x64', format: 'DMG', url: desktopRedirectUrl(ctx, 'darwin', 'x64', 'dmg')},
|
||||
],
|
||||
};
|
||||
case 'linux':
|
||||
return {
|
||||
platformId: 'linux',
|
||||
platformName: ctx.i18n.getMessage('platform_support.platforms.linux.name', ctx.locale),
|
||||
icon: <LinuxIcon class="h-6 w-6 shrink-0" />,
|
||||
options: linuxDownloadOptions(ctx),
|
||||
};
|
||||
default:
|
||||
return {platformId: '', platformName: '', icon: <span />, options: []};
|
||||
}
|
||||
}
|
||||
|
||||
function linuxDownloadOptions(ctx: MarketingContext): ReadonlyArray<PlatformOption> {
|
||||
return [
|
||||
{arch: 'x64', format: 'AppImage', url: desktopRedirectUrl(ctx, 'linux', 'x64', 'appimage')},
|
||||
{arch: 'arm64', format: 'AppImage', url: desktopRedirectUrl(ctx, 'linux', 'arm64', 'appimage')},
|
||||
{arch: 'x64', format: 'DEB', url: desktopRedirectUrl(ctx, 'linux', 'x64', 'deb')},
|
||||
{arch: 'arm64', format: 'DEB', url: desktopRedirectUrl(ctx, 'linux', 'arm64', 'deb')},
|
||||
{arch: 'x64', format: 'RPM', url: desktopRedirectUrl(ctx, 'linux', 'x64', 'rpm')},
|
||||
{arch: 'arm64', format: 'RPM', url: desktopRedirectUrl(ctx, 'linux', 'arm64', 'rpm')},
|
||||
{arch: 'x64', format: 'tar.gz', url: desktopRedirectUrl(ctx, 'linux', 'x64', 'tar_gz')},
|
||||
{arch: 'arm64', format: 'tar.gz', url: desktopRedirectUrl(ctx, 'linux', 'arm64', 'tar_gz')},
|
||||
];
|
||||
}
|
||||
|
||||
function getDesktopButtonClasses(style: ButtonStyle): [string, string] {
|
||||
if (style === 'light') {
|
||||
return [
|
||||
`${btnBase} gap-2 ${lightBg} ${lightText} ${lightHover}`,
|
||||
`${chevronBase} ${lightBg} border-l border-gray-200 ${lightText} ${lightHover}`,
|
||||
];
|
||||
}
|
||||
return [
|
||||
`${btnBase} gap-2 ${darkBg} ${darkText} ${darkHover}`,
|
||||
`${chevronBase} ${darkBg} border-l border-white/20 ${darkText} ${darkHover}`,
|
||||
];
|
||||
}
|
||||
|
||||
function formatOverlayLabel(ctx: MarketingContext, platform: MarketingPlatform, arch: string, format: string): string {
|
||||
if (platform === 'macos') {
|
||||
return arch === 'arm64'
|
||||
? `${ctx.i18n.getMessage('platform_support.platforms.macos.apple_silicon', ctx.locale)} (${format})`
|
||||
: `${ctx.i18n.getMessage('platform_support.platforms.macos.intel', ctx.locale)} (${format})`;
|
||||
}
|
||||
return `${format} (${arch})`;
|
||||
}
|
||||
|
||||
function defaultArchitecture(ctx: MarketingContext, platform: MarketingPlatform): string {
|
||||
if (platform === 'macos') {
|
||||
if (ctx.architecture === 'arm64') return 'arm64';
|
||||
if (ctx.architecture === 'unknown') return 'arm64';
|
||||
return 'x64';
|
||||
}
|
||||
if (ctx.architecture === 'arm64') return 'arm64';
|
||||
return 'x64';
|
||||
}
|
||||
|
||||
function channelSegment(ctx: MarketingContext): string {
|
||||
return isCanary(ctx) ? 'canary' : 'stable';
|
||||
}
|
||||
|
||||
function desktopRedirectUrl(ctx: MarketingContext, platform: string, arch: string, format: string): string {
|
||||
return apiUrl(ctx, `/dl/desktop/${channelSegment(ctx)}/${platform}/${arch}/latest/${format}`);
|
||||
}
|
||||
91
packages/marketing/src/components/PlutoniumSection.tsx
Normal file
91
packages/marketing/src/components/PlutoniumSection.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {HashIcon} from '@fluxer/marketing/src/components/icons/HashIcon';
|
||||
import {SparkleIcon} from '@fluxer/marketing/src/components/icons/SparkleIcon';
|
||||
import {VideoCameraIcon} from '@fluxer/marketing/src/components/icons/VideoCameraIcon';
|
||||
import {MarketingButton} from '@fluxer/marketing/src/components/MarketingButton';
|
||||
import {Section} from '@fluxer/marketing/src/components/Section';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
import {getFormattedPrice, PricingTier} from '@fluxer/marketing/src/PricingUtils';
|
||||
import {href} from '@fluxer/marketing/src/UrlUtils';
|
||||
|
||||
interface PlutoniumSectionProps {
|
||||
ctx: MarketingContext;
|
||||
}
|
||||
|
||||
export function PlutoniumSection(props: PlutoniumSectionProps): JSX.Element {
|
||||
const {ctx} = props;
|
||||
const monthlyPrice = getFormattedPrice(PricingTier.Monthly, ctx.countryCode);
|
||||
const yearlyPrice = getFormattedPrice(PricingTier.Yearly, ctx.countryCode);
|
||||
|
||||
return (
|
||||
<Section
|
||||
variant="light"
|
||||
title={ctx.i18n.getMessage('pricing_and_tiers.plutonium.get_more_with_plutonium', ctx.locale)}
|
||||
className="md:py-28"
|
||||
>
|
||||
<div class="mb-6 flex flex-col items-center justify-center gap-3 sm:flex-row">
|
||||
<span class="font-bold text-3xl text-black md:text-4xl">
|
||||
{`${monthlyPrice}${ctx.i18n.getMessage('pricing_and_tiers.billing.per_month', ctx.locale)} ${ctx.i18n.getMessage('general.or', ctx.locale)} ${yearlyPrice}${ctx.i18n.getMessage('pricing_and_tiers.billing.per_year_full', ctx.locale)}`}
|
||||
</span>
|
||||
<span class="inline-flex items-center rounded-xl bg-[#4641D9] px-4 py-2 font-semibold text-sm text-white md:text-base">
|
||||
{ctx.i18n.getMessage('pricing_and_tiers.billing.save_percent', ctx.locale)}
|
||||
</span>
|
||||
</div>
|
||||
<p class="lead mx-auto max-w-2xl text-gray-700">
|
||||
{ctx.i18n.getMessage('pricing_and_tiers.plutonium.higher_limits_and_early_access', ctx.locale)}
|
||||
</p>
|
||||
<div class="mx-auto mb-12 grid max-w-4xl grid-cols-1 gap-8 md:mb-16 md:grid-cols-3 md:gap-12">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-3xl bg-gradient-to-br from-[#4641D9]/10 to-[#4641D9]/5 md:h-20 md:w-20">
|
||||
<HashIcon class="h-8 w-8 text-[#4641D9] md:h-10 md:w-10" />
|
||||
</div>
|
||||
<h3 class="title whitespace-nowrap text-black text-lg md:text-xl">
|
||||
{ctx.i18n.getMessage('app.profiles_identity.custom_identity', ctx.locale)}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-3xl bg-gradient-to-br from-[#4641D9]/10 to-[#4641D9]/5 md:h-20 md:w-20">
|
||||
<VideoCameraIcon class="h-8 w-8 text-[#4641D9] md:h-10 md:w-10" />
|
||||
</div>
|
||||
<h3 class="title whitespace-nowrap text-black text-lg md:text-xl">
|
||||
{ctx.i18n.getMessage('press_branding.assets.premium_quality', ctx.locale)}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-3xl bg-gradient-to-br from-[#4641D9]/10 to-[#4641D9]/5 md:h-20 md:w-20">
|
||||
<SparkleIcon class="h-8 w-8 text-[#4641D9] md:h-10 md:w-10" />
|
||||
</div>
|
||||
<h3 class="title whitespace-nowrap text-black text-lg md:text-xl">
|
||||
{ctx.i18n.getMessage('misc_labels.exclusive_features', ctx.locale)}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<MarketingButton href={href(ctx, '/plutonium')} size="large" class="label md:text-xl">
|
||||
{ctx.i18n.getMessage('partner_program.become_partner.learn_more', ctx.locale)}
|
||||
</MarketingButton>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
271
packages/marketing/src/components/PwaInstallDialog.tsx
Normal file
271
packages/marketing/src/components/PwaInstallDialog.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {DevicesIcon} from '@fluxer/marketing/src/components/icons/DevicesIcon';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
|
||||
export function renderPwaInstallTrigger(ctx: MarketingContext): JSX.Element {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
id="pwa-install-button"
|
||||
class="inline-flex items-center gap-2 rounded-xl bg-[#4641D9] px-5 py-3 font-medium text-sm text-white shadow-md transition-colors hover:bg-[#3832B8]"
|
||||
>
|
||||
<DevicesIcon class="h-5 w-5" />
|
||||
{ctx.i18n.getMessage('platform_support.mobile.install_as_app.title', ctx.locale)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderPwaInstallModal(ctx: MarketingContext): JSX.Element {
|
||||
return (
|
||||
<div id="pwa-modal-backdrop" class="pwa-modal-backdrop">
|
||||
<div class="pwa-modal">
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="flex items-center justify-between p-6 pb-4">
|
||||
<h2 class="font-bold text-gray-900 text-xl">
|
||||
{ctx.i18n.getMessage('platform_support.mobile.install_as_app.install_fluxer_as_app', ctx.locale)}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg p-2 text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||
id="pwa-close"
|
||||
aria-label={ctx.i18n.getMessage('navigation.close', ctx.locale)}
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-6">
|
||||
<div class="flex gap-1 rounded-xl bg-gray-100 p-1" id="pwa-tabs">
|
||||
{renderTabButton(
|
||||
'android',
|
||||
ctx.i18n.getMessage('platform_support.platforms.android.name', ctx.locale),
|
||||
true,
|
||||
)}
|
||||
{renderTabButton(
|
||||
'ios',
|
||||
ctx.i18n.getMessage('platform_support.platforms.ios.ios_ipados', ctx.locale),
|
||||
false,
|
||||
)}
|
||||
{renderTabButton('desktop', ctx.i18n.getMessage('platform_support.desktop.label', ctx.locale), false)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pwa-panels-container flex-1 overflow-y-auto p-6 pt-4">
|
||||
<div id="pwa-panel-android" class="pwa-panel">
|
||||
{renderAndroidSteps(ctx)}
|
||||
</div>
|
||||
<div id="pwa-panel-ios" class="pwa-panel hidden">
|
||||
{renderIosSteps(ctx)}
|
||||
</div>
|
||||
<div id="pwa-panel-desktop" class="pwa-panel hidden">
|
||||
{renderDesktopSteps(ctx)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-gray-100 border-t px-6 py-4 text-center">
|
||||
<p class="text-gray-400 text-xs">
|
||||
{ctx.i18n.getMessage('download.screenshots_courtesy_of', ctx.locale)}
|
||||
<a
|
||||
href="https://installpwa.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-blue-500 underline hover:text-blue-600"
|
||||
>
|
||||
installpwa.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderTabButton(id: string, label: string, active: boolean): JSX.Element {
|
||||
const className = active
|
||||
? 'pwa-tab flex-1 px-4 py-2 text-sm font-medium rounded-lg transition-colors bg-white text-gray-900 shadow-sm'
|
||||
: 'pwa-tab flex-1 px-4 py-2 text-sm font-medium rounded-lg transition-colors text-gray-600 hover:text-gray-900';
|
||||
|
||||
return (
|
||||
<button type="button" data-tab={id} class={className}>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function renderAndroidSteps(ctx: MarketingContext): JSX.Element {
|
||||
return (
|
||||
<div class="flex flex-col gap-6 md:flex-row">
|
||||
<div class="flex justify-center md:w-1/3">{renderImage(ctx, 'android', '240', '320', '480')}</div>
|
||||
<div class="md:w-2/3">
|
||||
<ol class="space-y-4">
|
||||
{renderStep(
|
||||
'1',
|
||||
<span>
|
||||
<a
|
||||
href="https://web.fluxer.app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-gray-900 underline hover:text-gray-700"
|
||||
>
|
||||
{ctx.i18n.getMessage('app.open.open_web_app', ctx.locale)}
|
||||
</a>
|
||||
{ctx.i18n.getMessage('platform_support.mobile.install_as_app.guides.in_chrome', ctx.locale)}
|
||||
</span>,
|
||||
)}
|
||||
{renderStep(
|
||||
'2',
|
||||
ctx.i18n.getMessage('platform_support.mobile.install_as_app.guides.steps.press_more_menu', ctx.locale),
|
||||
)}
|
||||
{renderStep(
|
||||
'3',
|
||||
ctx.i18n.getMessage('platform_support.mobile.install_as_app.guides.steps.press_install_app', ctx.locale),
|
||||
)}
|
||||
{renderStep('4', ctx.i18n.getMessage('platform_support.mobile.install_as_app.done_mobile', ctx.locale))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderIosSteps(ctx: MarketingContext): JSX.Element {
|
||||
return (
|
||||
<div class="flex flex-col gap-6 md:flex-row">
|
||||
<div class="flex justify-center md:w-1/2">{renderImage(ctx, 'ios', '320', '480', '640')}</div>
|
||||
<div class="md:w-1/2">
|
||||
<ol class="space-y-4">
|
||||
{renderStep(
|
||||
'1',
|
||||
<span>
|
||||
<a
|
||||
href="https://web.fluxer.app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-gray-900 underline hover:text-gray-700"
|
||||
>
|
||||
{ctx.i18n.getMessage('app.open.open_web_app', ctx.locale)}
|
||||
</a>
|
||||
{ctx.i18n.getMessage('platform_support.mobile.install_as_app.guides.in_safari', ctx.locale)}
|
||||
</span>,
|
||||
)}
|
||||
{renderStep(
|
||||
'2',
|
||||
ctx.i18n.getMessage('platform_support.mobile.install_as_app.guides.steps.press_share_button', ctx.locale),
|
||||
)}
|
||||
{renderStep(
|
||||
'3',
|
||||
ctx.i18n.getMessage(
|
||||
'platform_support.mobile.install_as_app.guides.steps.press_add_to_home_screen',
|
||||
ctx.locale,
|
||||
),
|
||||
)}
|
||||
{renderStep(
|
||||
'4',
|
||||
ctx.i18n.getMessage(
|
||||
'platform_support.mobile.install_as_app.guides.steps.press_add_upper_right',
|
||||
ctx.locale,
|
||||
),
|
||||
)}
|
||||
{renderStep('5', ctx.i18n.getMessage('platform_support.mobile.install_as_app.done_mobile', ctx.locale))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDesktopSteps(ctx: MarketingContext): JSX.Element {
|
||||
return (
|
||||
<div class="flex flex-col gap-6 md:flex-row">
|
||||
<div class="flex justify-center md:w-1/2">{renderImage(ctx, 'desktop', '320', '480', '640')}</div>
|
||||
<div class="md:w-1/2">
|
||||
<ol class="space-y-4">
|
||||
{renderStep(
|
||||
'1',
|
||||
<span>
|
||||
<a
|
||||
href="https://web.fluxer.app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-gray-900 underline hover:text-gray-700"
|
||||
>
|
||||
{ctx.i18n.getMessage('app.open.open_web_app', ctx.locale)}
|
||||
</a>
|
||||
{ctx.i18n.getMessage(
|
||||
'platform_support.mobile.install_as_app.guides.in_chrome_or_another_browser',
|
||||
ctx.locale,
|
||||
)}
|
||||
</span>,
|
||||
)}
|
||||
{renderStep(
|
||||
'2',
|
||||
ctx.i18n.getMessage(
|
||||
'platform_support.mobile.install_as_app.guides.steps.press_install_button_address_bar',
|
||||
ctx.locale,
|
||||
),
|
||||
)}
|
||||
{renderStep(
|
||||
'3',
|
||||
ctx.i18n.getMessage(
|
||||
'platform_support.mobile.install_as_app.guides.steps.press_install_in_popup',
|
||||
ctx.locale,
|
||||
),
|
||||
)}
|
||||
{renderStep('4', ctx.i18n.getMessage('platform_support.mobile.install_as_app.done_desktop', ctx.locale))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderStep(number: string, content: JSX.Element | string): JSX.Element {
|
||||
return (
|
||||
<li class="flex items-start gap-4">
|
||||
<div class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-gray-100 font-semibold text-gray-600 text-sm">
|
||||
{number}
|
||||
</div>
|
||||
<div class="pt-1.5 text-left text-gray-700">{content}</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function renderImage(ctx: MarketingContext, name: string, small: string, medium: string, large: string): JSX.Element {
|
||||
const basePath = `${ctx.staticCdnEndpoint}/marketing/pwa-install/${name}`;
|
||||
const srcsetAvif = `${basePath}-${small}w.avif 1x, ${basePath}-${medium}w.avif 1.5x, ${basePath}-${large}w.avif 2x`;
|
||||
const srcsetWebp = `${basePath}-${small}w.webp 1x, ${basePath}-${medium}w.webp 1.5x, ${basePath}-${large}w.webp 2x`;
|
||||
const srcsetPng = `${basePath}-${small}w.png 1x, ${basePath}-${medium}w.png 1.5x, ${basePath}-${large}w.png 2x`;
|
||||
|
||||
return (
|
||||
<picture>
|
||||
<source type="image/avif" srcset={srcsetAvif} />
|
||||
<source type="image/webp" srcset={srcsetWebp} />
|
||||
<img
|
||||
src={`${basePath}-${medium}w.png`}
|
||||
srcset={srcsetPng}
|
||||
alt={ctx.i18n.getMessage('platform_support.mobile.install_as_app.guides.pwa_installation_guide', ctx.locale, {
|
||||
name,
|
||||
})}
|
||||
class="h-auto max-w-full rounded-lg border border-gray-200 shadow-lg"
|
||||
/>
|
||||
</picture>
|
||||
);
|
||||
}
|
||||
64
packages/marketing/src/components/Section.tsx
Normal file
64
packages/marketing/src/components/Section.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {GRADIENTS} from '@fluxer/ui/src/styles/Gradients';
|
||||
|
||||
const GRADIENT_MAP = {
|
||||
cta: GRADIENTS.cta,
|
||||
dark: GRADIENTS.purple,
|
||||
light: GRADIENTS.light,
|
||||
white: 'bg-white',
|
||||
} as const;
|
||||
|
||||
export type SectionVariant = keyof typeof GRADIENT_MAP;
|
||||
|
||||
interface SectionProps {
|
||||
variant?: SectionVariant;
|
||||
title?: string;
|
||||
description?: string;
|
||||
children: JSX.Element | Array<JSX.Element> | string;
|
||||
className?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export function Section(props: SectionProps): JSX.Element {
|
||||
const {variant = 'dark', title, description, children, className = '', id = undefined} = props;
|
||||
|
||||
const gradientClass = GRADIENT_MAP[variant];
|
||||
const isDark = variant === 'dark' || variant === 'cta';
|
||||
const textColorClass = isDark ? 'text-white' : 'text-black';
|
||||
const descriptionColorClass = isDark ? 'text-white/90' : 'text-gray-700';
|
||||
|
||||
return (
|
||||
<section id={id} class={`${gradientClass} px-6 py-16 sm:px-8 md:px-12 md:py-24 lg:px-16 xl:px-20 ${className}`}>
|
||||
<div class="mx-auto max-w-7xl">
|
||||
{title && description && (
|
||||
<div class="mb-12 text-center md:mb-16">
|
||||
<h2 class={`display mb-6 text-4xl md:mb-8 md:text-5xl lg:text-6xl ${textColorClass}`}>{title}</h2>
|
||||
<p class={`lead mx-auto max-w-3xl ${descriptionColorClass}`}>{description}</p>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
98
packages/marketing/src/components/SupportCard.tsx
Normal file
98
packages/marketing/src/components/SupportCard.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {Icon} from '@fluxer/marketing/src/components/icons/IconRegistry';
|
||||
import {MarketingButton} from '@fluxer/marketing/src/components/MarketingButton';
|
||||
import {MarketingCard} from '@fluxer/marketing/src/components/MarketingCard';
|
||||
import type {MarketingContext} from '@fluxer/marketing/src/MarketingContext';
|
||||
|
||||
type SupportIcon =
|
||||
| 'rocket_launch'
|
||||
| 'fluxer_partner'
|
||||
| 'chat_centered_text'
|
||||
| 'bluesky'
|
||||
| 'bug'
|
||||
| 'code'
|
||||
| 'translate'
|
||||
| 'shield_check';
|
||||
|
||||
interface SupportCardProps {
|
||||
ctx: MarketingContext;
|
||||
icon: SupportIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
buttonText: string;
|
||||
buttonHref: string;
|
||||
theme?: 'light' | 'dark';
|
||||
}
|
||||
|
||||
export function SupportCard(props: SupportCardProps): JSX.Element {
|
||||
const linkProps = getLinkProps(props.buttonHref);
|
||||
const theme = props.theme ?? 'light';
|
||||
|
||||
return (
|
||||
<MarketingCard
|
||||
theme={theme}
|
||||
padding="md"
|
||||
style="box-shadow: 0 0 0 1px rgba(0,0,0,0.03), 0 4px 12px rgba(0,0,0,0.08), 0 2px 6px rgba(0,0,0,0.05);"
|
||||
>
|
||||
<div class="mb-8 text-center">
|
||||
<div
|
||||
class={`mb-6 inline-flex h-20 w-20 items-center justify-center rounded-3xl md:h-24 md:w-24 ${
|
||||
theme === 'dark' ? 'bg-white/20' : 'bg-[#4641D9]'
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
name={props.icon}
|
||||
class={`md:h-12 md:w-12 ${theme === 'dark' ? 'h-10 w-10 text-white' : 'h-10 w-10 text-white'}`}
|
||||
/>
|
||||
</div>
|
||||
<h3 class={`title mb-4 text-xl md:text-2xl ${theme === 'dark' ? 'text-white' : 'text-gray-900'}`}>
|
||||
{props.title}
|
||||
</h3>
|
||||
<p class={`body-lg leading-relaxed ${theme === 'dark' ? 'text-white/80' : 'text-gray-700'}`}>
|
||||
{props.description}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-auto flex flex-col items-center">
|
||||
<MarketingButton
|
||||
href={props.buttonHref}
|
||||
size="medium"
|
||||
target={linkProps.target}
|
||||
rel={linkProps.rel}
|
||||
class={`label w-full text-center shadow-md md:text-lg ${
|
||||
theme === 'dark' ? 'bg-white text-[#4641D9] hover:bg-gray-100' : ''
|
||||
}`}
|
||||
>
|
||||
{props.buttonText}
|
||||
</MarketingButton>
|
||||
</div>
|
||||
</MarketingCard>
|
||||
);
|
||||
}
|
||||
|
||||
function getLinkProps(href: string): {target?: '_blank' | '_self' | '_parent' | '_top'; rel?: string} {
|
||||
if (href.startsWith('https://') || href.startsWith('http://') || href.startsWith('mailto:')) {
|
||||
return {target: '_blank', rel: 'noopener noreferrer'};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
33
packages/marketing/src/components/icons/AndroidIcon.tsx
Normal file
33
packages/marketing/src/components/icons/AndroidIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface AndroidIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function AndroidIcon(props: AndroidIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path 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" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/AppleIcon.tsx
Normal file
33
packages/marketing/src/components/icons/AppleIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface AppleIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function AppleIcon(props: AppleIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M128.23,30A40,40,0,0,1,167,0h1a8,8,0,0,1,0,16h-1a24,24,0,0,0-23.24,18,8,8,0,1,1-15.5-4ZM223.3,169.59a8.07,8.07,0,0,0-2.8-3.4C203.53,154.53,200,134.64,200,120c0-17.67,13.47-33.06,21.5-40.67a8,8,0,0,0,0-11.62C208.82,55.74,187.82,48,168,48a72.23,72.23,0,0,0-40,12.13,71.56,71.56,0,0,0-90.71,9.09A74.63,74.63,0,0,0,16,123.4a127,127,0,0,0,40.14,89.73A39.8,39.8,0,0,0,83.59,224h87.68a39.84,39.84,0,0,0,29.12-12.57,125,125,0,0,0,17.82-24.6C225.23,174,224.33,172,223.3,169.59Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
43
packages/marketing/src/components/icons/ArrowRightIcon.tsx
Normal file
43
packages/marketing/src/components/icons/ArrowRightIcon.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface ArrowRightIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function ArrowRightIcon(props: ArrowRightIconProps) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="24"
|
||||
class={props.class}
|
||||
>
|
||||
<line x1="40" y1="128" x2="216" y2="128" />
|
||||
<polyline points="144,56 216,128 144,200" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/ArrowUpIcon.tsx
Normal file
33
packages/marketing/src/components/icons/ArrowUpIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface ArrowUpIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function ArrowUpIcon(props: ArrowUpIconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M208.49,120.49a12,12,0,0,1-17,0L140,69V216a12,12,0,0,1-24,0V69L64.49,120.49a12,12,0,0,1-17-17l72-72a12,12,0,0,1,17,0l72,72A12,12,0,0,1,208.49,120.49Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
36
packages/marketing/src/components/icons/BlueskyIcon.tsx
Normal file
36
packages/marketing/src/components/icons/BlueskyIcon.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface BlueskyIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function BlueskyIcon(props: BlueskyIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 530" fill="currentColor" class={props.class}>
|
||||
<path
|
||||
d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/BugIcon.tsx
Normal file
33
packages/marketing/src/components/icons/BugIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface BugIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function BugIcon(props: BugIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M168,92a12,12,0,1,1-12-12A12,12,0,0,1,168,92ZM100,80a12,12,0,1,0,12,12A12,12,0,0,0,100,80Zm116,64A87.76,87.76,0,0,1,213,167l22.24,9.72A8,8,0,0,1,232,192a7.89,7.89,0,0,1-3.2-.67L207.38,182a88,88,0,0,1-158.76,0L27.2,191.33A7.89,7.89,0,0,1,24,192a8,8,0,0,1-3.2-15.33L43,167A87.76,87.76,0,0,1,40,144v-8H16a8,8,0,0,1,0-16H40v-8a87.76,87.76,0,0,1,3-23L20.8,79.33a8,8,0,1,1,6.4-14.66L48.62,74a88,88,0,0,1,158.76,0l21.42-9.36a8,8,0,0,1,6.4,14.66L213,89.05a87.76,87.76,0,0,1,3,23v8h24a8,8,0,0,1,0,16H216Zm-80,0a8,8,0,0,0-16,0v64a8,8,0,0,0,16,0Zm64-32a72,72,0,0,0-144,0v8H200Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface CalendarCheckIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function CalendarCheckIcon(props: CalendarCheckIconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM169.66,133.66l-48,48a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L116,164.69l42.34-42.35a8,8,0,0,1,11.32,11.32ZM48,80V48H72v8a8,8,0,0,0,16,0V48h80v8a8,8,0,0,0,16,0V48h24V80Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
40
packages/marketing/src/components/icons/CaretDownIcon.tsx
Normal file
40
packages/marketing/src/components/icons/CaretDownIcon.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface CaretDownIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function CaretDownIcon(props: CaretDownIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="none" class={props.class}>
|
||||
<polyline
|
||||
points="208 96 128 176 48 96"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="24"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface ChatCenteredTextIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function ChatCenteredTextIcon(props: ChatCenteredTextIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M216,40H40A16,16,0,0,0,24,56V184a16,16,0,0,0,16,16h60.43l13.68,23.94a16,16,0,0,0,27.78,0L155.57,200H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40ZM160,144H96a8,8,0,0,1,0-16h64a8,8,0,0,1,0,16Zm0-32H96a8,8,0,0,1,0-16h64a8,8,0,0,1,0,16Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/ChatsCircleIcon.tsx
Normal file
33
packages/marketing/src/components/icons/ChatsCircleIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface ChatsCircleIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function ChatsCircleIcon(props: ChatsCircleIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M232.07,186.76a80,80,0,0,0-62.5-114.17A80,80,0,1,0,23.93,138.76l-7.27,24.71a16,16,0,0,0,19.87,19.87l24.71-7.27a80.39,80.39,0,0,0,25.18,7.35,80,80,0,0,0,108.34,40.65l24.71,7.27a16,16,0,0,0,19.87-19.86Zm-16.25,1.47L224,216l-27.76-8.17a8,8,0,0,0-6,.63,64.05,64.05,0,0,1-85.87-24.88A79.93,79.93,0,0,0,174.7,89.71a64,64,0,0,1,41.75,92.48A8,8,0,0,0,215.82,188.23Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/ChatsIcon.tsx
Normal file
33
packages/marketing/src/components/icons/ChatsIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface ChatsIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function ChatsIcon(props: ChatsIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M232,96a16,16,0,0,0-16-16H184V48a16,16,0,0,0-16-16H40A16,16,0,0,0,24,48V176a8,8,0,0,0,13,6.22L72,154V184a16,16,0,0,0,16,16h93.59L219,230.22a8,8,0,0,0,5,1.78,8,8,0,0,0,8-8Zm-42.55,89.78a8,8,0,0,0-5-1.78H88V152h80a16,16,0,0,0,16-16V96h32V207.25Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
42
packages/marketing/src/components/icons/CheckIcon.tsx
Normal file
42
packages/marketing/src/components/icons/CheckIcon.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface CheckIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function CheckIcon(props: CheckIconProps) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="24"
|
||||
class={props.class}
|
||||
>
|
||||
<polyline points="40,144 96,200 224,72" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
60
packages/marketing/src/components/icons/CodeIcon.tsx
Normal file
60
packages/marketing/src/components/icons/CodeIcon.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface CodeIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function CodeIcon(props: CodeIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class={props.class}>
|
||||
<rect width="256" height="256" fill="none" />
|
||||
<polyline
|
||||
points="64 88 16 128 64 168"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="24"
|
||||
/>
|
||||
<polyline
|
||||
points="192 88 240 128 192 168"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="24"
|
||||
/>
|
||||
<line
|
||||
x1="160"
|
||||
y1="40"
|
||||
x2="96"
|
||||
y2="216"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="24"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/CoinsIcon.tsx
Normal file
33
packages/marketing/src/components/icons/CoinsIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface CoinsIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function CoinsIcon(props: CoinsIconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M184,89.57V84c0-25.08-37.83-44-88-44S8,58.92,8,84v40c0,20.89,26.25,37.49,64,42.46V172c0,25.08,37.83,44,88,44s88-18.92,88-44V132C248,111.3,222.58,94.68,184,89.57ZM56,146.87C36.41,141.4,24,132.39,24,124V109.93c8.16,5.78,19.09,10.44,32,13.57Zm80-23.37c12.91-3.13,23.84-7.79,32-13.57V124c0,8.39-12.41,17.4-32,22.87Zm-16,71.37C100.41,189.4,88,180.39,88,172v-4.17c2.63.1,5.29.17,8,.17,3.88,0,7.67-.13,11.39-.35A121.92,121.92,0,0,0,120,171.41Zm0-44.62A163,163,0,0,1,96,152a163,163,0,0,1-24-1.75V126.46A183.74,183.74,0,0,0,96,128a183.74,183.74,0,0,0,24-1.54Zm64,48a165.45,165.45,0,0,1-48,0V174.4a179.48,179.48,0,0,0,24,1.6,183.74,183.74,0,0,0,24-1.54ZM232,172c0,8.39-12.41,17.4-32,22.87V171.5c12.91-3.13,23.84-7.79,32-13.57Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
43
packages/marketing/src/components/icons/CrossIcon.tsx
Normal file
43
packages/marketing/src/components/icons/CrossIcon.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface CrossIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function CrossIcon(props: CrossIconProps) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="24"
|
||||
class={props.class}
|
||||
>
|
||||
<line x1="200" y1="56" x2="56" y2="200" />
|
||||
<line x1="200" y1="200" x2="56" y2="56" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/CrownIcon.tsx
Normal file
33
packages/marketing/src/components/icons/CrownIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface CrownIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function CrownIcon(props: CrownIconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M248,80a28,28,0,1,0-51.12,15.77l-26.79,33L146,73.4a28,28,0,1,0-36.06,0L85.91,128.74l-26.79-33a28,28,0,1,0-26.6,12L47,194.63A16,16,0,0,0,62.78,208H193.22A16,16,0,0,0,209,194.63l14.47-86.85A28,28,0,0,0,248,80ZM128,40a12,12,0,1,1-12,12A12,12,0,0,1,128,40ZM24,80A12,12,0,1,1,36,92,12,12,0,0,1,24,80ZM220,92a12,12,0,1,1,12-12A12,12,0,0,1,220,92Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/DevicesIcon.tsx
Normal file
33
packages/marketing/src/components/icons/DevicesIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface DevicesIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function DevicesIcon(props: DevicesIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M224,72H208V64a24,24,0,0,0-24-24H40A24,24,0,0,0,16,64v96a24,24,0,0,0,24,24H152v8a24,24,0,0,0,24,24h48a24,24,0,0,0,24-24V96A24,24,0,0,0,224,72Zm8,120a8,8,0,0,1-8,8H176a8,8,0,0,1-8-8V96a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8Zm-96,16a8,8,0,0,1-8,8H88a8,8,0,0,1,0-16h40A8,8,0,0,1,136,208Zm80-96a8,8,0,0,1-8,8H192a8,8,0,0,1,0-16h16A8,8,0,0,1,216,112Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface DownloadArrowIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function DownloadArrowIcon(props: DownloadArrowIconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M224,144v64a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V144a8,8,0,0,1,16,0v56H208V144a8,8,0,0,1,16,0Zm-101.66,5.66a8,8,0,0,0,11.32,0l40-40a8,8,0,0,0-11.32-11.32L136,124.69V40a8,8,0,0,0-16,0v84.69L93.66,98.34a8,8,0,0,0-11.32,11.32Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/DownloadIcon.tsx
Normal file
33
packages/marketing/src/components/icons/DownloadIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface DownloadIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function DownloadIcon(props: DownloadIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M224,144v64a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V144a8,8,0,0,1,16,0v56H208V144a8,8,0,0,1,16,0Zm-101.66,5.66a8,8,0,0,0,11.32,0l40-40a8,8,0,0,0-11.32-11.32L136,124.69V40a8,8,0,0,0-16,0v84.69L93.66,98.34a8,8,0,0,0-11.32,11.32Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface FluxerLogoWordmarkIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function FluxerLogoWordmarkIcon(props: FluxerLogoWordmarkIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 1959 512" class={props.class}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M585 431.4V93.48h82.944V431.4H585Zm41.472-120.32v-67.584h172.032v67.584H626.472Zm0-148.48V93.48h189.44v69.12h-189.44ZM843.951 431.4V73h82.944v358.4h-82.944ZM1047.92 438.568c-29.35 0-51.2-10.24-65.536-30.72-13.995-20.48-20.992-51.883-20.992-94.208V161.064h82.948v151.552c0 19.797 2.73 33.621 8.19 41.472 5.8 7.851 13.99 11.776 24.57 11.776 7.51 0 14.34-1.877 20.48-5.632 6.49-4.096 12.12-10.069 16.9-17.92 4.78-8.192 8.36-18.603 10.75-31.232 2.73-12.629 4.1-27.819 4.1-45.568V161.064h82.94V431.4h-70.65V325.928h-3.59c-2.05 26.283-6.65 47.787-13.82 64.512-7.17 16.384-17.07 28.501-29.7 36.352-12.63 7.851-28.16 11.776-46.59 11.776ZM1232.57 431.4l84.99-135.68-83.97-134.656h96.26l39.42 87.552h2.56l38.4-87.552h95.23l-81.4 134.656 82.43 135.68h-97.79l-37.38-86.016h-2.05L1327.8 431.4h-95.23Z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M1630.96 438.568c-25.6 0-47.1-3.584-64.51-10.752-17.06-7.509-30.89-17.579-41.47-30.208-10.58-12.971-18.26-27.648-23.04-44.032-4.44-16.384-6.66-33.621-6.66-51.712 0-19.456 2.39-38.059 7.17-55.808 5.12-17.749 12.8-33.451 23.04-47.104 10.58-13.995 24.07-24.917 40.45-32.768 16.73-8.192 36.69-12.288 59.9-12.288s43.01 4.096 59.4 12.288c16.72 7.851 30.03 18.944 39.93 33.28 9.9 14.336 16.22 30.891 18.95 49.664 3.07 18.773 2.73 39.083-1.03 60.928L1547 313.128v-45.056l132.6-2.56-10.75 26.112c2.05-15.701 1.71-28.843-1.02-39.424-2.39-10.923-7-19.115-13.83-24.576-6.82-5.803-16.21-8.704-28.16-8.704-12.63 0-22.69 3.243-30.2 9.728-7.51 6.485-12.8 15.701-15.88 27.648-3.07 11.605-4.6 25.429-4.6 41.472 0 27.648 4.6 47.787 13.82 60.416 9.22 12.629 23.38 18.944 42.5 18.944 8.19 0 15.01-1.024 20.48-3.072 5.46-2.048 9.89-4.949 13.31-8.704 3.41-4.096 5.8-8.875 7.17-14.336 1.36-5.803 1.87-12.288 1.53-19.456l75.78 4.096c1.02 11.264-.17 22.869-3.59 34.816-3.07 11.947-9.04 23.04-17.92 33.28-8.87 10.24-21.33 18.603-37.37 25.088-15.7 6.485-35.67 9.728-59.91 9.728ZM1778.45 431.4V161.064h71.68v107.52h4.1c2.05-28.672 5.97-51.2 11.77-67.584 6.15-16.725 13.66-28.501 22.53-35.328 9.22-7.168 19.46-10.752 30.72-10.752 6.15 0 12.46.853 18.95 2.56 6.82 1.707 13.48 4.437 19.96 8.192l-4.09 92.16c-7.51-4.437-14.85-7.68-22.02-9.728-7.17-2.389-13.99-3.584-20.48-3.584-10.92 0-20.14 3.072-27.65 9.216-7.51 6.144-13.31 15.189-17.4 27.136-3.76 11.947-5.64 26.453-5.64 43.52V431.4h-82.43ZM256 0c141.385 0 256 114.615 256 256S397.385 512 256 512 0 397.385 0 256 114.615 0 256 0Zm-68.47 266.057c-15.543 0-30.324 3.505-44.343 10.514-13.866 7.01-25.143 18.21-33.828 33.6-5.616 10.129-9.318 22.403-11.1061 36.822-1.6543 13.341 9.5761 24.207 23.0181 24.207 13.778 0 24.065-11.574 27.402-24.941 1.891-7.579 4.939-13.589 9.142-18.03 8.076-8.534 18.286-12.8 30.629-12.8 8.229 0 15.772 2.057 22.629 6.171 6.857 3.962 15.771 10.743 26.742 20.343 16.762 14.781 31.544 25.524 44.344 32.228 12.8 6.553 26.971 9.829 42.514 9.829 15.543 0 30.324-3.505 44.343-10.514 14.019-7.01 25.371-18.21 34.057-33.6 5.738-10.168 9.448-22.497 11.129-36.987 1.543-13.302-9.704-24.042-23.096-24.042-13.863.001-24.202 11.704-27.888 25.07-1.797 6.515-4.512 12.025-8.145 16.53-7.619 9.448-18.057 14.172-31.314 14.172-8.229 0-15.696-1.982-22.4-5.943-6.553-4.115-15.543-10.972-26.972-20.572-16.914-14.171-31.772-24.685-44.572-31.543-12.647-7.009-26.742-10.514-42.285-10.514Zm0-138.057c-15.543 0-30.324 3.505-44.343 10.514-13.866 7.01-25.143 18.21-33.828 33.6-5.616 10.129-9.318 22.403-11.1061 36.821-1.6544 13.341 9.5761 24.207 23.0181 24.208 13.778 0 24.065-11.574 27.402-24.941 1.891-7.579 4.939-13.589 9.142-18.031 8.076-8.533 18.286-12.8 30.629-12.8 8.229 0 15.772 2.058 22.629 6.172 6.857 3.962 15.771 10.743 26.742 20.343 16.762 14.781 31.544 25.524 44.344 32.228 12.8 6.553 26.971 9.829 42.514 9.829 15.543 0 30.324-3.505 44.343-10.514 14.019-7.01 25.371-18.21 34.057-33.6 5.738-10.168 9.448-22.497 11.129-36.987 1.543-13.303-9.704-24.042-23.096-24.042-13.863 0-24.202 11.704-27.888 25.07-1.797 6.515-4.512 12.025-8.145 16.53-7.619 9.448-18.057 14.171-31.314 14.171-8.229 0-15.696-1.981-22.4-5.942-6.553-4.115-15.543-10.972-26.972-20.572-16.914-14.171-31.772-24.686-44.572-31.543C217.168 131.505 203.073 128 187.53 128Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface FluxerPartnerIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function FluxerPartnerIcon(props: FluxerPartnerIconProps): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 512 512"
|
||||
fill="none"
|
||||
class={props.class}
|
||||
>
|
||||
<path
|
||||
d="M372.514 144.155C335.074 142.235 301.794 158.235 280.034 184.635C280.034 184.635 255.554 216.315 255.554 216.155C255.554 216.155 224.674 256.635 224.834 256.635L198.274 291.515L196.194 294.235C180.994 314.555 154.113 325.915 125.793 317.595C104.353 311.195 87.5529 293.275 82.7529 271.515C73.3129 230.075 104.833 193.115 144.673 193.115C164.034 193.115 178.434 201.275 188.354 209.915C198.914 219.195 215.074 217.755 223.714 206.555C231.234 196.795 230.274 182.715 221.314 174.395C170.754 127.995 75.0729 138.555 44.5129 203.035C4.83292 286.715 65.313 369.275 144.673 369.275C179.554 369.275 210.754 353.755 231.394 328.795L238.274 319.835C238.274 319.835 255.714 296.955 255.714 297.115C255.714 297.115 286.594 256.635 286.434 256.635L313.474 221.275C314.114 220.475 314.754 219.675 315.394 218.875C329.314 199.995 353.314 189.275 379.714 194.395C403.074 199.035 422.434 217.275 428.194 240.475C438.754 282.555 407.074 320.475 366.594 320.475C347.554 320.475 333.154 312.315 323.234 303.515C312.674 294.235 296.354 295.835 287.874 307.035C280.034 317.275 281.474 331.515 291.074 340.155C305.954 353.435 331.394 369.595 366.594 369.595C433.794 369.595 487.394 310.555 478.274 241.595C471.234 188.475 425.794 146.875 372.514 144.155Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface FluxerPremiumIconProps {
|
||||
class?: string;
|
||||
fillColor?: string;
|
||||
}
|
||||
|
||||
export function FluxerPremiumIcon(props: FluxerPremiumIconProps): JSX.Element {
|
||||
const fill = props.fillColor ?? 'white';
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="336"
|
||||
height="274"
|
||||
viewBox="0 0 336 274"
|
||||
fill="none"
|
||||
class={props.class}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M59.2774 198C59.2774 208.471 50.7886 216.96 40.3174 216.96C29.8461 216.96 21.3574 208.471 21.3574 198C21.3574 187.529 29.8461 179.04 40.3174 179.04C50.7886 179.04 59.2774 187.529 59.2774 198ZM314.398 198C314.398 208.471 305.909 216.96 295.438 216.96C284.966 216.96 276.478 208.471 276.478 198C276.478 187.529 284.966 179.04 295.438 179.04C305.909 179.04 314.398 187.529 314.398 198Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M98.9976 75.24L137.158 126.48L58.0781 135.6L83.5181 76.92C77.3981 74.52 73.0781 68.52 73.0781 61.56C73.0781 52.44 80.5181 45 89.6381 45C98.7576 45 106.198 52.44 106.198 61.56C106.198 67.32 103.318 72.24 98.9976 75.24ZM252.12 76.92L277.56 135.6L198.48 126.48L236.64 75.24C232.32 72.24 229.44 67.2 229.44 61.56C229.44 52.44 236.88 45 246 45C255.12 45 262.56 52.44 262.56 61.56C262.56 68.52 258.24 74.52 252.12 76.92Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M335.76 73.08C335.76 63.96 328.32 56.52 319.2 56.52C310.08 56.52 302.64 63.96 302.64 73.08C302.64 76.44 303.72 79.68 305.4 82.2C302.76 82.2 300.12 82.8 297.6 84.48L250.44 114.96C244.2 118.92 236.04 117.48 231.6 111.48L179.16 40.32C178.8 39.72 178.32 39.24 177.84 38.76C184.2 35.2801 188.52 28.44 188.52 20.64C188.52 9.24 179.28 0 167.88 0C156.48 0 147.24 9.24 147.24 20.64C147.24 28.44 151.56 35.2801 157.92 38.76C157.44 39.24 156.96 39.72 156.6 40.32L104.16 111.48C99.84 117.48 91.56 118.92 85.32 114.96L38.16 84.48C35.64 82.8 33 82.2 30.36 82.2C32.16 79.56 33.12 76.44 33.12 73.08C33.12 63.96 25.68 56.52 16.56 56.52C7.44 56.52 0 63.96 0 73.08C0 82.2 7.44 89.64 16.56 89.64C17.16 89.64 17.76 89.64 18.24 89.52C16.68 92.28 16.08 95.52 16.8 99L48 254.64C50.28 265.8 60.12 273.84 71.52 273.84H264.24C275.64 273.84 285.48 265.8 287.76 254.64L318.96 99C319.68 95.52 318.96 92.16 317.52 89.52C318.12 89.52 318.72 89.64 319.2 89.64C328.32 89.64 335.76 82.2 335.76 73.08Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M167.88 228C184.448 228 197.88 214.568 197.88 198C197.88 181.432 184.448 168 167.88 168C151.311 168 137.88 181.432 137.88 198C137.88 214.568 151.311 228 167.88 228Z"
|
||||
fill={fill}
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M108 198C108 207.941 99.9408 216 90 216C80.0588 216 72 207.941 72 198C72 188.059 80.0588 180 90 180C99.9408 180 108 188.059 108 198ZM264 198C264 207.941 255.941 216 246 216C236.059 216 228 207.941 228 198C228 188.059 236.059 180 246 180C255.941 180 264 188.059 264 198Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
55
packages/marketing/src/components/icons/FluxerStaffIcon.tsx
Normal file
55
packages/marketing/src/components/icons/FluxerStaffIcon.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface FluxerStaffIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function FluxerStaffIcon(props: FluxerStaffIconProps) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 512 512"
|
||||
fill="none"
|
||||
class={props.class}
|
||||
>
|
||||
<path
|
||||
d="M320.878 273.038L469.518 124.399C472.718 121.199 472.718 116.079 469.518 112.719L446.318 89.5191C443.118 86.3191 437.998 86.3191 434.638 89.5191L285.998 238.158C280.878 243.278 273.838 246.158 266.478 246.158C258.958 246.158 251.598 249.198 246.318 254.478L86.3188 414.638C81.5187 419.438 81.5187 427.278 86.3188 432.078L126.959 472.718C131.759 477.518 139.599 477.518 144.399 472.718L304.558 312.558C309.838 307.278 312.878 299.918 312.878 292.398C312.878 285.198 315.758 278.158 320.878 273.038Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M262.483 243.76L113.842 95.1207C110.642 91.9207 105.522 91.9207 102.162 95.1207L78.9625 118.481C75.7625 121.681 75.7625 126.801 78.9625 130.161L227.603 278.8C232.723 283.92 235.603 290.96 235.603 298.32C235.603 305.84 238.643 313.2 243.923 318.48L398.003 472.56C402.803 477.36 410.643 477.36 415.443 472.56L456.083 431.92C460.883 427.12 460.883 419.28 456.083 414.48L302.003 260.4C296.723 255.12 289.363 252.08 281.843 252.08C274.643 251.92 267.603 248.88 262.483 243.76Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M116.72 266.32L250.16 132.88C254.96 128.08 254.96 120.24 250.32 115.44L186.48 51.6C181.68 46.8 173.84 46.8 169.04 51.6L35.6 185.04C30.8 189.84 30.8 197.68 35.6 202.48L99.28 266.32C104.08 271.12 111.92 271.12 116.72 266.32Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M303.92 132.88L428.4 257.36C438.16 267.12 454 267.12 463.76 257.2C467.28 253.68 469.68 249.04 470.64 243.92L478.64 198.32C483.6 170 474.48 140.88 454 120.4L385.2 51.6C380.4 46.8 372.56 46.8 367.76 51.6L303.92 115.44C299.12 120.24 299.12 128.08 303.92 132.88Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/GearIcon.tsx
Normal file
33
packages/marketing/src/components/icons/GearIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface GearIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function GearIcon(props: GearIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M216,130.16q.06-2.16,0-4.32l14.92-18.64a8,8,0,0,0,1.48-7.06,107.6,107.6,0,0,0-10.88-26.25,8,8,0,0,0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186,40.54a8,8,0,0,0-3.94-6,107.29,107.29,0,0,0-26.25-10.86,8,8,0,0,0-7.06,1.48L130.16,40Q128,40,125.84,40L107.2,25.11a8,8,0,0,0-7.06-1.48A107.6,107.6,0,0,0,73.89,34.51a8,8,0,0,0-3.93,6L67.32,64.27q-1.56,1.49-3,3L40.54,70a8,8,0,0,0-6,3.94,107.71,107.71,0,0,0-10.87,26.25,8,8,0,0,0,1.49,7.06L40,125.84Q40,128,40,130.16L25.11,148.8a8,8,0,0,0-1.48,7.06,107.6,107.6,0,0,0,10.88,26.25,8,8,0,0,0,6,3.93l23.72,2.64q1.49,1.56,3,3L70,215.46a8,8,0,0,0,3.94,6,107.71,107.71,0,0,0,26.25,10.87,8,8,0,0,0,7.06-1.49L125.84,216q2.16.06,4.32,0l18.64,14.92a8,8,0,0,0,7.06,1.48,107.21,107.21,0,0,0,26.25-10.88,8,8,0,0,0,3.93-6l2.64-23.72q1.56-1.48,3-3L215.46,186a8,8,0,0,0,6-3.94,107.71,107.71,0,0,0,10.87-26.25,8,8,0,0,0-1.49-7.06ZM128,168a40,40,0,1,1,40-40A40,40,0,0,1,128,168Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/GifIcon.tsx
Normal file
33
packages/marketing/src/components/icons/GifIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface GifIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function GifIcon(props: GifIconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40ZM112,144a32,32,0,0,1-64,0V112a32,32,0,0,1,55.85-21.33,8,8,0,1,1-11.92,10.66A16,16,0,0,0,64,112v32a16,16,0,0,0,32,0v-8H88a8,8,0,0,1,0-16h16a8,8,0,0,1,8,8Zm32,24a8,8,0,0,1-16,0V88a8,8,0,0,1,16,0Zm60-72H176v24h20a8,8,0,0,1,0,16H176v32a8,8,0,0,1-16,0V88a8,8,0,0,1,8-8h36a8,8,0,0,1,0,16Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
37
packages/marketing/src/components/icons/GithubIcon.tsx
Normal file
37
packages/marketing/src/components/icons/GithubIcon.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface GithubIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function GithubIcon(props: GithubIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<rect width="256" height="256" fill="none" />
|
||||
<path
|
||||
d="M216,104v8a56.06,56.06,0,0,1-48.44,55.47A39.8,39.8,0,0,1,176,192v40a8,8,0,0,1-8,8H104a8,8,0,0,1-8-8V216H72a40,40,0,0,1-40-40A24,24,0,0,0,8,152a8,8,0,0,1,0-16,40,40,0,0,1,40,40,24,24,0,0,0,24,24H96v-8a39.8,39.8,0,0,1,8.44-24.53A56.06,56.06,0,0,1,56,112v-8a58.14,58.14,0,0,1,7.69-28.32A59.78,59.78,0,0,1,69.07,28a8,8,0,0,1,6.93-4,59.75,59.75,0,0,1,48,24h24a59.75,59.75,0,0,1,48-24,8,8,0,0,1,6.93,4,59.74,59.74,0,0,1,5.37,47.68A58,58,0,0,1,216,104Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/GlobeIcon.tsx
Normal file
33
packages/marketing/src/components/icons/GlobeIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface GlobeIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function GlobeIcon(props: GlobeIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M128,24h0A104,104,0,1,0,232,128,104.12,104.12,0,0,0,128,24Zm78.36,64H170.71a135.28,135.28,0,0,0-22.3-45.6A88.29,88.29,0,0,1,206.37,88ZM216,128a87.61,87.61,0,0,1-3.33,24H174.16a157.44,157.44,0,0,0,0-48h38.51A87.61,87.61,0,0,1,216,128ZM128,43a115.27,115.27,0,0,1,26,45H102A115.11,115.11,0,0,1,128,43ZM102,168H154a115.11,115.11,0,0,1-26,45A115.27,115.27,0,0,1,102,168Zm-3.9-16a140.84,140.84,0,0,1,0-48h59.88a140.84,140.84,0,0,1,0,48Zm50.35,61.6a135.28,135.28,0,0,0,22.3-45.6h35.66A88.29,88.29,0,0,1,148.41,213.6Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
45
packages/marketing/src/components/icons/HashIcon.tsx
Normal file
45
packages/marketing/src/components/icons/HashIcon.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface HashIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function HashIcon(props: HashIconProps): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="24"
|
||||
class={props.class}
|
||||
>
|
||||
<line x1="48" y1="96" x2="224" y2="96" />
|
||||
<line x1="176" y1="40" x2="144" y2="216" />
|
||||
<line x1="112" y1="40" x2="80" y2="216" />
|
||||
<line x1="32" y1="160" x2="208" y2="160" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/HeartIcon.tsx
Normal file
33
packages/marketing/src/components/icons/HeartIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface HeartIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function HeartIcon(props: HeartIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M240,102c0,70-103.79,126.66-108.21,129a8,8,0,0,1-7.58,0C119.79,228.66,16,172,16,102A62.07,62.07,0,0,1,78,40c20.65,0,38.73,8.88,50,23.89C139.27,48.88,157.35,40,178,40A62.07,62.07,0,0,1,240,102Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
113
packages/marketing/src/components/icons/IconRegistry.tsx
Normal file
113
packages/marketing/src/components/icons/IconRegistry.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
import {ArrowUpIcon} from '@fluxer/marketing/src/components/icons/ArrowUpIcon';
|
||||
import {BlueskyIcon} from '@fluxer/marketing/src/components/icons/BlueskyIcon';
|
||||
import {BugIcon} from '@fluxer/marketing/src/components/icons/BugIcon';
|
||||
import {CalendarCheckIcon} from '@fluxer/marketing/src/components/icons/CalendarCheckIcon';
|
||||
import {ChatCenteredTextIcon} from '@fluxer/marketing/src/components/icons/ChatCenteredTextIcon';
|
||||
import {ChatsCircleIcon} from '@fluxer/marketing/src/components/icons/ChatsCircleIcon';
|
||||
import {ChatsIcon} from '@fluxer/marketing/src/components/icons/ChatsIcon';
|
||||
import {CodeIcon} from '@fluxer/marketing/src/components/icons/CodeIcon';
|
||||
import {CoinsIcon} from '@fluxer/marketing/src/components/icons/CoinsIcon';
|
||||
import {DevicesIcon} from '@fluxer/marketing/src/components/icons/DevicesIcon';
|
||||
import {FluxerPartnerIcon} from '@fluxer/marketing/src/components/icons/FluxerPartnerIcon';
|
||||
import {FluxerPremiumIcon} from '@fluxer/marketing/src/components/icons/FluxerPremiumIcon';
|
||||
import {FluxerStaffIcon} from '@fluxer/marketing/src/components/icons/FluxerStaffIcon';
|
||||
import {GearIcon} from '@fluxer/marketing/src/components/icons/GearIcon';
|
||||
import {GifIcon} from '@fluxer/marketing/src/components/icons/GifIcon';
|
||||
import {GlobeIcon} from '@fluxer/marketing/src/components/icons/GlobeIcon';
|
||||
import {HashIcon} from '@fluxer/marketing/src/components/icons/HashIcon';
|
||||
import {HeartIcon} from '@fluxer/marketing/src/components/icons/HeartIcon';
|
||||
import {InfinityIcon} from '@fluxer/marketing/src/components/icons/InfinityIcon';
|
||||
import {LinkIcon} from '@fluxer/marketing/src/components/icons/LinkIcon';
|
||||
import {MagnifyingGlassIcon} from '@fluxer/marketing/src/components/icons/MagnifyingGlassIcon';
|
||||
import {MedalIcon} from '@fluxer/marketing/src/components/icons/MedalIcon';
|
||||
import {MicrophoneIcon} from '@fluxer/marketing/src/components/icons/MicrophoneIcon';
|
||||
import {NewspaperIcon} from '@fluxer/marketing/src/components/icons/NewspaperIcon';
|
||||
import {PaletteIcon} from '@fluxer/marketing/src/components/icons/PaletteIcon';
|
||||
import {RocketIcon} from '@fluxer/marketing/src/components/icons/RocketIcon';
|
||||
import {RocketLaunchIcon} from '@fluxer/marketing/src/components/icons/RocketLaunchIcon';
|
||||
import {SealCheckIcon} from '@fluxer/marketing/src/components/icons/SealCheckIcon';
|
||||
import {ShieldCheckIcon} from '@fluxer/marketing/src/components/icons/ShieldCheckIcon';
|
||||
import {SmileyIcon} from '@fluxer/marketing/src/components/icons/SmileyIcon';
|
||||
import {SparkleIcon} from '@fluxer/marketing/src/components/icons/SparkleIcon';
|
||||
import {SpeakerHighIcon} from '@fluxer/marketing/src/components/icons/SpeakerHighIcon';
|
||||
import {TranslateIcon} from '@fluxer/marketing/src/components/icons/TranslateIcon';
|
||||
import {TshirtIcon} from '@fluxer/marketing/src/components/icons/TshirtIcon';
|
||||
import {UserCircleIcon} from '@fluxer/marketing/src/components/icons/UserCircleIcon';
|
||||
import {UserPlusIcon} from '@fluxer/marketing/src/components/icons/UserPlusIcon';
|
||||
import {VideoCameraIcon} from '@fluxer/marketing/src/components/icons/VideoCameraIcon';
|
||||
import {VideoIcon} from '@fluxer/marketing/src/components/icons/VideoIcon';
|
||||
|
||||
const ICON_MAP = {
|
||||
chats: ChatsIcon,
|
||||
microphone: MicrophoneIcon,
|
||||
palette: PaletteIcon,
|
||||
magnifying_glass: MagnifyingGlassIcon,
|
||||
devices: DevicesIcon,
|
||||
gear: GearIcon,
|
||||
heart: HeartIcon,
|
||||
globe: GlobeIcon,
|
||||
server: GlobeIcon,
|
||||
newspaper: NewspaperIcon,
|
||||
|
||||
rocket_launch: RocketLaunchIcon,
|
||||
fluxer_partner: FluxerPartnerIcon,
|
||||
chat_centered_text: ChatCenteredTextIcon,
|
||||
bluesky: BlueskyIcon,
|
||||
bug: BugIcon,
|
||||
code: CodeIcon,
|
||||
translate: TranslateIcon,
|
||||
shield_check: ShieldCheckIcon,
|
||||
|
||||
fluxer_premium: FluxerPremiumIcon,
|
||||
fluxer_staff: FluxerStaffIcon,
|
||||
seal_check: SealCheckIcon,
|
||||
link: LinkIcon,
|
||||
arrow_up: ArrowUpIcon,
|
||||
rocket: RocketIcon,
|
||||
coins: CoinsIcon,
|
||||
tshirt: TshirtIcon,
|
||||
gif: GifIcon,
|
||||
|
||||
video: VideoIcon,
|
||||
video_camera: VideoCameraIcon,
|
||||
user_circle: UserCircleIcon,
|
||||
user_plus: UserPlusIcon,
|
||||
speaker_high: SpeakerHighIcon,
|
||||
calendar_check: CalendarCheckIcon,
|
||||
hash: HashIcon,
|
||||
smiley: SmileyIcon,
|
||||
sparkle: SparkleIcon,
|
||||
|
||||
infinity: InfinityIcon,
|
||||
medal: MedalIcon,
|
||||
chats_circle: ChatsCircleIcon,
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof ICON_MAP;
|
||||
|
||||
export function Icon({name, class: className}: {name: IconName; class?: string}): JSX.Element {
|
||||
const Component = ICON_MAP[name];
|
||||
return <Component class={className} />;
|
||||
}
|
||||
42
packages/marketing/src/components/icons/InfinityIcon.tsx
Normal file
42
packages/marketing/src/components/icons/InfinityIcon.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface InfinityIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function InfinityIcon(props: InfinityIconProps): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="24"
|
||||
class={props.class}
|
||||
>
|
||||
<path d="M101.28,158.17l-3.34,3.77a48,48,0,1,1,0-67.88l60.12,67.88a48,48,0,1,0,0-67.88l-3.34,3.77" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
49
packages/marketing/src/components/icons/LinkIcon.tsx
Normal file
49
packages/marketing/src/components/icons/LinkIcon.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface LinkIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function LinkIcon(props: LinkIconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class={props.class}>
|
||||
<rect width="256" height="256" fill="none" />
|
||||
<path
|
||||
d="M141.38,64.68l11-11a46.62,46.62,0,0,1,65.94,0h0a46.62,46.62,0,0,1,0,65.94L193.94,144,183.6,154.34a46.63,46.63,0,0,1-66-.05h0A46.48,46.48,0,0,1,104,120.06"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="16"
|
||||
/>
|
||||
<path
|
||||
d="M114.62,191.32l-11,11a46.63,46.63,0,0,1-66-.05h0a46.63,46.63,0,0,1,.06-65.89L72.4,101.66a46.62,46.62,0,0,1,65.94,0h0A46.45,46.45,0,0,1,152,135.94"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="16"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/LinuxIcon.tsx
Normal file
33
packages/marketing/src/components/icons/LinuxIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface LinuxIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function LinuxIcon(props: LinuxIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M161.22,209.74a4,4,0,0,1-3.31,6.26H98.1a4,4,0,0,1-3.31-6.26,40,40,0,0,1,66.43,0Zm68.93,3.37a8.29,8.29,0,0,1-6.43,2.89H184.56a4,4,0,0,1-3.76-2.65,56,56,0,0,0-105.59,0A4,4,0,0,1,71.45,216H32.23a8.2,8.2,0,0,1-6.42-2.93A8,8,0,0,1,25.75,203c.06-.07,7.64-9.78,15.12-28.72C47.77,156.8,56,127.64,56,88a72,72,0,0,1,144,0c0,39.64,8.23,68.8,15.13,86.28,7.48,18.94,15.06,28.65,15.13,28.74A8,8,0,0,1,230.15,213.11ZM88,100a12,12,0,1,0,12-12A12,12,0,0,0,88,100Zm79.16,32.42a8,8,0,0,0-10.73-3.58L128,143.06,99.58,128.84a8,8,0,0,0-7.15,14.32l32,16a8,8,0,0,0,7.15,0l32-16A8,8,0,0,0,167.16,132.42ZM168,100a12,12,0,1,0-12,12A12,12,0,0,0,168,100Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface MagnifyingGlassIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function MagnifyingGlassIcon(props: MagnifyingGlassIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M168,112a56,56,0,1,1-56-56A56,56,0,0,1,168,112Zm61.66,117.66a8,8,0,0,1-11.32,0l-50.06-50.07a88,88,0,1,1,11.32-11.31l50.06,50.06A8,8,0,0,1,229.66,229.66ZM112,184a72,72,0,1,0-72-72A72.08,72.08,0,0,0,112,184Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/MedalIcon.tsx
Normal file
33
packages/marketing/src/components/icons/MedalIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface MedalIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function MedalIcon(props: MedalIconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M216,96A88,88,0,1,0,72,163.83V240a8,8,0,0,0,11.58,7.16L128,225l44.43,22.21A8.07,8.07,0,0,0,176,248a8,8,0,0,0,8-8V163.83A87.85,87.85,0,0,0,216,96ZM56,96a72,72,0,1,1,72,72A72.08,72.08,0,0,1,56,96Zm16,0a56,56,0,1,1,56,56A56.06,56.06,0,0,1,72,96Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
44
packages/marketing/src/components/icons/MenuIcon.tsx
Normal file
44
packages/marketing/src/components/icons/MenuIcon.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface MenuIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function MenuIcon(props: MenuIconProps): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="24"
|
||||
class={props.class}
|
||||
>
|
||||
<line x1="40" y1="64" x2="216" y2="64" />
|
||||
<line x1="40" y1="128" x2="216" y2="128" />
|
||||
<line x1="40" y1="192" x2="216" y2="192" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/MicrophoneIcon.tsx
Normal file
33
packages/marketing/src/components/icons/MicrophoneIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface MicrophoneIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function MicrophoneIcon(props: MicrophoneIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M80,128V64a48,48,0,0,1,96,0v64a48,48,0,0,1-96,0Zm128,0a8,8,0,0,0-16,0,64,64,0,0,1-128,0,8,8,0,0,0-16,0,80.11,80.11,0,0,0,72,79.6V240a8,8,0,0,0,16,0V207.6A80.11,80.11,0,0,0,208,128Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/NewspaperIcon.tsx
Normal file
33
packages/marketing/src/components/icons/NewspaperIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface NewspaperIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function NewspaperIcon(props: NewspaperIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M216,48H56A16,16,0,0,0,40,64V184a8,8,0,0,1-16,0V88A8,8,0,0,0,8,88v96.11A24,24,0,0,0,32,208H208a24,24,0,0,0,24-24V64A16,16,0,0,0,216,48ZM176,152H96a8,8,0,0,1,0-16h80a8,8,0,0,1,0,16Zm0-32H96a8,8,0,0,1,0-16h80a8,8,0,0,1,0,16Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/PaletteIcon.tsx
Normal file
33
packages/marketing/src/components/icons/PaletteIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface PaletteIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function PaletteIcon(props: PaletteIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M200.77,53.89A103.27,103.27,0,0,0,128,24h-1.07A104,104,0,0,0,24,128c0,43,26.58,79.06,69.36,94.17A32,32,0,0,0,136,192a16,16,0,0,1,16-16h46.21a31.81,31.81,0,0,0,31.2-24.88,104.43,104.43,0,0,0,2.59-24A103.28,103.28,0,0,0,200.77,53.89ZM84,168a12,12,0,1,1,12-12A12,12,0,0,1,84,168Zm0-56a12,12,0,1,1,12-12A12,12,0,0,1,84,112Zm44-24a12,12,0,1,1,12-12A12,12,0,0,1,128,88Zm44,24a12,12,0,1,1,12-12A12,12,0,0,1,172,112Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/RocketIcon.tsx
Normal file
33
packages/marketing/src/components/icons/RocketIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface RocketIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function RocketIcon(props: RocketIconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M152,224a8,8,0,0,1-8,8H112a8,8,0,0,1,0-16h32A8,8,0,0,1,152,224Zm71.62-68.17-12.36,55.63a16,16,0,0,1-25.51,9.11L158.51,200h-61L70.25,220.57a16,16,0,0,1-25.51-9.11L32.38,155.83a16.09,16.09,0,0,1,3.32-13.71l28.56-34.26a123.07,123.07,0,0,1,8.57-36.67c12.9-32.34,36-52.63,45.37-59.85a16,16,0,0,1,19.6,0c9.34,7.22,32.47,27.51,45.37,59.85a123.07,123.07,0,0,1,8.57,36.67l28.56,34.26A16.09,16.09,0,0,1,223.62,155.83Zm-139.23,34Q68.28,160.5,64.83,132.16L48,152.36,60.36,208l.18-.13ZM140,100a12,12,0,1,0-12,12A12,12,0,0,0,140,100Zm68,52.36-16.83-20.2q-3.42,28.28-19.56,57.69l23.85,18,.18.13Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/RocketLaunchIcon.tsx
Normal file
33
packages/marketing/src/components/icons/RocketLaunchIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface RocketLaunchIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function RocketLaunchIcon(props: RocketLaunchIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M101.85,191.14C97.34,201,82.29,224,40,224a8,8,0,0,1-8-8c0-42.29,23-57.34,32.86-61.85a8,8,0,0,1,6.64,14.56c-6.43,2.93-20.62,12.36-23.12,38.91,26.55-2.5,36-16.69,38.91-23.12a8,8,0,1,1,14.56,6.64Zm122-144a16,16,0,0,0-15-15c-12.58-.75-44.73.4-71.4,27.07h0L88,108.7A8,8,0,0,1,76.67,97.39l26.56-26.57A4,4,0,0,0,100.41,64H74.35A15.9,15.9,0,0,0,63,68.68L28.7,103a16,16,0,0,0,9.07,27.16l38.47,5.37,44.21,44.21,5.37,38.49a15.94,15.94,0,0,0,10.78,12.92,16.11,16.11,0,0,0,5.1.83A15.91,15.91,0,0,0,153,227.3L187.32,193A16,16,0,0,0,192,181.65V155.59a4,4,0,0,0-6.83-2.82l-26.57,26.56a8,8,0,0,1-11.71-.42,8.2,8.2,0,0,1,.6-11.1l49.27-49.27h0C223.45,91.86,224.6,59.71,223.85,47.12Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
45
packages/marketing/src/components/icons/RssIcon.tsx
Normal file
45
packages/marketing/src/components/icons/RssIcon.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface RssIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function RssIcon(props: RssIconProps): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="24"
|
||||
class={props.class}
|
||||
>
|
||||
<path d="M56,136a64,64,0,0,1,64,64" />
|
||||
<path d="M56,88A112,112,0,0,1,168,200" />
|
||||
<path d="M56,40A160,160,0,0,1,216,200" />
|
||||
<circle cx="60" cy="196" r="16" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/SealCheckIcon.tsx
Normal file
33
packages/marketing/src/components/icons/SealCheckIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface SealCheckIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function SealCheckIcon(props: SealCheckIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M225.86,102.82c-3.77-3.94-7.67-8-9.14-11.57-1.36-3.27-1.44-8.69-1.52-13.94-.15-9.76-.31-20.82-8-28.51s-18.75-7.85-28.51-8c-5.25-.08-10.67-.16-13.94-1.52-3.56-1.47-7.63-5.37-11.57-9.14C146.28,23.51,138.44,16,128,16s-18.27,7.51-25.18,14.14c-3.94,3.77-8,7.67-11.57,9.14C88,40.64,82.56,40.72,77.31,40.8c-9.76.15-20.82.31-28.51,8S41,67.55,40.8,77.31c-.08,5.25-.16,10.67-1.52,13.94-1.47,3.56-5.37,7.63-9.14,11.57C23.51,109.72,16,117.56,16,128s7.51,18.27,14.14,25.18c3.77,3.94,7.67,8,9.14,11.57,1.36,3.27,1.44,8.69,1.52,13.94.15,9.76.31,20.82,8,28.51s18.75,7.85,28.51,8c5.25.08,10.67.16,13.94,1.52,3.56,1.47,7.63,5.37,11.57,9.14C109.72,232.49,117.56,240,128,240s18.27-7.51,25.18-14.14c3.94-3.77,8-7.67,11.57-9.14,3.27-1.36,8.69-1.44,13.94-1.52,9.76-.15,20.82-.31,28.51-8s7.85-18.75,8-28.51c.08-5.25.16-10.67,1.52-13.94,1.47-3.56,5.37-7.63,9.14-11.57C232.49,146.28,240,138.44,240,128S232.49,109.73,225.86,102.82Zm-52.2,6.84-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/ShieldCheckIcon.tsx
Normal file
33
packages/marketing/src/components/icons/ShieldCheckIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface ShieldCheckIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function ShieldCheckIcon(props: ShieldCheckIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M208,40H48A16,16,0,0,0,32,56v56c0,52.72,25.52,84.67,46.93,102.19,23.06,18.86,46,25.26,47,25.53a8,8,0,0,0,4.2,0c1-.27,23.91-6.67,47-25.53C198.48,196.67,224,164.72,224,112V56A16,16,0,0,0,208,40Zm-34.32,69.66-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/SmileyIcon.tsx
Normal file
33
packages/marketing/src/components/icons/SmileyIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface SmileyIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function SmileyIcon(props: SmileyIconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24ZM92,96a12,12,0,1,1-12,12A12,12,0,0,1,92,96Zm82.92,60c-10.29,17.79-27.39,28-46.92,28s-36.63-10.2-46.92-28a8,8,0,1,1,13.84-8c7.47,12.91,19.21,20,33.08,20s25.61-7.1,33.08-20a8,8,0,1,1,13.84,8ZM164,120a12,12,0,1,1,12-12A12,12,0,0,1,164,120Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/SparkleIcon.tsx
Normal file
33
packages/marketing/src/components/icons/SparkleIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface SparkleIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function SparkleIcon(props: SparkleIconProps): JSX.Element {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M208,144a15.78,15.78,0,0,1-10.42,14.94L146,178l-19,51.62a15.92,15.92,0,0,1-29.88,0L78,178l-51.62-19a15.92,15.92,0,0,1,0-29.88L78,110l19-51.62a15.92,15.92,0,0,1,29.88,0L146,110l51.62,19A15.78,15.78,0,0,1,208,144ZM152,48h16V64a8,8,0,0,0,16,0V48h16a8,8,0,0,0,0-16H184V16a8,8,0,0,0-16,0V32H152a8,8,0,0,0,0,16Zm88,32h-8V72a8,8,0,0,0-16,0v8h-8a8,8,0,0,0,0,16h8v8a8,8,0,0,0,16,0V96h8a8,8,0,0,0,0-16Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/SpeakerHighIcon.tsx
Normal file
33
packages/marketing/src/components/icons/SpeakerHighIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface SpeakerHighIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function SpeakerHighIcon(props: SpeakerHighIconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M160,32.25V223.69a8.29,8.29,0,0,1-3.91,7.18,8,8,0,0,1-9-.56l-65.57-51A4,4,0,0,1,80,176.16V79.84a4,4,0,0,1,1.55-3.15l65.57-51a8,8,0,0,1,10,.16A8.27,8.27,0,0,1,160,32.25ZM60,80H32A16,16,0,0,0,16,96v64a16,16,0,0,0,16,16H60a4,4,0,0,0,4-4V84A4,4,0,0,0,60,80Zm126.77,20.84a8,8,0,0,0-.72,11.3,24,24,0,0,1,0,31.72,8,8,0,1,0,12,10.58,40,40,0,0,0,0-52.88A8,8,0,0,0,186.74,100.84Zm40.89-26.17a8,8,0,1,0-11.92,10.66,64,64,0,0,1,0,85.34,8,8,0,1,0,11.92,10.66,80,80,0,0,0,0-106.66Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
118
packages/marketing/src/components/icons/SwishIcon.tsx
Normal file
118
packages/marketing/src/components/icons/SwishIcon.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface SwishIconProps {
|
||||
size?: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function SwishIcon(props: SwishIconProps) {
|
||||
const size = props.size ?? 40;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 420 420"
|
||||
fill-rule="evenodd"
|
||||
width={size}
|
||||
height={size}
|
||||
style={{width: `${size}px`, height: `${size}px`, flexShrink: 0}}
|
||||
class={props.class}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="swish-grad-1"
|
||||
x1="-746"
|
||||
y1="822.6"
|
||||
x2="-746.2"
|
||||
y2="823.1"
|
||||
gradientTransform="translate(224261.6 305063) scale(300.3 -370.5)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stop-color="#ef2131" />
|
||||
<stop offset="1" stop-color="#fecf2c" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="swish-grad-2"
|
||||
x1="-745.4"
|
||||
y1="823"
|
||||
x2="-745.9"
|
||||
y2="822.1"
|
||||
gradientTransform="translate(204470.4 247194.2) scale(273.8 -300.2)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stop-color="#fbc52c" />
|
||||
<stop offset=".3" stop-color="#f87130" />
|
||||
<stop offset=".6" stop-color="#ef52e2" />
|
||||
<stop offset="1" stop-color="#661eec" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="swish-grad-3"
|
||||
x1="-746"
|
||||
y1="823"
|
||||
x2="-745.8"
|
||||
y2="822.5"
|
||||
gradientTransform="translate(224142 305014) scale(300.3 -370.5)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stop-color="#78f6d8" />
|
||||
<stop offset=".3" stop-color="#77d1f6" />
|
||||
<stop offset=".6" stop-color="#70a4f3" />
|
||||
<stop offset="1" stop-color="#661eec" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="swish-grad-4"
|
||||
x1="-746.1"
|
||||
y1="822.3"
|
||||
x2="-745.6"
|
||||
y2="823.2"
|
||||
gradientTransform="translate(204377.3 247074.5) scale(273.8 -300.2)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stop-color="#536eed" />
|
||||
<stop offset=".2" stop-color="#54c3ec" />
|
||||
<stop offset=".6" stop-color="#64d769" />
|
||||
<stop offset="1" stop-color="#fecf2c" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g>
|
||||
<path
|
||||
fill="url(#swish-grad-1)"
|
||||
d="M119.3,399.2c84.3,40.3,188.3,20.4,251.2-54.5,74.5-88.8,62.9-221.1-25.8-295.5l-59,70.3c69.3,58.2,78.4,161.5,20.2,230.9-46.4,55.3-122.8,73.7-186.5,48.9"
|
||||
/>
|
||||
<path
|
||||
fill="url(#swish-grad-2)"
|
||||
d="M119.3,399.2c84.3,40.3,188.3,20.4,251.2-54.5,7.7-9.2,14.5-18.8,20.3-28.8,9.9-61.7-11.9-126.9-63.2-169.9-13-10.9-27.2-19.8-41.9-26.5,69.3,58.2,78.4,161.5,20.2,230.9-46.4,55.3-122.8,73.7-186.5,48.9"
|
||||
/>
|
||||
<path
|
||||
fill="url(#swish-grad-3)"
|
||||
d="M300.3,20.4C216-19.9,111.9,0,49.1,74.9c-74.5,88.8-62.9,221.1,25.8,295.5l59-70.3c-69.3-58.2-78.4-161.5-20.2-230.9C160.2,14,236.6-4.5,300.3,20.4"
|
||||
/>
|
||||
<path
|
||||
fill="url(#swish-grad-4)"
|
||||
d="M300.3,20.4C216-19.9,111.9,0,49.1,74.9c-7.7,9.2-14.5,18.8-20.3,28.8-9.9,61.7,11.9,126.9,63.2,169.9,13,10.9,27.2,19.8,41.9,26.5-69.3-58.2-78.4-161.5-20.2-230.9C160.2,14,236.6-4.5,300.3,20.4"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
47
packages/marketing/src/components/icons/TranslateIcon.tsx
Normal file
47
packages/marketing/src/components/icons/TranslateIcon.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface TranslateIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function TranslateIcon(props: TranslateIconProps): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="24"
|
||||
class={props.class}
|
||||
>
|
||||
<polyline points="240,216 184,104 128,216" />
|
||||
<line x1="144" y1="184" x2="224" y2="184" />
|
||||
<line x1="96" y1="32" x2="96" y2="56" />
|
||||
<line x1="32" y1="56" x2="160" y2="56" />
|
||||
<path d="M128,56a96,96,0,0,1-96,96" />
|
||||
<path d="M72.7,96A96,96,0,0,0,160,152" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
33
packages/marketing/src/components/icons/TshirtIcon.tsx
Normal file
33
packages/marketing/src/components/icons/TshirtIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/** @jsxRuntime automatic */
|
||||
/** @jsxImportSource hono/jsx */
|
||||
|
||||
interface TshirtIconProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function TshirtIcon(props: TshirtIconProps) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor" class={props.class}>
|
||||
<path d="M247.59,61.22,195.83,33A8,8,0,0,0,192,32H160a8,8,0,0,0-8,8,24,24,0,0,1-48,0,8,8,0,0,0-8-8H64a8,8,0,0,0-3.84,1L8.41,61.22A15.76,15.76,0,0,0,1.82,82.48l19.27,36.81A16.37,16.37,0,0,0,35.67,128H56v80a16,16,0,0,0,16,16H184a16,16,0,0,0,16-16V128h20.34a16.37,16.37,0,0,0,14.58-8.71l19.27-36.81A15.76,15.76,0,0,0,247.59,61.22ZM35.67,112a.62.62,0,0,1-.41-.13L16.09,75.26,56,53.48V112Zm185.07-.14a.55.55,0,0,1-.41.14H200V53.48l39.92,21.78Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user