initial commit

This commit is contained in:
Yangshun 2023-03-20 08:59:10 +08:00
commit dbcc913a58
2123 changed files with 178177 additions and 0 deletions

37
.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnp
.pnp.js
.yarn
# testing
coverage
# next.js
.next/
out/
build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# turbo
.turbo
# build
dist

2
.prettierignore Normal file
View File

@ -0,0 +1,2 @@
.next
dist

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"bracketSameLine": true,
"printWidth": 80,
"proseWrap": "never",
"singleQuote": true,
"trailingComma": "all"
}

21
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,21 @@
{
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"cSpell.autoFormatConfigFile": true,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file",
"javascript.format.enable": true,
"json.format.enable": true,
"eslint.format.enable": false,
"css.format.enable": true,
"css.format.newlineBetweenRules": true,
"css.format.newlineBetweenSelectors": true,
"css.format.preserveNewLines": true,
"typescript.format.enable": true,
"editor.codeActionsOnSave": {
"source.fixAll": true
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}

73
README.md Normal file
View File

@ -0,0 +1,73 @@
# Turborepo starter
This is an official Yarn v1 starter turborepo.
## What's inside?
This turborepo uses [Yarn](https://classic.yarnpkg.com/) as a package manager. It includes the following packages/apps:
### Apps and Packages
- `docs`: a [Next.js](https://nextjs.org/) app
- `web`: another [Next.js](https://nextjs.org/) app
- `ui`: a stub React component library shared by both `web` and `docs` applications
- `eslint-config-custom`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
- `tsconfig`: `tsconfig.json`s used throughout the monorepo
Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
### Utilities
This turborepo has some additional tools already setup for you:
- [TypeScript](https://www.typescriptlang.org/) for static type checking
- [ESLint](https://eslint.org/) for code linting
- [Prettier](https://prettier.io) for code formatting
### Build
To build all apps and packages, run the following command:
```
cd my-turborepo
yarn run build
```
### Develop
To develop all apps and packages, run the following command:
```
cd my-turborepo
yarn run dev
```
### Remote Caching
Turborepo can use a technique known as [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines.
By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup), then enter the following commands:
```
cd my-turborepo
npx turbo login
```
This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview).
Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your turborepo:
```
npx turbo link
```
## Useful Links
Learn more about the power of Turborepo:
- [Tasks](https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks)
- [Caching](https://turbo.build/repo/docs/core-concepts/caching)
- [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching)
- [Filtering](https://turbo.build/repo/docs/core-concepts/monorepos/filtering)
- [Configuration Options](https://turbo.build/repo/docs/reference/configuration)
- [CLI Usage](https://turbo.build/repo/docs/reference/command-line-reference)

View File

@ -0,0 +1,3 @@
{
"presets": ["next/babel"]
}

View File

@ -0,0 +1,14 @@
module.exports = {
root: true,
extends: [
'next/core-web-vitals',
// Put gfe-core behind so that it can reset
// some of the eslint-plugin-react configs
// set by eslint-config-next which we don't want.
'@gfe/eslint-config-gfe-apps',
],
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
};

36
apps/i18n-example/.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
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@ -0,0 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}

View File

@ -0,0 +1,23 @@
# Internationalized Routing
Next.js doesn't support internationalized routing in `app` directory out of the box. But you can easily implement it yourself. This example shows how to implement internationalized routing on the Edge.
## Deploy your own
Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) or preview live with [StackBlitz](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/app-dir-i18n-routing)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/app-dir-i18n-routing&project-name=app-dir-i18n-routing&repository-name=app-dir-i18n-routing)
## How to use
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
```bash
npx create-next-app --example app-dir-i18n-routing i18n-app
# or
yarn create next-app --example app-dir-i18n-routing i18n-app
# or
pnpm create next-app --example app-dir-i18n-routing i18n-app
```
Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).

View File

@ -0,0 +1,12 @@
project_id: "572367"
api_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJ3UUVxdmhVM3ZMT2EyWGljbVV5VCIsImp0aSI6ImJjN2QzOGYxZGY4MTg2NGE1ZWYyNjFhN2RlN2MwNzJkNmU1ZDEzZDBmMDRhZDQ5ODBmZDY0ZWIyZGQ2YWY1ZWU5ZWI4NzhkNDJlY2IyNWM3IiwiaWF0IjoxNjc4MDk1NDYyLjkwODg3NiwibmJmIjoxNjc4MDk1NDYyLjkwODg4NCwiZXhwIjoxNjgwNjgzODYyLjgwMDA2NSwic3ViIjoiMTMyNjY4NTUiLCJzY29wZXMiOlsicHJvamVjdCJdLCJkb21haW4iOm51bGwsImFzc29jaWF0aW9ucyI6WyIqIl0sInNlc3Npb24iOjB9.LUt9qwqGbYURAn4mkpRRIPKujv0-2Q93LRPm_fmlh8H6nARFqGegKKcXyOYxccSpeYWulA4BdCMFvgebmIWtU3hG7VJMymrizIpaBgEF0r_qLolVkRUFoGXCV26Dbk0YCF50dWb_6rh9jT6kSh48kKYkfh0Acb8Helm_q3tQwH9XHhLvCC6HXucXwUOnzr6DAuH3j7GRzciBxUfuPNu4s7NoqwV4G5gg4U0s4niso83_JM5JlUKCWgv4youmVqm3FCLoXNSu9cibb5AnUtV80XHtW6OSJbmb4moFIx5djS3iE6UEoZ8MPWsFitserZAcqj_kL_aRClJ-gM-dd4KHW85aoV_cyzYDJFzcAwRw26N4NacLjHkfi9-VJumBpkujweJj-LiwTxeMYeJ8j5DKfjgaTfZ-hMHCHdSPi-5EeScawfGW0keL3cuhr1-b_6MGOjvvuaP59lJ6bESOM4ZINhr0cYV5QLB4hKjrMAdgTkal9HspA37avW0mgUed0N1cr4VUpusDQHQZ0uP-Ltfcz4r65BOn330L4h7xixiNKwYQehidsO5fw_j_w8w32HAIwcMkVNyE0b3TelIIiKfBWFa6GvYu3n-1WnFKo8te0Ys8VODSe-_UbsmKuag0bZ94FnNpR6ptchgJuleaCVqomUN1pn2D-fseNwlhYv89djQ
base_path: .
base_url: https://api.crowdin.com
preserve_hierarchy: true
files:
# JSON translation files
- source: /src/lang/en.json
translation: /src/lang/%locale%.json
# Docs Markdown files
- source: /src/**/en.mdx
translation: /%original_path%/%locale%.mdx

View File

@ -0,0 +1,2 @@
/* eslint-disable spaced-comment */
/// <reference types="next-i18nostic/config" />

View File

@ -0,0 +1,12 @@
const config = {
defaultLocale: 'en',
locales: ['en', 'de', 'zh'],
localeHrefLangs: {
en: 'en-US',
de: 'de-DE',
zh: 'zh-CN',
},
trailingSlash: false,
};
export default config;

View File

@ -0,0 +1,17 @@
// @ts-check
import nextI18nostic from './nextI18nostic.mjs';
/**
* @type {import('next').NextConfig}
**/
const nextConfig = {
experimental: {
appDir: true,
serverComponentsExternalPackages: ['mdx-bundler'],
},
transpilePackages: ['next-i18nostic'],
pageExtensions: ['ts', 'tsx'],
};
export default nextI18nostic()(nextConfig);

View File

@ -0,0 +1,26 @@
// import { NextConfig, Metadata } from "next";
// export type NextI18nosticHrefLang = keyof NonNullable<
// NonNullable<Metadata["alternates"]>["languages"]
// >;
// export type WithI18nostic = (config: NextConfig) => NextConfig;
export default function nextI18nostic() {
return (nextConfig) => {
return Object.assign({}, nextConfig, {
// TODO: Derive from NextJsWebpackConfig.
webpack(config, options) {
config.resolve.alias['next-i18nostic/config'] = [
'private-next-root-dir/next-i18nostic.config',
];
if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, options);
}
return config;
},
});
};
}

View File

@ -0,0 +1,36 @@
{
"name": "i18n-example",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
"lint": "next lint",
"tsc": "tsc",
"i18n:extract": "formatjs extract 'src/**/*.ts*' --ignore='**/*.d.ts' --out-file src/lang/en.json"
},
"dependencies": {
"esbuild": "^0.17.11",
"mdx-bundler": "^9.2.1",
"next": "13.2.3",
"next-i18nostic": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-intl": "^6.2.8",
"remark-gfm": "^3.0.1",
"server-only": "0.0.1"
},
"devDependencies": {
"@gfe/eslint-config-gfe-apps": "*",
"@formatjs/cli": "^6.0.1",
"@types/node": "^18.11.5",
"@types/react": "^18.0.23",
"@types/react-dom": "^18.0.7",
"@typescript-eslint/eslint-plugin": "^5.53.0",
"@typescript-eslint/parser": "^5.53.0",
"eslint": "8.34.0",
"eslint-config-next": "13.2.3",
"typescript": "^4.8.4"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -0,0 +1,26 @@
'use client';
import { I18nProvider } from 'next-i18nostic';
import nextI18nosticConfig from 'next-i18nostic/config';
import { IntlProvider } from 'react-intl';
import type { IntlMessages } from '~/intl';
type Props = Readonly<{
children: React.ReactNode;
intlMessages: IntlMessages;
locale: string;
}>;
export function Providers({ children, intlMessages, locale }: Props) {
return (
<I18nProvider locale={locale}>
<IntlProvider
defaultLocale={nextI18nosticConfig.defaultLocale}
locale={locale}
messages={intlMessages}>
{children}
</IntlProvider>
</I18nProvider>
);
}

View File

@ -0,0 +1,20 @@
'use client';
import { FormattedMessage } from 'react-intl';
import Counter from './components/Counter';
export default function IndexPage() {
return (
<div>
<h1>
<FormattedMessage
defaultMessage="Welcome"
description="Welcome message"
id="iyaPkC"
/>
</h1>
<Counter />
</div>
);
}

View File

@ -0,0 +1,28 @@
'use client';
import { useState } from 'react';
import { FormattedMessage } from 'react-intl';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<button type="button" onClick={() => setCount((n) => n - 1)}>
<FormattedMessage
defaultMessage="Decrement"
description="Decrease"
id="ADeXOi"
/>
</button>{' '}
{count}{' '}
<button type="button" onClick={() => setCount((n) => n + 1)}>
<FormattedMessage
defaultMessage="Increment"
description="Increase"
id="MCH3lP"
/>
</button>
</div>
);
}

View File

@ -0,0 +1,23 @@
'use client';
import { I18nLink, useI18nPathname } from 'next-i18nostic';
import nextI18nosticConfig from 'next-i18nostic/config';
export default function LocaleSwitcher() {
const { pathname } = useI18nPathname();
return (
<div style={{ display: 'flex' }}>
<p>Language:</p>
<ul style={{ columnGap: 10, display: 'flex', listStyleType: 'none' }}>
{nextI18nosticConfig.locales.map((locale) => (
<li key={locale}>
<I18nLink href={pathname!} locale={locale}>
{locale}
</I18nLink>
</li>
))}
</ul>
</div>
);
}

View File

@ -0,0 +1,46 @@
'use client';
import { I18nLink } from 'next-i18nostic';
import { FormattedMessage } from 'react-intl';
export default function Navbar() {
return (
<nav style={{ display: 'flex' }}>
<ul
style={{
columnGap: 10,
display: 'flex',
listStyleType: 'none',
paddingLeft: 0,
}}>
<li>
<I18nLink href="/">
<FormattedMessage
defaultMessage="Home"
description="Link to home page"
id="OmkkJd"
/>
</I18nLink>
</li>
<li>
<I18nLink href="/profile">
<FormattedMessage
defaultMessage="Profile"
description="Profile page title"
id="81i+95"
/>
</I18nLink>
</li>
<li>
<I18nLink href="/docs">
<FormattedMessage
defaultMessage="Docs"
description="Docs page"
id="/x1uyJ"
/>
</I18nLink>
</li>
</ul>
</nav>
);
}

View File

@ -0,0 +1,15 @@
# Overview
[De] Lorem ipsum dolor sit amet.
| Tabellen | Ar | Kalt |
| -------- | :-----------: | ----: |
| col 1 is | left-aligned | $1600 |
| col 2 is | centered | $12 |
| col 3 is | right-aligned | $1 |
The translated content should be downloaded in `i18n/fr`.
```jsx
Hello World
```

View File

@ -0,0 +1,15 @@
# Overview
Lorem ipsum dolor sit amet.
| Tables | Are | Cool |
| -------- | :-----------: | ----: |
| col 1 is | left-aligned | $1600 |
| col 2 is | centered | $12 |
| col 3 is | right-aligned | $1 |
The translated content should be downloaded in `i18n/fr`.
```jsx
Hello World
```

View File

@ -0,0 +1,29 @@
import { bundleMDX } from 'mdx-bundler';
import { getMDXComponent } from 'mdx-bundler/client';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import remarkGfm from 'remark-gfm';
export default async function Page({
params: { locale },
}: Readonly<{ params: { locale: string } }>) {
const mdxSource = fs
.readFileSync(
path.join(fileURLToPath(path.dirname(import.meta.url)), `${locale}.mdx`),
)
.toString();
const { code } = await bundleMDX({
mdxOptions(options) {
options.remarkPlugins = [...(options.remarkPlugins ?? []), remarkGfm];
return options;
},
source: mdxSource,
});
const Markdown = getMDXComponent(code);
return <Markdown />;
}

View File

@ -0,0 +1,15 @@
# Overview
[Zh] Lorem ipsum dolor sit amet.
| 表 | Are | 冷色 |
| -------- | :-----------: | ----: |
| col 1 is | left-aligned | $1600 |
| col 2 is | centered | $12 |
| col 3 is | right-aligned | $1 |
The translated content should be downloaded in `i18n/fr`.
```jsx
Hello World
```

View File

@ -0,0 +1,66 @@
import type { Metadata } from 'next';
import { i18nMetadata } from 'next-i18nostic';
import nextI18nosticConfig from 'next-i18nostic/config';
import { getIntlServerOnly, getLocaleMessages } from '~/intl';
import LocaleSwitcher from './components/LocaleSwitcher';
import Navbar from './components/Navbar';
import { Providers } from '../Providers';
import '~/styles/globals.css';
export async function generateStaticParams() {
return nextI18nosticConfig.locales.map((locale) => ({ locale }));
}
export async function generateMetadata({
params: { locale },
}: Props): Promise<Metadata> {
const intl = await getIntlServerOnly(locale);
return i18nMetadata({
alternates: {
canonical: '/',
},
title: intl.formatMessage({
defaultMessage: 'Home',
description: 'Link to home page',
id: 'OmkkJd',
}),
});
}
type Props = Readonly<{
children: React.ReactNode;
params: { locale: string };
}>;
export default async function RootLayout({
children,
params: { locale },
}: Props) {
const localeMessages = await getLocaleMessages(locale);
return (
<html lang={locale}>
<body>
<Providers intlMessages={localeMessages} locale={locale}>
<div style={{ margin: '0 auto', maxWidth: 600 }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
}}>
<Navbar />
<LocaleSwitcher />
</div>
<p>Current locale: {locale}</p>
<hr />
{children}
</div>
</Providers>
</body>
</html>
);
}

View File

@ -0,0 +1,27 @@
import type { Metadata } from 'next';
import { i18nMetadata } from 'next-i18nostic';
import { getIntlServerOnly } from '~/intl';
import IndexPage from './IndexPage';
export async function generateMetadata({
params: { locale },
}: Readonly<{ params: { locale: string } }>): Promise<Metadata> {
const intl = await getIntlServerOnly(locale);
return i18nMetadata({
alternates: {
canonical: '/',
},
title: intl.formatMessage({
defaultMessage: 'Welcome',
description: 'Welcome message',
id: 'iyaPkC',
}),
});
}
export default function Page() {
return <IndexPage />;
}

View File

@ -0,0 +1,24 @@
'use client';
import { FormattedMessage } from 'react-intl';
export default function ProfilePage() {
return (
<div>
<h1>
<FormattedMessage
defaultMessage="Profile Contents"
description="Profile page contents"
id="v0BzAj"
/>
</h1>
<p>
<FormattedMessage
defaultMessage="Body content"
description="Some content"
id="SMjxq8"
/>
</p>
</div>
);
}

View File

@ -0,0 +1,27 @@
import type { Metadata } from 'next';
import { i18nMetadata } from 'next-i18nostic';
import { getIntlServerOnly } from '~/intl';
import ProfilePage from './ProfilePage';
export async function generateMetadata({
params: { locale },
}: Readonly<{ params: { locale: string } }>): Promise<Metadata> {
const intl = await getIntlServerOnly(locale);
return i18nMetadata({
alternates: {
canonical: '/profile',
},
title: intl.formatMessage({
defaultMessage: 'Profile',
description: 'Profile page title',
id: '81i+95',
}),
});
}
export default async function Page() {
return <ProfilePage />;
}

View File

@ -0,0 +1,37 @@
import type { ResolvedIntlConfig } from 'react-intl';
import { createIntl,createIntlCache } from '@formatjs/intl';
export type IntlMessages = ResolvedIntlConfig['messages'];
// We enumerate all dictionaries here for better linting and typescript support
// We also get the default import for cleaner types
const locales = {
de: () => import('../lang/de.json').then((module) => module.default),
en: () => import('../lang/en.json').then((module) => module.default),
zh: () => import('../lang/zh.json').then((module) => module.default),
};
export async function getLocaleMessages(locale: string): Promise<IntlMessages> {
const i18nStrings = await locales[locale as keyof typeof locales]();
const strings: IntlMessages = {};
Object.entries(i18nStrings).map(([messageId, value]) => {
strings[messageId] = value.defaultMessage;
});
return strings;
}
export async function getIntlServerOnly(locale: string) {
const cache = createIntlCache();
const messages = await getLocaleMessages(locale);
return createIntl(
{
locale,
messages,
},
cache,
);
}

View File

@ -0,0 +1,35 @@
{
"/x1uyJ": {
"defaultMessage": "Dokumentation",
"description": "Docs page"
},
"81i+95": {
"defaultMessage": "Profil",
"description": "Profile page title"
},
"ADeXOi": {
"defaultMessage": "Reduzieren",
"description": "Decrease"
},
"MCH3lP": {
"defaultMessage": "Schrittweite",
"description": "Increase"
},
"OmkkJd": {
"defaultMessage": "Zurück",
"description": "Link to home page"
},
"SMjxq8": {
"defaultMessage": "Body Inhalt",
"description": "Some content"
},
"iyaPkC": {
"defaultMessage": "Willkommen",
"description": "Welcome message"
},
"v0BzAj": {
"defaultMessage": "Schulprofil Inhalt",
"description": "Profile page contents"
}
}

View File

@ -0,0 +1,34 @@
{
"/x1uyJ": {
"defaultMessage": "Docs",
"description": "Docs page"
},
"81i+95": {
"defaultMessage": "Profile",
"description": "Profile page title"
},
"ADeXOi": {
"defaultMessage": "Decrement",
"description": "Decrease"
},
"MCH3lP": {
"defaultMessage": "Increment",
"description": "Increase"
},
"OmkkJd": {
"defaultMessage": "Home",
"description": "Link to home page"
},
"SMjxq8": {
"defaultMessage": "Body content",
"description": "Some content"
},
"iyaPkC": {
"defaultMessage": "Welcome",
"description": "Welcome message"
},
"v0BzAj": {
"defaultMessage": "Profile Contents",
"description": "Profile page contents"
}
}

View File

@ -0,0 +1,35 @@
{
"/x1uyJ": {
"defaultMessage": "文件",
"description": "Docs page"
},
"81i+95": {
"defaultMessage": "资料",
"description": "Profile page title"
},
"ADeXOi": {
"defaultMessage": "递减",
"description": "Decrease"
},
"MCH3lP": {
"defaultMessage": "递增",
"description": "Increase"
},
"OmkkJd": {
"defaultMessage": "主页",
"description": "Link to home page"
},
"SMjxq8": {
"defaultMessage": "正文内容",
"description": "Some content"
},
"iyaPkC": {
"defaultMessage": "欢迎",
"description": "Welcome message"
},
"v0BzAj": {
"defaultMessage": "个人资料内容",
"description": "Profile page contents"
}
}

View File

@ -0,0 +1,3 @@
export function useMDXComponents(components) {
return components;
}

View File

@ -0,0 +1,23 @@
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { i18nMiddleware } from 'next-i18nostic';
export function middleware(req: NextRequest) {
// // `/_next/` and `/api/` are ignored by the watcher, but we need to ignore files in `public` manually.
// // If you have one
// if (
// [
// '/manifest.json',
// '/favicon.ico',
// // Your other files in `public`
// ].includes(pathname)
// )
// return
return i18nMiddleware(req) ?? NextResponse.next();
}
export const config = {
// Matcher ignoring `/_next/` and `/api/`
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

View File

@ -0,0 +1,10 @@
html {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif;
-webkit-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: transparent;
line-height: 1.5;
tab-size: 4;
}

View File

@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@gfe/tsconfig/nextjs.json",
"compilerOptions": {
"plugins": [
{
"name": "next"
}
],
"baseUrl": "./src",
"paths": {
"~/*": ["*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"next-i18nostic-env.d.ts",
"src/mdx-components.jsx"
],
"exclude": ["node_modules"]
}

6
apps/web/.babelrc Normal file
View File

@ -0,0 +1,6 @@
{
"presets": [
"next/babel"
],
"plugins": []
}

View File

@ -0,0 +1,11 @@
# Update these with your Supabase details from your project settings > API
SUPABASE_SERVICE_ROLE_KEY=
NEXT_PUBLIC_SUPABASE_URL=https://SUPABASE_REFERENCE_ID.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=pk_something
STRIPE_SECRET_KEY=sk_something
STRIPE_SIGNING_SECRET=whsec_something
STRIPE_MAIN_PRODUCT_ID=prod_MCPDYV6lv5QS5z
API_ROUTE_SECRET=some_generated_hash
CLIENT_URL=http://localhost:3000
DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.[SUPABASE_REFERENCE_ID].supabase.co:5432/postgres

15
apps/web/.eslintrc.cjs Normal file
View File

@ -0,0 +1,15 @@
module.exports = {
root: true,
extends: [
'next/core-web-vitals',
// Put gfe-core behind so that it can reset
// some of the eslint-plugin-react configs
// set by eslint-config-next which we don't want.
'@gfe/eslint-config-gfe-apps',
],
ignorePatterns: ['src/supabase/database.types.ts'],
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
};

43
apps/web/.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# 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
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
tsconfig.tsbuildinfo

2
apps/web/.prettierignore Normal file
View File

@ -0,0 +1,2 @@
src/__generated__
src/supabase/database.types.ts

4
apps/web/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}

1
apps/web/.watchmanconfig Normal file
View File

@ -0,0 +1 @@
{}

30
apps/web/README.md Normal file
View File

@ -0,0 +1,30 @@
## Getting Started
First, run the development server:
```bash
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

238
apps/web/docs/i18n.md Normal file
View File

@ -0,0 +1,238 @@
# i18n
## Setting Up
We use AST Explorer as a tool to help convert the strings in the TypeScript files with `react-intl` APIs.
1. Go to <https://astexplorer.net>.
1. Select "typescript" for both the parser option and the transform option.
1. Find the most updated version of the codemod in `~/transforms/intlify.ts`.
1. Copy and paste the code into the bottom left window of <https://astexplorer.net>.
1. Replace the top left window with your contents.
The codemod handles the following basic cases:
1. Object `StringLiteral` values used within functions, which are mostly React components/hooks.
```diff
function Foo() {
const items = {
- key: 'Some text',
+ key: intl.formatMessage({
+ defaultMessage: 'Some text'
+ }),
};
return <div>{items.key}</div>;
}
```
2. Plain `JSXText` nodes.
```diff
function Foo() {
- return <div>Some text</div>;
+ return <div><Format defaultMessage="Some text" /></div>;
}
```
3. JSX attributes which are `StringLiteral`s.
```diff
function Foo() {
- return <div label="Some text" />;
+ return <div label={intl.formatMessage({ defaultMessage: "Some text" })} />;
}
```
## Converting Strings Not Within Functions
Because strings need to read from React context, strings have to be wrapped within functions.
```jsx
const tabs = [
{
label: 'JavaScript',
value: 'javascript',
},
{
label: 'System Design',
value: 'system-design',
},
];
function Page() {
return (
<ul>
{tabs.map((tab) => (
<li key={tab.value}>{tab.label}</li>
))}
</ul>
);
}
```
### Wrap in React Hooks (preferred)
Wrap a function around the object and return it. The function has to start with `use`.
```jsx
import { useIntl } from 'react-intl';
function useTabs() {
// Now that it's in a React hook, you can use intl.formatMessage
const intl = useIntl();
return [
{
label: intl.formatMessage({
/* ... */
}),
value: 'javascript',
},
{
label: intl.formatMessage({
/* ... */
}),
value: 'system-design',
},
];
}
function Page() {
const tabs = useTabs(); // Obtain via the hook.
return (
<ul>
{tabs.map((tab) => (
<li key={tab.value}>{tab.label}</li>
))}
</ul>
);
}
```
### Shift into React Component
You can shift the contents into a React component as well, but this makes the translated less reusable and cannot be used in other components.
```jsx
import { useIntl } from 'react-intl';
function Page() {
const intl = useIntl();
const tabs [
{
label: intl.formatMessage({
/* ... */
}),
value: 'javascript',
},
{
label: intl.formatMessage({
/* ... */
}),
value: 'system-design',
},
];
return (
<ul>
{tabs.map((tab) => (
<li key={tab.value}>{tab.label}</li>
))}
</ul>
);
}
```
## Rich Text Formatting
If a string contains some markup, use the [Rich Text Formatting](https://formatjs.io/docs/react-intl/components#rich-text-formatting) syntax.
```jsx
// Before
<Heading>
All the essentials for front end{' '}
<span className="font-bold">interviews and more</span>
</Heading>
```
```jsx
// After
<Heading>
<FormattedMessage
defaultMessage="All the essentials for front end <span>interviews and more</span>"
values={{
span: chunks => <span className="font-bold">{chunks}<span>
}}
/>
</Heading>
```
### Nested Rich Text Formatting
Rich text can also be nested like typical HTML tags. Just pass the children into the mapped elements and it'll be fine.
```jsx
// Before
<Heading>
<span>All the essentials for front end</span>{' '}
<span className="inline-block">
interviews <span className="font-bold">and more</span>
</span>
</Heading>
```
```jsx
// After
<Heading>
<FormattedMessage
defaultMessage="All the essentials for front end <span>interviews <bold>and more</bold></span>"
description="foo"
id="QULf19"
values={{
span: (chunks) => <span className="inline-block">{chunks}</span>,
bold: (chunks) => <span className="font-bold">{chunks}</span>,
}}
/>
</Heading>
```
### Strings which Contain Unicode
Unicode within JSX text are mostly for decorative purposes, such as `&arr;`, `&middot;`. Regardless, they shouldn't be translated and have to remain within JSX otherwise they will not be converted by the browser into a symbol.
### Take them out of the translatable content
```jsx
// Before
<p>Front End Engineer &middot; Web Developer &middot; Full Stack Engineer</p>
// After
<p><span>Front End Engineer</span> &middot; <span>Web Developer</span> &middot; <span>Full Stack Engineer</span></p>
// Then transform this instead.
```
```jsx
// Before
<button>Click here now &rarr;</button>
// After
<button><FormattedMessage defaultMessage="Click here now"> &rarr;</button>
```
#### Use Rich Text
We can use the rich text syntax as well and leave the contents to be empty.
```jsx
// Before
<button>Click here now &rarr;</button>
// After
<button>
<FormattedMessage
defaultMessage="Click here now <arrow></arrow>"
values={{arrow: () => <span>&rarr;</span>}}
/>
</button>
```

View File

@ -0,0 +1,5 @@
import MDXComponents from './src/components/mdx/MDXComponents';
export function useMDXComponents() {
return MDXComponents;
}

6
apps/web/next-env.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

2
apps/web/next-i18nostic-env.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/* eslint-disable spaced-comment */
/// <reference types="next-i18nostic/config" />

View File

@ -0,0 +1,10 @@
const config = {
defaultLocale: 'en',
locales: ['en'],
localeHrefLangs: {
en: 'en-US',
},
trailingSlash: false,
};
export default config;

View File

@ -0,0 +1,29 @@
// @ts-check
import codingQuestionsList from './src/__generated__/questions/CodingQuestionsList.json' assert { type: 'json' };
const priority = 0.7;
const changefreq = 'daily';
/** @type {import('next-sitemap').IConfig} */
export default {
siteUrl: process.env.SITE_URL || 'https://www.greatfrontend.com',
exclude: [
'/dev__/*',
'/logout',
'/password/reset',
'/payment/success',
'/profile',
'/profile/*',
],
priority,
changefreq,
sitemapSize: 5000,
generateIndexSitemap: false,
additionalPaths: async (config) =>
await Promise.all(
codingQuestionsList.map(
async ({ href }) => await config.transform(config, href),
),
),
};

82
apps/web/next.config.mjs Normal file
View File

@ -0,0 +1,82 @@
// @ts-check
import remarkFrontmatter from 'remark-frontmatter';
import remarkMdxFrontmatter from 'remark-mdx-frontmatter';
import remarkGfm from 'remark-gfm';
import remarkSlug from 'remark-slug';
import remarkExtractToc from '@stefanprobst/remark-extract-toc';
import remarkExtractTocExport from '@stefanprobst/remark-extract-toc/mdx';
import nextMDX from '@next/mdx';
import nextI18nostic from './nextI18nostic.mjs';
const withMDX = nextMDX({
extension: /\.mdx?$/,
options: {
format: 'mdx',
remarkPlugins: [
remarkGfm,
remarkFrontmatter,
remarkMdxFrontmatter,
remarkSlug,
remarkExtractToc,
remarkExtractTocExport, // Adds toc as exported const for MDX files.
],
},
});
const withNextI18nostic = nextI18nostic();
/**
* @type {import('next').NextConfig}
**/
const nextConfig = {
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
reactStrictMode: true,
experimental: {
appDir: true,
serverComponentsExternalPackages: ['mdx-bundler'],
},
transpilePackages: ['next-i18nostic'],
async redirects() {
return [
{
source: '/questions/javascript',
destination: '/questions/js',
permanent: false,
},
{
source: '/questions/coding',
destination: '/prepare/coding',
permanent: false,
},
{
source: '/questions/quiz',
destination: '/prepare/quiz',
permanent: false,
},
{
source: '/questions/system-design',
destination: '/prepare/system-design',
permanent: false,
},
{
source: '/questions/quiz/css/:path*',
destination: '/questions/quiz/:path*',
permanent: false,
},
{
source: '/questions/quiz/html/:path*',
destination: '/questions/quiz/:path*',
permanent: false,
},
{
source: '/questions/quiz/javascript/:path*',
destination: '/questions/quiz/:path*',
permanent: false,
},
];
},
};
export default withNextI18nostic(withMDX(nextConfig));

View File

@ -0,0 +1,18 @@
export default function nextI18nostic() {
return (nextConfig) => {
return Object.assign({}, nextConfig, {
// TODO: Derive from NextJsWebpackConfig.
webpack(config, options) {
config.resolve.alias['next-i18nostic/config'] = [
'private-next-root-dir/next-i18nostic.config',
];
if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, options);
}
return config;
},
});
};
}

117
apps/web/package.json Normal file
View File

@ -0,0 +1,117 @@
{
"name": "greatfrontend",
"version": "0.0.1",
"type": "module",
"scripts": {
"ci": "yarn tsc && next lint",
"dev": "next dev",
"dev:gen": "concurrently -n gfe,next -c green,blue -k \"yarn gen:watch\" \"next dev\"",
"build": "yarn gen:all && next build",
"sitemap": "next-sitemap",
"start": "yarn gen:all && next start",
"lint": "next lint",
"lint:fix": "eslint src questions --fix",
"test": "jest --env=jsdom",
"tsc": "tsc",
"gen": "NODE_ENV=production ts-node --esm --experimentalSpecifierResolution node src/scripts/questions-gen-cli.ts",
"gen:watch": "yarn gen:all && ts-node --esm --experimentalSpecifierResolution node src/scripts/questions-watcher.ts",
"gen:all": "yarn gen:clean && yarn gen",
"gen:clean": "rm -rf src/__generated__",
"postinstall": "prisma generate",
"i18n:extract": "formatjs extract 'src/**/*.ts*' --ignore='**/*.d.ts' --out-file src/locales/en.json",
"ppp": "ts-node --esm --experimentalSpecifierResolution node ./src/scripts/ppp-update.ts",
"supabase": "supabase gen types typescript --project-id vaqybtnqyonvlwtskzmv --schema public > src/supabase/database.types.ts",
"intl:js": "ts-node --esm --experimentalSpecifierResolution node ./src/scripts/questions-javascript-intl.ts",
"prisma": "prisma"
},
"dependencies": {
"@codesandbox/sandpack-react": "^1.20.9",
"@headlessui/react": "^1.7.11",
"@heroicons/react": "2.0.13",
"@mdx-js/react": "^2.3.0",
"@monaco-editor/react": "^4.4.6",
"@next/mdx": "^13.2.3",
"@prisma/client": "^4.11.0",
"@stefanprobst/remark-extract-toc": "^2.2.0",
"@stripe/stripe-js": "^1.46.0",
"@supabase/auth-helpers-nextjs": "^0.5.4",
"@supabase/auth-helpers-react": "^0.3.1",
"@supabase/supabase-js": "^2.10.0",
"@tanstack/react-query": "^4.2.3",
"@vercel/analytics": "^0.1.10",
"axios": "^0.27.2",
"clsx": "^1.2.1",
"console-feed": "3.3.0",
"cookie": "^0.5.0",
"copy-text-to-clipboard": "^3.0.1",
"cors": "^2.8.5",
"esbuild": "^0.17.8",
"framer-motion": "^9.0.4",
"gray-matter": "^4.0.3",
"js-cookie": "^3.0.1",
"lodash-es": "^4.17.21",
"mdx-bundler": "^9.2.1",
"micro": "^9.4.1",
"mitt": "^3.0.0",
"monaco-themes": "^0.4.3",
"next": "13.2.3",
"next-absolute-url": "^1.2.2",
"next-i18nostic": "*",
"next-seo": "^5.15.0",
"prism-react-renderer": "^1.3.5",
"radix-ui": "^1.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-intl": "^6.2.8",
"remark-frontmatter": "^4.0.1",
"remark-gfm": "^3.0.1",
"remark-mdx-frontmatter": "^2.1.1",
"remark-slug": "^7.0.1",
"server-only": "^0.0.1",
"stripe": "^11.11.0"
},
"devDependencies": {
"@formatjs/cli": "^6.0.1",
"@gfe/eslint-config-gfe-apps": "*",
"@mdx-js/loader": "^2.3.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.2",
"@tailwindcss/typography": "^0.5.9",
"@types/cookie": "^0.5.1",
"@types/cors": "^2.8.13",
"@types/glob": "^8.0.1",
"@types/gtag.js": "^0.0.12",
"@types/jest": "^29.4.1",
"@types/js-cookie": "^3.0.3",
"@types/lodash-es": "^4.17.6",
"@types/mdx": "^2.0.3",
"@types/node": "^18.14.0",
"@types/nprogress": "^0.2.0",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@typescript-eslint/eslint-plugin": "^5.53.0",
"@typescript-eslint/parser": "^5.53.0",
"autoprefixer": "^10.4.13",
"chalk": "^5.2.0",
"chokidar": "^3.5.3",
"concurrently": "^7.6.0",
"encoding": "^0.1.13",
"eslint": "8.34.0",
"eslint-config-next": "13.2.3",
"glob": "^8.1.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"lodash": "^4.17.21",
"next-sitemap": "^3.1.52",
"postcss": "^8.4.21",
"prettier": "^2.8.4",
"prettier-plugin-tailwindcss": "^0.2.3",
"prisma": "^4.11.0",
"slugify": "^1.6.5",
"supabase": "^1.41.1",
"tailwindcss": "^3.2.7",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -0,0 +1,85 @@
-- Manually added, so that uuid_generate_v4() can be used.
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- CreateTable
CREATE TABLE "EmailSubscriber" (
"id" UUID NOT NULL DEFAULT uuid_generate_v4(),
"email" TEXT NOT NULL,
"createdAt" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "EmailSubscribers_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Event" (
"id" BIGSERIAL NOT NULL,
"userId" UUID NOT NULL,
"action" TEXT NOT NULL,
"payload" JSON,
"createdAt" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"referer" VARCHAR,
"country" TEXT,
CONSTRAINT "Event_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "FeedbackMessage" (
"id" UUID NOT NULL DEFAULT uuid_generate_v4(),
"message" VARCHAR NOT NULL,
"email" TEXT,
"createdAt" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP,
"resolved" BOOLEAN NOT NULL DEFAULT false,
"owner" TEXT DEFAULT 'yangshun',
"userEmail" TEXT,
"comments" VARCHAR,
"metadata" JSON,
CONSTRAINT "FeedbackMessage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Profile" (
"id" UUID NOT NULL,
"premium" BOOLEAN NOT NULL DEFAULT false,
"plan" TEXT,
"stripeCustomer" TEXT,
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "profile_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "QuestionProgress" (
"format" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"id" UUID NOT NULL DEFAULT uuid_generate_v4(),
"status" TEXT NOT NULL,
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" UUID NOT NULL,
CONSTRAINT "question_progress_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SitePerformance" (
"createdAt" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"duration" DOUBLE PRECISION,
"country" TEXT,
"userEmail" TEXT,
"event" TEXT,
"id" UUID NOT NULL DEFAULT uuid_generate_v4(),
"url" TEXT,
"referrer" TEXT,
CONSTRAINT "SitePerformance_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "EmailSubscribers_email_key" ON "EmailSubscriber"("email");
-- AddForeignKey
ALTER TABLE "Event" ADD CONSTRAINT "Event_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Profile"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE "QuestionProgress" ADD CONSTRAINT "QuestionProgress_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Profile"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;

View File

@ -0,0 +1,68 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model EmailSubscriber {
id String @id(map: "EmailSubscribers_pkey") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
email String @unique(map: "EmailSubscribers_email_key")
createdAt DateTime? @default(now()) @db.Timestamp(6)
}
model Event {
id BigInt @id @default(autoincrement())
userId String @db.Uuid
action String
payload Json? @db.Json
createdAt DateTime? @default(now()) @db.Timestamptz(6)
referer String? @db.VarChar
country String?
Profile Profile @relation(fields: [userId], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
model FeedbackMessage {
id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
message String @db.VarChar
email String?
createdAt DateTime? @default(now()) @db.Timestamp(6)
resolved Boolean @default(false)
owner String? @default("yangshun")
userEmail String?
comments String? @db.VarChar
metadata Json? @db.Json
}
model Profile {
id String @id(map: "profile_pkey") @db.Uuid
premium Boolean @default(false)
plan String?
stripeCustomer String?
createdAt DateTime @default(now()) @db.Timestamptz(6)
Event Event[]
QuestionProgress QuestionProgress[]
}
model QuestionProgress {
format String
slug String
id String @id(map: "question_progress_pkey") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
status String
createdAt DateTime @default(now()) @db.Timestamptz(6)
userId String @db.Uuid
Profile Profile @relation(fields: [userId], references: [id], onDelete: NoAction, onUpdate: NoAction)
}
model SitePerformance {
createdAt DateTime? @default(now()) @db.Timestamptz(6)
duration Float?
country String?
userEmail String?
event String?
id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
url String?
referrer String?
}

BIN
apps/web/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1, 0, 0, 1, 27.009001, -39.238998)">
<path fill="#4285F4" d="M -3.264 51.509 C -3.264 50.719 -3.334 49.969 -3.454 49.239 L -14.754 49.239 L -14.754 53.749 L -8.284 53.749 C -8.574 55.229 -9.424 56.479 -10.684 57.329 L -10.684 60.329 L -6.824 60.329 C -4.564 58.239 -3.264 55.159 -3.264 51.509 Z"/>
<path fill="#34A853" d="M -14.754 63.239 C -11.514 63.239 -8.804 62.159 -6.824 60.329 L -10.684 57.329 C -11.764 58.049 -13.134 58.489 -14.754 58.489 C -17.884 58.489 -20.534 56.379 -21.484 53.529 L -25.464 53.529 L -25.464 56.619 C -23.494 60.539 -19.444 63.239 -14.754 63.239 Z"/>
<path fill="#FBBC05" d="M -21.484 53.529 C -21.734 52.809 -21.864 52.039 -21.864 51.239 C -21.864 50.439 -21.724 49.669 -21.484 48.949 L -21.484 45.859 L -25.464 45.859 C -26.284 47.479 -26.754 49.299 -26.754 51.239 C -26.754 53.179 -26.284 54.999 -25.464 56.619 L -21.484 53.529 Z"/>
<path fill="#EA4335" d="M -14.754 43.989 C -12.984 43.989 -11.404 44.599 -10.154 45.789 L -6.734 42.369 C -8.804 40.429 -11.514 39.239 -14.754 39.239 C -19.444 39.239 -23.494 41.939 -25.464 45.859 L -21.484 48.949 C -20.534 46.099 -17.884 43.989 -14.754 43.989 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
width="603"
height="182"
style="fill:#000">
<path
d="m 374.00642,142.18404 c -34.99948,25.79739 -85.72909,39.56123 -129.40634,39.56123 -61.24255,0 -116.37656,-22.65135 -158.08757,-60.32496 -3.2771,-2.96252 -0.34083,-6.9999 3.59171,-4.69283 45.01431,26.19064 100.67269,41.94697 158.16623,41.94697 38.774689,0 81.4295,-8.02237 120.6499,-24.67006 5.92501,-2.51683 10.87999,3.88009 5.08607,8.17965"
id="path8"
style="fill:#000" />
<path
d="m 388.55678,125.53635 c -4.45688,-5.71527 -29.57261,-2.70033 -40.84585,-1.36327 -3.43442,0.41947 -3.95874,-2.56925 -0.86517,-4.71905 20.00346,-14.07844 52.82696,-10.01483 56.65462,-5.2958 3.82764,4.74526 -0.99624,37.64741 -19.79373,53.35128 -2.88385,2.41195 -5.63662,1.12734 -4.35198,-2.07113 4.2209,-10.53917 13.68519,-34.16054 9.20211,-39.90203"
id="path10"
style="fill:#000" />
<path
d="M 348.49744,20.06598 V 6.38079 c 0,-2.07113 1.57301,-3.46062 3.46062,-3.46062 h 61.26875 c 1.96628,0 3.53929,1.41571 3.53929,3.46062 v 11.71893 c -0.0262,1.96626 -1.67788,4.53551 -4.61418,8.59912 l -31.74859,45.32893 c 11.79759,-0.28837 24.25059,1.46814 34.94706,7.49802 2.41195,1.36327 3.06737,3.35575 3.25089,5.32203 V 99.4506 c 0,1.99248 -2.20222,4.32576 -4.5093,3.1198 -18.84992,-9.88376 -43.887,-10.95865 -64.72939,0.10487 -2.12356,1.15354 -4.35199,-1.15354 -4.35199,-3.14602 V 85.66054 c 0,-2.22843 0.0262,-6.02989 2.25463,-9.41186 l 36.78224,-52.74829 h -32.01076 c -1.96626,0 -3.53927,-1.38948 -3.53927,-3.43441" />
<path
d="m 124.99883,105.45424 h -18.64017 c -1.78273,-0.13107 -3.19845,-1.46813 -3.32954,-3.17224 V 6.61676 c 0,-1.91383 1.59923,-3.43442 3.59171,-3.43442 h 17.38176 c 1.80898,0.0786 3.25089,1.46814 3.38199,3.19845 v 12.50545 h 0.34082 c 4.53551,-12.08598 13.05597,-17.7226 24.53896,-17.7226 11.66649,0 18.95477,5.63662 24.19814,17.7226 4.5093,-12.08598 14.76008,-17.7226 25.74495,-17.7226 7.81262,0 16.35931,3.22467 21.57646,10.46052 5.89879,8.04857 4.69281,19.74128 4.69281,29.99208 l -0.0262,60.37739 c 0,1.91383 -1.59923,3.46061 -3.59171,3.46061 h -18.61397 c -1.86138,-0.13107 -3.35574,-1.62543 -3.35574,-3.46061 V 51.29025 c 0,-4.03739 0.36702,-14.10466 -0.52434,-17.93233 -1.38949,-6.42311 -5.55797,-8.23209 -10.95865,-8.23209 -4.5093,0 -9.22833,3.01494 -11.14216,7.83885 -1.91383,4.8239 -1.73031,12.89867 -1.73031,18.32557 v 50.70338 c 0,1.91383 -1.59923,3.46061 -3.59171,3.46061 h -18.61395 c -1.88761,-0.13107 -3.35576,-1.62543 -3.35576,-3.46061 L 152.946,51.29025 c 0,-10.67025 1.75651,-26.37415 -11.48298,-26.37415 -13.39682,0 -12.87248,15.31063 -12.87248,26.37415 v 50.70338 c 0,1.91383 -1.59923,3.46061 -3.59171,3.46061" />
<path
d="m 469.51439,1.16364 c 27.65877,0 42.62858,23.75246 42.62858,53.95427 0,29.17934 -16.54284,52.32881 -42.62858,52.32881 -27.16066,0 -41.94697,-23.75246 -41.94697,-53.35127 0,-29.78234 14.96983,-52.93181 41.94697,-52.93181 m 0.15729,19.53156 c -13.73761,0 -14.60278,18.71881 -14.60278,30.38532 0,11.69271 -0.18352,36.65114 14.44549,36.65114 14.44548,0 15.12712,-20.13452 15.12712,-32.40403 0,-8.07477 -0.34082,-17.72257 -2.779,-25.3779 -2.09735,-6.65906 -6.26581,-9.25453 -12.19083,-9.25453" />
<path
d="M 548.00762,105.45424 H 529.4461 c -1.86141,-0.13107 -3.35577,-1.62543 -3.35577,-3.46061 l -0.0262,-95.69149 c 0.1573,-1.75653 1.7041,-3.1198 3.59171,-3.1198 h 17.27691 c 1.62543,0.0786 2.96249,1.17976 3.32954,2.67412 v 14.62899 h 0.3408 c 5.21717,-13.0822 12.53165,-19.32181 25.40412,-19.32181 8.36317,0 16.51662,3.01494 21.75999,11.27324 4.87633,7.65532 4.87633,20.5278 4.87633,29.78233 v 60.22011 c -0.20973,1.67786 -1.75653,3.01492 -3.59169,3.01492 h -18.69262 c -1.70411,-0.13107 -3.11982,-1.38948 -3.30332,-3.01492 V 50.47753 c 0,-10.46052 1.20597,-25.77117 -11.66651,-25.77117 -4.5355,0 -8.70399,3.04117 -10.77512,7.65532 -2.62167,5.84637 -2.96249,11.66651 -2.96249,18.11585 v 51.5161 c -0.0262,1.91383 -1.65166,3.46061 -3.64414,3.46061" />
<use
xlink:href="#path30"
transform="translate(244.36719)" />
<path
d="M 55.288261,59.75829 V 55.7209 c -13.475471,0 -27.711211,2.88385 -27.711211,18.77125 0,8.04857 4.16847,13.50169 11.32567,13.50169 5.24337,0 9.93618,-3.22467 12.8987,-8.46805 3.670341,-6.44935 3.486841,-12.50544 3.486841,-19.7675 m 18.79747,45.43378 c -1.23219,1.10111 -3.01495,1.17976 -4.40444,0.4457 -6.18716,-5.1385 -7.28828,-7.52423 -10.69647,-12.42678 -10.224571,10.4343 -17.460401,13.55409 -30.726141,13.55409 -15.67768,0 -27.89471,-9.67401 -27.89471,-29.04824 0,-15.12713 8.20587,-25.43035 19.87236,-30.46398 10.1197,-4.45688 24.25058,-5.24337 35.051931,-6.47556 v -2.41195 c 0,-4.43066 0.34082,-9.67403 -2.25465,-13.50167 -2.280881,-3.43442 -6.632861,-4.85013 -10.460531,-4.85013 -7.10475,0 -13.44924,3.64414 -14.99603,11.19459 -0.31461,1.67789 -1.5468,3.32955 -3.22467,3.4082 L 6.26276,32.67628 C 4.74218,32.33548 3.0643,31.10327 3.48377,28.76999 7.65225,6.85271 27.44596,0.24605 45.16856,0.24605 c 9.071011,0 20.921021,2.41195 28.078221,9.28076 9.07104,8.46804 8.20587,19.7675 8.20587,32.06321 v 29.04826 c 0,8.73022 3.61794,12.55786 7.02613,17.27691 1.20597,1.67786 1.46814,3.69656 -0.05244,4.95497 -3.80144,3.17225 -10.56538,9.07104 -14.28819,12.37436 l -0.05242,-0.0525"
id="path30" />
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 272 92" width="272" height="92"><path fill="#000" d="M115.75 47.18c0 12.77-9.99 22.18-22.25 22.18s-22.25-9.41-22.25-22.18C71.25 34.32 81.24 25 93.5 25s22.25 9.32 22.25 22.18zm-9.74 0c0-7.98-5.79-13.44-12.51-13.44S80.99 39.2 80.99 47.18c0 7.9 5.79 13.44 12.51 13.44s12.51-5.55 12.51-13.44z"/><path fill="#000" d="M163.75 47.18c0 12.77-9.99 22.18-22.25 22.18s-22.25-9.41-22.25-22.18c0-12.85 9.99-22.18 22.25-22.18s22.25 9.32 22.25 22.18zm-9.74 0c0-7.98-5.79-13.44-12.51-13.44s-12.51 5.46-12.51 13.44c0 7.9 5.79 13.44 12.51 13.44s12.51-5.55 12.51-13.44z"/><path fill="#000" d="M209.75 26.34v39.82c0 16.38-9.66 23.07-21.08 23.07-10.75 0-17.22-7.19-19.66-13.07l8.48-3.53c1.51 3.61 5.21 7.87 11.17 7.87 7.31 0 11.84-4.51 11.84-13v-3.19h-.34c-2.18 2.69-6.38 5.04-11.68 5.04-11.09 0-21.25-9.66-21.25-22.09 0-12.52 10.16-22.26 21.25-22.26 5.29 0 9.49 2.35 11.68 4.96h.34v-3.61h9.25zm-8.56 20.92c0-7.81-5.21-13.52-11.84-13.52-6.72 0-12.35 5.71-12.35 13.52 0 7.73 5.63 13.36 12.35 13.36 6.63 0 11.84-5.63 11.84-13.36z"/><path fill="#000" d="M225 3v65h-9.5V3h9.5z"/><path fill="#000" d="M262.02 54.48l7.56 5.04c-2.44 3.61-8.32 9.83-18.48 9.83-12.6 0-22.01-9.74-22.01-22.18 0-13.19 9.49-22.18 20.92-22.18 11.51 0 17.14 9.16 18.98 14.11l1.01 2.52-29.65 12.28c2.27 4.45 5.8 6.72 10.75 6.72 4.96 0 8.4-2.44 10.92-6.14zm-23.27-7.98l19.82-8.23c-1.09-2.77-4.37-4.7-8.23-4.7-4.95 0-11.84 4.37-11.59 12.93z"/><path fill="#000" d="M35.29 41.41V32H67c.31 1.64.47 3.58.47 5.68 0 7.06-1.93 15.79-8.15 22.01-6.05 6.3-13.78 9.66-24.02 9.66C16.32 69.35.36 53.89.36 34.91.36 15.93 16.32.47 35.3.47c10.5 0 17.98 4.12 23.6 9.49l-6.64 6.64c-4.03-3.78-9.49-6.72-16.97-6.72-13.86 0-24.7 11.17-24.7 25.03 0 13.86 10.84 25.03 24.7 25.03 8.99 0 14.11-3.61 17.39-6.89 2.66-2.66 4.41-6.46 5.1-11.65l-22.49.01z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:cc="http://creativecommons.org/ns#" width="948" height="191">
<path id="Logo0" style="fill:#000" d="m31.06,125.96c0,10.98 2.41,19.41 5.56,24.51 4.13,6.68 10.29,9.51 16.57,9.51 8.1,0 15.51-2.01 29.79-21.76 11.44-15.83 24.92-38.05 33.99-51.98l15.36-23.6c10.67-16.39 23.02-34.61 37.18-46.96 11.56-10.08 24.03-15.68 36.58-15.68 21.07,0 41.14,12.21 56.5,35.11 16.81,25.08 24.97,56.67 24.97,89.27 0,19.38-3.82,33.62-10.32,44.87-6.28,10.88-18.52,21.75-39.11,21.75l0-31.02c17.63,0 22.03-16.2 22.03-34.74 0-26.42-6.16-55.74-19.73-76.69-9.63-14.86-22.11-23.94-35.84-23.94-14.85,0-26.8,11.2-40.23,31.17-7.14,10.61-14.47,23.54-22.7,38.13l-9.06,16.05c-18.2,32.27-22.81,39.62-31.91,51.75-15.95,21.24-29.57,29.29-47.5,29.29-21.27,0-34.72-9.21-43.05-23.09-6.8-11.31-10.14-26.15-10.14-43.06z"/>
<path id="Logo1" style="fill:#000" d="m24.49,37.3c14.24-21.95 34.79-37.3 58.36-37.3 13.65,0 27.22,4.04 41.39,15.61 15.5,12.65 32.02,33.48 52.63,67.81l7.39,12.32c17.84,29.72 27.99,45.01 33.93,52.22 7.64,9.26 12.99,12.02 19.94,12.02 17.63,0 22.03-16.2 22.03-34.74l27.4-.86c0,19.38-3.82,33.62-10.32,44.87-6.28,10.88-18.52,21.75-39.11,21.75-12.8,0-24.14-2.78-36.68-14.61-9.64-9.08-20.91-25.21-29.58-39.71l-25.79-43.08c-12.94-21.62-24.81-37.74-31.68-45.04-7.39-7.85-16.89-17.33-32.05-17.33-12.27,0-22.69,8.61-31.41,21.78z"/>
<path id="Logo2" style="fill:#000" d="m82.35,31.23c-12.27,0-22.69,8.61-31.41,21.78-12.33,18.61-19.88,46.33-19.88,72.95 0,10.98 2.41,19.41 5.56,24.51l-26.48,17.44c-6.8-11.31-10.14-26.15-10.14-43.06 0-30.75 8.44-62.8 24.49-87.55 14.24-21.95 34.79-37.3 58.36-37.3z"/>
<path id="Text" style="fill:#000" d="m347.94,6.04h35.93l61.09,110.52 61.1-110.52h35.15v181.6h-29.31v-139.18l-53.58,96.38h-27.5l-53.57-96.38v139.18h-29.31z
m285.11,67.71c-21.02,0-33.68,15.82-36.71,35.41h71.34c-1.47-20.18-13.11-35.41-34.63-35.41z
m-65.77,46.57c0-41.22 26.64-71.22 66.28-71.22 38.99,0 62.27,29.62 62.27,73.42v8.05h-99.49c3.53,21.31 17.67,35.67 40.47,35.67 18.19,0 29.56-5.55 40.34-15.7l15.57,19.07c-14.67,13.49-33.33,21.27-56.95,21.27-42.91,0-68.49-31.29-68.49-70.56z
m164.09-43.97h-26.98v-24h26.98v-39.69h28.28v39.69h40.99v24h-40.99v60.83c0,20.77 6.64,28.15 22.96,28.15 7.45,0 11.72-.64 18.03-1.69v23.74c-7.86,2.22-15.36,3.24-23.48,3.24-30.53,0-45.79-16.68-45.79-50.07z
m188.35,23.34c-5.68-14.34-18.35-24.9-36.97-24.9-24.2,0-39.69,17.17-39.69,45.14 0,27.27 14.26,45.27 38.53,45.27 19.08,0 32.7-11.1 38.13-24.91z
m28.28,87.95h-27.76v-18.94c-7.76,11.15-21.88,22.18-44.75,22.18-36.78,0-61.36-30.79-61.36-70.95 0-40.54 25.17-70.83 62.92-70.83 18.66,0 33.3,7.46 43.19,20.63v-17.38h27.76z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Some files were not shown because too many files have changed in this diff Show More