diff --git a/.gitignore b/.gitignore index 490c5636..213e0863 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ yarn-error.log* .turbo +# tamagui +**/.tamagui/* + build/** **/dist/** .next/** diff --git a/.vscode/settings.json b/.vscode/settings.json index cfaf935a..51013989 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,7 @@ "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "graphql-config.load.filePath": "./frontend/graphql.config.js", - "cSpell.words": ["fastify"], + "cSpell.words": ["fastify", "tamagui"], "eslint.workingDirectories": ["./frontend", "./backend"], "files.watcherExclude": { "**/.git/objects/**": true, diff --git a/apps/frontend/.babelrc b/apps/frontend/.babelrc deleted file mode 100644 index 05dcd233..00000000 --- a/apps/frontend/.babelrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "presets": [["next/babel"]], - "plugins": [ - "babel-plugin-twin", - "macros", - ["styled-components", { "ssr": true }] - ] -} diff --git a/apps/frontend/jest-preprocess.js b/apps/frontend/jest-preprocess.js index fafe0afd..d97dc802 100644 --- a/apps/frontend/jest-preprocess.js +++ b/apps/frontend/jest-preprocess.js @@ -1,8 +1,6 @@ -// setup for twin.macro to work - const babelOptions = { presets: ['@babel/preset-typescript'], - plugins: ['babel-plugin-macros'], + // plugins: ['babel-plugin-macros'], } // eslint-disable-next-line @typescript-eslint/no-var-requires diff --git a/apps/frontend/next-env.d.ts b/apps/frontend/next-env.d.ts index 4f11a03d..2532e77a 100644 --- a/apps/frontend/next-env.d.ts +++ b/apps/frontend/next-env.d.ts @@ -1,5 +1,4 @@ /// -/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/frontend/next.config.js b/apps/frontend/next.config.js index 0cc1a774..403313d6 100644 --- a/apps/frontend/next.config.js +++ b/apps/frontend/next.config.js @@ -1,10 +1,81 @@ -// eslint-disable-next-line @typescript-eslint/no-var-requires +/* eslint-disable @typescript-eslint/no-var-requires */ const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }) +/** @type {import('next').NextConfig} */ +const { withTamagui } = require('@tamagui/next-plugin') +const withImages = require('next-images') +const { join } = require('path') -const config = { - pageExtensions: ['page.tsx', 'api.ts'], +process.env.IGNORE_TS_CONFIG_PATHS = 'true' +process.env.TAMAGUI_TARGET = 'web' +process.env.TAMAGUI_DISABLE_WARN_DYNAMIC_LOAD = '1' + +const boolVals = { + true: true, + false: false, } -module.exports = withBundleAnalyzer(config) +const disableExtraction = + boolVals[process.env.DISABLE_EXTRACTION] ?? + process.env.NODE_ENV === 'development' + +const plugins = [ + withBundleAnalyzer, + withImages, + withTamagui({ + config: './tamagui.config.ts', + components: ['tamagui', 'ui'], + importsWhitelist: ['constants.js', 'colors.js'], + logTimings: true, + disableExtraction, + // experiment - reduced bundle size react-native-web + useReactNativeWebLite: false, + shouldExtract: (path) => { + if (path.includes(join('packages', 'app'))) { + return true + } + }, + excludeReactNativeWebExports: [ + 'Switch', + 'ProgressBar', + 'Picker', + 'CheckBox', + 'Touchable', + ], + }), +] + +module.exports = function () { + /** @type {import('next').NextConfig} */ + let config = { + typescript: { + ignoreBuildErrors: true, + }, + images: { + disableStaticImages: true, + }, + pageExtensions: ['page.tsx', 'api.ts'], + transpilePackages: [ + 'solito', + 'react-native-web', + 'expo-linking', + 'expo-constants', + 'expo-modules-core', + ], + experimental: { + optimizeCss: true, + scrollRestoration: true, + legacyBrowsers: false, + }, + } + + for (const plugin of plugins) { + config = { + ...config, + ...plugin(config), + } + } + + return config +} diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 7eb49018..05d81c16 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -2,6 +2,9 @@ "name": "frontend", "version": "0.1.0", "private": true, + "sideEffects": [ + "*.css" + ], "scripts": { "dev": "next dev & yarn run codegen:watch", "build": "next build", @@ -17,23 +20,31 @@ "dependencies": { "@auth0/auth0-react": "1.12.0", "@heroicons/react": "1.0.6", + "@next/font": "^13.1.5", "@radix-ui/colors": "0.1.8", "@radix-ui/react-avatar": "1.0.1", "@radix-ui/react-dialog": "1.0.2", "@radix-ui/react-icons": "1.1.1", "@radix-ui/react-portal": "1.0.1", + "@tamagui/next-theme": "^1.1.2", + "eslint-config-next": "^13.1.2", + "expo-linear-gradient": "^12.0.1", "graphql": "15.8.0", "graphql-anywhere": "4.2.8", - "next": "12.2.5", - "react": "18.2.0", - "react-dom": "18.2.0", + "next": "^13.1.2", + "next-images": "^1.8.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", "react-hook-form": "7.34.1", + "react-native": "0.0.0", + "react-native-web": "^0.18.10", + "react-native-web-lite": "^1.0.15", "spinners-react": "1.0.7", "styled-components": "5.3.6", - "tailwindcss": "2.2.19", "ts-pattern": "3.3.5", - "twin.macro": "2.8.2", + "tsconfig": "*", "typescript": "4.7.4", + "ui": "*", "urql": "2.2.3", "wonka": "4.0.15", "zod": "3.19.1" @@ -52,27 +63,31 @@ "@graphql-codegen/typescript-urql": "3.6.4", "@graphql-typed-document-node/core": "3.1.1", "@next/bundle-analyzer": "12.2.5", + "@tamagui/next-plugin": "^1.0.15", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "13.3.0", "@testing-library/user-event": "14.4.3", "@types/jest": "27.5.2", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", + "@types/react-native": "^0.71.0", "@types/styled-components": "5.1.26", "babel-jest": "27.5.1", "babel-plugin-macros": "3.1.0", "babel-plugin-styled-components": "2.0.7", "babel-plugin-twin": "1.0.2", "config": "*", + "critters": "^0.0.16", "graphql-playground-html": "1.6.30", "graphql-typescript-integration": "1.2.1", "isomorphic-unfetch": "3.1.0", "jest": "27.5.1", "msw": "0.44.2", + "next-compose-plugins": "^2.2.1", + "next-transpile-modules": "^10.0.0", "react-is": "18.2.0", "react-test-renderer": "18.2.0", - "ts-jest": "27.1.5", - "tsconfig": "*" + "ts-jest": "27.1.5" }, "babelMacros": { "twin": { diff --git a/apps/frontend/src/components/dataDisplay/Avatar.tsx b/apps/frontend/src/components/dataDisplay/Avatar.tsx index a0a99dee..6c3a593d 100644 --- a/apps/frontend/src/components/dataDisplay/Avatar.tsx +++ b/apps/frontend/src/components/dataDisplay/Avatar.tsx @@ -1,36 +1,53 @@ import * as AvatarPrimitive from '@radix-ui/react-avatar' -import tw from 'twin.macro' +// export function OldAvatar({ +// name, +// size = 'm', +// }: { +// name: User['name'] +// size?: 's' | 'm' | 'l' +// }) { +// const firstTwoChar = (name ?? 'NN').substring(0, 2) +// return ( +// +// +// +// {firstTwoChar} +// +// +// ) +// } +import { Avatar as TAvatar, SizeTokens } from 'tamagui' +// import tw from 'twin.macro' import { User } from '~/generated/graphql' - export function Avatar({ name, - size = 'm', + size, }: { name: User['name'] - size?: 's' | 'm' | 'l' + size?: SizeTokens }) { const firstTwoChar = (name ?? 'NN').substring(0, 2) + return ( - - - - {firstTwoChar} - - + + + + + ) } diff --git a/apps/frontend/src/components/feedback/Dialog.tsx b/apps/frontend/src/components/feedback/_Dialog.tsx similarity index 98% rename from apps/frontend/src/components/feedback/Dialog.tsx rename to apps/frontend/src/components/feedback/_Dialog.tsx index b7e9b9bb..b905db79 100644 --- a/apps/frontend/src/components/feedback/Dialog.tsx +++ b/apps/frontend/src/components/feedback/_Dialog.tsx @@ -1,7 +1,7 @@ import * as DialogPrimitive from '@radix-ui/react-dialog' import { Cross1Icon } from '@radix-ui/react-icons' import React from 'react' -import tw from 'twin.macro' +// import tw from 'twin.macro' export function Dialog({ children, ...props }: { children: React.ReactNode }) { return ( diff --git a/apps/frontend/src/components/general/Button.tsx b/apps/frontend/src/components/general/Button.tsx index 9f200742..a8eabacd 100644 --- a/apps/frontend/src/components/general/Button.tsx +++ b/apps/frontend/src/components/general/Button.tsx @@ -1,5 +1,5 @@ import React from 'react' -import tw from 'twin.macro' +// import tw from 'twin.macro' interface Props { children: React.ReactNode @@ -30,14 +30,14 @@ export const Button = React.forwardRef( className={className} disabled={disabled} onClick={onClick} - tw="px-4 py-2 rounded-lg disabled:bg-placeholder" - css={[ - primary && - tw` - text-white bg-blue-500 - `, - secondary && tw`text-white bg-gray-500`, - ]} + // tw="px-4 py-2 rounded-lg disabled:bg-placeholder" + // css={[ + // primary && + // tw` + // text-white bg-blue-500 + // `, + // secondary && tw`text-white bg-gray-500`, + // ]} ref={forwardedRef} > {children} diff --git a/apps/frontend/src/components/index.ts b/apps/frontend/src/components/index.ts index b36bc0b9..341cf7d2 100644 --- a/apps/frontend/src/components/index.ts +++ b/apps/frontend/src/components/index.ts @@ -1,12 +1,9 @@ // General export * from './general/Button' -// Layout -export * from './layout/Flex' - // Feedback export * from './feedback/Spinner' -export * from './feedback/Dialog' +// export * from './feedback/_Dialog' // Form export * from './form/TextField' diff --git a/apps/frontend/src/components/layout/Container.tsx b/apps/frontend/src/components/layout/Container.tsx new file mode 100644 index 00000000..5eb947e4 --- /dev/null +++ b/apps/frontend/src/components/layout/Container.tsx @@ -0,0 +1,69 @@ +import { styled, YStack } from 'ui' + +const variants = { + hide: { + true: { + pointerEvents: 'none', + opacity: 0, + }, + }, +} as const + +export const Container = styled(YStack, { + mx: 'auto', + px: '$4', + width: '100%', + + $gtSm: { + maxWidth: 700, + pr: '$2', + }, + + $gtMd: { + maxWidth: 740, + pr: '$2', + }, + + $gtLg: { + maxWidth: 800, + pr: '$10', + }, + + variants, +}) + +export const ContainerLarge = styled(YStack, { + mx: 'auto', + px: '$4', + width: '100%', + + $gtSm: { + maxWidth: 980, + }, + + $gtMd: { + maxWidth: 1140, + }, + + variants, +}) + +export const ContainerXL = styled(YStack, { + mx: 'auto', + px: '$4', + width: '100%', + + $gtSm: { + maxWidth: 980, + }, + + $gtMd: { + maxWidth: 1240, + }, + + $gtLg: { + maxWidth: 1440, + }, + + variants, +}) diff --git a/apps/frontend/src/components/layout/Flex.tsx b/apps/frontend/src/components/layout/Flex.tsx deleted file mode 100644 index 7de092b7..00000000 --- a/apps/frontend/src/components/layout/Flex.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import tw from 'twin.macro' - -export const Flex = tw.div`flex` diff --git a/apps/frontend/src/components/navigation/Navbar.tsx b/apps/frontend/src/components/navigation/Navbar.tsx index 8a2e9440..70849a6f 100644 --- a/apps/frontend/src/components/navigation/Navbar.tsx +++ b/apps/frontend/src/components/navigation/Navbar.tsx @@ -2,33 +2,42 @@ import { PersonIcon } from '@radix-ui/react-icons' import Link, { LinkProps } from 'next/link' import { useRouter } from 'next/router' import React from 'react' -import tw from 'twin.macro' +import { Anchor, Button, Text, useTheme, XStack } from 'ui' import { useCurrentUser } from '~/contexts/currentUser' export function Navbar() { const { isOnboarded } = useCurrentUser() + return ( - + ) } @@ -44,12 +53,17 @@ function NavItem({ const isCurrent = router.pathname === href return ( - -
-

+ + + {children} -

-
- + + + ) } diff --git a/apps/frontend/src/generated/gql.ts b/apps/frontend/src/generated/gql.ts index 1506b171..c5775256 100644 --- a/apps/frontend/src/generated/gql.ts +++ b/apps/frontend/src/generated/gql.ts @@ -1,30 +1,80 @@ /* eslint-disable */ -import * as graphql from './graphql'; +import * as types from './graphql'; import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +/** + * Map of all GraphQL operations in the project. + * + * This map has several performance disadvantages: + * 1. It is not tree-shakeable, so it will include all operations in the project. + * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. + * 3. It does not support dead code elimination, so it will add unused operations. + * + * Therefore it is highly recommended to use the babel-plugin for production. + */ const documents = { - "\n query GetProfile {\n getProfile {\n __typename\n ... on User {\n id\n email\n name\n role\n __typename\n }\n ... on UserNotFound {\n message\n role\n __typename\n }\n }\n }\n": graphql.GetProfileDocument, - "\n query GetAllTodos {\n allTodos {\n id\n createdAt\n updatedAt\n title\n content\n completed\n author {\n name\n }\n }\n }\n": graphql.GetAllTodosDocument, - "\n mutation SaveUser($user: UserInput!) {\n saveUser(user: $user) {\n id\n __typename\n }\n }\n": graphql.SaveUserDocument, - "\n query GetCurrentUser {\n currentUser {\n id\n name\n email\n }\n }\n": graphql.GetCurrentUserDocument, - "\n mutation SaveTodo($todo: TodoInput!) {\n saveTodo(todo: $todo) {\n id\n __typename\n }\n }\n": graphql.SaveTodoDocument, - "\n mutation CompleteTodo($id: ID!) {\n completeTodo(id: $id) {\n id\n }\n }\n": graphql.CompleteTodoDocument, - "\n fragment TodoItem_Todo on Todo {\n id\n title\n }\n": graphql.TodoItem_TodoFragmentDoc, - "\n query GetTodos {\n todosByCurrentUser {\n ...TodoItem_Todo\n id\n completed\n }\n }\n": graphql.GetTodosDocument, - "\n query GetAllUsers {\n allUsers {\n id\n name\n }\n }\n": graphql.GetAllUsersDocument, + "\n query GetProfile {\n getProfile {\n __typename\n ... on User {\n id\n email\n name\n role\n __typename\n }\n ... on UserNotFound {\n message\n role\n __typename\n }\n }\n }\n": types.GetProfileDocument, + "\n query GetAllTodos {\n allTodos {\n id\n createdAt\n updatedAt\n title\n content\n completed\n author {\n name\n }\n }\n }\n": types.GetAllTodosDocument, + "\n mutation SaveUser($user: UserInput!) {\n saveUser(user: $user) {\n id\n __typename\n }\n }\n": types.SaveUserDocument, + "\n query GetCurrentUser {\n currentUser {\n id\n name\n email\n }\n }\n": types.GetCurrentUserDocument, + "\n mutation SaveTodo($todo: TodoInput!) {\n saveTodo(todo: $todo) {\n id\n __typename\n }\n }\n": types.SaveTodoDocument, + "\n mutation CompleteTodo($id: ID!) {\n completeTodo(id: $id) {\n id\n }\n }\n": types.CompleteTodoDocument, + "\n fragment TodoItem_Todo on Todo {\n id\n title\n }\n": types.TodoItem_TodoFragmentDoc, + "\n query GetTodos {\n todosByCurrentUser {\n ...TodoItem_Todo\n id\n completed\n }\n }\n": types.GetTodosDocument, + "\n query GetAllUsers {\n allUsers {\n id\n name\n }\n }\n": types.GetAllUsersDocument, }; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + * + * + * @example + * ```ts + * const query = gql(`query GetUser($id: ID!) { user(id: $id) { name } }`); + * ``` + * + * The query argument is unknown! + * Please regenerate the types. + */ +export function gql(source: string): unknown; + +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ export function gql(source: "\n query GetProfile {\n getProfile {\n __typename\n ... on User {\n id\n email\n name\n role\n __typename\n }\n ... on UserNotFound {\n message\n role\n __typename\n }\n }\n }\n"): (typeof documents)["\n query GetProfile {\n getProfile {\n __typename\n ... on User {\n id\n email\n name\n role\n __typename\n }\n ... on UserNotFound {\n message\n role\n __typename\n }\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ export function gql(source: "\n query GetAllTodos {\n allTodos {\n id\n createdAt\n updatedAt\n title\n content\n completed\n author {\n name\n }\n }\n }\n"): (typeof documents)["\n query GetAllTodos {\n allTodos {\n id\n createdAt\n updatedAt\n title\n content\n completed\n author {\n name\n }\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ export function gql(source: "\n mutation SaveUser($user: UserInput!) {\n saveUser(user: $user) {\n id\n __typename\n }\n }\n"): (typeof documents)["\n mutation SaveUser($user: UserInput!) {\n saveUser(user: $user) {\n id\n __typename\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ export function gql(source: "\n query GetCurrentUser {\n currentUser {\n id\n name\n email\n }\n }\n"): (typeof documents)["\n query GetCurrentUser {\n currentUser {\n id\n name\n email\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ export function gql(source: "\n mutation SaveTodo($todo: TodoInput!) {\n saveTodo(todo: $todo) {\n id\n __typename\n }\n }\n"): (typeof documents)["\n mutation SaveTodo($todo: TodoInput!) {\n saveTodo(todo: $todo) {\n id\n __typename\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ export function gql(source: "\n mutation CompleteTodo($id: ID!) {\n completeTodo(id: $id) {\n id\n }\n }\n"): (typeof documents)["\n mutation CompleteTodo($id: ID!) {\n completeTodo(id: $id) {\n id\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ export function gql(source: "\n fragment TodoItem_Todo on Todo {\n id\n title\n }\n"): (typeof documents)["\n fragment TodoItem_Todo on Todo {\n id\n title\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ export function gql(source: "\n query GetTodos {\n todosByCurrentUser {\n ...TodoItem_Todo\n id\n completed\n }\n }\n"): (typeof documents)["\n query GetTodos {\n todosByCurrentUser {\n ...TodoItem_Todo\n id\n completed\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ export function gql(source: "\n query GetAllUsers {\n allUsers {\n id\n name\n }\n }\n"): (typeof documents)["\n query GetAllUsers {\n allUsers {\n id\n name\n }\n }\n"]; -export function gql(source: string): unknown; export function gql(source: string) { return (documents as any)[source] ?? {}; } diff --git a/apps/frontend/src/pages/_app.page.tsx b/apps/frontend/src/pages/_app.page.tsx index 46878aa7..7833c803 100644 --- a/apps/frontend/src/pages/_app.page.tsx +++ b/apps/frontend/src/pages/_app.page.tsx @@ -1,16 +1,22 @@ -import 'twin.macro' +import '@tamagui/core/reset.css' +import '../style/reset.css' import { Auth0Provider } from '@auth0/auth0-react' +import { Inter } from '@next/font/google' +import { NextThemeProvider, useRootTheme } from '@tamagui/next-theme' import { AppProps } from 'next/app' +import { startTransition } from 'react' +import { config, TamaguiProvider, Theme } from 'ui' import { Navbar, UrqlClientProvider } from '~/components/' +import { Container, ContainerLarge } from '~/components/layout/Container' import { CurrentUserProvider } from '~/contexts/currentUser' -import { GlobalStyles } from '~/style/GlobalStyles' + +const inter = Inter({ subsets: ['latin'] }) function MyApp({ Component, pageProps }: AppProps) { return ( - <> - +
- - - -
- -
-
-
+ + + + + + + + + + + +
-
+ ) +} + +function ThemeProvider({ children }: { children: React.ReactNode }) { + const [theme, setTheme] = useRootTheme() + + return ( + { + startTransition(() => { + setTheme(next) + }) + }} + > + + {children} + + ) } diff --git a/apps/frontend/src/pages/_document.tsx b/apps/frontend/src/pages/_document.tsx new file mode 100644 index 00000000..42f8173c --- /dev/null +++ b/apps/frontend/src/pages/_document.tsx @@ -0,0 +1,44 @@ +import NextDocument, { Head, Html, Main, NextScript } from 'next/document' +import { Children } from 'react' +import { AppRegistry } from 'react-native' + +import Tamagui from '../../tamagui.config' + +export default class Document extends NextDocument { + static async getInitialProps({ renderPage }: any) { + AppRegistry.registerComponent('Main', () => Main) + const page = await renderPage() + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const { getStyleElement } = AppRegistry.getApplication('Main') + + /** + * Note: be sure to keep tamagui styles after react-native-web styles like it is here! + * So Tamagui styles can override the react-native-web styles. + */ + const styles = [ + getStyleElement(), +