├── .env.example ├── .eslintrc.json ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .tool-versions ├── .yarnrc ├── LICENSE ├── README.md ├── additional.d.ts ├── assets └── svgs │ ├── android_robot_head.svg │ ├── apple.svg │ ├── close.svg │ ├── copy.svg │ ├── desktop.svg │ ├── facebook.svg │ ├── fdroid-logo.svg │ ├── github.svg │ ├── hamburger.svg │ ├── instagram.svg │ ├── link.svg │ ├── linux.svg │ ├── mastodon.svg │ ├── minus.svg │ ├── plus.svg │ ├── rss.svg │ ├── twitter.svg │ ├── windows.svg │ └── youtube.svg ├── components ├── BlogPost │ └── index.tsx ├── Container │ └── index.tsx ├── CustomHead │ └── index.tsx ├── CustomQRCode │ └── index.tsx ├── EmbedContent │ └── index.tsx ├── LockedPage │ └── index.tsx ├── MarketResearchSignup │ └── index.tsx ├── RedirectPage │ └── index.tsx ├── RichBody │ └── index.tsx ├── RichPage │ └── index.tsx ├── VideoPlayer │ └── index.tsx ├── cards │ ├── BenefitsCard │ │ └── index.tsx │ ├── PostCard │ │ └── index.tsx │ └── index.tsx ├── navigation │ ├── Footer │ │ └── index.tsx │ ├── Nav │ │ └── index.tsx │ ├── NavItem │ │ └── index.tsx │ └── index.tsx ├── posts │ ├── Post │ │ └── index.tsx │ ├── PostList │ │ └── index.tsx │ └── index.tsx ├── sections │ ├── About │ │ └── index.tsx │ ├── Benefits │ │ └── index.tsx │ ├── EmailSignup │ │ └── index.tsx │ ├── Features │ │ └── index.tsx │ ├── GroupNotice │ │ └── index.tsx │ ├── Hero │ │ └── index.tsx │ └── index.tsx └── ui │ ├── Accordion │ └── index.tsx │ ├── Banner │ └── index.tsx │ ├── Button │ └── index.tsx │ ├── Headline │ └── index.tsx │ ├── Layout │ └── index.tsx │ └── index.tsx ├── constants ├── banner.ts ├── cms.ts ├── index.ts ├── links.ts ├── metadata.ts ├── navigation.ts ├── shortcodes.ts ├── signups.ts ├── tos.ts └── ui.ts ├── contentful ├── config.json └── export.json ├── contexts └── screen.tsx ├── hooks └── screen.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── 404.tsx ├── [slug].tsx ├── _app.tsx ├── _document.tsx ├── api │ ├── download │ │ └── [platform].ts │ ├── email │ │ └── [list].ts │ ├── feed │ │ ├── atom.ts │ │ ├── json.ts │ │ └── rss.ts │ ├── litepaper.ts │ ├── login.ts │ ├── logout.ts │ └── sitemap.ts ├── blog │ └── index.tsx ├── community.tsx ├── download.tsx ├── faq.tsx ├── how-to-help.tsx ├── index.tsx ├── litepaper.tsx ├── preview │ └── [...slug].tsx └── tag │ └── [tag].tsx ├── postcss.config.js ├── public ├── android-chrome-192x192.png ├── android-chrome-256x256.png ├── apple-touch-icon.png ├── assets │ ├── downloads │ │ └── Session-Brandmarks.zip │ ├── fonts │ │ ├── PublicSans │ │ │ ├── OFL.txt │ │ │ ├── PublicSans-Italic-VariableFont_wght.ttf │ │ │ ├── PublicSans-VariableFont_wght.ttf │ │ │ ├── README.txt │ │ │ └── static │ │ │ │ ├── PublicSans-Black.ttf │ │ │ │ ├── PublicSans-BlackItalic.ttf │ │ │ │ ├── PublicSans-Bold.ttf │ │ │ │ ├── PublicSans-BoldItalic.ttf │ │ │ │ ├── PublicSans-ExtraBold.ttf │ │ │ │ ├── PublicSans-ExtraBoldItalic.ttf │ │ │ │ ├── PublicSans-ExtraLight.ttf │ │ │ │ ├── PublicSans-ExtraLightItalic.ttf │ │ │ │ ├── PublicSans-Italic.ttf │ │ │ │ ├── PublicSans-Light.ttf │ │ │ │ ├── PublicSans-LightItalic.ttf │ │ │ │ ├── PublicSans-Medium.ttf │ │ │ │ ├── PublicSans-MediumItalic.ttf │ │ │ │ ├── PublicSans-Regular.ttf │ │ │ │ ├── PublicSans-SemiBold.ttf │ │ │ │ ├── PublicSans-SemiBoldItalic.ttf │ │ │ │ ├── PublicSans-Thin.ttf │ │ │ │ └── PublicSans-ThinItalic.ttf │ │ └── SpaceMono │ │ │ ├── OFL.txt │ │ │ ├── SpaceMono-Bold.ttf │ │ │ ├── SpaceMono-BoldItalic.ttf │ │ │ ├── SpaceMono-Italic.ttf │ │ │ └── SpaceMono-Regular.ttf │ ├── images │ │ ├── blog.png │ │ ├── faq.png │ │ ├── help.png │ │ ├── litepaper.png │ │ ├── logo-black.png │ │ ├── logo-icon-black.png │ │ ├── logo-white.png │ │ ├── logo.png │ │ ├── mockup-desktop.png │ │ ├── qr-logo.png │ │ ├── send-messages-not-metadata.jpg │ │ ├── session-ui-add.png │ │ ├── session-ui-community.png │ │ ├── ui-create-account.png │ │ ├── ui-direct-message.png │ │ ├── ui-group-message.png │ │ └── ui-showcase.png │ ├── papers │ │ └── Litepaper_Session_private_messenger.pdf │ ├── svgs │ │ ├── censorship-resistant-redacted.svg │ │ ├── censorship-resistant.svg │ │ ├── no-data-redacted.svg │ │ ├── no-data.svg │ │ ├── no-footprint-redacted.svg │ │ ├── no-footprint.svg │ │ ├── no-phone-redacted.svg │ │ ├── no-phone.svg │ │ ├── open-source-redacted.svg │ │ └── open-source.svg │ └── videos │ │ └── this-is-session │ │ ├── 1080p.mp4 │ │ ├── 144p.mp4 │ │ ├── 240p.mp4 │ │ ├── 360p.mp4 │ │ ├── 480p.mp4 │ │ ├── 720p.mp4 │ │ └── thumbnail.webp ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── mstile-150x150.png ├── robots.txt ├── safari-pinned-tab.svg └── site.webmanifest ├── services ├── cms.ts ├── embed.ts ├── redirect.ts └── render.tsx ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json ├── types ├── cms.ts └── himalaya.ts ├── utils ├── capitalize.ts ├── clipboard.ts ├── environment.ts ├── links.ts ├── lockPageTitle.ts ├── redact.ts ├── rss.ts ├── sanitize.ts └── shortcodes.tsx └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | CONTENTFUL_SPACE_ID= 2 | CONTENTFUL_ENVIRONMENT_ID= 3 | CONTENTFUL_ACCESS_TOKEN= 4 | CONTENTFUL_PREVIEW_TOKEN= 5 | CAMPAIGN_MONITOR_CLIENT_ID= 6 | CAMPAIGN_MONITOR_API_KEY= 7 | CAMPAIGN_MONITOR_LIST_SESSION_ID= 8 | CAMPAIGN_MONITOR_LIST_MARKET_RESEARCH_ID= 9 | STAGING_SECRET= 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | /public/rss 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | .vscode 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | 35 | # vercel 36 | .vercel 37 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.20.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | coverage 3 | build 4 | out 5 | .next 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 16.20.1 2 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | --install.frozen-lockfile true 2 | -------------------------------------------------------------------------------- /additional.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import * as React from 'react'; 3 | 4 | const ReactComponent: React.FunctionComponent>; 5 | export { ReactComponent }; 6 | export default string; 7 | } 8 | -------------------------------------------------------------------------------- /assets/svgs/android_robot_head.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svgs/apple.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svgs/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svgs/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/svgs/desktop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svgs/facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svgs/fdroid-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/svgs/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svgs/hamburger.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svgs/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /assets/svgs/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /assets/svgs/linux.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svgs/mastodon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svgs/minus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svgs/plus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svgs/rss.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svgs/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svgs/windows.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svgs/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /components/BlogPost/index.tsx: -------------------------------------------------------------------------------- 1 | import { IPost } from '@/types/cms'; 2 | import { Layout } from '@/components/ui'; 3 | import METADATA from '@/constants/metadata'; 4 | import { Post } from '@/components/posts'; 5 | import { ReactElement } from 'react'; 6 | 7 | interface Props { 8 | post: IPost; 9 | otherPosts?: IPost[]; 10 | } 11 | 12 | export default function BlogPost(props: Props): ReactElement { 13 | const { post } = props; 14 | return ( 15 | <> 16 | 34 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /components/Container/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, ReactNode } from 'react'; 2 | import classNames from 'classnames'; 3 | import { useScreen } from '@/contexts/screen'; 4 | 5 | export interface IContainerSizes { 6 | small: string; 7 | medium: string; 8 | large: string; 9 | huge?: string; 10 | enormous?: string; 11 | } 12 | 13 | interface Props { 14 | id?: string; 15 | hasMinHeight?: boolean; 16 | heights?: IContainerSizes; 17 | fullWidth?: boolean; 18 | classes?: string; 19 | children: ReactNode; 20 | } 21 | 22 | export default function Container(props: Props): ReactElement { 23 | const { 24 | id, 25 | hasMinHeight = false, 26 | heights, 27 | fullWidth = false, 28 | classes, 29 | children, 30 | } = props; 31 | const minHeights: IContainerSizes = { 32 | small: '568px', 33 | medium: '1024px', 34 | large: '768px', 35 | huge: '900px', 36 | enormous: '968px', 37 | }; 38 | const { isSmall, isMedium, isLarge, isHuge, isEnormous } = useScreen(); 39 | const getHeight = (sizes: IContainerSizes): string => { 40 | if (isSmall) return sizes?.small; 41 | if (isMedium) return sizes?.medium; 42 | if (isLarge) return sizes?.large; 43 | if (isHuge) return sizes?.huge ?? sizes?.large; 44 | if (isEnormous) return sizes?.enormous ?? sizes?.large; 45 | return ''; 46 | }; 47 | const height = heights && getHeight(heights); 48 | return ( 49 |
66 | {children} 67 |
68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /components/CustomQRCode/index.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { MouseEvent, useEffect, useRef, useState } from 'react'; 3 | import { QRCode } from 'react-qrcode-logo'; 4 | 5 | export type SessionQRCodeProps = { 6 | id: string; 7 | value: string; 8 | size: number; 9 | backgroundColor?: string; 10 | foregroundColor?: string; 11 | logoImage?: string; 12 | logoSize?: number; 13 | }; 14 | 15 | export function CustomQRCode(props: SessionQRCodeProps) { 16 | const { 17 | id, 18 | value, 19 | size, 20 | backgroundColor = '#FFF', 21 | foregroundColor = '#000', 22 | logoImage, 23 | logoSize, 24 | } = props; 25 | const [logo, setLogo] = useState(logoImage); 26 | const [bgColor, setBgColor] = useState(backgroundColor); 27 | const [fgColor, setFgColor] = useState(foregroundColor); 28 | 29 | const qrRef = useRef(null); 30 | const qrCanvasSize = 1000; 31 | const canvasLogoSize = logoSize 32 | ? (qrCanvasSize * 0.3 * logoSize) / logoSize 33 | : 250; 34 | 35 | const handleOnClick = () => { 36 | qrRef.current?.download('png', 'session-community-qr-code.png'); 37 | }; 38 | 39 | useEffect(() => { 40 | // Don't pass the component props to the QR component directly instead update it's props in the next render cycle to prevent janky renders 41 | 42 | if (bgColor !== backgroundColor) { 43 | setBgColor(backgroundColor); 44 | } 45 | 46 | if (fgColor !== foregroundColor) { 47 | setFgColor(foregroundColor); 48 | } 49 | 50 | if (logoImage && logo !== logoImage) { 51 | setLogo(logoImage); 52 | } 53 | }, [backgroundColor, bgColor, fgColor, foregroundColor, logo, logoImage]); 54 | 55 | return ( 56 |
) => { 64 | event.preventDefault(); 65 | void handleOnClick(); 66 | }} 67 | style={{ width: `${size}px`, height: `${size}px` }} 68 | > 69 | 87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /components/EmbedContent/index.tsx: -------------------------------------------------------------------------------- 1 | import { IEmbed, INoembed, isNoembed } from '@/services/embed'; 2 | import { ReactElement, useEffect, useRef, useState } from 'react'; 3 | 4 | import { Button } from '../ui'; 5 | import Image from 'next/image'; 6 | import Link from 'next/link'; 7 | import { TOS } from '@/constants'; 8 | import classNames from 'classnames'; 9 | 10 | interface Props { 11 | content: IEmbed | INoembed; // is sanitized in embed service 12 | classes?: string; 13 | textDirection: string; 14 | } 15 | 16 | export default function EmbedContent(props: Props): ReactElement { 17 | const { content, classes, textDirection } = props; 18 | const htmlRef = useRef(null); 19 | const [allowExternalContent, setAllowExternalContent] = useState(false); 20 | 21 | useEffect(() => { 22 | if (isNoembed(content) && null !== htmlRef.current) { 23 | htmlRef.current.innerHTML = content.html; 24 | } 25 | if (allowExternalContent) { 26 | if (isNoembed(content) && null !== htmlRef.current) { 27 | htmlRef.current.innerHTML = content.html; 28 | } 29 | } 30 | }, [allowExternalContent, content]); 31 | 32 | if (isNoembed(content)) { 33 | if (content.isExternalVideo) { 34 | return allowExternalContent ? ( 35 |
39 | ) : ( 40 |
45 |

48 | This content is hosted by {content.site_name}. 49 |

50 |

55 | By showing the external content you accept their{' '} 56 | {TOS[content.site_name] && TOS[content.site_name].length > 0 ? ( 57 | 63 | Terms and Conditions 64 | 65 | ) : ( 66 | 'Terms and Conditions' 67 | )} 68 | . 69 |

70 |
74 | 84 |
85 |
86 | ); 87 | } else { 88 | return ( 89 |
93 | ); 94 | } 95 | } else { 96 | return ( 97 | 98 | 99 |
106 | {content.image && ( 107 |
108 | link thumbnail image 115 |
116 | )} 117 |
118 |

122 | {content.description && ( 123 |

124 | )} 125 | {content.site_name && ( 126 |

130 | )} 131 |

132 |
133 |
134 | 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /components/LockedPage/index.tsx: -------------------------------------------------------------------------------- 1 | import Container from '@/components/Container'; 2 | import classNames from 'classnames'; 3 | 4 | export default function LockedPage() { 5 | return ( 6 |
7 | 20 |

21 | You don't have access to this page. 22 |

23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /components/RedirectPage/index.tsx: -------------------------------------------------------------------------------- 1 | import Container from '@/components/Container'; 2 | import classNames from 'classnames'; 3 | import { useRouter } from 'next/router'; 4 | 5 | export default function RedirectPage() { 6 | const router = useRouter(); 7 | return ( 8 |
9 | 22 |

23 | Redirecting... 24 |

25 |

28 | Click{' '} 29 | {' '} 35 | to return to the previous page. 36 |

37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /components/RichPage/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | import { IPage } from '@/types/cms'; 5 | import Container from '@/components/Container'; 6 | import { Headline, Layout } from '@/components/ui'; 7 | import RichBody from '@/components/RichBody'; 8 | 9 | interface Props { 10 | page: IPage; 11 | } 12 | 13 | export default function RichPage(props: Props): ReactElement { 14 | const { page } = props; 15 | const pageTitle = page ? page.title : ''; 16 | return ( 17 | 18 |
19 | {page?.headline && ( 20 | 32 | {page.headline} 33 | 34 | )} 35 | 36 |
37 | 45 |
46 |
47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /components/VideoPlayer/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, useRef, useEffect } from 'react'; 2 | import videojs, { VideoJsPlayerOptions } from 'video.js'; 3 | // @ts-ignore 4 | import QualitySelector from '@silvermine/videojs-quality-selector'; 5 | import 'video.js/dist/video-js.min.css'; 6 | import '@silvermine/videojs-quality-selector/dist/css/quality-selector.css'; 7 | import { UI } from '@/constants'; 8 | import { useScreen } from '@/contexts/screen'; 9 | import classNames from 'classnames'; 10 | 11 | type Source = { 12 | src: string; 13 | type: string; 14 | label?: string; // video quality label 15 | selected?: boolean; // default video quality to load 16 | }; 17 | 18 | export interface VideoPlayerProps { 19 | hasQualityLevels?: boolean; 20 | poster?: string; 21 | sources: Source[]; 22 | shape: 'square' | 'rounded'; 23 | } 24 | 25 | const videoOptions: VideoJsPlayerOptions = { 26 | controls: true, 27 | controlBar: { 28 | children: [ 29 | 'playToggle', 30 | 'volumePanel', 31 | 'currentTimeDisplay', 32 | 'timeDivider', 33 | 'durationDisplay', 34 | 'progressControl', 35 | 'qualitySelector', 36 | 'fullscreenToggle', 37 | ], 38 | }, 39 | fluid: true, 40 | }; 41 | 42 | export default function VideoPlayer(props: VideoPlayerProps): ReactElement { 43 | const { isMedium, isLarge, isHuge, isEnormous } = useScreen(); 44 | const { hasQualityLevels = false, poster, sources, shape = 'square' } = props; 45 | 46 | const videoWidth = (() => { 47 | let width = 320; 48 | if (isMedium) { 49 | width = 720; 50 | } 51 | if (isLarge || isHuge) { 52 | width = 672; 53 | } 54 | if (isEnormous) { 55 | width = 920; 56 | } 57 | return width; 58 | })(); 59 | 60 | if (hasQualityLevels) { 61 | QualitySelector(videojs); 62 | } 63 | videoOptions.poster = poster; 64 | videoOptions.sources = sources; 65 | 66 | const key = sources[0].src; 67 | const videoRef = useRef(null); 68 | 69 | const shapeClasses = [ 70 | shape === 'square' && '', 71 | shape === 'rounded' && 'rounded-2xl overflow-hidden', 72 | ]; 73 | 74 | useEffect(() => { 75 | if (null !== videoRef.current) { 76 | videojs(videoRef.current, videoOptions); 77 | } 78 | return () => { 79 | const players = videojs.getAllPlayers(); 80 | if (players && players.length > 0) { 81 | players.forEach((player) => { 82 | player.dispose(); 83 | }); 84 | } 85 | }; 86 | }, []); 87 | 88 | return ( 89 |
90 |
91 |
93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /components/cards/BenefitsCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import Image from 'next/image'; 3 | import classNames from 'classnames'; 4 | import redact from '@/utils/redact'; 5 | import { useScreen } from '@/contexts/screen'; 6 | 7 | interface Props { 8 | title: string; 9 | description?: string[]; 10 | images: string[]; // toggle images on hover [original, redacted] 11 | imageAlt: string; 12 | imageWidth: string; 13 | imageHeight: string; 14 | classes?: string; 15 | } 16 | 17 | export default function BenefitsCard(props: Props): ReactElement { 18 | const { isSmall } = useScreen(); 19 | const { 20 | title, 21 | description, 22 | images, 23 | imageAlt, 24 | imageWidth, 25 | imageHeight, 26 | classes, 27 | } = props; 28 | const redactedClasses = redact({ 29 | redactColor: 'gray-dark', 30 | textColor: 'gray-dark', 31 | }); 32 | const renderImages = (() => { 33 | if (isSmall) { 34 | return ( 35 | {imageAlt} 43 | ); 44 | } else { 45 | return images.map((img, index) => { 46 | return ( 47 |
55 | {imageAlt} 63 |
64 | ); 65 | }); 66 | } 67 | })(); 68 | 69 | const renderDescription = (() => { 70 | return description?.map((line, index) => { 71 | return ( 72 |

76 | {line} 77 |

78 | ); 79 | }); 80 | })(); 81 | 82 | // parent container must have 'flex' class 83 | return ( 84 |
91 |
92 | {renderImages} 93 |
94 |

{title}

95 |
96 | {renderDescription} 97 |
98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /components/cards/PostCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { IPost } from '@/types/cms'; 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | import { ReactElement } from 'react'; 5 | import classNames from 'classnames'; 6 | 7 | interface Props extends IPost { 8 | route: string; 9 | featured?: boolean; 10 | hoverEffect?: boolean; 11 | compact?: boolean; 12 | classes?: string; 13 | } 14 | 15 | export default function PostCard(props: Props): ReactElement { 16 | const { 17 | title, 18 | description, 19 | subtitle, 20 | featureImage, 21 | publishedDate, 22 | author, 23 | slug, 24 | route, 25 | featured, 26 | hoverEffect = !featured, 27 | compact = false, 28 | classes, 29 | } = props; 30 | const headingClasses = 'cursor-pointer text-2xl font-bold mb-3'; 31 | // parent container must have 'flex' class 32 | return ( 33 |
40 | {featureImage?.imageUrl && ( 41 | 42 |
52 | {featureImage?.description 66 |
67 | 68 | )} 69 |
72 | 73 | 74 | {featured ? ( 75 |

81 | {title} 82 |

83 | ) : ( 84 |

{title}

85 | )} 86 |
87 | 88 |

89 | {publishedDate} 90 | {author && author.name && / {author.name}} 91 |

92 | {!compact && ( 93 |

99 | {description} 100 |

101 | )} 102 | {featured && ( 103 | 104 | 105 | Read More » 106 | 107 | 108 | )} 109 |
110 |
111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /components/cards/index.tsx: -------------------------------------------------------------------------------- 1 | import PostCard from './PostCard'; 2 | import BenefitsCard from './BenefitsCard'; 3 | 4 | export { PostCard, BenefitsCard }; 5 | -------------------------------------------------------------------------------- /components/navigation/Nav/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, useState } from 'react'; 2 | 3 | import { Button } from '@/components/ui'; 4 | import { ReactComponent as CloseSVG } from '@/assets/svgs/close.svg'; 5 | import Image from 'next/image'; 6 | import Link from 'next/link'; 7 | import { ReactComponent as MenuSVG } from '@/assets/svgs/hamburger.svg'; 8 | import { NAVIGATION } from '@/constants'; 9 | import { NavItem } from '@/components/navigation'; 10 | import classNames from 'classnames'; 11 | 12 | export default function Nav(): ReactElement { 13 | const [isExpanded, setIsExpanded] = useState(false); 14 | const toggleNav = () => { 15 | setIsExpanded(!isExpanded); 16 | }; 17 | const mobileNavButtonClasses = 'w-5 h-5 fill-current'; 18 | return ( 19 | 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /components/navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import Footer from './Footer'; 2 | import Nav from './Nav'; 3 | import NavItem from './NavItem'; 4 | 5 | export { Nav, NavItem, Footer }; 6 | -------------------------------------------------------------------------------- /components/posts/Post/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | import classNames from 'classnames'; 5 | 6 | import { IPost } from '@/types/cms'; 7 | import { PostList } from '@/components/posts'; 8 | import RichBody from '@/components/RichBody'; 9 | import { useScreen } from '@/contexts/screen'; 10 | import Container from '@/components/Container'; 11 | 12 | interface Props { 13 | post: IPost; 14 | otherPosts?: IPost[]; 15 | } 16 | 17 | export default function Post(props: Props): ReactElement { 18 | const { isSmall, isMedium } = useScreen(); 19 | const { post, otherPosts } = props; 20 | const { 21 | title, 22 | subtitle, 23 | author, 24 | tags, 25 | publishedDate, 26 | featureImage, 27 | fullHeader, 28 | description, 29 | body, 30 | } = post; 31 | const renderTags = (() => { 32 | return tags.map((tag, index) => { 33 | return ( 34 | 35 | 36 | 37 | {tag} 38 | 39 | 40 | {index < tags.length - 1 && ', '} 41 | 42 | ); 43 | }); 44 | })(); 45 | return ( 46 |
47 | 56 | {featureImage?.imageUrl && ( 57 |
63 | {fullHeader ? ( 64 | {featureImage?.description 74 | ) : ( 75 | {featureImage?.description 84 | )} 85 |
86 | )} 87 |
88 | 95 |

96 | {title} 97 |

98 |

104 | {publishedDate} 105 | {author && author.name && / {author.name}} 106 | {renderTags} 107 |

108 | 112 |
113 | {otherPosts && ( 114 | 121 | )} 122 |
123 | ); 124 | } 125 | -------------------------------------------------------------------------------- /components/posts/PostList/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | import { IPost } from '@/types/cms'; 5 | import { generateRoute } from '@/services/cms'; 6 | import Container from '@/components/Container'; 7 | import { Headline } from '@/components/ui'; 8 | import { PostCard } from '@/components/cards'; 9 | import { UI } from '@/constants'; 10 | 11 | interface Props { 12 | posts: IPost[]; 13 | gridStyle?: 'blog' | 'normal'; 14 | showHeading?: boolean; 15 | hoverEffect?: boolean; 16 | compact?: boolean; 17 | classes?: string; 18 | } 19 | 20 | export default function PostList(props: Props): ReactElement { 21 | const { 22 | posts, 23 | gridStyle = 'normal', 24 | showHeading = true, 25 | hoverEffect, 26 | compact, 27 | classes, 28 | } = props; 29 | const cardClasses = classNames( 30 | 'md:w-1/2 mb-5', 31 | 'lg:w-1/3 lg:max-w-sm lg:px-3' 32 | ); 33 | const gridClasses = [ 34 | gridStyle === 'normal' && 'lg:max-w-screen-xl', 35 | gridStyle === 'blog' && 'lg:max-w-screen-lg', 36 | ]; 37 | return ( 38 |
39 | {showHeading && ( 40 | 50 | More posts 51 | 52 | )} 53 | 61 | {posts?.map((post) => { 62 | return ( 63 | 71 | ); 72 | })} 73 | 74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /components/posts/index.tsx: -------------------------------------------------------------------------------- 1 | import Post from './Post'; 2 | import PostList from './PostList'; 3 | 4 | export { Post, PostList }; 5 | -------------------------------------------------------------------------------- /components/sections/About/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | import { ReactElement, useEffect, useRef, useState } from 'react'; 3 | 4 | import Container from '@/components/Container'; 5 | import { Headline } from '@/components/ui'; 6 | import { VideoPlayerProps } from '@/components/VideoPlayer'; 7 | import classNames from 'classnames'; 8 | import dynamic from 'next/dynamic'; 9 | import redact from '@/utils/redact'; 10 | import { useScreen } from '@/contexts/screen'; 11 | 12 | // optimise build sizes by loading dynamically 13 | const DynamicVideoPlayer = dynamic(() => import('@/components/VideoPlayer')); 14 | 15 | export default function About(): ReactElement { 16 | const textRef = useRef(null); 17 | const { isSmall, isMedium } = useScreen(); 18 | const redactedOptions = { 19 | redactColor: 'primary', 20 | textColor: 'white', 21 | animate: true, 22 | classes: 'p-1', 23 | }; 24 | const [redactedClasses, setRedactedClasses] = useState( 25 | redact(redactedOptions) 26 | ); 27 | const videoProps: VideoPlayerProps = { 28 | hasQualityLevels: true, 29 | shape: 'square', 30 | poster: '/assets/videos/this-is-session/thumbnail.webp', 31 | sources: [ 32 | { 33 | src: '/assets/videos/this-is-session/1080p.mp4', 34 | type: 'video/mp4', 35 | label: '1080p', 36 | }, 37 | { 38 | src: '/assets/videos/this-is-session/720p.mp4', 39 | type: 'video/mp4', 40 | label: '720p', 41 | selected: true, 42 | }, 43 | { 44 | src: '/assets/videos/this-is-session/480p.mp4', 45 | type: 'video/mp4', 46 | label: '480p', 47 | }, 48 | { 49 | src: '/assets/videos/this-is-session/360p.mp4', 50 | type: 'video/mp4', 51 | label: '360p', 52 | }, 53 | { 54 | src: '/assets/videos/this-is-session/240p.mp4', 55 | type: 'video/mp4', 56 | label: '240p', 57 | }, 58 | { 59 | src: '/assets/videos/this-is-session/144p.mp4', 60 | type: 'video/mp4', 61 | label: '144p', 62 | }, 63 | ], 64 | }; 65 | 66 | useEffect(() => { 67 | if (isSmall || isMedium) { 68 | const onScroll = () => { 69 | const scrollEffectStart = 70 | textRef.current?.offsetTop! - textRef.current?.scrollHeight! - 28; 71 | const scrollEffectStop = textRef.current?.offsetTop! - 28; 72 | 73 | if ( 74 | window.scrollY >= scrollEffectStart && 75 | window.scrollY < scrollEffectStop 76 | ) { 77 | setRedactedClasses(redact({ ...redactedOptions, disabled: true })); 78 | } 79 | if ( 80 | window.scrollY < scrollEffectStart || 81 | window.scrollY >= scrollEffectStop 82 | ) { 83 | setRedactedClasses(redact(redactedOptions)); 84 | } 85 | }; 86 | document.addEventListener('scroll', onScroll); 87 | return () => { 88 | document.removeEventListener('scroll', onScroll); 89 | }; 90 | } 91 | }, [isSmall, isMedium]); 92 | return ( 93 |
94 | 102 |

What is Session?

103 |
104 | {/* Full screen height - Headline height */} 105 | 120 |

131 | Session is an end-to-end{' '} 132 | encrypted messenger that minimises{' '} 133 | sensitive metadata,{' '} 134 | designed and built for people 135 | who want absolute privacy and 136 | freedom from any form of{' '} 137 | surveillance. 138 |

139 | 140 |
141 |
142 | ); 143 | } 144 | -------------------------------------------------------------------------------- /components/sections/Benefits/index.tsx: -------------------------------------------------------------------------------- 1 | import { BenefitsCard } from '@/components/cards'; 2 | import Container from '@/components/Container'; 3 | import { Headline } from '@/components/ui'; 4 | import { ReactElement } from 'react'; 5 | import classNames from 'classnames'; 6 | 7 | export default function Benefits(): ReactElement { 8 | const cardClasses = classNames('w-1/2 mb-5', 'lg:w-full lg:max-w-sm lg:px-8'); 9 | const imageWidth = '500px'; 10 | const imageHeight = '500px'; 11 | return ( 12 |
13 | 22 |

Benefits

23 |
24 | 38 |
47 | 62 | 77 | 92 | 107 | 122 |
123 |
124 |
125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /components/sections/EmailSignup/index.tsx: -------------------------------------------------------------------------------- 1 | import { FormEventHandler, ReactElement, useRef, useState } from 'react'; 2 | 3 | import { Button } from '@/components/ui'; 4 | import Container from '@/components/Container'; 5 | import { GroupNotice } from '@/components/sections'; 6 | import classNames from 'classnames'; 7 | import { useScreen } from '@/contexts/screen'; 8 | 9 | export default function EmailSignup(): ReactElement { 10 | const { isSmall } = useScreen(); 11 | const buttonRef = useRef(null); 12 | const setButtonText = (value: string) => { 13 | if (null !== buttonRef.current) { 14 | buttonRef.current.innerText = value; 15 | } 16 | }; 17 | const [email, setEmail] = useState(''); 18 | const handleSubscription: FormEventHandler = async (event) => { 19 | event.preventDefault(); 20 | setButtonText('Subscribing...'); 21 | let response; 22 | try { 23 | response = await fetch('/api/email/session', { 24 | method: 'POST', 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | }, 28 | body: JSON.stringify({ email }), 29 | }); 30 | switch (response.status) { 31 | case 201: 32 | setEmail(''); 33 | setButtonText('Signed up ✓'); 34 | break; 35 | case 400: 36 | default: 37 | setButtonText('Signup failed ✗'); 38 | console.error( 39 | 'Email API Code', 40 | response.status, 41 | await response.json() 42 | ); 43 | break; 44 | } 45 | } catch (error) { 46 | response = error; 47 | } 48 | }; 49 | return ( 50 |
51 | {isSmall && } 52 | 56 |

62 | Friends don’t let friends use compromised messengers. 63 |

64 |

65 | Sign up to the mailing list and start taking action! 66 |

67 |
68 | setEmail(e.target.value)} 73 | className={classNames( 74 | 'block w-5/6 mb-4 text-sm border border-black rounded-sm bg-primary', 75 | 'md:w-1/2', 76 | 'lg:w-2/5', 77 | 'placeholder-black placeholder-opacity-60' 78 | )} 79 | required 80 | /> 81 | 92 |
93 |
94 |
95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /components/sections/Features/index.tsx: -------------------------------------------------------------------------------- 1 | import Container from '@/components/Container'; 2 | import { Headline } from '@/components/ui'; 3 | import Image from 'next/image'; 4 | import { ReactElement } from 'react'; 5 | import classNames from 'classnames'; 6 | import { useScreen } from '@/contexts/screen'; 7 | 8 | export default function Features(): ReactElement { 9 | const { isSmall, isMedium, isLarge, isHuge, isEnormous } = useScreen(); 10 | const headingClasses = classNames( 11 | 'font-helvetica text-4xl font-bold text-gray-dark mb-1' 12 | ); 13 | const paragraphClasses = classNames( 14 | 'text-gray-lighter leading-6 mb-8', 15 | 'md:mb-12' 16 | ); 17 | return ( 18 |
19 | 28 |

Features

29 |
30 | 43 |
50 | {(isSmall || isMedium) && ( 51 |
54 | mobile app direct message screenshot 63 |
64 | )} 65 |
73 |

Group chats

74 |

75 | Talk to your friends or talk to the world. You decide. 76 | Groups let you talk to up to 100 friends at once, with the same 77 | encrypted protections as one-on-one chats. Got a bigger crowd? Use 78 | a community to connect with as many people as you want. 79 |

80 |

Voice messages

81 |

82 | Sometimes, a text just isn’t enough. Voice messages let you send 83 | something a little more personal, so nothing gets lost in 84 | translation. 85 |

86 |

Attachments

87 |

88 | Don’t leak those docs. Send all your files, images, and 89 | attachments through a network that takes your privacy seriously. 90 |

91 |
92 | {(isLarge || isHuge || isEnormous) && ( 93 |
100 | desktop app screenshot 108 |
109 | )} 110 |
111 |
112 |
113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /components/sections/GroupNotice/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { ReactElement } from 'react'; 3 | import classNames from 'classnames'; 4 | 5 | interface Props { 6 | classes?: string; 7 | } 8 | 9 | export default function GroupNotice(props: Props): ReactElement { 10 | const { classes } = props; 11 | return ( 12 |
19 |

20 | Join the movement to keep the internet private! 21 |

22 |

23 | Chat with like-minded individuals in the{' '} 24 | 25 | 32 | Session Community. 33 | 34 | 35 |

36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /components/sections/Hero/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactComponent as AndroidSVG } from '@/assets/svgs/android_robot_head.svg'; 2 | import { ReactComponent as AppleSVG } from '@/assets/svgs/apple.svg'; 3 | import { Button } from '@/components/ui'; 4 | import Container from '@/components/Container'; 5 | import { ReactComponent as DesktopSVG } from '@/assets/svgs/desktop.svg'; 6 | import Image from 'next/image'; 7 | import Link from 'next/link'; 8 | import { ReactElement } from 'react'; 9 | import classNames from 'classnames'; 10 | import { useScreen } from '@/contexts/screen'; 11 | import { ReactComponent as FDroidSVG } from '@/assets/svgs/fdroid-logo.svg'; 12 | 13 | export default function Hero(): ReactElement { 14 | const { isSmall, isMedium, isLarge, isHuge, isEnormous } = useScreen(); 15 | const headingClasses = classNames( 16 | 'text-5xl font-semibold text-gray-dark', 17 | 'lg:text-6xl' 18 | ); 19 | const downloadLinkClasses = 'text-3xl font-bold text-primary mb-7'; 20 | const downloadSVGClasses = 'inline-block mx-3 -mt-2 fill-current'; 21 | return ( 22 |
23 | 31 |
37 |
38 |

39 | Send 40 | 41 | Messages, 42 | 43 | Not Metadata. 44 |

45 | 93 | 94 | 95 | 98 | 99 | 100 |
101 | {(isSmall || isMedium) && ( 102 |
103 | mobile app creat account screenshot 111 |
112 | )} 113 | {(isLarge || isHuge || isEnormous) && ( 114 |
115 | mobile app ui showcase 123 |
124 | )} 125 |
126 |
127 |
128 | ); 129 | } 130 | -------------------------------------------------------------------------------- /components/sections/index.tsx: -------------------------------------------------------------------------------- 1 | import About from './About'; 2 | import Benefits from './Benefits'; 3 | import EmailSignup from './EmailSignup'; 4 | import Features from './Features'; 5 | import GroupNotice from './GroupNotice'; 6 | import Hero from './Hero'; 7 | 8 | export { About, Benefits, EmailSignup, Features, GroupNotice, Hero }; 9 | -------------------------------------------------------------------------------- /components/ui/Accordion/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | import { ReactElement, useEffect, useRef, useState } from 'react'; 3 | 4 | import { ReactComponent as LinkSVG } from '@/assets/svgs/link.svg'; 5 | import { Document } from '@contentful/rich-text-types'; 6 | import Link from 'next/link'; 7 | import { ReactComponent as MinusSVG } from '@/assets/svgs/minus.svg'; 8 | import { ReactComponent as PlusSVG } from '@/assets/svgs/plus.svg'; 9 | import RichBody from '@/components/RichBody'; 10 | import classNames from 'classnames'; 11 | 12 | interface Props { 13 | id: string; 14 | question: string; 15 | answer: Document; 16 | expand?: boolean; 17 | classes?: string; 18 | } 19 | 20 | const handleNewHeight = (e: Event, container: HTMLElement, id: string) => { 21 | const oldHeight = Number(container?.style.height.slice(0, -2)); 22 | if ( 23 | document.querySelector(`#${id}container .showExternalVideoButton`) !== null 24 | ) { 25 | const target = e.currentTarget as HTMLButtonElement; 26 | // adding the height of the video (500|240) - the height of the disappearing button's component (185.5) = 314.5|54.5 27 | const isYoutube = target.getAttribute('data-video-site') === 'YouTube'; 28 | container.style.height = `${oldHeight + (isYoutube ? 314.5 : 54.5)}px`; 29 | } 30 | }; 31 | 32 | export default function Accordion(props: Props): ReactElement { 33 | const { id, question, answer, expand, classes } = props; 34 | const content = useRef(null); 35 | const [isExpanded, setIsExpanded] = useState(true); 36 | const [height, setHeight] = useState(`${content?.current?.scrollHeight}px`); 37 | const [loaded, setLoaded] = useState(false); 38 | 39 | const handleExpand = () => { 40 | setIsExpanded(!isExpanded); 41 | setHeight(isExpanded ? '0px' : `${content?.current?.scrollHeight}px`); 42 | }; 43 | const svgClasses = classNames('w-3 h-3 fill-current mb-1 mr-2'); 44 | 45 | useEffect(() => { 46 | const buttons = window?.document.querySelectorAll( 47 | '.showExternalVideoButton' 48 | ); 49 | const container = window?.document.getElementById(id + 'container')!; 50 | 51 | buttons.forEach((button) => { 52 | button?.addEventListener('click', (e: Event) => 53 | handleNewHeight(e, container, id) 54 | ); 55 | }); 56 | 57 | return () => { 58 | buttons.forEach((button) => { 59 | button?.removeEventListener('click', (e: Event) => 60 | handleNewHeight(e, container, id) 61 | ); 62 | }); 63 | }; 64 | }, []); 65 | 66 | useEffect(() => { 67 | if (!expand) { 68 | handleExpand(); 69 | } else { 70 | setHeight(`${content?.current?.scrollHeight}px`); 71 | } 72 | setLoaded(true); 73 | }, []); 74 | 75 | useEffect(() => { 76 | if (loaded && expand) { 77 | handleExpand(); 78 | } 79 | }, [expand]); 80 | 81 | return ( 82 |
90 |
101 | {loaded && ( 102 | <> 103 | 109 | 115 | 116 | )} 117 | {question} 118 | 119 | 123 | 131 | 132 | 133 |
134 |
144 | 148 |
149 |
150 | ); 151 | } 152 | -------------------------------------------------------------------------------- /components/ui/Banner/index.tsx: -------------------------------------------------------------------------------- 1 | import { BANNER } from '@/constants'; 2 | import Button from '../Button'; 3 | import Link from 'next/link'; 4 | import { ReactElement } from 'react'; 5 | import classNames from 'classnames'; 6 | import { useScreen } from '@/contexts/screen'; 7 | 8 | export default function Banner(): ReactElement { 9 | const { isSmall } = useScreen(); 10 | return ( 11 |
18 | 19 | {isSmall ? BANNER.TEXT.MOBILE : BANNER.TEXT.DESKTOP} 20 | 21 | 24 | 25 | 26 | 33 | 34 | 35 | 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /components/ui/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import { LegacyRef, ReactElement } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface Props { 5 | bgColor?: 'primary' | 'black' | 'none'; 6 | textColor?: 'primary' | 'black'; 7 | size?: 'small' | 'medium' | 'large'; 8 | shape?: 'round' | 'semiround' | 'square'; 9 | fontWeight?: 'normal' | 'semibold' | 'bold'; 10 | animate?: boolean; 11 | hoverEffect?: boolean; 12 | type?: 'submit'; 13 | reference?: LegacyRef; 14 | classes?: string; 15 | children?: string; 16 | onClick?(): any; 17 | } 18 | 19 | export default function Button(props: Props): ReactElement { 20 | const { 21 | bgColor = 'primary', 22 | textColor = 'black', 23 | fontWeight = 'normal', 24 | size = 'medium', 25 | shape = 'round', 26 | type, 27 | reference, 28 | animate = false, 29 | hoverEffect = true, 30 | classes, 31 | children, 32 | onClick, 33 | } = props; 34 | // See Gotchas in README 35 | const bgClasses = [ 36 | bgColor === 'primary' && 'bg-primary', 37 | bgColor === 'black' && 'bg-black', 38 | bgColor === 'none' && 'bg-transparent', 39 | ]; 40 | const textClasses = [ 41 | textColor === 'primary' && 'text-primary', 42 | textColor === 'black' && 'text-black', 43 | ]; 44 | const hoverClasses = [ 45 | bgColor === 'primary' && 'hover:bg-black hover:text-primary', 46 | bgColor === 'black' && 'hover:bg-primary hover:text-black', 47 | (hoverEffect || animate) && 'transition-colors duration-300', 48 | ]; 49 | const sizeClasses = [ 50 | size === 'small' && 'text-sm py-1 px-7', 51 | size === 'medium' && 'py-2 px-7', 52 | size === 'large' && 'py-2 px-11', 53 | ]; 54 | const shapeClasses = [ 55 | shape === 'round' && 'rounded-3xl', 56 | shape === 'semiround' && 'rounded-lg', 57 | shape === 'square' && 'rounded-sm', 58 | ]; 59 | const fontClasses = [ 60 | fontWeight === 'normal' && 'font-normal', 61 | fontWeight === 'semibold' && 'font-semibold', 62 | fontWeight === 'bold' && 'font-bold', 63 | ]; 64 | 65 | return ( 66 | 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /components/ui/Headline/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import classNames from 'classnames'; 3 | import { IContainerSizes } from '@/components/Container'; 4 | import { useScreen } from '@/contexts/screen'; 5 | 6 | interface Props { 7 | color?: 'primary' | 'gray-dark'; 8 | containerWidths?: IContainerSizes; 9 | classes?: string; 10 | children?: ReactElement | string; 11 | } 12 | 13 | export default function Headline(props: Props): ReactElement { 14 | const { color = 'primary', containerWidths, classes, children } = props; 15 | const { isSmall, isMedium, isLarge, isHuge, isEnormous } = useScreen(); 16 | const containerWidth: string | undefined = (() => { 17 | if (isSmall) return containerWidths?.small; 18 | if (isMedium) return containerWidths?.medium; 19 | if (isLarge) return containerWidths?.large; 20 | if (isHuge || isEnormous) 21 | return containerWidths?.huge ?? containerWidths?.large; 22 | })(); 23 | const colorClasses = [ 24 | color === 'primary' && 'text-primary', 25 | color === 'gray-dark' && 'text-gray-dark', 26 | ]; 27 | const borderClasses = [ 28 | color === 'primary' && 'border-primary', 29 | color === 'gray-dark' && 'border-gray-dark', 30 | ]; 31 | return ( 32 |
33 |
42 |
43 |
47 | {children} 48 |
49 |
50 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /components/ui/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Footer, Nav } from '@/components/navigation'; 2 | import { ReactElement, ReactNode, useEffect, useState } from 'react'; 3 | 4 | import { Banner } from '@/components/ui'; 5 | import CustomHead from '@/components/CustomHead'; 6 | import { EmailSignup } from '@/components/sections'; 7 | import { IMetadata } from '@/constants/metadata'; 8 | import LockedPage from '@/components/LockedPage'; 9 | import { useRouter } from 'next/router'; 10 | 11 | interface Props { 12 | title?: string; 13 | metadata?: IMetadata; 14 | children: ReactNode; 15 | showBanner?: boolean; 16 | } 17 | 18 | export default function Layout({ 19 | title, 20 | metadata, 21 | children, 22 | showBanner = false, 23 | }: Props): ReactElement { 24 | const router = useRouter(); 25 | const [locked, setLocked] = useState(false); 26 | 27 | useEffect(() => { 28 | // deny access to the staging environment unless you login with the correct token 29 | if ( 30 | process.env.NODE_ENV === 'production' && 31 | process.env.NEXT_PUBLIC_SITE_ENV === 'development' && 32 | !router.isPreview 33 | ) { 34 | setLocked(true); 35 | } 36 | }, [router.isPreview]); 37 | 38 | return ( 39 | <> 40 | 41 | {showBanner && } 42 |