From 31ea621e44a6b6034c1db7a92279a47268343d89 Mon Sep 17 00:00:00 2001 From: Emmanuel Pelletier Date: Thu, 11 Jul 2024 18:16:18 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20reorganize=20sta?= =?UTF-8?q?rting=20frontend=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - we now have "features" to try to organize code by intent instead of code type. everything at the root of frontend, not in feature/, is global - customized the panda config a bunch to try to begin to have an actual design system. The idea is to prevent using arbitrary values here and there in the code, but rather semantic tokens - changed the userAuth code logic to handle the fact that a 401 on the users/me call is not really an error per say, but rather an indication the user is not logged in --- src/frontend/package-lock.json | 6 - src/frontend/package.json | 1 - src/frontend/panda.config.ts | 309 +++++++++++++++--- .../fonts/sourcecodepro-regular-subset.woff2 | Bin 0 -> 9468 bytes .../fonts/sourcesans3-bold-subset.woff2 | Bin 0 -> 18272 bytes .../public/fonts/sourcesans3-it-subset.woff2 | Bin 0 -> 14376 bytes .../fonts/sourcesans3-regular-subset.woff2 | Bin 0 -> 18648 bytes src/frontend/src/App.tsx | 8 +- src/frontend/src/api/apiUrl.ts | 9 +- src/frontend/src/api/fetchRoom.ts | 6 - src/frontend/src/api/fetchUser.ts | 6 - .../src/{queries/keys.ts => api/queryKeys.ts} | 0 .../src/{ => features/auth}/api/ApiUser.ts | 0 .../src/features/auth/api/fetchUser.ts | 25 ++ .../src/features/auth/api/useUser.tsx | 17 + src/frontend/src/features/auth/index.ts | 2 + .../src/features/auth/utils/authUrl.ts | 5 + .../src/{ => features/rooms}/api/ApiRoom.ts | 0 .../src/features/rooms/api/fetchRoom.ts | 20 ++ .../features/rooms/components/Conference.tsx | 48 +++ .../src/features/rooms/components/Join.tsx | 17 + src/frontend/src/features/rooms/index.ts | 2 + .../rooms/navigation/navigateToNewRoom.ts | 6 + .../src/features/rooms/routes/Room.tsx | 18 + .../features/rooms/utils/generateRoomId.ts | 5 + src/frontend/src/layout/Box.tsx | 28 ++ src/frontend/src/layout/BoxScreen.tsx | 26 +- src/frontend/src/layout/ErrorScreen.tsx | 12 +- src/frontend/src/layout/ForbiddenScreen.tsx | 15 +- src/frontend/src/layout/Header.tsx | 33 +- src/frontend/src/layout/NotFoundScreen.tsx | 14 +- src/frontend/src/layout/QueryAware.tsx | 20 ++ src/frontend/src/layout/Screen.tsx | 5 +- src/frontend/src/navigation/navigateToHome.ts | 5 + src/frontend/src/primitives/A.tsx | 23 +- src/frontend/src/primitives/Badge.tsx | 29 ++ src/frontend/src/primitives/Bold.tsx | 10 +- src/frontend/src/primitives/Box.tsx | 51 +++ src/frontend/src/primitives/Button.tsx | 54 ++- src/frontend/src/primitives/Div.tsx | 3 + src/frontend/src/primitives/H.tsx | 24 +- src/frontend/src/primitives/Hr.tsx | 15 +- src/frontend/src/primitives/Italic.tsx | 5 + src/frontend/src/primitives/Link.tsx | 18 + src/frontend/src/primitives/P.tsx | 24 +- src/frontend/src/primitives/Text.tsx | 78 +++++ src/frontend/src/primitives/index.ts | 12 + src/frontend/src/queries/useUser.tsx | 30 -- src/frontend/src/routes/Conference.tsx | 81 ----- src/frontend/src/routes/Home.tsx | 78 ++--- src/frontend/src/routes/NotFound.tsx | 9 +- src/frontend/src/styles/fonts.css | 43 +++ src/frontend/src/styles/index.css | 1 + src/frontend/src/styles/livekit.css | 208 ++++++------ src/frontend/src/utils/createRandomRoom.ts | 5 - 55 files changed, 985 insertions(+), 484 deletions(-) create mode 100644 src/frontend/public/fonts/sourcecodepro-regular-subset.woff2 create mode 100644 src/frontend/public/fonts/sourcesans3-bold-subset.woff2 create mode 100644 src/frontend/public/fonts/sourcesans3-it-subset.woff2 create mode 100644 src/frontend/public/fonts/sourcesans3-regular-subset.woff2 delete mode 100644 src/frontend/src/api/fetchRoom.ts delete mode 100644 src/frontend/src/api/fetchUser.ts rename src/frontend/src/{queries/keys.ts => api/queryKeys.ts} (100%) rename src/frontend/src/{ => features/auth}/api/ApiUser.ts (100%) create mode 100644 src/frontend/src/features/auth/api/fetchUser.ts create mode 100644 src/frontend/src/features/auth/api/useUser.tsx create mode 100644 src/frontend/src/features/auth/index.ts create mode 100644 src/frontend/src/features/auth/utils/authUrl.ts rename src/frontend/src/{ => features/rooms}/api/ApiRoom.ts (100%) create mode 100644 src/frontend/src/features/rooms/api/fetchRoom.ts create mode 100644 src/frontend/src/features/rooms/components/Conference.tsx create mode 100644 src/frontend/src/features/rooms/components/Join.tsx create mode 100644 src/frontend/src/features/rooms/index.ts create mode 100644 src/frontend/src/features/rooms/navigation/navigateToNewRoom.ts create mode 100644 src/frontend/src/features/rooms/routes/Room.tsx create mode 100644 src/frontend/src/features/rooms/utils/generateRoomId.ts create mode 100644 src/frontend/src/layout/Box.tsx create mode 100644 src/frontend/src/layout/QueryAware.tsx create mode 100644 src/frontend/src/navigation/navigateToHome.ts create mode 100644 src/frontend/src/primitives/Badge.tsx create mode 100644 src/frontend/src/primitives/Box.tsx create mode 100644 src/frontend/src/primitives/Div.tsx create mode 100644 src/frontend/src/primitives/Italic.tsx create mode 100644 src/frontend/src/primitives/Link.tsx create mode 100644 src/frontend/src/primitives/Text.tsx create mode 100644 src/frontend/src/primitives/index.ts delete mode 100644 src/frontend/src/queries/useUser.tsx delete mode 100644 src/frontend/src/routes/Conference.tsx create mode 100644 src/frontend/src/styles/fonts.css delete mode 100644 src/frontend/src/utils/createRandomRoom.ts diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index fa157a67..34acfae9 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -12,7 +12,6 @@ "@livekit/components-styles": "1.0.12", "@pandacss/preset-panda": "0.41.0", "@tanstack/react-query": "5.49.2", - "classnames": "2.5.1", "livekit-client": "2.3.1", "react": "18.2.0", "react-aria-components": "1.2.1", @@ -4732,11 +4731,6 @@ "node": ">= 6" } }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" - }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", diff --git a/src/frontend/package.json b/src/frontend/package.json index ee3a9775..058dab22 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -14,7 +14,6 @@ "@livekit/components-styles": "1.0.12", "@pandacss/preset-panda": "0.41.0", "@tanstack/react-query": "5.49.2", - "classnames": "2.5.1", "livekit-client": "2.3.1", "react": "18.2.0", "react-aria-components": "1.2.1", diff --git a/src/frontend/panda.config.ts b/src/frontend/panda.config.ts index dfdfffe7..e46a9dab 100644 --- a/src/frontend/panda.config.ts +++ b/src/frontend/panda.config.ts @@ -1,7 +1,35 @@ import pandaPreset from '@pandacss/preset-panda' -import { defineConfig, defineTokens } from '@pandacss/dev' +import { + Config, + Tokens, + defineConfig, + defineSemanticTokens, + defineTextStyles, + defineTokens, +} from '@pandacss/dev' -export default defineConfig({ +const spacing: Tokens['spacing'] = { + 0: { value: '0rem' }, + 0.125: { value: '0.125rem' }, + 0.25: { value: '0.25rem' }, + 0.375: { value: '0.375rem' }, + 0.5: { value: '0.5rem' }, + 0.625: { value: '0.625rem' }, + 0.75: { value: '0.75rem' }, + 1: { value: '1rem' }, + 1.25: { value: '1.25rem' }, + 1.5: { value: '1.5rem' }, + 1.75: { value: '1.75rem' }, + 2: { value: '2rem' }, + 2.25: { value: '2.25rem' }, + 2.5: { value: '2.5rem' }, + 2.75: { value: '2.75rem' }, + 3: { value: '3rem' }, + 3.5: { value: '3.5rem' }, + 4: { value: '4rem' }, +} + +const config: Config = { preflight: true, include: ['./src/**/*.{js,jsx,ts,tsx}'], exclude: [], @@ -9,61 +37,244 @@ export default defineConfig({ outdir: 'src/styled-system', conditions: { extend: { - // React Aria builds upon data attributes instead of css pseudo-classes, make sure to only work based on react aria stuff - hover: '&:is([data-hovered])', - focus: '&:is([data-focused])', - focusVisible: '&:is([data-focus-visible])', - disabled: '&:is([data-disabled])', + // React Aria builds upon data attributes instead of css pseudo-classes, in case we style a React Aria component + // we dont want to trigger pseudo class related styles + 'ra-hover': '&:is([data-hovered])', + 'ra-focus': '&:is([data-focused])', + 'ra-focusVisible': '&:is([data-focus-visible])', + 'ra-disabled': '&:is([data-disabled])', pressed: '&:is([data-pressed])', + 'ra-pressed': '&:is([data-pressed])', }, }, theme: { ...pandaPreset.theme, + // media queries are defined in em so that zooming with text-only mode triggers breakpoints + breakpoints: { + xs: '22.6em', // 360px (we assume less than that are old/entry level mobile phones) + sm: '40em', // 640px + md: '48em', // 768px + lg: '64em', // 1024px + xl: '80em', // 1280px + '2xl': '96em', // 1536px + }, tokens: defineTokens({ + /* we take a few things from the panda preset but for now we clear out some stuff. + * This way we'll only add the things we need step by step and prevent using lots of differents things. + */ ...pandaPreset.theme.tokens, + animations: {}, + blurs: {}, + /* just directly use values as tokens. This allows us to follow a specific design scale, + * without having to remember what 'sm' or '2xl' actually means. + * + * see semanticTokens for tokens targeting specific usages + */ + fonts: { + sans: { + value: [ + 'Source Sans', + 'Source Sans fallback', + 'ui-sans-serif', + 'system-ui', + '-apple-system', + 'BlinkMacSystemFont', + '"Segoe UI"', + 'Roboto', + '"Helvetica Neue"', + 'Arial', + '"Noto Sans"', + 'sans-serif', + '"Apple Color Emoji"', + '"Segoe UI Emoji"', + '"Segoe UI Symbol"', + '"Noto Color Emoji"', + ], + }, + serif: { + value: [ + 'ui-serif', + 'Georgia', + 'Cambria', + '"Times New Roman"', + 'Times', + 'serif', + ], + }, + mono: { + value: [ + 'Source Code Pro', + 'ui-monospace', + 'SFMono-Regular', + 'Menlo', + 'Monaco', + 'Consolas', + '"Liberation Mono"', + '"Courier New"', + 'monospace', + ], + }, + }, + fontSizes: { + 10: { value: '0.625rem' }, + 12: { value: '0.75rem' }, + 14: { value: '0.875rem' }, + 16: { value: '1rem' }, + 20: { value: '1.25rem' }, + 24: { value: '1.5rem' }, + 28: { value: '1.75rem' }, + 32: { value: '2rem' }, + 40: { value: '2.375rem' }, + 48: { value: '3rem' }, + 64: { value: '4rem' }, + }, + letterSpacings: {}, + shadows: { + sm: { + value: [ + '0 1px 3px 0 rgb(0 0 0 / 0.1)', + '0 1px 2px -1px rgb(0 0 0 / 0.1)', + ], + }, + }, + lineHeights: { + 1: { value: '1' }, + 1.25: { value: '1.25' }, + 1.375: { value: '1.375' }, + 1.5: { value: '1.5' }, + 1.625: { value: '1.625' }, + 2: { value: '2' }, + }, + radii: { + 6: { value: '0.375rem' }, + 8: { value: '0.5rem' }, + 16: { value: '1rem' }, + full: { value: '9999px' }, + }, + sizes: { + ...spacing, + full: { value: '100%' }, + min: { value: 'min-content' }, + max: { value: 'max-content' }, + fit: { value: 'fit-content' }, + }, + spacing, + }), + semanticTokens: defineSemanticTokens({ + colors: { + default: { + text: { value: '{colors.gray.900}' }, + bg: { value: '{colors.slate.50}' }, + subtle: { value: '{colors.gray.100}' }, + 'subtle-text': { value: '{colors.gray.600}' }, + }, + box: { + text: { value: '{colors.default.text}' }, + bg: { value: '{colors.white}' }, + border: { value: '{colors.gray.300}' }, + }, + control: { + DEFAULT: { value: '{colors.gray.100}' }, + hover: { value: '{colors.gray.200}' }, + active: { value: '{colors.gray.300}' }, + text: { value: '{colors.default.text}' }, + border: { value: '{colors.gray.300}' }, + }, + primary: { + DEFAULT: { value: '{colors.blue.700}' }, + hover: { value: '{colors.blue.800}' }, + active: { value: '{colors.blue.900}' }, + text: { value: '{colors.white}' }, + warm: { value: '{colors.blue.300}' }, + subtle: { value: '{colors.blue.100}' }, + 'subtle-text': { value: '{colors.sky.700}' }, + }, + danger: { + DEFAULT: { value: '{colors.red.600}' }, + hover: { value: '{colors.red.700}' }, + active: { value: '{colors.red.800}' }, + text: { value: '{colors.white}' }, + subtle: { value: '{colors.red.100}' }, + 'subtle-text': { value: '{colors.red.700}' }, + }, + success: { + DEFAULT: { value: '{colors.emerald.700}' }, + hover: { value: '{colors.emerald.800}' }, + active: { value: '{colors.emerald.900}' }, + text: { value: '{colors.white}' }, + subtle: { value: '{colors.emerald.100}' }, + 'subtle-text': { value: '{colors.emerald.700}' }, + }, + warning: { + DEFAULT: { value: '{colors.amber.700}' }, + hover: { value: '{colors.amber.800}' }, + active: { value: '{colors.amber.900}' }, + text: { value: '{colors.white}' }, + subtle: { value: '{colors.amber.100}' }, + 'subtle-text': { value: '{colors.amber.700}' }, + }, + }, + shadows: { + box: { value: '{shadows.sm}' }, + }, spacing: { - 0: { value: '0rem' }, - 0.125: { value: '0.125rem' }, - 0.25: { value: '0.25rem' }, - 0.375: { value: '0.375rem' }, - 0.5: { value: '0.5rem' }, - 0.75: { value: '0.75rem' }, - 1: { value: '1rem' }, - 1.25: { value: '1.25rem' }, - 1.5: { value: '1.5rem' }, - 1.75: { value: '1.75rem' }, - 2: { value: '2rem' }, - 2.25: { value: '2.25rem' }, - 2.5: { value: '2.5rem' }, - 2.75: { value: '2.75rem' }, - 3: { value: '3rem' }, - 3.5: { value: '3.5rem' }, - 4: { value: '4rem' }, - 5: { value: '5rem' }, - 6: { value: '6rem' }, - 7: { value: '7rem' }, - 8: { value: '8rem' }, - 9: { value: '9rem' }, - 10: { value: '10rem' }, - 12: { value: '12rem' }, - 14: { value: '14rem' }, - 16: { value: '16rem' }, - 20: { value: '20rem' }, - 24: { value: '24rem' }, - 28: { value: '28rem' }, - 32: { value: '32rem' }, - 36: { value: '36rem' }, - 40: { value: '40rem' }, - 44: { value: '44rem' }, - 48: { value: '48rem' }, - 52: { value: '52rem' }, - 56: { value: '56rem' }, - 60: { value: '60rem' }, - 64: { value: '64rem' }, - 72: { value: '72rem' }, - 80: { value: '80rem' }, - 96: { value: '96rem' }, + boxPadding: { + DEFAULT: { value: '{spacing.2}' }, + sm: { value: '{spacing.1}' }, + xs: { value: '{spacing.0.5}' }, + }, + boxMargin: { + xs: { value: '{spacing.0.5}' }, + DEFAULT: { value: '{spacing.1}' }, + lg: { value: '{spacing.2}' }, + }, + paragraph: { value: '{spacing.1}' }, + heading: { value: '{spacing.1}' }, + gutter: { value: '{spacing.1}' }, + }, + }), + textStyles: defineTextStyles({ + h1: { + value: { + fontSize: '1.5rem', + lineHeight: '2rem', + fontWeight: 700, + }, + }, + h2: { + value: { + fontSize: '1.25rem', + lineHeight: '1.75rem', + fontWeight: 700, + }, + }, + h3: { + value: { + fontSize: '1.125rem', + lineHeight: '1.75rem', + fontWeight: 700, + }, + }, + body: { + value: { + fontSize: '1rem', + lineHeight: '1.5', + }, + }, + small: { + value: { + fontSize: '0.875rem', + lineHeight: '1.25rem', + }, + }, + badge: { + value: { + fontSize: '0.75rem', + lineHeight: '1rem', + }, }, }), }, -}) +} + +export default defineConfig(config) diff --git a/src/frontend/public/fonts/sourcecodepro-regular-subset.woff2 b/src/frontend/public/fonts/sourcecodepro-regular-subset.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..4b1b188b024465ca8a6a0acb5625e7aaaa72a5e0 GIT binary patch literal 9468 zcmV)bPew8T0RR9103`eX5dZ)H09o(=03?|J0RR9100000000000000000000 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 O=8;j!LPZnw#1sJed@?@( literal 0 HcmV?d00001 diff --git a/src/frontend/public/fonts/sourcesans3-bold-subset.woff2 b/src/frontend/public/fonts/sourcesans3-bold-subset.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..199c4ace27c287c90dd25c9e22e7ad9aa08f1896 GIT binary patch literal 18272 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 literal 0 HcmV?d00001 diff --git a/src/frontend/public/fonts/sourcesans3-it-subset.woff2 b/src/frontend/public/fonts/sourcesans3-it-subset.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3440a8ba97e062996f1eba961490dc4aa8abb2cf GIT binary patch literal 14376 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 literal 0 HcmV?d00001 diff --git a/src/frontend/public/fonts/sourcesans3-regular-subset.woff2 b/src/frontend/public/fonts/sourcesans3-regular-subset.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3ef2d301edf0c0889f7e07ccaa4cab1799289fa7 GIT binary patch literal 18648 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 literal 0 HcmV?d00001 diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 63fd9e7a..6c327ad7 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -4,8 +4,8 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { Route, Switch } from 'wouter' import { Home } from './routes/Home' -import { Conference } from './routes/Conference' -import { NotFoundScreen } from './layout/NotFoundScreen' +import { NotFound } from './routes/NotFound' +import { RoomRoute } from '@/features/rooms' const queryClient = new QueryClient() @@ -14,8 +14,8 @@ function App() { - - + + diff --git a/src/frontend/src/api/apiUrl.ts b/src/frontend/src/api/apiUrl.ts index f4032197..a7d666bb 100644 --- a/src/frontend/src/api/apiUrl.ts +++ b/src/frontend/src/api/apiUrl.ts @@ -1,12 +1,11 @@ export const apiUrl = (path: string, apiVersion = '1.0') => { - const origin = - import.meta.env.VITE_API_BASE_URL - || (typeof window !== 'undefined' ? window.location.origin : ''); + import.meta.env.VITE_API_BASE_URL || + (typeof window !== 'undefined' ? window.location.origin : '') // Remove leading/trailing slashes from origin/path if it exists const sanitizedOrigin = origin.replace(/\/$/, '') - const sanitizedPath = path.replace(/^\//, ''); + const sanitizedPath = path.replace(/^\//, '') - return `${sanitizedOrigin}/api/v${apiVersion}/${sanitizedPath}`; + return `${sanitizedOrigin}/api/v${apiVersion}/${sanitizedPath}` } diff --git a/src/frontend/src/api/fetchRoom.ts b/src/frontend/src/api/fetchRoom.ts deleted file mode 100644 index 5a7c255f..00000000 --- a/src/frontend/src/api/fetchRoom.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ApiRoom } from './ApiRoom' -import { fetchApi } from './fetchApi' - -export const fetchRoom = (roomId: string) => { - return fetchApi(`/rooms/${roomId}`) -} diff --git a/src/frontend/src/api/fetchUser.ts b/src/frontend/src/api/fetchUser.ts deleted file mode 100644 index 44bb1f9e..00000000 --- a/src/frontend/src/api/fetchUser.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { ApiUser } from './ApiUser' -import { fetchApi } from './fetchApi' - -export const fetchUser = () => { - return fetchApi('/users/me') -} diff --git a/src/frontend/src/queries/keys.ts b/src/frontend/src/api/queryKeys.ts similarity index 100% rename from src/frontend/src/queries/keys.ts rename to src/frontend/src/api/queryKeys.ts diff --git a/src/frontend/src/api/ApiUser.ts b/src/frontend/src/features/auth/api/ApiUser.ts similarity index 100% rename from src/frontend/src/api/ApiUser.ts rename to src/frontend/src/features/auth/api/ApiUser.ts diff --git a/src/frontend/src/features/auth/api/fetchUser.ts b/src/frontend/src/features/auth/api/fetchUser.ts new file mode 100644 index 00000000..c2a60c9a --- /dev/null +++ b/src/frontend/src/features/auth/api/fetchUser.ts @@ -0,0 +1,25 @@ +import { ApiError } from '@/api/ApiError' +import { fetchApi } from '@/api/fetchApi' +import { type ApiUser } from './ApiUser' + +/** + * fetch the logged in user from the api. + * + * If the user is not logged in, the api returns a 401 error. + * Here our wrapper just returns false in that case, without triggering an error: + * this is done to prevent unnecessary query retries with react query + */ +export const fetchUser = (): Promise => { + return new Promise((resolve, reject) => { + fetchApi('/users/me') + .then(resolve) + .catch((error) => { + // we assume that a 401 means the user is not logged in + if (error instanceof ApiError && error.statusCode === 401) { + resolve(false) + } else { + reject(error) + } + }) + }) +} diff --git a/src/frontend/src/features/auth/api/useUser.tsx b/src/frontend/src/features/auth/api/useUser.tsx new file mode 100644 index 00000000..6286ca12 --- /dev/null +++ b/src/frontend/src/features/auth/api/useUser.tsx @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query' +import { keys } from '@/api/queryKeys' +import { fetchUser } from './fetchUser' + +export const useUser = () => { + const query = useQuery({ + queryKey: [keys.user], + queryFn: fetchUser, + }) + + return { + ...query, + // if fetchUser returns false, it means the user is not logged in: expose that + user: query.data === false ? undefined : query.data, + isLoggedIn: query.data !== undefined && query.data !== false, + } +} diff --git a/src/frontend/src/features/auth/index.ts b/src/frontend/src/features/auth/index.ts new file mode 100644 index 00000000..4d2344cf --- /dev/null +++ b/src/frontend/src/features/auth/index.ts @@ -0,0 +1,2 @@ +export { useUser } from './api/useUser' +export { authUrl } from './utils/authUrl' diff --git a/src/frontend/src/features/auth/utils/authUrl.ts b/src/frontend/src/features/auth/utils/authUrl.ts new file mode 100644 index 00000000..111d1399 --- /dev/null +++ b/src/frontend/src/features/auth/utils/authUrl.ts @@ -0,0 +1,5 @@ +import { apiUrl } from '@/api/apiUrl' + +export const authUrl = () => { + return apiUrl('/authenticate') +} diff --git a/src/frontend/src/api/ApiRoom.ts b/src/frontend/src/features/rooms/api/ApiRoom.ts similarity index 100% rename from src/frontend/src/api/ApiRoom.ts rename to src/frontend/src/features/rooms/api/ApiRoom.ts diff --git a/src/frontend/src/features/rooms/api/fetchRoom.ts b/src/frontend/src/features/rooms/api/fetchRoom.ts new file mode 100644 index 00000000..cd2f9c0d --- /dev/null +++ b/src/frontend/src/features/rooms/api/fetchRoom.ts @@ -0,0 +1,20 @@ +import { ApiError } from '@/api/ApiError' +import { type ApiRoom } from './ApiRoom' +import { fetchApi } from '@/api/fetchApi' + +export const fetchRoom = ({ + roomId, + username = '', +}: { + roomId: string + username?: string +}) => { + return fetchApi( + `/rooms/${roomId}?username=${encodeURIComponent(username)}` + ).then((room) => { + if (!room.livekit?.token || !room.livekit?.url) { + throw new ApiError(500, 'LiveKit info not found') + } + return room + }) +} diff --git a/src/frontend/src/features/rooms/components/Conference.tsx b/src/frontend/src/features/rooms/components/Conference.tsx new file mode 100644 index 00000000..18c6e963 --- /dev/null +++ b/src/frontend/src/features/rooms/components/Conference.tsx @@ -0,0 +1,48 @@ +import { useParams } from 'wouter' +import { useQuery } from '@tanstack/react-query' +import { + LiveKitRoom, + VideoConference, + type LocalUserChoices, +} from '@livekit/components-react' +import { keys } from '@/api/queryKeys' +import { QueryAware } from '@/layout/QueryAware' +import { navigateToHome } from '@/navigation/navigateToHome' +import { fetchRoom } from '../api/fetchRoom' + +export const Conference = ({ + userConfig, +}: { + userConfig: LocalUserChoices +}) => { + const { roomId } = useParams() + const { status, data } = useQuery({ + queryKey: [keys.room, roomId, userConfig.username], + queryFn: () => + fetchRoom({ + roomId: roomId as string, + username: userConfig.username, + }), + }) + + return ( + + { + navigateToHome() + }} + > + + + + ) +} diff --git a/src/frontend/src/features/rooms/components/Join.tsx b/src/frontend/src/features/rooms/components/Join.tsx new file mode 100644 index 00000000..3ef5d411 --- /dev/null +++ b/src/frontend/src/features/rooms/components/Join.tsx @@ -0,0 +1,17 @@ +import { Box } from '@/layout/Box' +import { PreJoin, type LocalUserChoices } from '@livekit/components-react' + +export const Join = ({ + onSubmit, +}: { + onSubmit: (choices: LocalUserChoices) => void +}) => { + return ( + + + + ) +} diff --git a/src/frontend/src/features/rooms/index.ts b/src/frontend/src/features/rooms/index.ts new file mode 100644 index 00000000..dcfe38ef --- /dev/null +++ b/src/frontend/src/features/rooms/index.ts @@ -0,0 +1,2 @@ +export { navigateToNewRoom } from './navigation/navigateToNewRoom' +export { Room as RoomRoute } from './routes/Room' diff --git a/src/frontend/src/features/rooms/navigation/navigateToNewRoom.ts b/src/frontend/src/features/rooms/navigation/navigateToNewRoom.ts new file mode 100644 index 00000000..4d20b364 --- /dev/null +++ b/src/frontend/src/features/rooms/navigation/navigateToNewRoom.ts @@ -0,0 +1,6 @@ +import { navigate } from 'wouter/use-browser-location' +import { generateRoomId } from '../utils/generateRoomId' + +export const navigateToNewRoom = () => { + navigate(`/${generateRoomId()}`) +} diff --git a/src/frontend/src/features/rooms/routes/Room.tsx b/src/frontend/src/features/rooms/routes/Room.tsx new file mode 100644 index 00000000..c2552f40 --- /dev/null +++ b/src/frontend/src/features/rooms/routes/Room.tsx @@ -0,0 +1,18 @@ +import { type LocalUserChoices } from '@livekit/components-react' +import { useState } from 'react' +import { Conference } from '../components/Conference' +import { Join } from '../components/Join' +import { Screen } from '@/layout/Screen' + +export const Room = () => { + const [userConfig, setUserConfig] = useState(null) + return ( + + {userConfig ? ( + + ) : ( + + )} + + ) +} diff --git a/src/frontend/src/features/rooms/utils/generateRoomId.ts b/src/frontend/src/features/rooms/utils/generateRoomId.ts new file mode 100644 index 00000000..f8be43bb --- /dev/null +++ b/src/frontend/src/features/rooms/utils/generateRoomId.ts @@ -0,0 +1,5 @@ +import { slugify } from '@/utils/slugify' + +export const generateRoomId = () => { + return slugify(crypto.randomUUID()) +} diff --git a/src/frontend/src/layout/Box.tsx b/src/frontend/src/layout/Box.tsx new file mode 100644 index 00000000..23fe93b9 --- /dev/null +++ b/src/frontend/src/layout/Box.tsx @@ -0,0 +1,28 @@ +import type { ReactNode } from 'react' +import { Box as BoxDiv, H, Link } from '@/primitives' + +export type BoxProps = { + children?: ReactNode + title?: ReactNode + withBackButton?: boolean +} + +export const Box = ({ + children, + title = '', + withBackButton = false, +}: BoxProps) => { + return ( + + {!!title && {title}} + {children} + {!!withBackButton && ( +

+ + Back to homescreen + +

+ )} +
+ ) +} diff --git a/src/frontend/src/layout/BoxScreen.tsx b/src/frontend/src/layout/BoxScreen.tsx index 6919359b..147639b2 100644 --- a/src/frontend/src/layout/BoxScreen.tsx +++ b/src/frontend/src/layout/BoxScreen.tsx @@ -1,30 +1,10 @@ -import type { ReactNode } from 'react' -import classNames from 'classnames' -import { css } from '@/styled-system/css' import { Screen } from './Screen' +import { Box, type BoxProps } from './Box' -export const BoxScreen = ({ children }: { children: ReactNode }) => { +export const BoxScreen = (props: BoxProps) => { return ( -
- {children} -
+
) } diff --git a/src/frontend/src/layout/ErrorScreen.tsx b/src/frontend/src/layout/ErrorScreen.tsx index 23551c96..31d5241f 100644 --- a/src/frontend/src/layout/ErrorScreen.tsx +++ b/src/frontend/src/layout/ErrorScreen.tsx @@ -1,17 +1,7 @@ -import { Link } from 'wouter' -import { A } from '@/primitives/A' -import { H1 } from '@/primitives/H' import { BoxScreen } from './BoxScreen' export const ErrorScreen = () => { return ( - -

An error occured while loading the page

-

- - Back to homescreen - -

-
+ ) } diff --git a/src/frontend/src/layout/ForbiddenScreen.tsx b/src/frontend/src/layout/ForbiddenScreen.tsx index 6e1e44c5..c74adfd2 100644 --- a/src/frontend/src/layout/ForbiddenScreen.tsx +++ b/src/frontend/src/layout/ForbiddenScreen.tsx @@ -1,17 +1,10 @@ -import { Link } from 'wouter' -import { A } from '@/primitives/A' -import { H1 } from '@/primitives/H' import { BoxScreen } from './BoxScreen' export const ForbiddenScreen = () => { return ( - -

You don't have the permission to view this page

-

- - Back to homescreen - -

-
+ ) } diff --git a/src/frontend/src/layout/Header.tsx b/src/frontend/src/layout/Header.tsx index ba0f94fb..338aba67 100644 --- a/src/frontend/src/layout/Header.tsx +++ b/src/frontend/src/layout/Header.tsx @@ -1,19 +1,22 @@ -import { apiUrl } from '@/api/apiUrl' -import { A } from '@/primitives/A' -import { useUser } from '@/queries/useUser' import { css } from '@/styled-system/css' import { flex } from '@/styled-system/patterns' +import { apiUrl } from '@/api/apiUrl' +import { A, Badge, Text } from '@/primitives' +import { useUser } from '@/features/auth/api/useUser' export const Header = () => { const { user, isLoggedIn } = useUser() return (
{ })} >
-

+ Meet -

+
{isLoggedIn === false && Login} {!!user && ( -

- {user.email}  Logout +

+ {user.email} + + Logout +

)}
diff --git a/src/frontend/src/layout/NotFoundScreen.tsx b/src/frontend/src/layout/NotFoundScreen.tsx index 87dba1c1..397a51a0 100644 --- a/src/frontend/src/layout/NotFoundScreen.tsx +++ b/src/frontend/src/layout/NotFoundScreen.tsx @@ -1,17 +1,5 @@ -import { Link } from 'wouter' -import { A } from '@/primitives/A' -import { H1 } from '@/primitives/H' import { BoxScreen } from './BoxScreen' export const NotFoundScreen = () => { - return ( - -

Page not found

-

- - Back to homescreen - -

-
- ) + return } diff --git a/src/frontend/src/layout/QueryAware.tsx b/src/frontend/src/layout/QueryAware.tsx new file mode 100644 index 00000000..466d6a7c --- /dev/null +++ b/src/frontend/src/layout/QueryAware.tsx @@ -0,0 +1,20 @@ +import { ErrorScreen } from './ErrorScreen' +import { LoadingScreen } from './LoadingScreen' + +export const QueryAware = ({ + status, + children, +}: { + status: 'error' | 'pending' | 'success' + children: React.ReactNode +}) => { + if (status === 'error') { + return + } + + if (status === 'pending') { + return + } + + return children +} diff --git a/src/frontend/src/layout/Screen.tsx b/src/frontend/src/layout/Screen.tsx index 211ad3d7..f9bc4d0f 100644 --- a/src/frontend/src/layout/Screen.tsx +++ b/src/frontend/src/layout/Screen.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import { css } from '@/styled-system/css' -import { Header } from '@/layout/Header' +import { Header } from './Header' export const Screen = ({ children }: { children: ReactNode }) => { return ( @@ -9,7 +9,8 @@ export const Screen = ({ children }: { children: ReactNode }) => { height: '100%', display: 'flex', flexDirection: 'column', - backgroundColor: 'slate.50', + backgroundColor: 'default.bg', + color: 'default.text', })} >
diff --git a/src/frontend/src/navigation/navigateToHome.ts b/src/frontend/src/navigation/navigateToHome.ts new file mode 100644 index 00000000..42f84d16 --- /dev/null +++ b/src/frontend/src/navigation/navigateToHome.ts @@ -0,0 +1,5 @@ +import { navigate } from 'wouter/use-browser-location' + +export const navigateToHome = () => { + navigate(`/`) +} diff --git a/src/frontend/src/primitives/A.tsx b/src/frontend/src/primitives/A.tsx index a3105d8e..3dc9e69b 100644 --- a/src/frontend/src/primitives/A.tsx +++ b/src/frontend/src/primitives/A.tsx @@ -1,8 +1,5 @@ -import { RecipeVariantProps, cva } from '@/styled-system/css' -import { - Link as Link, - type LinkProps as LinksProps, -} from 'react-aria-components' +import { Link, type LinkProps } from 'react-aria-components' +import { cva, type RecipeVariantProps } from '@/styled-system/css' const link = cva({ base: { @@ -10,25 +7,27 @@ const link = cva({ textUnderlineOffset: '2', transition: 'all 200ms', cursor: 'pointer', - _hover: { + '_ra-hover': { textDecoration: 'none', }, - _pressed: { + '_ra-pressed': { textDecoration: 'underline', }, }, variants: { size: { small: { - fontSize: 'sm', + textStyle: 'small', }, }, }, }) -export const A = ({ - size, - ...props -}: LinksProps & RecipeVariantProps) => { +export type AProps = LinkProps & RecipeVariantProps + +/** + * anchor component styled with underline + */ +export const A = ({ size, ...props }: AProps) => { return } diff --git a/src/frontend/src/primitives/Badge.tsx b/src/frontend/src/primitives/Badge.tsx new file mode 100644 index 00000000..08ac9b06 --- /dev/null +++ b/src/frontend/src/primitives/Badge.tsx @@ -0,0 +1,29 @@ +import { cva, type RecipeVariantProps } from '@/styled-system/css' + +const badge = cva({ + base: { + display: 'inline-block', + padding: '0.25rem 0.5rem', + backgroundColor: 'primary.subtle', + color: 'primary.subtle-text', + borderRadius: '6', + }, + variants: { + size: { + small: { + textStyle: 'badge', + }, + normal: {}, + }, + }, + defaultVariants: { + size: 'normal', + }, +}) + +export type BadgeProps = React.HTMLAttributes & + RecipeVariantProps + +export const Badge = ({ size, ...props }: BadgeProps) => { + return +} diff --git a/src/frontend/src/primitives/Bold.tsx b/src/frontend/src/primitives/Bold.tsx index 50be31b6..9eeb11c6 100644 --- a/src/frontend/src/primitives/Bold.tsx +++ b/src/frontend/src/primitives/Bold.tsx @@ -1,9 +1,5 @@ -import { css } from '@/styled-system/css' +import { Text, type As } from './Text' -const bold = css({ - fontWeight: 'bold', -}) - -export const Bold = (props: React.HTMLAttributes) => { - return +export const Bold = (props: React.HTMLAttributes & As) => { + return } diff --git a/src/frontend/src/primitives/Box.tsx b/src/frontend/src/primitives/Box.tsx new file mode 100644 index 00000000..a59486b0 --- /dev/null +++ b/src/frontend/src/primitives/Box.tsx @@ -0,0 +1,51 @@ +import { cva } from '@/styled-system/css' +import { styled } from '../styled-system/jsx' + +const box = cva({ + base: { + gap: 'gutter', + borderRadius: 8, + padding: 'boxPadding', + flex: 1, + }, + variants: { + asScreen: { + true: { + margin: 'auto', + width: '38rem', + maxWidth: '100%', + marginTop: '6rem', + textAlign: 'center', + }, + }, + variant: { + default: { + borderWidth: '1px', + borderStyle: 'solid', + borderColor: 'box.border', + backgroundColor: 'box.bg', + color: 'box.text', + boxShadow: 'box', + }, + subtle: { + color: 'default.subtle-text', + backgroundColor: 'default.subtle', + }, + }, + size: { + default: { + padding: 'boxPadding', + }, + sm: { + padding: 'boxPadding.sm', + }, + }, + }, + defaultVariants: { + asScreen: false, + variant: 'default', + size: 'default', + }, +}) + +export const Box = styled('div', box) diff --git a/src/frontend/src/primitives/Button.tsx b/src/frontend/src/primitives/Button.tsx index 7f37079b..4e47470c 100644 --- a/src/frontend/src/primitives/Button.tsx +++ b/src/frontend/src/primitives/Button.tsx @@ -1,8 +1,58 @@ import { Button as RACButton, type ButtonProps as RACButtonsProps, + Link, + LinkProps, } from 'react-aria-components' +import { cva, type RecipeVariantProps } from '@/styled-system/css' -export const Button = (props: RACButtonsProps) => { - return +const button = cva({ + base: { + display: 'inline-block', + paddingX: '1', + paddingY: '0.625', + transition: 'all 200ms', + borderRadius: 8, + cursor: 'pointer', + }, + variants: { + variant: { + default: { + color: 'control.text', + backgroundColor: 'control', + '_ra-hover': { + backgroundColor: 'control.hover', + }, + '_ra-pressed': { + backgroundColor: 'control.active', + }, + }, + primary: { + color: 'primary.text', + backgroundColor: 'primary', + '_ra-hover': { + backgroundColor: 'primary.hover', + }, + '_ra-pressed': { + backgroundColor: 'primary.active', + }, + }, + }, + }, +}) + +type ButtonProps = RecipeVariantProps & + (RACButtonsProps | LinkProps) + +export const Button = (props: ButtonProps) => { + const [variantProps, componentProps] = button.splitVariantProps(props) + if ((props as LinkProps).href !== undefined) { + return + } + return ( + + ) } diff --git a/src/frontend/src/primitives/Div.tsx b/src/frontend/src/primitives/Div.tsx new file mode 100644 index 00000000..47e3d574 --- /dev/null +++ b/src/frontend/src/primitives/Div.tsx @@ -0,0 +1,3 @@ +import { Box } from '@/styled-system/jsx' + +export const Div = Box diff --git a/src/frontend/src/primitives/H.tsx b/src/frontend/src/primitives/H.tsx index b9b1b5eb..99b1e122 100644 --- a/src/frontend/src/primitives/H.tsx +++ b/src/frontend/src/primitives/H.tsx @@ -1,24 +1,14 @@ -import type { HTMLAttributes } from 'react' -import classNames from 'classnames' -import { css } from '@/styled-system/css' +import { Text } from './Text' -export const H1 = ({ +export const H = ({ children, - className, + lvl, ...props -}: HTMLAttributes) => { +}: React.HTMLAttributes & { lvl: 1 | 2 | 3 }) => { + const tag = `h${lvl}` as const return ( -

+ {children} -

+
) } diff --git a/src/frontend/src/primitives/Hr.tsx b/src/frontend/src/primitives/Hr.tsx index 43ddf348..2faee93c 100644 --- a/src/frontend/src/primitives/Hr.tsx +++ b/src/frontend/src/primitives/Hr.tsx @@ -1,10 +1,13 @@ import { css } from '@/styled-system/css' -const hr = css({ - marginY: '1', - borderColor: 'neutral.300', -}) - export const Hr = (props: React.HTMLAttributes) => { - return
+ return ( +
+ ) } diff --git a/src/frontend/src/primitives/Italic.tsx b/src/frontend/src/primitives/Italic.tsx new file mode 100644 index 00000000..544b6a36 --- /dev/null +++ b/src/frontend/src/primitives/Italic.tsx @@ -0,0 +1,5 @@ +import { Text, type As } from './Text' + +export const Italic = (props: React.HTMLAttributes & As) => { + return +} diff --git a/src/frontend/src/primitives/Link.tsx b/src/frontend/src/primitives/Link.tsx new file mode 100644 index 00000000..1b834a5d --- /dev/null +++ b/src/frontend/src/primitives/Link.tsx @@ -0,0 +1,18 @@ +import { Link as WouterLink } from 'wouter' +import { A, type AProps } from './A' + +/** + * Wouter link wrapper to use our A primitive + */ +export const Link = ({ + to, + ...props +}: { + to: string +} & AProps) => { + return ( + + + + ) +} diff --git a/src/frontend/src/primitives/P.tsx b/src/frontend/src/primitives/P.tsx index f7701460..c613d357 100644 --- a/src/frontend/src/primitives/P.tsx +++ b/src/frontend/src/primitives/P.tsx @@ -1,23 +1,5 @@ -import type { HTMLAttributes } from 'react' -import classNames from 'classnames' -import { css } from '@/styled-system/css' +import { Text, type As } from './Text' -export const P = ({ - children, - className, - ...props -}: HTMLAttributes) => { - return ( -

- {children} -

- ) +export const P = (props: React.HTMLAttributes & As) => { + return } diff --git a/src/frontend/src/primitives/Text.tsx b/src/frontend/src/primitives/Text.tsx new file mode 100644 index 00000000..4fc1dcbe --- /dev/null +++ b/src/frontend/src/primitives/Text.tsx @@ -0,0 +1,78 @@ +import type { HTMLAttributes } from 'react' +import { RecipeVariantProps, cva, cx } from '@/styled-system/css' + +const text = cva({ + base: {}, + variants: { + variant: { + h1: { + textStyle: 'h1', + marginBottom: 'heading', + }, + h2: { + textStyle: 'h2', + marginBottom: 'heading', + }, + h3: { + textStyle: 'h3', + marginBottom: 'heading', + }, + body: { + textStyle: 'body', + }, + paragraph: { + textStyle: 'body', + marginBottom: 'paragraph', + }, + small: { + textStyle: 'small', + }, + inherits: {}, + }, + bold: { + true: { + fontWeight: 'bold', + }, + false: { + fontWeight: 'normal', + }, + }, + italic: { + true: { + fontStyle: 'italic', + }, + }, + margin: { + false: { + margin: '0!', + }, + }, + }, + defaultVariants: { + variant: 'inherits', + }, +}) + +type TextHTMLProps = HTMLAttributes +type TextElement = + | 'h1' + | 'h2' + | 'h3' + | 'h4' + | 'h5' + | 'h6' + | 'p' + | 'span' + | 'strong' + | 'em' + | 'div' +export type As = { as?: TextElement } +export type TextProps = RecipeVariantProps & TextHTMLProps & As + +export function Text(props: TextProps) { + const [variantProps, componentProps] = text.splitVariantProps(props) + const { as: Component = 'p', className, ...tagProps } = componentProps + return ( + + ) +} diff --git a/src/frontend/src/primitives/index.ts b/src/frontend/src/primitives/index.ts new file mode 100644 index 00000000..a13a55e1 --- /dev/null +++ b/src/frontend/src/primitives/index.ts @@ -0,0 +1,12 @@ +export { A } from './A' +export { Badge } from './Badge' +export { Bold } from './Bold' +export { Box } from './Box' +export { Button } from './Button' +export { Div } from './Div' +export { H } from './H' +export { Hr } from './Hr' +export { Italic } from './Italic' +export { Link } from './Link' +export { P } from './P' +export { Text } from './Text' diff --git a/src/frontend/src/queries/useUser.tsx b/src/frontend/src/queries/useUser.tsx deleted file mode 100644 index 8262e06e..00000000 --- a/src/frontend/src/queries/useUser.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useQuery } from '@tanstack/react-query' -import { keys } from './keys' -import { fetchUser } from '@/api/fetchUser' - -export const useUser = () => { - const query = useQuery({ - queryKey: [keys.user], - queryFn: fetchUser, - refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retryOnMount: false, - retry: (_, error) => { - return error.statusCode !== 401 - }, - }) - - let isLoggedIn - if (query.status === 'success' && query.data?.email !== null) { - isLoggedIn = true - } - if (query.status === 'error' && query.failureReason?.statusCode === 401) { - isLoggedIn = false - } - return { - ...query, - user: query.data, - isLoggedIn, - } -} diff --git a/src/frontend/src/routes/Conference.tsx b/src/frontend/src/routes/Conference.tsx deleted file mode 100644 index ab45cb8a..00000000 --- a/src/frontend/src/routes/Conference.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { fetchRoom } from '@/api/fetchRoom' -import { BoxScreen } from '@/layout/BoxScreen' -import { ErrorScreen } from '@/layout/ErrorScreen' -import { ForbiddenScreen } from '@/layout/ForbiddenScreen' -import { LoadingScreen } from '@/layout/LoadingScreen' -import { Screen } from '@/layout/Screen' -import { A } from '@/primitives/A' -import { H1 } from '@/primitives/H' -import { keys } from '@/queries/keys' -import { - LiveKitRoom, - VideoConference, - PreJoin, - LocalUserChoices, -} from '@livekit/components-react' -import { useQuery } from '@tanstack/react-query' -import { useState } from 'react' -import { Link, useLocation, useParams } from 'wouter' - -export const Conference = () => { - const { roomId } = useParams() - const [, navigate] = useLocation() - const { status, data } = useQuery({ - queryKey: [keys.room, roomId], - queryFn: ({ queryKey }) => fetchRoom(queryKey[1] as string), - }) - - const [userConfig, setUserConfig] = useState(null) - - if (!userConfig) { - return ( - -

Verify your settings before joining

- { - setUserConfig(choices) - }} - /> -

- - Back to homescreen - -

- - ) - } - - if (status === 'error' || (status === 'success' && !data?.livekit)) { - return - } - - if (data?.is_public === false) { - return - } - - if (data?.livekit?.token && data?.livekit?.url) { - return ( - - { - navigate('/') - }} - > - - - - ) - } - - return -} diff --git a/src/frontend/src/routes/Home.tsx b/src/frontend/src/routes/Home.tsx index 38f036c8..4118235a 100644 --- a/src/frontend/src/routes/Home.tsx +++ b/src/frontend/src/routes/Home.tsx @@ -1,49 +1,41 @@ -import { useLocation } from 'wouter' -import { useUser } from '@/queries/useUser' -import { Button } from '@/primitives/Button' -import { A } from '@/primitives/A' -import { Bold } from '@/primitives/Bold' -import { H1 } from '@/primitives/H' -import { P } from '@/primitives/P' -import { Hr } from '@/primitives/Hr' +import { A, Button, Italic, P, Div, H, Box } from '@/primitives' +import { useUser } from '@/features/auth' import { apiUrl } from '@/api/apiUrl' -import { createRandomRoom } from '@/utils/createRandomRoom' -import { LoadingScreen } from '@/layout/LoadingScreen' -import { BoxScreen } from '@/layout/BoxScreen' +import { navigateToNewRoom } from '@/features/rooms' +import { Screen } from '@/layout/Screen' export const Home = () => { - const { status, isLoggedIn } = useUser() - const [, navigate] = useLocation() - - if (status === 'pending') { - return - } - + const { isLoggedIn } = useUser() return ( - -

- Welcome in Meet! -

- {isLoggedIn ? ( - - ) : ( - <> -

What do you want to do? You can either:

- -

- - Login to create a conference call - -

- -

- Or copy a URL in your - browser address bar to join an existing conference call -

- - )} -
+ + + Welcome in Meet +

What do you want to do? You can either:

+
+ + {isLoggedIn ? ( + + ) : ( +

+ + Login to create a conference call + +

+ )} +
+
+

+ Or +

+ +

+ copy a meeting URL in your browser address bar to join an existing + conference call +

+
+
+
) } diff --git a/src/frontend/src/routes/NotFound.tsx b/src/frontend/src/routes/NotFound.tsx index 5054f24c..0dedc9ca 100644 --- a/src/frontend/src/routes/NotFound.tsx +++ b/src/frontend/src/routes/NotFound.tsx @@ -1,8 +1,5 @@ +import { NotFoundScreen } from '@/layout/NotFoundScreen' + export const NotFound = () => { - return ( -
-

404

-

Page not found

-
- ) + return } diff --git a/src/frontend/src/styles/fonts.css b/src/frontend/src/styles/fonts.css new file mode 100644 index 00000000..5af0abc7 --- /dev/null +++ b/src/frontend/src/styles/fonts.css @@ -0,0 +1,43 @@ +@font-face { + font-family: 'Source Sans'; + src: url('/fonts/sourcesans3-regular-subset.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Source Sans'; + src: url('/fonts/sourcesans3-it-subset.woff2') format('woff2'); + font-weight: 400; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'Source Sans'; + src: url('/fonts/sourcesans3-bold-subset.woff2') format('woff2'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Source Code Pro'; + src: url('/fonts/sourcecodepro-regular-subset.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +/* +* to reduce CLS +* values taken from https://github.com/khempenius/font-fallbacks-dataset/blob/main/font-metric-overrides.csv#L2979 +*/ +@font-face { + font-family: 'Source Sans fallback'; + src: local('Arial'); + ascent-override: 98.4%; + descent-override: 27.3%; + line-gap-override: 0%; +} diff --git a/src/frontend/src/styles/index.css b/src/frontend/src/styles/index.css index 45024c16..630c8f8a 100644 --- a/src/frontend/src/styles/index.css +++ b/src/frontend/src/styles/index.css @@ -1,3 +1,4 @@ +@import './fonts.css'; @import './livekit.css'; @layer reset, base, tokens, recipes, utilities; html, diff --git a/src/frontend/src/styles/livekit.css b/src/frontend/src/styles/livekit.css index ed66de4c..11b940b0 100644 --- a/src/frontend/src/styles/livekit.css +++ b/src/frontend/src/styles/livekit.css @@ -1,114 +1,57 @@ /* based on https://github.com/livekit/components-js/blob/main/packages/styles/scss/themes/default.scss -for now only "visio-light" is actually used */ -[data-lk-theme='visio-dark'] { - color-scheme: dark; - --lk-bg: #111; - --lk-bg2: #1e1e1e; - --lk-bg3: #2b2b2b; - --lk-bg4: #373737; - --lk-bg5: #444444; - --lk-fg: #fff; - --lk-fg2: whitesmoke; - --lk-fg3: #ebebeb; - --lk-fg4: #e0e0e0; - --lk-fg5: #d6d6d6; - --lk-border-color: rgba(255, 255, 255, 0.1); - --lk-accent-fg: #fff; - --lk-accent-bg: #1f8cf9; - --lk-accent2: #3396fa; - --lk-accent3: #47a0fa; - --lk-accent4: #5babfb; - --lk-danger-fg: #fff; - --lk-danger: #842029; - --lk-danger2: #b02a37; - --lk-danger3: #dc3545; - --lk-danger4: #e35d6a; - --lk-success-fg: #fff; - --lk-success: #146c43; - --lk-success2: #198754; - --lk-success3: #479f76; - --lk-success4: #75b798; - --lk-control-fg: var(--lk-fg); - --lk-control-bg: var(--lk-bg2); - --lk-control-hover-bg: var(--lk-bg3); - --lk-control-active-bg: var(--lk-bg4); - --lk-control-active-hover-bg: var(--lk-bg5); - --lk-connection-excellent: #198754; - --lk-connection-good: #fd7e14; - --lk-connection-poor: #dc3545; - --lk-font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, - Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; - --lk-font-size: 16px; - --lk-line-height: 1.5; - --lk-border-radius: 0.5rem; - --lk-box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.15); - --lk-grid-gap: 0.5rem; - --lk-control-bar-height: 69px; - --lk-chat-header-height: 69px; - - --lk-bg6: #777; - --lk-control-border-width: 1px; - --lk-control-border-color: var(--lk-bg5); - --lk-control-border: var(--lk-control-border-color); - --lk-control-hover-border: var(--lk-bg6); -} - [data-lk-theme='visio-light'] { color-scheme: light; - --lk-bg: #fff; - --lk-bg2: #f3f4f6; - --lk-bg3: #e5e7eb; - --lk-bg4: #d1d5db; - --lk-bg5: #9ca3af; - --lk-fg: #111; - --lk-fg2: #18181b; - --lk-fg3: #27272a; - --lk-fg4: #3f3f46; + --lk-bg: var(--colors-white); + --lk-room-bg: var(--colors-default-bg); + --lk-bg2: var(--colors-gray-100); + --lk-bg3: var(--colors-gray-200); + --lk-bg4: var(--colors-gray-300); + --lk-bg5: var(--colors-gray-400); + --lk-fg: var(--colors-text); --lk-fg5: #52525b; - --lk-border-color: rgba(255, 255, 255, 0.1); - --lk-accent-fg: #fff; - --lk-accent-bg: #1f8cf9; - --lk-accent2: #3396fa; - --lk-accent3: #47a0fa; - --lk-accent4: #5babfb; - --lk-danger-fg: var(--lk-fg); - --lk-danger: #f8d7da; - --lk-danger2: #f1aeb5; - --lk-danger3: #ea868f; - --lk-danger4: #e35d6a; - --lk-success-fg: #fff; - --lk-success: #146c43; - --lk-success2: #198754; - --lk-success3: #479f76; - --lk-success4: #75b798; - --lk-control-fg: var(--lk-fg); - --lk-control-bg: var(--lk-bg2); - --lk-control-hover-bg: var(--lk-bg3); - --lk-control-active-bg: var(--lk-bg4); - --lk-control-active-hover-bg: var(--lk-bg5); - --lk-connection-excellent: #198754; - --lk-connection-good: #fd7e14; - --lk-connection-poor: #dc3545; - --lk-font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, - Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; - --lk-font-size: 16px; - --lk-line-height: 1.5; - --lk-border-radius: 0.5rem; - --lk-box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.15); - --lk-grid-gap: 0.5rem; + --lk-border-color: var(--colors-gray-400); + --lk-accent-fg: var(--colors-primary-text); + --lk-accent-bg: var(--colors-primary); + --lk-accent2: var(--colors-primary-hover); + --lk-accent3: var(--colors-primary-active); + --lk-accent4: var(--colors-primary-active); + --lk-danger-fg: var(--colors-danger-text); + --lk-danger: var(--colors-danger); + --lk-danger-hover-bg: var(--colors-danger-hover); + --lk-danger-active-bg: var(--colors-danger-active); + --lk-success-fg: var(--colors-success-text); + --lk-success: var(--colors-success); + --lk-control-fg: var(--colors-control-text); + --lk-control-bg: var(--colors-control); + --lk-control-hover-bg: var(--colors-control-hover); + --lk-control-toggled-on-bg: var(--colors-control-hover); + --lk-control-active-bg: var(--colors-control-active); + --lk-control-active-hover-bg: var(--colors-control-active); + --lk-connection-excellent: var(--colors-success); + --lk-connection-good: var(--colors-warning); + --lk-connection-poor: var(--colors-danger); + --lk-line-height: var(--line-heights-1\.5); + --lk-border-radius: var(--radii-8); + --lk-box-shadow: var(--shadows-sm); + --lk-grid-gap: 1.5rem; --lk-control-bar-height: 69px; --lk-chat-header-height: 69px; + --lk-font-family: var(--fonts-sans); + --lk-font-size: 1rem; --lk-bg6: #6b7280; --lk-control-border-width: 1px; --lk-control-border-color: var(--lk-bg5); --lk-control-border: var(--lk-control-border-color); --lk-control-hover-border: var(--lk-bg6); + + --lk-controlbar-bg: var(--colors-gray-300); + --lk-participant-border: var(--colors-gray-400); } -[data-lk-theme] { - background-color: var(--lk-bg); +[data-lk-theme] .lk-room-container { + background-color: var(--lk-room-bg); } .lk-button, @@ -133,14 +76,77 @@ for now only "visio-light" is actually used border-color: var(--lk-control-hover-border); } -.lk-disconnect-button { - --lk-control-border: var(--lk-danger3); +.lk-button:not(:disabled):active, +.lk-start-audio-button:not(:disabled):active, +.lk-close-button:not(:disabled):active, +.lk-chat-toggle:not(:disabled):active, +.lk-button:not(:disabled):is([data-pressed]) { + background-color: var(--lk-control-active-bg); +} + +.lk-button[aria-pressed='true'], +[aria-pressed='true'].lk-start-audio-button, +[aria-pressed='true'].lk-chat-toggle, +[aria-pressed='true'].lk-disconnect-button { + background-color: var(--lk-control-toggled-on-bg); +} + +[data-lk-theme] .lk-disconnect-button { + --lk-control-border: var(--colors-danger-hover); +} + +[data-lk-theme] .lk-disconnect-button:not(:disabled):hover { + --lk-control-hover-bg: var(--lk-danger-hover-bg); } -.lk-disconnect-button:not(:disabled):hover { - --lk-control-hover-bg: var(--lk-danger2); +[data-lk-theme] .lk-disconnect-button:not(:disabled):active { + background-color: var(--lk-danger-active-bg); } -.lk-prejoin { +[data-lk-theme='visio-light'] .lk-close-button path { + fill: var(--lk-fg); +} + +[data-lk-theme='visio-light'] .lk-participant-metadata-item, +[data-lk-theme='visio-light'] + .lk-participant-tile + .lk-focus-toggle-button:not(:hover) { + color: white; +} + +[data-lk-theme='visio-light'] + .lk-chat-entry[data-lk-message-origin='local'] + .lk-message-body { + align-self: flex-end; + background-color: var(--colors-primary-subtle); +} +[data-lk-theme='visio-light'] + .lk-chat-entry[data-lk-message-origin='remote'] + .lk-message-body { + background-color: var(--colors-blue-300); +} + +[data-lk-theme] .lk-chat-header { + font-weight: bold; +} + +[data-lk-theme] .lk-chat-messages { + padding: 0.5rem; +} + +.lk-chat-entry .lk-meta-data { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +[data-lk-theme] .lk-control-bar { + background-color: var(--lk-controlbar-bg); +} + +[data-lk-theme] .lk-prejoin { padding-top: 0; } + +[data-lk-theme] .lk-participant-tile { + box-shadow: var(--lk-box-shadow); +} diff --git a/src/frontend/src/utils/createRandomRoom.ts b/src/frontend/src/utils/createRandomRoom.ts deleted file mode 100644 index 10f0c412..00000000 --- a/src/frontend/src/utils/createRandomRoom.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { slugify } from './slugify' - -export const createRandomRoom = () => { - return slugify(crypto.randomUUID()) -}