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
This commit is contained in:
Tommy Chow 2024-09-15 21:43:51 -07:00 • committed by GitHub
parent 07c23e64e9
commit c7d6de8924
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 4544 additions and 0 deletions

2
web/.eslintignore Normal file
View file

@ -0,0 +1,2 @@
*.js
*.mjs

24
web/.eslintrc.json Normal file
View 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
View 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
View file

@ -0,0 +1 @@
pnpm-lock.yaml

5
web/.prettierrc.json Normal file
View file

@ -0,0 +1,5 @@
{
"singleQuote": true,
"jsxSingleQuote": true,
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"]
}

21
web/LICENSE Normal file
View 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
View 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
View file

@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

40
web/package.json Normal file
View 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

File diff suppressed because it is too large Load diff

9
web/postcss.config.mjs Normal file
View 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
View 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

Binary file not shown.

3
web/src/app/globals.css Normal file
View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

19
web/src/app/icon.svg Normal file
View 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
View 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>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

221
web/src/app/page.tsx Normal file
View 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>
);
}

View 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>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>;
}

View 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
View 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
View 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
View 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"]
}