Migrate and redesign website (#398)
* add initial web redesign * add emote marquee * add faq section * tweak copy * tweak faq styles * update video and style tweaks * add header links * fix bg color * tweak styles * delete old image * update metadata and favicon * add ogimage * update faq * upgrade packages
2
web/.eslintignore
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
*.js
|
||||||
|
*.mjs
|
||||||
24
web/.eslintrc.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"parserOptions": {
|
||||||
|
"project": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:jsx-a11y/recommended",
|
||||||
|
"next/core-web-vitals",
|
||||||
|
"plugin:@typescript-eslint/recommended-type-checked",
|
||||||
|
"plugin:@typescript-eslint/stylistic-type-checked",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"jsx-a11y/control-has-associated-label": "warn",
|
||||||
|
"@typescript-eslint/consistent-type-imports": [
|
||||||
|
"warn",
|
||||||
|
{ "fixStyle": "inline-type-imports" }
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-misused-promises": [
|
||||||
|
"error",
|
||||||
|
{ "checksVoidReturn": { "attributes": false } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
36
web/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
1
web/.prettierignore
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pnpm-lock.yaml
|
||||||
5
web/.prettierrc.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"]
|
||||||
|
}
|
||||||
21
web/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Tommy Chow
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
36
web/README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# [next-template](https://github.com/tommyxchow/next-template)
|
||||||
|
|
||||||
|
A tailored Next.js template project for getting started quickly.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
I spend a bit of tme repeating the same steps when setting up Next.js projects. This template builds upon [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app) to include my essential packages, configurations, linting rules, and more by default.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Precofigured packages
|
||||||
|
- Preconfigured ESLint and Prettier rules
|
||||||
|
- Default boilerplate removed
|
||||||
|
- Dark mode support
|
||||||
|
- Formatted README
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- [Next.js](https://nextjs.org/)
|
||||||
|
- [next-themes](https://github.com/pacocoursey/next-themes)
|
||||||
|
- [Tailwind CSS](https://tailwindcss.com/) (with [Forms](https://github.com/tailwindlabs/tailwindcss-forms) and [Typography](https://tailwindcss.com/docs/typography-plugin) plugins)
|
||||||
|
- [tailwind-merge](https://github.com/dcastil/tailwind-merge)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Setup instructions are here if necessary. Usually adding environment variables like so:
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
# .env.local
|
||||||
|
CLIENT_ID=
|
||||||
|
SECRET=
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
next-template is licensed under [MIT](LICENSE).
|
||||||
4
web/next.config.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
40
web/package.json
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"name": "frostyapp.io",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "^14.2.11",
|
||||||
|
"next-themes": "^0.3.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-fast-marquee": "^1.6.5",
|
||||||
|
"react-icons": "^5.3.0",
|
||||||
|
"tailwind-merge": "^2.5.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
|
"@types/node": "^22.5.5",
|
||||||
|
"@types/react": "^18.3.5",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
||||||
|
"@typescript-eslint/parser": "^8.5.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-config-next": "^14.2.11",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.10.0",
|
||||||
|
"postcss": "^8.4.47",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.6",
|
||||||
|
"tailwindcss": "^3.4.11",
|
||||||
|
"typescript": "^5.6.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
3797
web/pnpm-lock.yaml
generated
Normal file
9
web/postcss.config.mjs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
19
web/public/logo.svg
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_2_12" x1="50" y1="0" x2="50" y2="100" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#9146FF"/>
|
||||||
|
<stop offset="1"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<circle cx="50" cy="50" r="50" fill="url(#paint0_linear_2_12)"/>
|
||||||
|
<mask id="mask0_2_12" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="15" y="32" width="70" height="33">
|
||||||
|
<rect x="15" y="32" width="69.7793" height="33" fill="white"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_2_12)">
|
||||||
|
<path d="M6.24302 73.762L44.1917 35.8133C46.1718 33.8332 47.1619 32.8431 48.3035 32.4722C49.3077 32.1459 50.3895 32.1459 51.3937 32.4722C52.5353 32.8431 53.5254 33.8332 55.5054 35.8133L56.3666 36.6744L12.761 80.28L6.24302 73.762Z" fill="white"/>
|
||||||
|
<path d="M44.2383 35.8133C46.2184 33.8332 47.2085 32.8432 48.3501 32.4722C49.3543 32.1459 50.4361 32.1459 51.4403 32.4722C52.5819 32.8432 53.572 33.8332 55.552 35.8133L93.5008 73.762L86.9827 80.28L43.3772 36.6745L44.2383 35.8133Z" fill="white"/>
|
||||||
|
<g style="mix-blend-mode:lighten">
|
||||||
|
<rect x="38.6633" y="42.4162" width="61.6676" height="9.21788" transform="rotate(45 38.6633 42.4162)" fill="white"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
web/public/video.webm
Normal file
3
web/src/app/globals.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
19
web/src/app/icon.svg
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_2_12" x1="50" y1="0" x2="50" y2="100" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#9146FF"/>
|
||||||
|
<stop offset="1"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<circle cx="50" cy="50" r="50" fill="url(#paint0_linear_2_12)"/>
|
||||||
|
<mask id="mask0_2_12" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="15" y="32" width="70" height="33">
|
||||||
|
<rect x="15" y="32" width="69.7793" height="33" fill="white"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_2_12)">
|
||||||
|
<path d="M6.24302 73.762L44.1917 35.8133C46.1718 33.8332 47.1619 32.8431 48.3035 32.4722C49.3077 32.1459 50.3895 32.1459 51.3937 32.4722C52.5353 32.8431 53.5254 33.8332 55.5054 35.8133L56.3666 36.6744L12.761 80.28L6.24302 73.762Z" fill="white"/>
|
||||||
|
<path d="M44.2383 35.8133C46.2184 33.8332 47.2085 32.8432 48.3501 32.4722C49.3543 32.1459 50.4361 32.1459 51.4403 32.4722C52.5819 32.8432 53.572 33.8332 55.552 35.8133L93.5008 73.762L86.9827 80.28L43.3772 36.6745L44.2383 35.8133Z" fill="white"/>
|
||||||
|
<g style="mix-blend-mode:lighten">
|
||||||
|
<rect x="38.6633" y="42.4162" width="61.6676" height="9.21788" transform="rotate(45 38.6633 42.4162)" fill="white"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
38
web/src/app/layout.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { Footer } from '@/components/Footer';
|
||||||
|
import { Header } from '@/components/Header';
|
||||||
|
import { Providers } from '@/components/Providers';
|
||||||
|
import { type Metadata } from 'next';
|
||||||
|
import { Inter } from 'next/font/google';
|
||||||
|
import { twJoin } from 'tailwind-merge';
|
||||||
|
import './globals.css';
|
||||||
|
|
||||||
|
const fontSans = Inter({ subsets: ['latin'], variable: '--font-sans' });
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Frosty for Twitch',
|
||||||
|
description: 'Frosty lets you watch Twitch with 7TV, BTTV, and FFZ emotes',
|
||||||
|
metadataBase: new URL('https://frostyapp.io'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang='en' suppressHydrationWarning>
|
||||||
|
<body
|
||||||
|
className={twJoin(
|
||||||
|
'mx-auto min-h-dvh max-w-screen-lg bg-neutral-100 font-sans text-neutral-950 dark:bg-black dark:text-neutral-100',
|
||||||
|
fontSans.variable,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Providers>
|
||||||
|
<Header />
|
||||||
|
<main className='grow px-4 lg:px-0'>{children}</main>
|
||||||
|
<Footer />
|
||||||
|
</Providers>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
web/src/app/opengraph-image.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
221
web/src/app/page.tsx
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
import screenshotCategories from '@/assets/screenshot-categories.png';
|
||||||
|
import screenshotChannel from '@/assets/screenshot-channel.png';
|
||||||
|
import screenshotFollowing from '@/assets/screenshot-following.png';
|
||||||
|
import screenshotSettings from '@/assets/screenshot-settings.png';
|
||||||
|
import { FeatureCard } from '@/components/FeatureCard';
|
||||||
|
import {
|
||||||
|
appStoreLink,
|
||||||
|
bttvLink,
|
||||||
|
emailLink,
|
||||||
|
ffzLink,
|
||||||
|
githubLink,
|
||||||
|
playStoreLink,
|
||||||
|
sevenTvLink,
|
||||||
|
twitchLink,
|
||||||
|
} from '@/lib/constants';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Marquee from 'react-fast-marquee';
|
||||||
|
import { SiApple, SiGoogleplay } from 'react-icons/si';
|
||||||
|
|
||||||
|
const NUM_ROWS = 10;
|
||||||
|
const NUM_COLUMNS = 40;
|
||||||
|
|
||||||
|
const coreFeatures = [
|
||||||
|
{
|
||||||
|
caption: 'See and pin followed channels',
|
||||||
|
screenshot: screenshotFollowing,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caption: 'Explore top streams and categories',
|
||||||
|
screenshot: screenshotCategories,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caption: 'Watch and chat with 7TV, BTTV, and FFZ emotes',
|
||||||
|
screenshot: screenshotChannel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caption: 'Customize a variety of settings',
|
||||||
|
screenshot: screenshotSettings,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const faqs = [
|
||||||
|
{
|
||||||
|
question: 'Why are some Twitch features not in Frosty?',
|
||||||
|
answer:
|
||||||
|
'The Twitch API only exposes a limited set of functionality to developers. Features like predictions, polls, pinned messages, VODs with chat, stream qualities, total view count for categories and more are not available.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Why is the stream delayed on iOS?',
|
||||||
|
answer:
|
||||||
|
'There is a delay of around 15 seconds due to how the native iOS player works. As a workaround, Frosty has a message delay option that lets you set the delay (in seconds) before each message is rendered.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Is ad block planned?',
|
||||||
|
answer:
|
||||||
|
'Ad block is not planned because it would probably violate the Twitch terms of service.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Will Frosty support Apple/Android TV?',
|
||||||
|
answer:
|
||||||
|
"Not yet, because Flutter (the framework that Frosty is built upon) doesn't officially support TVs.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Where can I report a bug or request a new feature?',
|
||||||
|
answer: (
|
||||||
|
<>
|
||||||
|
You can open a new issue on the{' '}
|
||||||
|
<a
|
||||||
|
className='underline'
|
||||||
|
href={githubLink}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
GitHub repo
|
||||||
|
</a>{' '}
|
||||||
|
or email{' '}
|
||||||
|
<a
|
||||||
|
className='underline'
|
||||||
|
href={emailLink}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
contact@frostyapp.io
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const downloadButtons = (
|
||||||
|
<div className='grid w-full grid-cols-2 gap-4 font-semibold text-neutral-100 md:gap-8'>
|
||||||
|
<a
|
||||||
|
className='flex items-center justify-center gap-2 rounded-xl bg-blue-800 p-4 transition hover:bg-blue-900'
|
||||||
|
href={appStoreLink}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
<SiApple />
|
||||||
|
App Store
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className='flex items-center justify-center gap-2 rounded-xl bg-green-800 p-4 transition hover:bg-green-900'
|
||||||
|
href={playStoreLink}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
<SiGoogleplay />
|
||||||
|
Google Play
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className='mt-24 flex flex-col gap-8 md:gap-16'>
|
||||||
|
<section className='grid w-full items-center rounded-2xl border border-neutral-300 dark:border-neutral-900 [&>*]:col-start-1 [&>*]:row-start-1'>
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
{Array.from({ length: NUM_ROWS }).map((_, rowIndex) => (
|
||||||
|
<Marquee key={rowIndex} direction={rowIndex % 2 ? 'left' : 'right'}>
|
||||||
|
{Array.from({ length: NUM_COLUMNS }).map((_, colIndex) => (
|
||||||
|
<Image
|
||||||
|
key={colIndex}
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
alt='pepeD'
|
||||||
|
unoptimized
|
||||||
|
src='https://cdn.7tv.app/emote/6072a16fdcae02001b44e614/4x.webp'
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Marquee>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='z-10 justify-self-center p-8'>
|
||||||
|
<video
|
||||||
|
className='h-[75dvh] max-h-[800px] rounded-xl border border-neutral-300 bg-black object-contain py-4 dark:border-neutral-900'
|
||||||
|
src='/video.webm'
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
disableRemotePlayback
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className='flex flex-col gap-4 md:gap-8'>
|
||||||
|
<h1 className='text-pretty text-center text-xl font-semibold decoration-2 underline-offset-4 md:text-2xl'>
|
||||||
|
Frosty lets you watch{' '}
|
||||||
|
<a
|
||||||
|
className='text-twitch-purple underline'
|
||||||
|
href={twitchLink}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
Twitch
|
||||||
|
</a>{' '}
|
||||||
|
with{' '}
|
||||||
|
<a
|
||||||
|
className='text-twitch-purple underline'
|
||||||
|
href={sevenTvLink}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
7TV
|
||||||
|
</a>
|
||||||
|
,{' '}
|
||||||
|
<a
|
||||||
|
className='text-twitch-purple underline'
|
||||||
|
href={bttvLink}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
BTTV
|
||||||
|
</a>
|
||||||
|
, and{' '}
|
||||||
|
<a
|
||||||
|
className='text-twitch-purple underline'
|
||||||
|
href={ffzLink}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
FFZ
|
||||||
|
</a>{' '}
|
||||||
|
emotes
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{downloadButtons}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-8 md:grid md:grid-cols-2'>
|
||||||
|
{coreFeatures.map((feature, index) => (
|
||||||
|
<FeatureCard key={index} {...feature} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section
|
||||||
|
className='mt-8 flex flex-col items-center gap-8 sm:mt-0'
|
||||||
|
id='faq'
|
||||||
|
>
|
||||||
|
<h2 className='text-lg font-semibold md:text-xl'>
|
||||||
|
Frequently asked questions
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className='w-full divide-y divide-neutral-300 overflow-clip rounded-xl border border-neutral-300 dark:divide-neutral-900 dark:border-neutral-900'>
|
||||||
|
{faqs.map((faq, index) => (
|
||||||
|
<details key={index}>
|
||||||
|
<summary className='p-8 font-medium transition hover:cursor-pointer hover:bg-neutral-200 dark:hover:bg-neutral-950'>
|
||||||
|
{faq.question}
|
||||||
|
</summary>
|
||||||
|
<p className='border-t border-neutral-300 p-8 text-neutral-600 dark:border-neutral-900 dark:text-neutral-300'>
|
||||||
|
{faq.answer}
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
77
web/src/app/privacy/page.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
export default function Privacy() {
|
||||||
|
return (
|
||||||
|
<article className='prose prose-neutral mt-32 !max-w-none dark:prose-invert'>
|
||||||
|
<h1>Privacy policy for Frosty</h1>
|
||||||
|
<p>Last updated: May 2023</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Frosty is an unofficial open-source mobile Twitch client/app for iOS and
|
||||||
|
Android. We are dedicated to protecting your privacy. Frosty does not
|
||||||
|
collect or share any personal information. However, we may gather
|
||||||
|
anonymous usage data and crash logs solely to improve the app. For more
|
||||||
|
information, please refer to the sections below.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Third-party services</h2>
|
||||||
|
<p>
|
||||||
|
Frosty uses and interacts with the following services in order to
|
||||||
|
provide the best experience possible:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Twitch</h3>
|
||||||
|
<p>
|
||||||
|
Frosty uses the official Twitch API to showcase live channels, connect
|
||||||
|
to chat, and provide additional features. You can optionally log in with
|
||||||
|
your Twitch account to access user-specific features, such as sending
|
||||||
|
chat messages and viewing your followed channels.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you log in using Twitch, Frosty will only ask you for the necessary
|
||||||
|
and required permissions to function. Frosty will then obtain your OAuth
|
||||||
|
access token and send requests to receive and transmit data to Twitch
|
||||||
|
only on your behalf. This access token is stored and encrypted locally
|
||||||
|
on your device only.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For more information on how Twitch handles your data, please refer to
|
||||||
|
their privacy policy.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>7TV, BetterTTV, and FrankerFaceZ</h3>
|
||||||
|
<p>
|
||||||
|
Frosty uses APIs from 7TV, BetterTTV (BTTV), and FrankerFaceZ (FFZ) to
|
||||||
|
display custom badges and emotes in chat. When you visit your own
|
||||||
|
channel, Frosty will request these services using your public Twitch ID
|
||||||
|
or username to obtain emotes and badges associated with your channel.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For more information on how 7TV, BTTV, and FFZ handle your data, please
|
||||||
|
refer to their respective privacy policies.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Firebase</h3>
|
||||||
|
<p>
|
||||||
|
Frosty utilizes Firebase for crash logs, usage data, and analytics to
|
||||||
|
aid in the development of new features, improvements, and bug fixes. The
|
||||||
|
collected data is anonymous and does not contain any personal
|
||||||
|
information. You can opt out of this data collection by turning off
|
||||||
|
crash logs and analytics in the settings.
|
||||||
|
</p>
|
||||||
|
<p>For more information, please refer to the Firebase privacy policy.</p>
|
||||||
|
|
||||||
|
<h2>Privacy policy updates</h2>
|
||||||
|
<p>
|
||||||
|
We may occasionally update this privacy policy, and the most recent
|
||||||
|
version will always be available on this page. We recommend reviewing
|
||||||
|
this privacy policy periodically for any changes. Changes to this
|
||||||
|
privacy policy become effective when they are posted on this page.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Contact</h2>
|
||||||
|
<p>
|
||||||
|
If you have any questions or suggestions about this privacy policy,
|
||||||
|
please feel free to contact us at contact@frostyapp.io.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
web/src/assets/screenshot-categories.png
Normal file
|
After Width: | Height: | Size: 990 KiB |
BIN
web/src/assets/screenshot-channel.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
web/src/assets/screenshot-following.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
web/src/assets/screenshot-settings.png
Normal file
|
After Width: | Height: | Size: 228 KiB |
26
web/src/components/FeatureCard.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import Image, { type StaticImageData } from 'next/image';
|
||||||
|
|
||||||
|
interface FeatureCardProps {
|
||||||
|
caption: string;
|
||||||
|
screenshot: StaticImageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeatureCard({ caption, screenshot }: FeatureCardProps) {
|
||||||
|
return (
|
||||||
|
<figure className='grid items-end overflow-clip rounded-2xl border border-neutral-300 dark:border-neutral-900 [&>*]:col-start-1 [&>*]:row-start-1'>
|
||||||
|
<div className='justify-self-center px-12 pt-12'>
|
||||||
|
<Image
|
||||||
|
className='max-h-[75dvh] w-fit rounded-xl border dark:border-neutral-900'
|
||||||
|
src={screenshot}
|
||||||
|
alt={caption}
|
||||||
|
placeholder='blur'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='border-t border-neutral-300 bg-neutral-100 px-4 py-8 dark:border-neutral-900 dark:bg-black'>
|
||||||
|
<figcaption className='text-pretty text-center font-medium text-neutral-600 dark:text-neutral-300'>
|
||||||
|
{caption}
|
||||||
|
</figcaption>
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
web/src/components/Footer.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { tommyLink } from '@/lib/constants';
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className='flex w-full items-center justify-center gap-4 px-4 py-8 text-sm lg:px-0'>
|
||||||
|
<p className='text-neutral-500 dark:text-neutral-400'>
|
||||||
|
Designed and developed by{' '}
|
||||||
|
<a
|
||||||
|
className='underline'
|
||||||
|
href={tommyLink}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
Tommy Chow
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
web/src/components/Header.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { donateLink, githubLink } from '@/lib/constants';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { SiBuymeacoffee, SiGithub } from 'react-icons/si';
|
||||||
|
import { ThemeToggle } from './ThemeToggle';
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
return (
|
||||||
|
<header className='fixed top-0 z-50 flex w-full max-w-screen-lg items-center justify-between gap-4 bg-gradient-to-b from-neutral-100 to-transparent px-4 py-8 dark:from-black lg:px-0'>
|
||||||
|
<Link className='flex items-center gap-2' href='/'>
|
||||||
|
<div className='relative size-8'>
|
||||||
|
<Image alt='Logo' src={`/logo.svg`} layout='fill' priority />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className='text-xl font-semibold'>Frosty</h1>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className='flex items-center gap-4'>
|
||||||
|
<a
|
||||||
|
className='transition-opacity hover:opacity-50'
|
||||||
|
href={donateLink}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
<SiBuymeacoffee className='size-6' />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
className='transition-opacity hover:opacity-50'
|
||||||
|
href={githubLink}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
<SiGithub className='size-6' />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
web/src/components/Providers.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ThemeProvider } from 'next-themes';
|
||||||
|
|
||||||
|
interface ProvidersProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Providers({ children }: ProvidersProps) {
|
||||||
|
return <ThemeProvider attribute='class'>{children}</ThemeProvider>;
|
||||||
|
}
|
||||||
33
web/src/components/ThemeToggle.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { HiMoon, HiSun } from 'react-icons/hi2';
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const { resolvedTheme, setTheme } = useTheme();
|
||||||
|
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted) return <div className='size-6' />;
|
||||||
|
|
||||||
|
const isDarkMode = resolvedTheme === 'dark';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className='transition-opacity hover:opacity-50'
|
||||||
|
aria-label={`Toggle ${isDarkMode ? 'light mode' : 'dark mode'}`}
|
||||||
|
onClick={() => setTheme(isDarkMode ? 'light' : 'dark')}
|
||||||
|
>
|
||||||
|
{isDarkMode ? (
|
||||||
|
<HiSun className='size-6' />
|
||||||
|
) : (
|
||||||
|
<HiMoon className='size-6' />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
web/src/lib/constants.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
export const githubLink = 'https://github.com/tommyxchow/frosty';
|
||||||
|
export const appStoreLink =
|
||||||
|
'https://apps.apple.com/us/app/frosty-for-twitch/id1603987585';
|
||||||
|
export const playStoreLink =
|
||||||
|
'https://play.google.com/store/apps/details?id=com.tommychow.frosty';
|
||||||
|
export const twitchLink = 'https://www.twitch.tv/';
|
||||||
|
export const bttvLink = 'https://www.betterttv.net/';
|
||||||
|
export const ffzLink = 'https://www.frankerfacez.com/';
|
||||||
|
export const sevenTvLink = 'https://7tv.app/';
|
||||||
|
export const donateLink = 'https://buymeacoffee.com/tommychow';
|
||||||
|
export const emailLink = 'mailto:contact@frostyapp.io';
|
||||||
|
export const tommyLink = 'https://www.tommychow.com/';
|
||||||
|
export const sourceLink = 'https://github.com/tommyxchow/frostyapp.io';
|
||||||
23
web/tailwind.config.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import forms from '@tailwindcss/forms';
|
||||||
|
import typography from '@tailwindcss/typography';
|
||||||
|
import { type Config } from 'tailwindcss';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
darkMode: 'class',
|
||||||
|
content: [
|
||||||
|
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['var(--font-sans)'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
'twitch-purple': '#9146ff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [forms, typography],
|
||||||
|
} satisfies Config;
|
||||||
26
web/tsconfig.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||