feat(site): rewrite the landing page around verified facts and accessible controls
- Replace claims that contradicted the app: the default hotkey is Right Alt (hold to talk), local features are free with no daily cap, there is no 14-day trial, Team/SSO/SCIM/ZDR are not offered, and the repository is not public. Remove invented metrics, status badges, the competitor table, the ad-mediation changelog, the hash calculator and the duplicate demo. - Seven sections: hero with one labelled example, features, how it works, privacy, pricing (Free / Pro 2,900 / Pro+ 8,900 KRW a month), download, FAQ. Footer links the privacy policy, terms and account deletion pages. - All copy moves into i18n and every one of the 10 locales is translated. Pricing and cloud quotas come from site/src/pricing.ts. - Accessibility: skip link, labelled sections, aria-expanded with Esc and focus return for the menu, language list and FAQ, live status for the example, reduced-motion support, 44px targets, AA contrast on the primary button. Korean keep-all line breaking is scoped to :lang(ko) because Tailwind's break-keep blocked wrapping in Japanese and Chinese. - JS 111 -> 98 KB gzip, CSS 7.8 -> 5.2 KB gzip.
This commit is contained in:
parent
ad6bb70c20
commit
e689683b72
44 changed files with 2198 additions and 3782 deletions
|
|
@ -1,110 +1,28 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
DESKTOP_VERSION,
|
||||
DESKTOP_WINDOWS_INSTALLER_FILENAME,
|
||||
DESKTOP_WINDOWS_INSTALLER_URL,
|
||||
} from '../release'
|
||||
|
||||
export type ClientOSType = 'android' | 'ios' | 'windows' | 'macos' | 'linux'
|
||||
export type ClientOSType = 'windows' | 'macos' | 'linux' | 'android' | 'ios'
|
||||
|
||||
export interface ClientOSInfo {
|
||||
os: ClientOSType
|
||||
osName: string
|
||||
isMobile: boolean
|
||||
downloadUrl: string
|
||||
downloadFilename: string
|
||||
badgeText: string
|
||||
buttonLabelKo: string
|
||||
buttonLabelEn: string
|
||||
subtextKo: string
|
||||
subtextEn: string
|
||||
interface NavigatorWithUAData extends Navigator {
|
||||
userAgentData?: { platform?: string }
|
||||
}
|
||||
|
||||
export const OS_CONFIGS: Record<ClientOSType, ClientOSInfo> = {
|
||||
android: {
|
||||
os: 'android',
|
||||
osName: 'Android',
|
||||
isMobile: true,
|
||||
downloadUrl: '#download',
|
||||
downloadFilename: '',
|
||||
badgeText: 'ANDROID RELEASE PENDING',
|
||||
buttonLabelKo: 'Android 공식 배포 준비 중',
|
||||
buttonLabelEn: 'Android release unavailable',
|
||||
subtextKo: '서명된 배포 증거가 확인될 때까지 다운로드를 제공하지 않습니다.',
|
||||
subtextEn: 'Download stays unavailable until signed release evidence is verified.',
|
||||
},
|
||||
ios: {
|
||||
os: 'ios',
|
||||
osName: 'iOS',
|
||||
isMobile: true,
|
||||
downloadUrl: '#download',
|
||||
downloadFilename: '',
|
||||
badgeText: 'iOS DISTRIBUTION UNAVAILABLE',
|
||||
buttonLabelKo: 'iOS 배포 미지원',
|
||||
buttonLabelEn: 'iOS distribution unavailable',
|
||||
subtextKo: '현재 검증된 App Store 또는 TestFlight 배포가 없습니다.',
|
||||
subtextEn: 'No verified App Store or TestFlight distribution is available.',
|
||||
},
|
||||
windows: {
|
||||
os: 'windows',
|
||||
osName: 'Windows',
|
||||
isMobile: false,
|
||||
downloadUrl: DESKTOP_WINDOWS_INSTALLER_URL,
|
||||
downloadFilename: DESKTOP_WINDOWS_INSTALLER_FILENAME,
|
||||
badgeText: `DESKTOP ${DESKTOP_VERSION} OFFICIAL RELEASE`,
|
||||
buttonLabelKo: `Windows용 다운로드 (x64) - v${DESKTOP_VERSION}`,
|
||||
buttonLabelEn: `Download for Windows (x64) - v${DESKTOP_VERSION}`,
|
||||
subtextKo: `서명된 공식 릴리스 v${DESKTOP_VERSION}이며, 설치 후 자동 업데이트 피드로 갱신됩니다.`,
|
||||
subtextEn: `Signed official release v${DESKTOP_VERSION}, updated through the auto-update feed after install.`,
|
||||
},
|
||||
macos: {
|
||||
os: 'macos',
|
||||
osName: 'macOS',
|
||||
isMobile: false,
|
||||
downloadUrl: '#download',
|
||||
downloadFilename: '',
|
||||
badgeText: 'MACOS APPLE SILICON PREPARATION',
|
||||
buttonLabelKo: 'macOS 릴리스 준비 중',
|
||||
buttonLabelEn: 'macOS release in preparation',
|
||||
subtextKo: 'Apple Silicon용 빌드는 서명과 설치 검증을 마친 뒤 제공합니다.',
|
||||
subtextEn: 'The Apple Silicon build will be published after signing and installation verification.',
|
||||
},
|
||||
linux: {
|
||||
os: 'linux',
|
||||
osName: 'Linux',
|
||||
isMobile: false,
|
||||
downloadUrl: '#download',
|
||||
downloadFilename: '',
|
||||
badgeText: 'LINUX PACKAGE UNAVAILABLE',
|
||||
buttonLabelKo: 'Linux 배포 미지원',
|
||||
buttonLabelEn: 'Linux package unavailable',
|
||||
subtextKo: '현재 검증된 AppImage 또는 DEB 패키지가 없습니다.',
|
||||
subtextEn: 'No verified AppImage or DEB package is available.',
|
||||
},
|
||||
function detectOS(): ClientOSType {
|
||||
const nav = navigator as NavigatorWithUAData
|
||||
const ua = nav.userAgent || ''
|
||||
const platform = nav.userAgentData?.platform || nav.platform || ''
|
||||
|
||||
if (/android/i.test(ua)) return 'android'
|
||||
if (/iphone|ipad|ipod/i.test(ua) || (platform === 'MacIntel' && nav.maxTouchPoints > 1)) return 'ios'
|
||||
if (/macintosh|mac os x/i.test(ua) || /mac/i.test(platform)) return 'macos'
|
||||
if (/linux/i.test(ua) || /linux/i.test(platform)) return 'linux'
|
||||
return 'windows'
|
||||
}
|
||||
|
||||
export function useClientOS(): ClientOSInfo {
|
||||
const [osInfo, setOsInfo] = useState<ClientOSInfo>(OS_CONFIGS.windows)
|
||||
|
||||
/** 방문자의 OS. 첫 렌더는 'windows'로 시작해 SSR·첫 페인트 차이를 없앤다. */
|
||||
export function useClientOS(): ClientOSType {
|
||||
const [os, setOs] = useState<ClientOSType>('windows')
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || !navigator) return
|
||||
const ua = navigator.userAgent || ''
|
||||
const platform = (navigator as any).userAgentData?.platform || navigator.platform || ''
|
||||
|
||||
if (/android/i.test(ua)) {
|
||||
setOsInfo(OS_CONFIGS.android)
|
||||
} else if (/iphone|ipad|ipod/i.test(ua) || (platform === 'MacIntel' && navigator.maxTouchPoints > 1)) {
|
||||
setOsInfo(OS_CONFIGS.ios)
|
||||
} else if (/macintosh|mac os x/i.test(ua) || /mac/i.test(platform)) {
|
||||
setOsInfo(OS_CONFIGS.macos)
|
||||
} else if (/linux/i.test(ua) || /linux/i.test(platform)) {
|
||||
setOsInfo(OS_CONFIGS.linux)
|
||||
} else if (/windows|win32|win64/i.test(ua) || /win/i.test(platform)) {
|
||||
setOsInfo(OS_CONFIGS.windows)
|
||||
} else {
|
||||
setOsInfo(OS_CONFIGS.windows)
|
||||
}
|
||||
setOs(detectOS())
|
||||
}, [])
|
||||
|
||||
return osInfo
|
||||
return os
|
||||
}
|
||||
|
|
|
|||
17
site/src/hooks/usePrefersReducedMotion.ts
Normal file
17
site/src/hooks/usePrefersReducedMotion.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
const QUERY = '(prefers-reduced-motion: reduce)'
|
||||
|
||||
/** 사용자의 감소 모션 설정을 따라간다. 설정이 바뀌면 다시 렌더한다. */
|
||||
export function usePrefersReducedMotion(): boolean {
|
||||
const [reduced, setReduced] = useState(() => window.matchMedia(QUERY).matches)
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia(QUERY)
|
||||
const onChange = () => setReduced(media.matches)
|
||||
media.addEventListener('change', onChange)
|
||||
return () => media.removeEventListener('change', onChange)
|
||||
}, [])
|
||||
|
||||
return reduced
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue