♻️(frontend) reorganize starting frontend code
- 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
This commit is contained in:
6
src/frontend/package-lock.json
generated
6
src/frontend/package-lock.json
generated
@@ -12,7 +12,6 @@
|
|||||||
"@livekit/components-styles": "1.0.12",
|
"@livekit/components-styles": "1.0.12",
|
||||||
"@pandacss/preset-panda": "0.41.0",
|
"@pandacss/preset-panda": "0.41.0",
|
||||||
"@tanstack/react-query": "5.49.2",
|
"@tanstack/react-query": "5.49.2",
|
||||||
"classnames": "2.5.1",
|
|
||||||
"livekit-client": "2.3.1",
|
"livekit-client": "2.3.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-aria-components": "1.2.1",
|
"react-aria-components": "1.2.1",
|
||||||
@@ -4732,11 +4731,6 @@
|
|||||||
"node": ">= 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": {
|
"node_modules/client-only": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
"@livekit/components-styles": "1.0.12",
|
"@livekit/components-styles": "1.0.12",
|
||||||
"@pandacss/preset-panda": "0.41.0",
|
"@pandacss/preset-panda": "0.41.0",
|
||||||
"@tanstack/react-query": "5.49.2",
|
"@tanstack/react-query": "5.49.2",
|
||||||
"classnames": "2.5.1",
|
|
||||||
"livekit-client": "2.3.1",
|
"livekit-client": "2.3.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-aria-components": "1.2.1",
|
"react-aria-components": "1.2.1",
|
||||||
|
|||||||
@@ -1,7 +1,35 @@
|
|||||||
import pandaPreset from '@pandacss/preset-panda'
|
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,
|
preflight: true,
|
||||||
include: ['./src/**/*.{js,jsx,ts,tsx}'],
|
include: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||||
exclude: [],
|
exclude: [],
|
||||||
@@ -9,61 +37,244 @@ export default defineConfig({
|
|||||||
outdir: 'src/styled-system',
|
outdir: 'src/styled-system',
|
||||||
conditions: {
|
conditions: {
|
||||||
extend: {
|
extend: {
|
||||||
// React Aria builds upon data attributes instead of css pseudo-classes, make sure to only work based on react aria stuff
|
// React Aria builds upon data attributes instead of css pseudo-classes, in case we style a React Aria component
|
||||||
hover: '&:is([data-hovered])',
|
// we dont want to trigger pseudo class related styles
|
||||||
focus: '&:is([data-focused])',
|
'ra-hover': '&:is([data-hovered])',
|
||||||
focusVisible: '&:is([data-focus-visible])',
|
'ra-focus': '&:is([data-focused])',
|
||||||
disabled: '&:is([data-disabled])',
|
'ra-focusVisible': '&:is([data-focus-visible])',
|
||||||
|
'ra-disabled': '&:is([data-disabled])',
|
||||||
pressed: '&:is([data-pressed])',
|
pressed: '&:is([data-pressed])',
|
||||||
|
'ra-pressed': '&:is([data-pressed])',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
...pandaPreset.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({
|
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,
|
...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: {
|
spacing: {
|
||||||
0: { value: '0rem' },
|
boxPadding: {
|
||||||
0.125: { value: '0.125rem' },
|
DEFAULT: { value: '{spacing.2}' },
|
||||||
0.25: { value: '0.25rem' },
|
sm: { value: '{spacing.1}' },
|
||||||
0.375: { value: '0.375rem' },
|
xs: { value: '{spacing.0.5}' },
|
||||||
0.5: { value: '0.5rem' },
|
},
|
||||||
0.75: { value: '0.75rem' },
|
boxMargin: {
|
||||||
1: { value: '1rem' },
|
xs: { value: '{spacing.0.5}' },
|
||||||
1.25: { value: '1.25rem' },
|
DEFAULT: { value: '{spacing.1}' },
|
||||||
1.5: { value: '1.5rem' },
|
lg: { value: '{spacing.2}' },
|
||||||
1.75: { value: '1.75rem' },
|
},
|
||||||
2: { value: '2rem' },
|
paragraph: { value: '{spacing.1}' },
|
||||||
2.25: { value: '2.25rem' },
|
heading: { value: '{spacing.1}' },
|
||||||
2.5: { value: '2.5rem' },
|
gutter: { value: '{spacing.1}' },
|
||||||
2.75: { value: '2.75rem' },
|
},
|
||||||
3: { value: '3rem' },
|
}),
|
||||||
3.5: { value: '3.5rem' },
|
textStyles: defineTextStyles({
|
||||||
4: { value: '4rem' },
|
h1: {
|
||||||
5: { value: '5rem' },
|
value: {
|
||||||
6: { value: '6rem' },
|
fontSize: '1.5rem',
|
||||||
7: { value: '7rem' },
|
lineHeight: '2rem',
|
||||||
8: { value: '8rem' },
|
fontWeight: 700,
|
||||||
9: { value: '9rem' },
|
},
|
||||||
10: { value: '10rem' },
|
},
|
||||||
12: { value: '12rem' },
|
h2: {
|
||||||
14: { value: '14rem' },
|
value: {
|
||||||
16: { value: '16rem' },
|
fontSize: '1.25rem',
|
||||||
20: { value: '20rem' },
|
lineHeight: '1.75rem',
|
||||||
24: { value: '24rem' },
|
fontWeight: 700,
|
||||||
28: { value: '28rem' },
|
},
|
||||||
32: { value: '32rem' },
|
},
|
||||||
36: { value: '36rem' },
|
h3: {
|
||||||
40: { value: '40rem' },
|
value: {
|
||||||
44: { value: '44rem' },
|
fontSize: '1.125rem',
|
||||||
48: { value: '48rem' },
|
lineHeight: '1.75rem',
|
||||||
52: { value: '52rem' },
|
fontWeight: 700,
|
||||||
56: { value: '56rem' },
|
},
|
||||||
60: { value: '60rem' },
|
},
|
||||||
64: { value: '64rem' },
|
body: {
|
||||||
72: { value: '72rem' },
|
value: {
|
||||||
80: { value: '80rem' },
|
fontSize: '1rem',
|
||||||
96: { value: '96rem' },
|
lineHeight: '1.5',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
small: {
|
||||||
|
value: {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
lineHeight: '1.25rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
value: {
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
lineHeight: '1rem',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
|
||||||
|
export default defineConfig(config)
|
||||||
|
|||||||
BIN
src/frontend/public/fonts/sourcecodepro-regular-subset.woff2
Normal file
BIN
src/frontend/public/fonts/sourcecodepro-regular-subset.woff2
Normal file
Binary file not shown.
BIN
src/frontend/public/fonts/sourcesans3-bold-subset.woff2
Normal file
BIN
src/frontend/public/fonts/sourcesans3-bold-subset.woff2
Normal file
Binary file not shown.
BIN
src/frontend/public/fonts/sourcesans3-it-subset.woff2
Normal file
BIN
src/frontend/public/fonts/sourcesans3-it-subset.woff2
Normal file
Binary file not shown.
BIN
src/frontend/public/fonts/sourcesans3-regular-subset.woff2
Normal file
BIN
src/frontend/public/fonts/sourcesans3-regular-subset.woff2
Normal file
Binary file not shown.
@@ -4,8 +4,8 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { Route, Switch } from 'wouter'
|
import { Route, Switch } from 'wouter'
|
||||||
import { Home } from './routes/Home'
|
import { Home } from './routes/Home'
|
||||||
import { Conference } from './routes/Conference'
|
import { NotFound } from './routes/NotFound'
|
||||||
import { NotFoundScreen } from './layout/NotFoundScreen'
|
import { RoomRoute } from '@/features/rooms'
|
||||||
|
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
@@ -14,8 +14,8 @@ function App() {
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/" component={Home} />
|
<Route path="/" component={Home} />
|
||||||
<Route path="/:roomId" component={Conference} />
|
<Route path="/:roomId" component={RoomRoute} />
|
||||||
<Route component={NotFoundScreen} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
export const apiUrl = (path: string, apiVersion = '1.0') => {
|
export const apiUrl = (path: string, apiVersion = '1.0') => {
|
||||||
|
|
||||||
const origin =
|
const origin =
|
||||||
import.meta.env.VITE_API_BASE_URL
|
import.meta.env.VITE_API_BASE_URL ||
|
||||||
|| (typeof window !== 'undefined' ? window.location.origin : '');
|
(typeof window !== 'undefined' ? window.location.origin : '')
|
||||||
|
|
||||||
// Remove leading/trailing slashes from origin/path if it exists
|
// Remove leading/trailing slashes from origin/path if it exists
|
||||||
const sanitizedOrigin = origin.replace(/\/$/, '')
|
const sanitizedOrigin = origin.replace(/\/$/, '')
|
||||||
const sanitizedPath = path.replace(/^\//, '');
|
const sanitizedPath = path.replace(/^\//, '')
|
||||||
|
|
||||||
return `${sanitizedOrigin}/api/v${apiVersion}/${sanitizedPath}`;
|
return `${sanitizedOrigin}/api/v${apiVersion}/${sanitizedPath}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { ApiRoom } from './ApiRoom'
|
|
||||||
import { fetchApi } from './fetchApi'
|
|
||||||
|
|
||||||
export const fetchRoom = (roomId: string) => {
|
|
||||||
return fetchApi<ApiRoom>(`/rooms/${roomId}`)
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import type { ApiUser } from './ApiUser'
|
|
||||||
import { fetchApi } from './fetchApi'
|
|
||||||
|
|
||||||
export const fetchUser = () => {
|
|
||||||
return fetchApi<ApiUser>('/users/me')
|
|
||||||
}
|
|
||||||
25
src/frontend/src/features/auth/api/fetchUser.ts
Normal file
25
src/frontend/src/features/auth/api/fetchUser.ts
Normal file
@@ -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<ApiUser | false> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fetchApi<ApiUser>('/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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
17
src/frontend/src/features/auth/api/useUser.tsx
Normal file
17
src/frontend/src/features/auth/api/useUser.tsx
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/frontend/src/features/auth/index.ts
Normal file
2
src/frontend/src/features/auth/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { useUser } from './api/useUser'
|
||||||
|
export { authUrl } from './utils/authUrl'
|
||||||
5
src/frontend/src/features/auth/utils/authUrl.ts
Normal file
5
src/frontend/src/features/auth/utils/authUrl.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { apiUrl } from '@/api/apiUrl'
|
||||||
|
|
||||||
|
export const authUrl = () => {
|
||||||
|
return apiUrl('/authenticate')
|
||||||
|
}
|
||||||
20
src/frontend/src/features/rooms/api/fetchRoom.ts
Normal file
20
src/frontend/src/features/rooms/api/fetchRoom.ts
Normal file
@@ -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<ApiRoom>(
|
||||||
|
`/rooms/${roomId}?username=${encodeURIComponent(username)}`
|
||||||
|
).then((room) => {
|
||||||
|
if (!room.livekit?.token || !room.livekit?.url) {
|
||||||
|
throw new ApiError(500, 'LiveKit info not found')
|
||||||
|
}
|
||||||
|
return room
|
||||||
|
})
|
||||||
|
}
|
||||||
48
src/frontend/src/features/rooms/components/Conference.tsx
Normal file
48
src/frontend/src/features/rooms/components/Conference.tsx
Normal file
@@ -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 (
|
||||||
|
<QueryAware status={status}>
|
||||||
|
<LiveKitRoom
|
||||||
|
serverUrl={data?.livekit?.url}
|
||||||
|
token={data?.livekit?.token}
|
||||||
|
connect={true}
|
||||||
|
audio={{
|
||||||
|
deviceId: userConfig.audioDeviceId,
|
||||||
|
}}
|
||||||
|
video={{
|
||||||
|
deviceId: userConfig.videoDeviceId,
|
||||||
|
}}
|
||||||
|
onDisconnected={() => {
|
||||||
|
navigateToHome()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VideoConference />
|
||||||
|
</LiveKitRoom>
|
||||||
|
</QueryAware>
|
||||||
|
)
|
||||||
|
}
|
||||||
17
src/frontend/src/features/rooms/components/Join.tsx
Normal file
17
src/frontend/src/features/rooms/components/Join.tsx
Normal file
@@ -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 (
|
||||||
|
<Box title="Verify your settings before joining" withBackButton>
|
||||||
|
<PreJoin
|
||||||
|
persistUserChoices
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
2
src/frontend/src/features/rooms/index.ts
Normal file
2
src/frontend/src/features/rooms/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { navigateToNewRoom } from './navigation/navigateToNewRoom'
|
||||||
|
export { Room as RoomRoute } from './routes/Room'
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { navigate } from 'wouter/use-browser-location'
|
||||||
|
import { generateRoomId } from '../utils/generateRoomId'
|
||||||
|
|
||||||
|
export const navigateToNewRoom = () => {
|
||||||
|
navigate(`/${generateRoomId()}`)
|
||||||
|
}
|
||||||
18
src/frontend/src/features/rooms/routes/Room.tsx
Normal file
18
src/frontend/src/features/rooms/routes/Room.tsx
Normal file
@@ -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<LocalUserChoices | null>(null)
|
||||||
|
return (
|
||||||
|
<Screen>
|
||||||
|
{userConfig ? (
|
||||||
|
<Conference userConfig={userConfig} />
|
||||||
|
) : (
|
||||||
|
<Join onSubmit={setUserConfig} />
|
||||||
|
)}
|
||||||
|
</Screen>
|
||||||
|
)
|
||||||
|
}
|
||||||
5
src/frontend/src/features/rooms/utils/generateRoomId.ts
Normal file
5
src/frontend/src/features/rooms/utils/generateRoomId.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { slugify } from '@/utils/slugify'
|
||||||
|
|
||||||
|
export const generateRoomId = () => {
|
||||||
|
return slugify(crypto.randomUUID())
|
||||||
|
}
|
||||||
28
src/frontend/src/layout/Box.tsx
Normal file
28
src/frontend/src/layout/Box.tsx
Normal file
@@ -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 (
|
||||||
|
<BoxDiv asScreen>
|
||||||
|
{!!title && <H lvl={1}>{title}</H>}
|
||||||
|
{children}
|
||||||
|
{!!withBackButton && (
|
||||||
|
<p>
|
||||||
|
<Link to="/" size="small">
|
||||||
|
Back to homescreen
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</BoxDiv>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,30 +1,10 @@
|
|||||||
import type { ReactNode } from 'react'
|
|
||||||
import classNames from 'classnames'
|
|
||||||
import { css } from '@/styled-system/css'
|
|
||||||
import { Screen } from './Screen'
|
import { Screen } from './Screen'
|
||||||
|
import { Box, type BoxProps } from './Box'
|
||||||
|
|
||||||
export const BoxScreen = ({ children }: { children: ReactNode }) => {
|
export const BoxScreen = (props: BoxProps) => {
|
||||||
return (
|
return (
|
||||||
<Screen>
|
<Screen>
|
||||||
<div
|
<Box {...props} />
|
||||||
className={classNames(
|
|
||||||
css({
|
|
||||||
width: '38rem',
|
|
||||||
maxWidth: '100%',
|
|
||||||
margin: 'auto',
|
|
||||||
border: '1px solid #ddd',
|
|
||||||
borderRadius: 'lg',
|
|
||||||
backgroundColor: 'white',
|
|
||||||
boxShadow: 'sm',
|
|
||||||
textAlign: 'center',
|
|
||||||
marginTop: '6',
|
|
||||||
gap: '1',
|
|
||||||
padding: '2',
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</Screen>
|
</Screen>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
import { Link } from 'wouter'
|
|
||||||
import { A } from '@/primitives/A'
|
|
||||||
import { H1 } from '@/primitives/H'
|
|
||||||
import { BoxScreen } from './BoxScreen'
|
import { BoxScreen } from './BoxScreen'
|
||||||
|
|
||||||
export const ErrorScreen = () => {
|
export const ErrorScreen = () => {
|
||||||
return (
|
return (
|
||||||
<BoxScreen>
|
<BoxScreen title="An error occured while loading the page" withBackButton />
|
||||||
<H1>An error occured while loading the page</H1>
|
|
||||||
<p>
|
|
||||||
<Link to="/" asChild>
|
|
||||||
<A size="small">Back to homescreen</A>
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</BoxScreen>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import { Link } from 'wouter'
|
|
||||||
import { A } from '@/primitives/A'
|
|
||||||
import { H1 } from '@/primitives/H'
|
|
||||||
import { BoxScreen } from './BoxScreen'
|
import { BoxScreen } from './BoxScreen'
|
||||||
|
|
||||||
export const ForbiddenScreen = () => {
|
export const ForbiddenScreen = () => {
|
||||||
return (
|
return (
|
||||||
<BoxScreen>
|
<BoxScreen
|
||||||
<H1>You don't have the permission to view this page</H1>
|
title="You don't have the permission to view this page"
|
||||||
<p>
|
withBackButton
|
||||||
<Link to="/" asChild>
|
/>
|
||||||
<A size="small">Back to homescreen</A>
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</BoxScreen>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { css } from '@/styled-system/css'
|
||||||
import { flex } from '@/styled-system/patterns'
|
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 = () => {
|
export const Header = () => {
|
||||||
const { user, isLoggedIn } = useUser()
|
const { user, isLoggedIn } = useUser()
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'primary.text',
|
||||||
padding: '1',
|
color: 'primary',
|
||||||
|
borderBottomColor: 'box.border',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomStyle: 'solid',
|
||||||
|
padding: 1,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
color: '#000091',
|
boxShadow: 'box',
|
||||||
boxShadow: '#00000040 0px 1px 4px',
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
@@ -23,20 +26,18 @@ export const Header = () => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p
|
<Text bold variant="h1" margin={false}>
|
||||||
className={css({
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: '2xl',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Meet
|
Meet
|
||||||
</p>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{isLoggedIn === false && <A href={apiUrl('/authenticate')}>Login</A>}
|
{isLoggedIn === false && <A href={apiUrl('/authenticate')}>Login</A>}
|
||||||
{!!user && (
|
{!!user && (
|
||||||
<p>
|
<p className={flex({ gap: 1, align: 'center' })}>
|
||||||
{user.email} <A href={apiUrl('/logout')}>Logout</A>
|
<Badge>{user.email}</Badge>
|
||||||
|
<A href={apiUrl('/logout')} size="small">
|
||||||
|
Logout
|
||||||
|
</A>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
import { Link } from 'wouter'
|
|
||||||
import { A } from '@/primitives/A'
|
|
||||||
import { H1 } from '@/primitives/H'
|
|
||||||
import { BoxScreen } from './BoxScreen'
|
import { BoxScreen } from './BoxScreen'
|
||||||
|
|
||||||
export const NotFoundScreen = () => {
|
export const NotFoundScreen = () => {
|
||||||
return (
|
return <BoxScreen title="Page not found" withBackButton />
|
||||||
<BoxScreen>
|
|
||||||
<H1>Page not found</H1>
|
|
||||||
<p>
|
|
||||||
<Link to="/" asChild>
|
|
||||||
<A size="small">Back to homescreen</A>
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</BoxScreen>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/frontend/src/layout/QueryAware.tsx
Normal file
20
src/frontend/src/layout/QueryAware.tsx
Normal file
@@ -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 <ErrorScreen />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'pending') {
|
||||||
|
return <LoadingScreen />
|
||||||
|
}
|
||||||
|
|
||||||
|
return children
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { Header } from '@/layout/Header'
|
import { Header } from './Header'
|
||||||
|
|
||||||
export const Screen = ({ children }: { children: ReactNode }) => {
|
export const Screen = ({ children }: { children: ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
@@ -9,7 +9,8 @@ export const Screen = ({ children }: { children: ReactNode }) => {
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
backgroundColor: 'slate.50',
|
backgroundColor: 'default.bg',
|
||||||
|
color: 'default.text',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
|
|||||||
5
src/frontend/src/navigation/navigateToHome.ts
Normal file
5
src/frontend/src/navigation/navigateToHome.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { navigate } from 'wouter/use-browser-location'
|
||||||
|
|
||||||
|
export const navigateToHome = () => {
|
||||||
|
navigate(`/`)
|
||||||
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import { RecipeVariantProps, cva } from '@/styled-system/css'
|
import { Link, type LinkProps } from 'react-aria-components'
|
||||||
import {
|
import { cva, type RecipeVariantProps } from '@/styled-system/css'
|
||||||
Link as Link,
|
|
||||||
type LinkProps as LinksProps,
|
|
||||||
} from 'react-aria-components'
|
|
||||||
|
|
||||||
const link = cva({
|
const link = cva({
|
||||||
base: {
|
base: {
|
||||||
@@ -10,25 +7,27 @@ const link = cva({
|
|||||||
textUnderlineOffset: '2',
|
textUnderlineOffset: '2',
|
||||||
transition: 'all 200ms',
|
transition: 'all 200ms',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
_hover: {
|
'_ra-hover': {
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
},
|
},
|
||||||
_pressed: {
|
'_ra-pressed': {
|
||||||
textDecoration: 'underline',
|
textDecoration: 'underline',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
small: {
|
small: {
|
||||||
fontSize: 'sm',
|
textStyle: 'small',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const A = ({
|
export type AProps = LinkProps & RecipeVariantProps<typeof link>
|
||||||
size,
|
|
||||||
...props
|
/**
|
||||||
}: LinksProps & RecipeVariantProps<typeof link>) => {
|
* anchor component styled with underline
|
||||||
|
*/
|
||||||
|
export const A = ({ size, ...props }: AProps) => {
|
||||||
return <Link {...props} className={link({ size })} />
|
return <Link {...props} className={link({ size })} />
|
||||||
}
|
}
|
||||||
|
|||||||
29
src/frontend/src/primitives/Badge.tsx
Normal file
29
src/frontend/src/primitives/Badge.tsx
Normal file
@@ -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<HTMLSpanElement> &
|
||||||
|
RecipeVariantProps<typeof badge>
|
||||||
|
|
||||||
|
export const Badge = ({ size, ...props }: BadgeProps) => {
|
||||||
|
return <span {...props} className={badge({ size })} />
|
||||||
|
}
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
import { css } from '@/styled-system/css'
|
import { Text, type As } from './Text'
|
||||||
|
|
||||||
const bold = css({
|
export const Bold = (props: React.HTMLAttributes<HTMLElement> & As) => {
|
||||||
fontWeight: 'bold',
|
return <Text as="strong" {...props} bold />
|
||||||
})
|
|
||||||
|
|
||||||
export const Bold = (props: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return <strong className={bold} {...props} />
|
|
||||||
}
|
}
|
||||||
|
|||||||
51
src/frontend/src/primitives/Box.tsx
Normal file
51
src/frontend/src/primitives/Box.tsx
Normal file
@@ -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)
|
||||||
@@ -1,8 +1,58 @@
|
|||||||
import {
|
import {
|
||||||
Button as RACButton,
|
Button as RACButton,
|
||||||
type ButtonProps as RACButtonsProps,
|
type ButtonProps as RACButtonsProps,
|
||||||
|
Link,
|
||||||
|
LinkProps,
|
||||||
} from 'react-aria-components'
|
} from 'react-aria-components'
|
||||||
|
import { cva, type RecipeVariantProps } from '@/styled-system/css'
|
||||||
|
|
||||||
export const Button = (props: RACButtonsProps) => {
|
const button = cva({
|
||||||
return <RACButton {...props} className="lk-button" />
|
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<typeof button> &
|
||||||
|
(RACButtonsProps | LinkProps)
|
||||||
|
|
||||||
|
export const Button = (props: ButtonProps) => {
|
||||||
|
const [variantProps, componentProps] = button.splitVariantProps(props)
|
||||||
|
if ((props as LinkProps).href !== undefined) {
|
||||||
|
return <Link className={button(variantProps)} {...componentProps} />
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<RACButton
|
||||||
|
className={button(variantProps)}
|
||||||
|
{...(componentProps as RACButtonsProps)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/frontend/src/primitives/Div.tsx
Normal file
3
src/frontend/src/primitives/Div.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { Box } from '@/styled-system/jsx'
|
||||||
|
|
||||||
|
export const Div = Box
|
||||||
@@ -1,24 +1,14 @@
|
|||||||
import type { HTMLAttributes } from 'react'
|
import { Text } from './Text'
|
||||||
import classNames from 'classnames'
|
|
||||||
import { css } from '@/styled-system/css'
|
|
||||||
|
|
||||||
export const H1 = ({
|
export const H = ({
|
||||||
children,
|
children,
|
||||||
className,
|
lvl,
|
||||||
...props
|
...props
|
||||||
}: HTMLAttributes<HTMLHeadingElement>) => {
|
}: React.HTMLAttributes<HTMLHeadingElement> & { lvl: 1 | 2 | 3 }) => {
|
||||||
|
const tag = `h${lvl}` as const
|
||||||
return (
|
return (
|
||||||
<h1
|
<Text as={tag} variant={tag} {...props}>
|
||||||
className={classNames(
|
|
||||||
css({
|
|
||||||
textStyle: '2xl',
|
|
||||||
marginBottom: '1',
|
|
||||||
}),
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</h1>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
|
|
||||||
const hr = css({
|
|
||||||
marginY: '1',
|
|
||||||
borderColor: 'neutral.300',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const Hr = (props: React.HTMLAttributes<HTMLHRElement>) => {
|
export const Hr = (props: React.HTMLAttributes<HTMLHRElement>) => {
|
||||||
return <hr className={hr} {...props} />
|
return (
|
||||||
|
<hr
|
||||||
|
className={css({
|
||||||
|
marginY: 1,
|
||||||
|
borderColor: 'neutral.300',
|
||||||
|
})}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
5
src/frontend/src/primitives/Italic.tsx
Normal file
5
src/frontend/src/primitives/Italic.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Text, type As } from './Text'
|
||||||
|
|
||||||
|
export const Italic = (props: React.HTMLAttributes<HTMLElement> & As) => {
|
||||||
|
return <Text as="em" {...props} italic />
|
||||||
|
}
|
||||||
18
src/frontend/src/primitives/Link.tsx
Normal file
18
src/frontend/src/primitives/Link.tsx
Normal file
@@ -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 (
|
||||||
|
<WouterLink to={to} asChild>
|
||||||
|
<A {...props} />
|
||||||
|
</WouterLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,23 +1,5 @@
|
|||||||
import type { HTMLAttributes } from 'react'
|
import { Text, type As } from './Text'
|
||||||
import classNames from 'classnames'
|
|
||||||
import { css } from '@/styled-system/css'
|
|
||||||
|
|
||||||
export const P = ({
|
export const P = (props: React.HTMLAttributes<HTMLElement> & As) => {
|
||||||
children,
|
return <Text as="p" variant="paragraph" {...props} />
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: HTMLAttributes<HTMLParagraphElement>) => {
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
className={classNames(
|
|
||||||
css({
|
|
||||||
marginBottom: '1',
|
|
||||||
}),
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
78
src/frontend/src/primitives/Text.tsx
Normal file
78
src/frontend/src/primitives/Text.tsx
Normal file
@@ -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<HTMLElement>
|
||||||
|
type TextElement =
|
||||||
|
| 'h1'
|
||||||
|
| 'h2'
|
||||||
|
| 'h3'
|
||||||
|
| 'h4'
|
||||||
|
| 'h5'
|
||||||
|
| 'h6'
|
||||||
|
| 'p'
|
||||||
|
| 'span'
|
||||||
|
| 'strong'
|
||||||
|
| 'em'
|
||||||
|
| 'div'
|
||||||
|
export type As = { as?: TextElement }
|
||||||
|
export type TextProps = RecipeVariantProps<typeof text> & TextHTMLProps & As
|
||||||
|
|
||||||
|
export function Text(props: TextProps) {
|
||||||
|
const [variantProps, componentProps] = text.splitVariantProps(props)
|
||||||
|
const { as: Component = 'p', className, ...tagProps } = componentProps
|
||||||
|
return (
|
||||||
|
<Component className={cx(text(variantProps), className)} {...tagProps} />
|
||||||
|
)
|
||||||
|
}
|
||||||
12
src/frontend/src/primitives/index.ts
Normal file
12
src/frontend/src/primitives/index.ts
Normal file
@@ -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'
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<LocalUserChoices | null>(null)
|
|
||||||
|
|
||||||
if (!userConfig) {
|
|
||||||
return (
|
|
||||||
<BoxScreen>
|
|
||||||
<H1>Verify your settings before joining</H1>
|
|
||||||
<PreJoin
|
|
||||||
persistUserChoices
|
|
||||||
onSubmit={(choices) => {
|
|
||||||
setUserConfig(choices)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<p>
|
|
||||||
<Link to="/" asChild>
|
|
||||||
<A size="small">Back to homescreen</A>
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</BoxScreen>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'error' || (status === 'success' && !data?.livekit)) {
|
|
||||||
return <ErrorScreen />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data?.is_public === false) {
|
|
||||||
return <ForbiddenScreen />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data?.livekit?.token && data?.livekit?.url) {
|
|
||||||
return (
|
|
||||||
<Screen>
|
|
||||||
<LiveKitRoom
|
|
||||||
serverUrl={data?.livekit?.url}
|
|
||||||
token={data?.livekit?.token}
|
|
||||||
connect={true}
|
|
||||||
audio={{
|
|
||||||
deviceId: userConfig.audioDeviceId,
|
|
||||||
}}
|
|
||||||
video={{
|
|
||||||
deviceId: userConfig.videoDeviceId,
|
|
||||||
}}
|
|
||||||
onDisconnected={() => {
|
|
||||||
navigate('/')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<VideoConference />
|
|
||||||
</LiveKitRoom>
|
|
||||||
</Screen>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <LoadingScreen />
|
|
||||||
}
|
|
||||||
@@ -1,49 +1,41 @@
|
|||||||
import { useLocation } from 'wouter'
|
import { A, Button, Italic, P, Div, H, Box } from '@/primitives'
|
||||||
import { useUser } from '@/queries/useUser'
|
import { useUser } from '@/features/auth'
|
||||||
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 { apiUrl } from '@/api/apiUrl'
|
import { apiUrl } from '@/api/apiUrl'
|
||||||
import { createRandomRoom } from '@/utils/createRandomRoom'
|
import { navigateToNewRoom } from '@/features/rooms'
|
||||||
import { LoadingScreen } from '@/layout/LoadingScreen'
|
import { Screen } from '@/layout/Screen'
|
||||||
import { BoxScreen } from '@/layout/BoxScreen'
|
|
||||||
|
|
||||||
export const Home = () => {
|
export const Home = () => {
|
||||||
const { status, isLoggedIn } = useUser()
|
const { isLoggedIn } = useUser()
|
||||||
const [, navigate] = useLocation()
|
|
||||||
|
|
||||||
if (status === 'pending') {
|
|
||||||
return <LoadingScreen asBox />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoxScreen>
|
<Screen>
|
||||||
<H1>
|
<Box asScreen>
|
||||||
Welcome in <Bold>Meet</Bold>!
|
<H lvl={1}>Welcome in Meet</H>
|
||||||
</H1>
|
<P>What do you want to do? You can either:</P>
|
||||||
{isLoggedIn ? (
|
<Div marginBottom="gutter">
|
||||||
<Button onPress={() => navigate(`/${createRandomRoom()}`)}>
|
<Box variant="subtle" size="sm">
|
||||||
Create a conference call
|
{isLoggedIn ? (
|
||||||
</Button>
|
<Button variant="primary" onPress={() => navigateToNewRoom()}>
|
||||||
) : (
|
Create a conference call
|
||||||
<>
|
</Button>
|
||||||
<P>What do you want to do? You can either:</P>
|
) : (
|
||||||
<Hr aria-hidden="true" />
|
<p>
|
||||||
<P>
|
<A href={apiUrl('/authenticate')}>
|
||||||
<A href={apiUrl('/authenticate')}>
|
Login to create a conference call
|
||||||
Login to create a conference call
|
</A>
|
||||||
</A>
|
</p>
|
||||||
</P>
|
)}
|
||||||
<Hr aria-hidden="true" />
|
</Box>
|
||||||
<P>
|
</Div>
|
||||||
<span style={{ fontStyle: 'italic' }}>Or </span> copy a URL in your
|
<P>
|
||||||
browser address bar to join an existing conference call
|
<Italic>Or</Italic>
|
||||||
</P>
|
</P>
|
||||||
</>
|
<Box variant="subtle" size="sm">
|
||||||
)}
|
<p>
|
||||||
</BoxScreen>
|
copy a meeting URL in your browser address bar to join an existing
|
||||||
|
conference call
|
||||||
|
</p>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Screen>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
|
import { NotFoundScreen } from '@/layout/NotFoundScreen'
|
||||||
|
|
||||||
export const NotFound = () => {
|
export const NotFound = () => {
|
||||||
return (
|
return <NotFoundScreen />
|
||||||
<div>
|
|
||||||
<h1>404</h1>
|
|
||||||
<p>Page not found</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
43
src/frontend/src/styles/fonts.css
Normal file
43
src/frontend/src/styles/fonts.css
Normal file
@@ -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%;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@import './fonts.css';
|
||||||
@import './livekit.css';
|
@import './livekit.css';
|
||||||
@layer reset, base, tokens, recipes, utilities;
|
@layer reset, base, tokens, recipes, utilities;
|
||||||
html,
|
html,
|
||||||
|
|||||||
@@ -1,114 +1,57 @@
|
|||||||
/* based on https://github.com/livekit/components-js/blob/main/packages/styles/scss/themes/default.scss
|
/* 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'] {
|
[data-lk-theme='visio-light'] {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
--lk-bg: #fff;
|
--lk-bg: var(--colors-white);
|
||||||
--lk-bg2: #f3f4f6;
|
--lk-room-bg: var(--colors-default-bg);
|
||||||
--lk-bg3: #e5e7eb;
|
--lk-bg2: var(--colors-gray-100);
|
||||||
--lk-bg4: #d1d5db;
|
--lk-bg3: var(--colors-gray-200);
|
||||||
--lk-bg5: #9ca3af;
|
--lk-bg4: var(--colors-gray-300);
|
||||||
--lk-fg: #111;
|
--lk-bg5: var(--colors-gray-400);
|
||||||
--lk-fg2: #18181b;
|
--lk-fg: var(--colors-text);
|
||||||
--lk-fg3: #27272a;
|
|
||||||
--lk-fg4: #3f3f46;
|
|
||||||
--lk-fg5: #52525b;
|
--lk-fg5: #52525b;
|
||||||
--lk-border-color: rgba(255, 255, 255, 0.1);
|
--lk-border-color: var(--colors-gray-400);
|
||||||
--lk-accent-fg: #fff;
|
--lk-accent-fg: var(--colors-primary-text);
|
||||||
--lk-accent-bg: #1f8cf9;
|
--lk-accent-bg: var(--colors-primary);
|
||||||
--lk-accent2: #3396fa;
|
--lk-accent2: var(--colors-primary-hover);
|
||||||
--lk-accent3: #47a0fa;
|
--lk-accent3: var(--colors-primary-active);
|
||||||
--lk-accent4: #5babfb;
|
--lk-accent4: var(--colors-primary-active);
|
||||||
--lk-danger-fg: var(--lk-fg);
|
--lk-danger-fg: var(--colors-danger-text);
|
||||||
--lk-danger: #f8d7da;
|
--lk-danger: var(--colors-danger);
|
||||||
--lk-danger2: #f1aeb5;
|
--lk-danger-hover-bg: var(--colors-danger-hover);
|
||||||
--lk-danger3: #ea868f;
|
--lk-danger-active-bg: var(--colors-danger-active);
|
||||||
--lk-danger4: #e35d6a;
|
--lk-success-fg: var(--colors-success-text);
|
||||||
--lk-success-fg: #fff;
|
--lk-success: var(--colors-success);
|
||||||
--lk-success: #146c43;
|
--lk-control-fg: var(--colors-control-text);
|
||||||
--lk-success2: #198754;
|
--lk-control-bg: var(--colors-control);
|
||||||
--lk-success3: #479f76;
|
--lk-control-hover-bg: var(--colors-control-hover);
|
||||||
--lk-success4: #75b798;
|
--lk-control-toggled-on-bg: var(--colors-control-hover);
|
||||||
--lk-control-fg: var(--lk-fg);
|
--lk-control-active-bg: var(--colors-control-active);
|
||||||
--lk-control-bg: var(--lk-bg2);
|
--lk-control-active-hover-bg: var(--colors-control-active);
|
||||||
--lk-control-hover-bg: var(--lk-bg3);
|
--lk-connection-excellent: var(--colors-success);
|
||||||
--lk-control-active-bg: var(--lk-bg4);
|
--lk-connection-good: var(--colors-warning);
|
||||||
--lk-control-active-hover-bg: var(--lk-bg5);
|
--lk-connection-poor: var(--colors-danger);
|
||||||
--lk-connection-excellent: #198754;
|
--lk-line-height: var(--line-heights-1\.5);
|
||||||
--lk-connection-good: #fd7e14;
|
--lk-border-radius: var(--radii-8);
|
||||||
--lk-connection-poor: #dc3545;
|
--lk-box-shadow: var(--shadows-sm);
|
||||||
--lk-font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica,
|
--lk-grid-gap: 1.5rem;
|
||||||
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-control-bar-height: 69px;
|
||||||
--lk-chat-header-height: 69px;
|
--lk-chat-header-height: 69px;
|
||||||
|
|
||||||
|
--lk-font-family: var(--fonts-sans);
|
||||||
|
--lk-font-size: 1rem;
|
||||||
--lk-bg6: #6b7280;
|
--lk-bg6: #6b7280;
|
||||||
--lk-control-border-width: 1px;
|
--lk-control-border-width: 1px;
|
||||||
--lk-control-border-color: var(--lk-bg5);
|
--lk-control-border-color: var(--lk-bg5);
|
||||||
--lk-control-border: var(--lk-control-border-color);
|
--lk-control-border: var(--lk-control-border-color);
|
||||||
--lk-control-hover-border: var(--lk-bg6);
|
--lk-control-hover-border: var(--lk-bg6);
|
||||||
|
|
||||||
|
--lk-controlbar-bg: var(--colors-gray-300);
|
||||||
|
--lk-participant-border: var(--colors-gray-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-lk-theme] {
|
[data-lk-theme] .lk-room-container {
|
||||||
background-color: var(--lk-bg);
|
background-color: var(--lk-room-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lk-button,
|
.lk-button,
|
||||||
@@ -133,14 +76,77 @@ for now only "visio-light" is actually used
|
|||||||
border-color: var(--lk-control-hover-border);
|
border-color: var(--lk-control-hover-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lk-disconnect-button {
|
.lk-button:not(:disabled):active,
|
||||||
--lk-control-border: var(--lk-danger3);
|
.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-disconnect-button:not(:disabled):hover {
|
.lk-button[aria-pressed='true'],
|
||||||
--lk-control-hover-bg: var(--lk-danger2);
|
[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);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lk-prejoin {
|
[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-lk-theme] .lk-disconnect-button:not(:disabled):active {
|
||||||
|
background-color: var(--lk-danger-active-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[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;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-lk-theme] .lk-participant-tile {
|
||||||
|
box-shadow: var(--lk-box-shadow);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { slugify } from './slugify'
|
|
||||||
|
|
||||||
export const createRandomRoom = () => {
|
|
||||||
return slugify(crypto.randomUUID())
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user