static.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. import { View, ScrollView, Row } from 'app/design/view'
  2. import { Text, H1 } from 'app/design/typography'
  3. import { Modal, NeoButton, NeoButtonLink } from 'app/design/controls'
  4. import Svg, { Path, Circle, Ellipse } from 'react-native-svg'
  5. import Link from 'app/ui/atoms/link'
  6. import { Icon } from 'app/ui/atoms/icon'
  7. import Card from 'app/ui/molecules/page/card'
  8. import { useFetch } from 'app/lib/hooks/use-fetch'
  9. import { useEffect, useState } from 'react'
  10. import React from 'react'
  11. import { Animated } from 'react-native'
  12. import { useTranslation } from 'react-i18next'
  13. import { tp } from 'app/lib/util'
  14. import ProfilesList from 'app/ui/molecules/profile/profile-list'
  15. import { useTheme } from 'app/design/theme'
  16. import { Platform } from 'react-native'
  17. import SvgFile from 'app/ui/molecules/content/svg-file'
  18. import { useIconClassColor } from 'app/ui/atoms/icon-class-color'
  19. import MenuFooter from 'app/components/nav/menu-footer'
  20. import { appSetting } from 'app/lib/util'
  21. const isWeb = Platform.OS === 'web'
  22. //mode can be 'adaptive', 'full', 'mark', 'text'
  23. const Logo = ({ mode = 'adaptive' }) => {
  24. // Resolve a concrete color (like IconFromSet) instead of relying on
  25. // `currentColor` inheritance — react-native-svg does not inherit a parent
  26. // View's text color, so on first render `currentColor` falls back to the
  27. // theme primary (blue) until the subtree re-renders.
  28. const { colors } = useTheme()
  29. const { t } = useTranslation()
  30. const logoColor = colors.default
  31. const textStyles = {
  32. adaptive: ' hidden sm:block ',
  33. mark: ' hidden ',
  34. full: ' ',
  35. }
  36. const markStyles = {
  37. text: 'hidden sm:block ',
  38. }
  39. return (
  40. <Row className="items-center gap-3 text-foreground web:motion-safe:hover:animate-pulse">
  41. <View className={`${markStyles[mode]}`}>
  42. <Svg
  43. aria-hidden={true}
  44. width={32}
  45. height={32}
  46. color={logoColor}
  47. viewBox="0 0 40 40"
  48. xmlns="http://www.w3.org/2000/svg"
  49. >
  50. <Path
  51. d="M11.9468 11.8909C14.0127 9.83909 16.8583 8.5714 20.0001 8.5714C20.3283 8.5714 20.6533 8.58523 20.9746 8.61237C21.2844 8.63852 21.567 8.42658 21.6347 8.12318C22.2838 5.21753 24.408 2.86848 27.1804 1.90314C27.4392 1.81299 27.4658 1.43791 27.2101 1.33905C24.9733 0.474227 22.5421 0 20.0001 0C17.0609 0 14.2699 0.633998 11.7562 1.77265C11.5553 1.86368 11.4286 2.06522 11.4286 2.28582V11.685C11.4286 11.9482 11.76 12.0763 11.9468 11.8909Z"
  52. fill="currentColor"
  53. />
  54. <Path
  55. d="M31.8734 36.0956C31.6877 36.2327 31.4285 36.0985 31.4285 35.8673V19.9999C31.4285 19.6717 31.4148 19.3466 31.3877 19.0254C31.3614 18.7156 31.5734 18.4331 31.8768 18.3653C34.7826 17.7162 37.1314 15.592 38.0969 12.8196C38.1869 12.5607 38.562 12.5342 38.6609 12.7899C39.5257 15.0267 40 17.4579 40 19.9999C40 26.5999 36.8031 32.453 31.8734 36.0956Z"
  56. fill="currentColor"
  57. />
  58. <Path
  59. d="M35.7142 9.99996C35.7142 13.1559 33.1559 15.7142 29.9999 15.7142C26.8439 15.7142 24.2856 13.1559 24.2856 9.99996C24.2856 6.84405 26.8439 4.28569 29.9999 4.28569C33.1559 4.28569 35.7142 6.84405 35.7142 9.99996Z"
  60. fill="currentColor"
  61. />
  62. <Path
  63. d="M1.90315 27.1802C1.81301 27.4391 1.43792 27.4656 1.33906 27.2099C0.47423 24.9732 0 22.542 0 20C0 13.4001 3.19687 7.54684 8.12659 3.90428C8.31231 3.76703 8.57145 3.90157 8.57145 4.13251V20C8.57145 20.3282 8.58528 20.6533 8.61242 20.9745C8.63857 21.2843 8.42662 21.5669 8.12322 21.6347C5.21756 22.2837 2.86849 24.4079 1.90315 27.1802Z"
  64. fill="currentColor"
  65. />
  66. <Path
  67. d="M28.2436 38.2273C28.4448 38.1362 28.5714 37.9348 28.5714 37.7142V28.3148C28.5714 28.0517 28.2399 27.9237 28.0531 28.1091C25.9873 30.1608 23.1416 31.4285 19.9999 31.4285C19.6716 31.4285 19.3466 31.4148 19.0254 31.3876C18.7156 31.3614 18.433 31.5734 18.3652 31.8768C17.7162 34.7825 15.5919 37.1313 12.8195 38.0968C12.5607 38.187 12.5342 38.5619 12.7898 38.6608C15.0267 39.5256 17.4579 39.9999 19.9999 39.9999C22.939 39.9999 25.7302 39.3659 28.2436 38.2273Z"
  68. fill="currentColor"
  69. />
  70. <Path
  71. d="M15.7145 29.9999C15.7145 33.1559 13.1561 35.7142 10.0002 35.7142C6.84426 35.7142 4.28588 33.1559 4.28588 29.9999C4.28588 26.8439 6.84426 24.2856 10.0002 24.2856C13.1561 24.2856 15.7145 26.8439 15.7145 29.9999Z"
  72. fill="currentColor"
  73. />
  74. </Svg>
  75. </View>
  76. <View className={`${textStyles[mode]}`}>
  77. <Svg
  78. aria-hidden={true}
  79. width={68}
  80. height={32}
  81. viewBox="0 0 68 32"
  82. color={logoColor}
  83. xmlns="http://www.w3.org/2000/svg"
  84. >
  85. <Path
  86. d="M17.5 16C17.5 20.1421 14.1421 23.5 10 23.5C5.85786 23.5 2.5 20.1421 2.5 16V7.25C2.5 6.55964 1.94036 6 1.25 6C0.559644 6 0 6.55964 0 7.25V16C0 21.5228 4.47715 26 10 26C15.5228 26 20 21.5228 20 16V7.25C20 6.55964 19.4404 6 18.75 6C18.0596 6 17.5 6.55964 17.5 7.25V16Z"
  87. fill="currentColor"
  88. />
  89. <Path
  90. d="M41.5 16V24.75C41.5 25.4404 42.0596 26 42.75 26C43.4404 26 44 25.4404 44 24.75V16C44 10.4772 39.5228 6 34 6C28.4772 6 24 10.4772 24 16V24.75C24 25.4404 24.5596 26 25.25 26C25.9404 26 26.5 25.4404 26.5 24.75V16C26.5 11.8579 29.8579 8.5 34 8.5C38.1421 8.5 41.5 11.8579 41.5 16Z"
  91. fill="currentColor"
  92. />
  93. <Path
  94. fillRule="evenodd"
  95. clipRule="evenodd"
  96. d="M65.5 22.6146V24.75C65.5 25.4404 66.0596 26 66.75 26C67.4404 26 68 25.4404 68 24.75V16C68 10.4772 63.5228 6 58 6C52.4772 6 48 10.4772 48 16C48 21.5228 52.4772 26 58 26C60.9867 26 63.6676 24.6906 65.5 22.6146ZM65.5 16C65.5 20.1421 62.1421 23.5 58 23.5C53.8579 23.5 50.5 20.1421 50.5 16C50.5 11.8579 53.8579 8.5 58 8.5C62.1421 8.5 65.5 11.8579 65.5 16Z"
  97. fill="currentColor"
  98. />
  99. </Svg>
  100. </View>
  101. </Row>
  102. )
  103. }
  104. const SplashTextInner = () => {
  105. const { t } = useTranslation()
  106. return (
  107. <View className="text-center max-w-3xl lg:text-start w-full flex-auto ">
  108. <H1 className="text-center lg:text-start sm:pb-6" fontFamily="font-title">
  109. {t('splash_page_title')} {appSetting('config', 'title')}
  110. </H1>
  111. <Text
  112. accessible={true}
  113. accessibilityRole="text"
  114. className=" text-secondary-foreground text-center lg:text-start text-base sm:text-lg text-pretty"
  115. >
  116. {t('splash_page_text')}
  117. </Text>
  118. </View>
  119. )
  120. }
  121. const SPLASH_PATHS = [
  122. 'M10 19a2 2 0 1 0 -4 0a2 2 0 0 0 4 0',
  123. 'M18 5a2 2 0 1 0 -4 0a2 2 0 0 0 4 0',
  124. 'M10 5a2 2 0 1 0 -4 0a2 2 0 0 0 4 0',
  125. 'M6 12a2 2 0 1 0 -4 0a2 2 0 0 0 4 0',
  126. 'M18 19a2 2 0 1 0 -4 0a2 2 0 0 0 4 0',
  127. 'M14 12a2 2 0 1 0 -4 0a2 2 0 0 0 4 0',
  128. 'M22 12a2 2 0 1 0 -4 0a2 2 0 0 0 4 0',
  129. 'M6 12h4',
  130. 'M14 12h4',
  131. 'M15 7l-2 3',
  132. 'M9 7l2 3',
  133. 'M11 14l-2 3',
  134. 'M13 14l2 3',
  135. 'M10 5h4',
  136. 'M10 19h4',
  137. 'M17 17l2 -3',
  138. 'M19 10l-2 -3',
  139. 'M7 7l-2 3',
  140. 'M5 14l2 3',
  141. ]
  142. // Inline (not a fetched file): on web it is in the SSR HTML and cannot be the LCP element,
  143. // on native there is no network fetch. Web: stroke inherits CSS `color` from the wrapper;
  144. // native: react-native-svg needs a concrete color resolved from the class.
  145. const SplashIllustration = () => {
  146. const className = 'text-secondary-foreground'
  147. const color = useIconClassColor(className) || 'currentColor'
  148. return (
  149. <View className={`w-full h-full ${className}`}>
  150. <Svg
  151. role="img"
  152. aria-label="Splash screen illustration"
  153. width="100%"
  154. height="100%"
  155. viewBox="0 0 24 24"
  156. fill="none"
  157. stroke={color}
  158. strokeWidth={1}
  159. strokeLinecap="round"
  160. strokeLinejoin="round"
  161. >
  162. {SPLASH_PATHS.map((d) => (
  163. <Path key={d} d={d} />
  164. ))}
  165. </Svg>
  166. </View>
  167. )
  168. }
  169. const SplashTextComponent = (props) => {
  170. return isWeb ? (
  171. <View className="items-center lg:items-start relative gap-6 my-auto flex-auto w-full ">
  172. <View className="relative flex-auto h-48 w-48">
  173. <SplashIllustration />
  174. </View>
  175. <View className="flex-auto w-full items-center lg:items-start">
  176. <SplashTextInner />
  177. </View>
  178. </View>
  179. ) : (
  180. <View className=" items-center lg:items-start flex-auto gap-6 w-full mx-auto">
  181. <View className="flex-auto w-64 h-64">
  182. <SplashIllustration />
  183. </View>
  184. <View className="flex-auto w-full">
  185. <SplashTextInner />
  186. </View>
  187. </View>
  188. )
  189. }
  190. const JoinTextComponent = (props) => {
  191. return (
  192. <View className="flex-auto w-1/2 max-w-64">
  193. <SvgFile
  194. src_dark="create-account-dark.svg"
  195. src_default="create-account-light.svg"
  196. alt="Create account illustration"
  197. />
  198. </View>
  199. )
  200. }
  201. const ComponentsCommentsEmpty = () => {
  202. const { t } = useTranslation()
  203. return (
  204. <>
  205. <View className="p-6 border-t border-border/40">
  206. <Row className=" gap-2 mx-auto my-auto py-4 px-6 rounded-2xl bg-muted/50 ">
  207. <View className="flex-col mx-auto text-muted-foreground ">
  208. <Icon icon="MessageCircle" width={28} height={28} />
  209. </View>
  210. <View className="flex-col gap-1">
  211. <Text className="text-lg text-muted-foreground font-semibold ">
  212. {t('No comments yet')}
  213. </Text>
  214. <Text className="text-sm text-muted-foreground ">
  215. {t('Be the first to share what you think')}
  216. </Text>
  217. </View>
  218. </Row>
  219. </View>
  220. </>
  221. )
  222. }
  223. const ComponentsCommentsLogin = () => {
  224. const { t } = useTranslation()
  225. return (
  226. <View className="py-2">
  227. <View className="flex-col gap-y-2 items-center opacity-80 justify-center mx-auto my-auto py-4 px-8 rounded-2xl bg-muted-foreground/10 ">
  228. <Text className="text-center text-base text-secondary-foreground ">
  229. <Link className="text-primary" href="/login">
  230. <Text className="text-primary">{t('Login')}</Text>
  231. </Link>{' '}
  232. {t('comments_login_or')}{' '}
  233. <Link className="text-primary" href="/create-account">
  234. <Text className="text-primary">{t('create an account')}</Text>
  235. </Link>{' '}
  236. {t('comments_login_to_comment')}
  237. </Text>
  238. </View>
  239. </View>
  240. )
  241. }
  242. const ComponentsContentEmpty = () => {
  243. const { t } = useTranslation()
  244. return (
  245. <>
  246. <View className="p-2">
  247. <View className="gap-y-2 items-center opacity-80 justify-center mx-auto my-auto mb-auto py-4 px-8 web:h-full rounded-2xl bg-muted-foreground/10 ">
  248. <View className="mx-auto m-4 text-secondary-foreground">
  249. <Icon icon="Binoculars" width={32} height={32} />
  250. </View>
  251. <Text className="text-center text-lg text-secondary-foreground lg:text-xl font-semibold ">
  252. {t('Nothing found')}
  253. </Text>
  254. <Text className="text-center text-base text-muted-foreground ">
  255. {t('Try again later')}
  256. </Text>
  257. </View>
  258. </View>
  259. </>
  260. )
  261. }
  262. const PageNotFound = () => {
  263. const { t } = useTranslation()
  264. return (
  265. <>
  266. <View className="p-8 mx-auto">
  267. <View className="gap-2 items-center justify-center mx-auto my-auto py-4 px-8 h-full rounded-2xl bg-muted ">
  268. <View className="mx-auto m-4 text-muted-foreground ">
  269. <Icon icon="Binoculars" width={32} height={32} />
  270. </View>
  271. <Text className="text-center text-lg text-foreground lg:text-xl font-semibold ">
  272. {t('404 - not found')}
  273. </Text>
  274. <Text className="text-center text-base text-muted-foreground ">
  275. {t('Page not found')}
  276. </Text>
  277. </View>
  278. </View>
  279. </>
  280. )
  281. }
  282. const PageError = ({ error, reset }) => {
  283. const { t } = useTranslation()
  284. return (
  285. <View className="mx-auto max-w-full px-2">
  286. <View className="m-8 gap-y-4 items-center justify-center mx-auto my-auto py-4 px-8 rounded-2xl bg-muted ">
  287. <View className=" mx-auto m-4 text-muted-foreground ">
  288. <Icon icon="BugOff" width={32} height={32} />
  289. </View>
  290. <Text className="text-center text-lg text-foreground lg:text-xl font-semibold ">
  291. {t('We ran into a problem')}
  292. </Text>
  293. <Text className="text-center text-base text-muted-foreground ">
  294. {t(
  295. 'Something didn’t go as planned. Please try again or reload the page.',
  296. )}
  297. </Text>
  298. <Text className="text-center text-sm text-muted-foreground ">
  299. {error.digest}
  300. {error.message}
  301. </Text>
  302. <NeoButton
  303. onPress={() => reset()}
  304. label={t('Reload page')}
  305. classNames={{ root: 'self-center' }}
  306. />
  307. </View>
  308. </View>
  309. )
  310. }
  311. const BootstrapOffline = ({ onRetry }) => {
  312. const { t } = useTranslation()
  313. return (
  314. <View className="flex-1 items-center justify-center bg-background p-6 gap-4">
  315. <Text className="text-center text-base text-foreground">
  316. {t('No internet connection')}
  317. </Text>
  318. {onRetry ? <NeoButton label={t('Retry')} onPress={onRetry} classNames={{ root: 'self-center' }} /> : null}
  319. </View>
  320. )
  321. }
  322. const PageNotAllowed = () => {
  323. const { t } = useTranslation()
  324. return (
  325. <>
  326. <View className="p-8 mx-auto">
  327. <View className="gap-y-2 items-center opacity-80 justify-center mx-auto my-auto mb-auto py-4 px-8 h-full rounded-2xl bg-muted-foreground/10 ">
  328. <View className="mx-auto m-4 text-secondary-foreground ">
  329. <Icon icon="Binoculars" width={32} height={32} />
  330. </View>
  331. <Text className="text-center text-lg text-card-foreground lg:text-xl font-semibold ">
  332. {t('403 - not allowed')}
  333. </Text>
  334. <Text className="text-center text-base text-muted-foreground ">
  335. {t('Page not allowed, sorry.')}
  336. </Text>
  337. </View>
  338. </View>
  339. </>
  340. )
  341. }
  342. const AnimatedCounter = ({ value, duration }) => {
  343. const animatedValue = useState(new Animated.Value(1))[0]
  344. const [displayValue, setDisplayValue] = useState(1)
  345. useEffect(() => {
  346. Animated.timing(animatedValue, {
  347. toValue: value,
  348. duration: duration,
  349. useNativeDriver: false,
  350. }).start()
  351. const listener = animatedValue.addListener(({ value }) => {
  352. setDisplayValue(Math.round(value))
  353. })
  354. return () => {
  355. animatedValue.removeListener(listener)
  356. }
  357. }, [value])
  358. return <Animated.Text>{displayValue}</Animated.Text>
  359. }
  360. export function ComponentsIntro(props) {
  361. const { t } = useTranslation()
  362. const { data: accountsCount } = useFetch('/api.php?r=q&q=accounts_count')
  363. const data = accountsCount ?? 1
  364. const { data: activePersons } = useFetch(
  365. '/api.php?r=bx_persons/browse/&params[]={%22params%22:{%22per_page%22:%2212%22,%22start%22:0,%22type%22:%22active%22}}',
  366. )
  367. const data2 = activePersons?.data?.[0]?.data?.data ?? []
  368. const CounterText = React.memo(({ data }) => {
  369. return (
  370. <View className="absolute right-0 flex-col bg-linear-to-r pl-16 from-transparent via-white to-white h-10 justify-end gap-y-0.5 items-end flex-none my-auto whitespace-nowrap nowrap ">
  371. <Text className="font-bold text-foreground leading-5 text-3xl">
  372. <AnimatedCounter
  373. value={data}
  374. duration={1000}
  375. startFrom={1}
  376. />
  377. </Text>
  378. <Text className=" text-muted-foreground text-xs ">
  379. {tp('members', data, true)}
  380. </Text>
  381. </View>
  382. )
  383. })
  384. return (
  385. <Card addClassName=" bg-red-500 flex-col gap-y-4 p-4 ">
  386. <View className="flex-row gap-y-2 overflow-hidden">
  387. <ProfilesList data={data2} showEmpty={true} maxCount={12} />
  388. <CounterText data={data} />
  389. </View>
  390. <Text className="text-sm text-muted-foreground ">
  391. {t('Community Intro')}
  392. </Text>
  393. </Card>
  394. )
  395. }
  396. const ComponentsLoginContentComponent = (props) => {
  397. const { t } = useTranslation()
  398. return (
  399. <View className="hidden my-auto flex-auto">
  400. <View className="w-[50%] max-w-80 aspect-square">
  401. <SvgFile
  402. src_dark="login-dark.svg"
  403. src_default="login-light.svg"
  404. alt="Login illustration"
  405. />
  406. </View>
  407. <View className="flex-auto items-center lg:items-start gap-y-4 sm:gap-y-6 max-w-md sm:max-w-lg lg:max-w-3xl">
  408. <View className="gap-y-8 flex-auto my-4 ">
  409. <H1 className="text-4xl tracking-tight font-bold text-secondary-foreground justify-center items-center ">
  410. {t('login_page_title')}
  411. </H1>
  412. <View className="gap-y-4">
  413. <View className="flex-row gap-x-4 ">
  414. <Icon
  415. className="text-secondary-foreground "
  416. icon="UsersRound"
  417. width={24}
  418. height={24}
  419. />
  420. <Text className="flex-auto my-auto text-secondary-foreground text-base font-medium">
  421. {t('login_feature_meet')}
  422. </Text>
  423. </View>
  424. <View className="flex-row gap-x-4 ">
  425. <Icon
  426. className="text-secondary-foreground "
  427. icon="Compass"
  428. width={24}
  429. height={24}
  430. />
  431. <Text className="flex-auto my-auto text-secondary-foreground text-base font-medium">
  432. {t('login_feature_discover')}
  433. </Text>
  434. </View>
  435. <View className="flex-row gap-x-4 ">
  436. <Icon
  437. className="text-secondary-foreground "
  438. icon="Share"
  439. width={24}
  440. height={24}
  441. />
  442. <Text className="flex-auto my-auto text-secondary-foreground text-base font-medium">
  443. {t('login_feature_share')}
  444. </Text>
  445. </View>
  446. </View>
  447. </View>
  448. </View>
  449. </View>
  450. )
  451. }
  452. const ComponentsFooter = () => {
  453. return (
  454. <MenuFooter />
  455. )
  456. }
  457. const ComponentsPricingHeader = () => {
  458. return <>TODO ComponentsPricingHeader</>
  459. }
  460. const ComponentsPricingFooter = () => {
  461. return <>TODO ComponentsPricingFooter</>
  462. }
  463. const ComponentsFullFooter = () => {
  464. const { t } = useTranslation()
  465. // The links wrap to a second row on narrow phones, so the spacer that keeps room
  466. // for the fixed bar follows its measured height (h-16 is the one-row height).
  467. // Web only: native has no `fixed`, the bar stays in flow and the spacer stays h-16.
  468. // On phones the bar starts at the screen edge, not at its inset block, so the
  469. // centred links don't run past the right edge (wider screens keep their placement).
  470. const [barHeight, setBarHeight] = useState()
  471. return (
  472. <>
  473. <View className="w-full min-h-16" style={{ height: barHeight }}></View>
  474. <View
  475. onLayout={isWeb ? (e) => setBarHeight(e.nativeEvent.layout.height) : undefined}
  476. className=" w-full p-3 flex-row flex-wrap justify-center gap-2 bg-background border-t border-border/60 fixed bottom-0 max-sm:left-0 "
  477. >
  478. <NeoButtonLink
  479. href="/"
  480. style="borderless"
  481. controlSize="small"
  482. label={t('Home')}
  483. classNames={{ root: 'mt-auto' }}
  484. />
  485. <NeoButtonLink
  486. href="/about"
  487. style="borderless"
  488. controlSize="small"
  489. label={t('About')}
  490. classNames={{ root: 'mt-auto' }}
  491. />
  492. <NeoButtonLink
  493. href="/contact"
  494. style="borderless"
  495. controlSize="small"
  496. label={t('Contact')}
  497. classNames={{ root: 'mt-auto' }}
  498. />
  499. <NeoButtonLink
  500. href="/privacy"
  501. style="borderless"
  502. controlSize="small"
  503. label={t('Privacy')}
  504. classNames={{ root: 'mt-auto' }}
  505. />
  506. <NeoButtonLink
  507. href="/terms"
  508. style="borderless"
  509. controlSize="small"
  510. label={t('Terms')}
  511. classNames={{ root: 'mt-auto' }}
  512. />
  513. </View>
  514. </>
  515. )
  516. }
  517. export { BootstrapOffline }
  518. // Upstream static component registry. appStatic() (lib/app-static.tsx) reads it through
  519. // customization/static.js, which re-exports this map upstream and is replaced by forks.
  520. // Every `components_*` key is also a page override slot: the default page layout renders
  521. // `components_<uri>` on top of the page with that URI, so don't name a non-page component
  522. // after a real page (a UNA page at /footer would render components_footer on top).
  523. export const staticDefault = {
  524. logo: Logo,
  525. page_not_found: PageNotFound,
  526. page_error: PageError,
  527. bootstrap_offline: BootstrapOffline,
  528. page_not_allowed: PageNotAllowed,
  529. components_comments_empty: ComponentsCommentsEmpty,
  530. components_content_empty: ComponentsContentEmpty,
  531. components_content_login: ComponentsCommentsLogin,
  532. components_intro: ComponentsIntro,
  533. components_pricing_header: ComponentsPricingHeader,
  534. components_pricing_footer: ComponentsPricingFooter,
  535. components_footer: ComponentsFooter,
  536. components_fullfooter: ComponentsFullFooter,
  537. components_logincontent: ComponentsLoginContentComponent,
  538. splash_text: SplashTextComponent,
  539. join_text: JoinTextComponent,
  540. }