Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f11369981e | |||
| 6129a7fb2d | |||
| feea9eb60c | |||
| 06dae1d266 | |||
| 491ca7f509 | |||
| b0e1c0f7a4 | |||
| e2887880c6 | |||
| 414b3720f1 | |||
| c61bae279e | |||
| e538fbdb77 | |||
| 75614181e4 | |||
| fb07dcdbf0 | |||
| 4ae3d965f9 | |||
| 8eab45b6d5 | |||
| 4347d87f33 | |||
| 1b52d76168 | |||
| a44b6e8e34 | |||
| 126de638cd | |||
| bf28c5cb84 | |||
| 4f0e7d2c52 | |||
| 3ab5c48296 | |||
| 7f1f573af8 | |||
| d48a18b36b | |||
| eac9158734 | |||
| 994f335266 | |||
| 30bde2fe66 |
@@ -175,25 +175,53 @@ jobs:
|
||||
- name: Check format
|
||||
run: cd src/frontend/ && npm run check
|
||||
|
||||
lint-sdk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd src/sdk/library/ && npm ci
|
||||
|
||||
- name: Check linting
|
||||
run: cd src/sdk/library/ && npm run lint
|
||||
|
||||
- name: Check format
|
||||
run: cd src/sdk/library/ && npm run check
|
||||
|
||||
build-sdk:
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint-sdk
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd src/sdk/library/ && npm ci
|
||||
|
||||
- name: Check linting
|
||||
run: cd src/sdk/library/ && npm run lint
|
||||
|
||||
- name: Check format
|
||||
run: cd src/sdk/library/ && npm run build
|
||||
|
||||
i18n-crowdin:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
uses: actions/create-github-app-token@v1
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: "infrastructure,secrets"
|
||||
-
|
||||
name: Checkout repository
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
-
|
||||
name: Load sops secrets
|
||||
- name: Load sops secrets
|
||||
uses: rouja/actions-sops@main
|
||||
with:
|
||||
secret-file: secrets/numerique-gouv/meet/secrets.enc.env
|
||||
|
||||
@@ -25,7 +25,7 @@ license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"boto3==1.36.2",
|
||||
"boto3==1.36.6",
|
||||
"Brotli==1.1.0",
|
||||
"brevo-python==1.1.2",
|
||||
"celery[redis]==5.4.0",
|
||||
@@ -83,7 +83,7 @@ dev = [
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.6.1",
|
||||
"responses==0.25.6",
|
||||
"ruff==0.9.2",
|
||||
"ruff==0.9.3",
|
||||
"types-requests==2.32.0.20241016",
|
||||
]
|
||||
|
||||
|
||||
Generated
+35
-32
@@ -8,7 +8,7 @@
|
||||
"name": "meet",
|
||||
"version": "0.1.12",
|
||||
"dependencies": {
|
||||
"@livekit/components-react": "2.8.0",
|
||||
"@livekit/components-react": "2.8.1",
|
||||
"@livekit/components-styles": "1.1.4",
|
||||
"@livekit/track-processors": "0.3.3",
|
||||
"@pandacss/preset-panda": "0.51.1",
|
||||
@@ -17,25 +17,25 @@
|
||||
"@tanstack/react-query": "5.64.2",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"hoofd": "1.7.3",
|
||||
"i18next": "24.2.1",
|
||||
"i18next": "24.2.2",
|
||||
"i18next-browser-languagedetector": "8.0.2",
|
||||
"i18next-parser": "9.1.0",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"livekit-client": "2.8.1",
|
||||
"posthog-js": "1.207.0",
|
||||
"posthog-js": "1.210.2",
|
||||
"react": "18.3.1",
|
||||
"react-aria-components": "1.6.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "15.1.1",
|
||||
"use-sound": "4.0.3",
|
||||
"valtio": "2.1.2",
|
||||
"wouter": "3.3.5"
|
||||
"valtio": "2.1.3",
|
||||
"wouter": "3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pandacss/dev": "0.51.1",
|
||||
"@tanstack/eslint-plugin-query": "5.64.2",
|
||||
"@tanstack/react-query-devtools": "5.64.2",
|
||||
"@types/node": "22.10.7",
|
||||
"@types/node": "22.10.10",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.21.0",
|
||||
@@ -49,7 +49,7 @@
|
||||
"postcss": "8.5.1",
|
||||
"prettier": "3.4.2",
|
||||
"typescript": "5.7.3",
|
||||
"vite": "6.0.10",
|
||||
"vite": "6.0.11",
|
||||
"vite-tsconfig-paths": "5.1.4"
|
||||
}
|
||||
},
|
||||
@@ -436,6 +436,7 @@
|
||||
},
|
||||
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
|
||||
"version": "1.3.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1240,9 +1241,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@livekit/components-react": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.8.0.tgz",
|
||||
"integrity": "sha512-Zgt2zv+j2gWJad0mcTDR8cS6KQNXYfAeOL9Si5Lb2Q2k3iPUJinPDMqubM6btnduhkPjy+EF4kA8bNAY0xu4Ig==",
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.8.1.tgz",
|
||||
"integrity": "sha512-XpuDu7iDMcN4pkV8CYNzHf9hLNdYOeEtbmCr7Zesy6Au3BxUl4aS1Ajmg0b75Rx7zTlkyCJt9Lm4VrEqbJCI6Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@livekit/components-core": "0.12.1",
|
||||
@@ -3879,9 +3880,9 @@
|
||||
"integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
|
||||
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
|
||||
"version": "22.10.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz",
|
||||
"integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6395,9 +6396,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "24.2.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.1.tgz",
|
||||
"integrity": "sha512-Q2wC1TjWcSikn1VAJg13UGIjc+okpFxQTxjVAymOnSA3RpttBQNMPf2ovcgoFVsV4QNxTfNZMAxorXZXsk4fBA==",
|
||||
"version": "24.2.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.2.tgz",
|
||||
"integrity": "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -8444,9 +8445,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.207.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.207.0.tgz",
|
||||
"integrity": "sha512-Sx+xamhg1/iKGAtUNh3uAUtAAza4j/yBhxcfUxfqR++WrZdw0V6nmh7LSfVNl7+QVl2qmiPSoZA7z+5ojaWDDQ==",
|
||||
"version": "1.210.2",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.210.2.tgz",
|
||||
"integrity": "sha512-rIbn/h9ur7uA0PS4dClOr9w6txLfHS94yh9yafA5VM2eXToM951XtMYtIQ6bi6wFzpvpFvTQFeYLQ/9/xZ59AQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-js": "^3.38.1",
|
||||
@@ -8503,9 +8504,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-compare": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.0.tgz",
|
||||
"integrity": "sha512-y44MCkgtZUCT9tZGuE278fB7PWVf7fRYy0vbRXAts2o5F0EfC4fIQrvQQGBJo1WJbFcVLXzApOscyJuZqHQc1w=="
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz",
|
||||
"integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
@@ -9791,12 +9793,12 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/valtio": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/valtio/-/valtio-2.1.2.tgz",
|
||||
"integrity": "sha512-fhekN5Rq7dvHULHHBlJeXHrQDl0Jj9GXfNavCm3gkD06crGchaG1nf/J7gSlfZU2wPcRdVS5jBKWHtE2NNz97A==",
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/valtio/-/valtio-2.1.3.tgz",
|
||||
"integrity": "sha512-q+dVczY5zf7kO39JPVq2qR0VXMeEpF1t+P1njHU7hAhwgtyVa/p38YmgyHZq2IQYwnkNrkvh412k4sfDmcIJ2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"proxy-compare": "^3.0.0"
|
||||
"proxy-compare": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
@@ -9890,9 +9892,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.10.tgz",
|
||||
"integrity": "sha512-MEszunEcMo6pFsfXN1GhCFQqnE25tWRH0MA4f0Q7uanACi4y1Us+ZGpTMnITwCTnYzB2b9cpmnelTlxgTBmaBA==",
|
||||
"version": "6.0.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz",
|
||||
"integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -10582,9 +10584,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wouter": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/wouter/-/wouter-3.3.5.tgz",
|
||||
"integrity": "sha512-bx3fLQAMn+EhYbBdY3W1gw9ZfO/uchudxYMwOIBzF3HVgqNEEIT199vEoh7FLTC0Vz5+rpMO6NdFsOkGX1QQCw==",
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/wouter/-/wouter-3.4.0.tgz",
|
||||
"integrity": "sha512-Qx4L+EnYk7AF+pNU9TM+lQtkceL5Qm+oDZU9720DUx+/7K6v7fnlHnRLI78KjEbCqdGFeroW5P0M1t8WYSloQA==",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"mitt": "^3.0.1",
|
||||
"regexparam": "^3.0.0",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"check": "prettier --check ./src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@livekit/components-react": "2.8.0",
|
||||
"@livekit/components-react": "2.8.1",
|
||||
"@livekit/components-styles": "1.1.4",
|
||||
"@livekit/track-processors": "0.3.3",
|
||||
"@pandacss/preset-panda": "0.51.1",
|
||||
@@ -22,25 +22,25 @@
|
||||
"@tanstack/react-query": "5.64.2",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"hoofd": "1.7.3",
|
||||
"i18next": "24.2.1",
|
||||
"i18next": "24.2.2",
|
||||
"i18next-browser-languagedetector": "8.0.2",
|
||||
"i18next-parser": "9.1.0",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"livekit-client": "2.8.1",
|
||||
"posthog-js": "1.207.0",
|
||||
"posthog-js": "1.210.2",
|
||||
"react": "18.3.1",
|
||||
"react-aria-components": "1.6.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "15.1.1",
|
||||
"use-sound": "4.0.3",
|
||||
"valtio": "2.1.2",
|
||||
"wouter": "3.3.5"
|
||||
"valtio": "2.1.3",
|
||||
"wouter": "3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pandacss/dev": "0.51.1",
|
||||
"@tanstack/eslint-plugin-query": "5.64.2",
|
||||
"@tanstack/react-query-devtools": "5.64.2",
|
||||
"@types/node": "22.10.7",
|
||||
"@types/node": "22.10.10",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.21.0",
|
||||
@@ -54,7 +54,7 @@
|
||||
"postcss": "8.5.1",
|
||||
"prettier": "3.4.2",
|
||||
"typescript": "5.7.3",
|
||||
"vite": "6.0.10",
|
||||
"vite": "6.0.11",
|
||||
"vite-tsconfig-paths": "5.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
+26
-10
@@ -13,24 +13,40 @@ import { routes } from './routes'
|
||||
import './i18n/init'
|
||||
import { queryClient } from '@/api/queryClient'
|
||||
import { AppInitialization } from '@/components/AppInitialization'
|
||||
import { SdkCreateButton } from './features/sdk/routes/CreateButton'
|
||||
|
||||
function App() {
|
||||
const { i18n } = useTranslation()
|
||||
useLang(i18n.language)
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppInitialization />
|
||||
<Suspense fallback={null}>
|
||||
<I18nProvider locale={i18n.language}>
|
||||
<Layout>
|
||||
<Switch>
|
||||
{Object.entries(routes).map(([, route], i) => (
|
||||
<Route key={i} path={route.path} component={route.Component} />
|
||||
))}
|
||||
<Route component={NotFoundScreen} />
|
||||
</Switch>
|
||||
</Layout>
|
||||
<ReactQueryDevtools initialIsOpen={false} buttonPosition="top-left" />
|
||||
<Switch>
|
||||
{/* We only want support and ReactQueryDevTools in non /sdk routes */}
|
||||
<Route path={/^(?!\/sdk).*$/}>
|
||||
<AppInitialization />
|
||||
<Layout>
|
||||
{Object.entries(routes).map(([, route], i) => (
|
||||
<Route
|
||||
key={i}
|
||||
path={route.path}
|
||||
component={route.Component}
|
||||
/>
|
||||
))}
|
||||
</Layout>
|
||||
<ReactQueryDevtools
|
||||
initialIsOpen={false}
|
||||
buttonPosition="top-left"
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/sdk" nest>
|
||||
<Route path="/create-button">
|
||||
<SdkCreateButton />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route component={NotFoundScreen} />
|
||||
</Switch>
|
||||
</I18nProvider>
|
||||
</Suspense>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export const VisioIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0.841702 8.95427C0.75 9.42479 0.75 10.0042 0.75 10.9748V13.3096C0.75 14.5138 0.75 15.1159 0.925134 15.6549C1.08009 16.1319 1.33356 16.5709 1.6691 16.9435C2.04833 17.3647 2.56977 17.6658 3.61264 18.2679L5.6346 19.4353C6.55753 19.9681 7.07208 20.2652 7.56247 20.4081V13.2043C7.56247 12.8442 7.36578 12.5129 7.04965 12.3404L0.841702 8.95427Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M17.747 10.0123V13.7685C17.747 14.2878 17.9727 14.7815 18.3654 15.1214L21.0688 17.4609C21.227 17.5947 21.3912 17.7042 21.5616 17.7894C21.738 17.8685 21.9084 17.9081 22.0726 17.9081C22.4255 17.9081 22.7084 17.7925 22.9213 17.5613C23.1404 17.324 23.2499 17.0168 23.2499 16.6396V7.14866C23.2499 6.77146 23.1404 6.46726 22.9213 6.23607C22.7084 5.9988 22.4255 5.88017 22.0726 5.88017C21.9084 5.88017 21.738 5.91971 21.5616 5.9988C21.3912 6.07789 21.227 6.1874 21.0688 6.32733L18.3675 8.65759C17.9735 8.99746 17.747 9.49201 17.747 10.0123Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M1.74517 7.61282C1.67 7.57182 1.59117 7.54405 1.51135 7.52872C1.56171 7.46443 1.61433 7.40178 1.66914 7.34091C2.04837 6.91973 2.56981 6.61868 3.61268 6.01657L5.63464 4.84919C6.67751 4.24708 7.19894 3.94603 7.7533 3.82819C8.2438 3.72394 8.75074 3.72394 9.24124 3.82819C9.7956 3.94603 10.317 4.24708 11.3599 4.84919L13.358 6.0028C13.366 6.00738 13.374 6.01197 13.3819 6.01658C14.4248 6.61868 14.9462 6.91973 15.3255 7.34091C15.661 7.71357 15.9145 8.15259 16.0694 8.62951C16.2446 9.16852 16.2446 9.77063 16.2446 10.9748V13.3096C16.2446 14.5138 16.2446 15.1159 16.0694 15.6549C15.9145 16.1319 15.661 16.5709 15.3255 16.9435C14.9462 17.3647 14.4248 17.6657 13.382 18.2678C13.373 18.273 13.364 18.2782 13.3551 18.2833L11.3599 19.4353C10.317 20.0374 9.7956 20.3384 9.24124 20.4562C9.21846 20.4611 9.19564 20.4657 9.17279 20.4701L9.17278 13.2043C9.17278 12.255 8.65422 11.3814 7.82079 10.9268L1.74517 7.61282Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -14,10 +14,11 @@ import { RiAddLine, RiLink } from '@remixicon/react'
|
||||
import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
|
||||
import { IntroSlider } from '@/features/home/components/IntroSlider'
|
||||
import { MoreLink } from '@/features/home/components/MoreLink'
|
||||
import { ReactNode, useState } from 'react'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
|
||||
import { css } from '@/styled-system/css'
|
||||
import { menuRecipe } from '@/primitives/menuRecipe.ts'
|
||||
import { SdkReverseClient } from '@/features/sdk/SdkReverseClient'
|
||||
|
||||
const Columns = ({ children }: { children?: ReactNode }) => {
|
||||
return (
|
||||
@@ -32,10 +33,10 @@ const Columns = ({ children }: { children?: ReactNode }) => {
|
||||
justifyContent: 'normal',
|
||||
padding: '0 1rem',
|
||||
width: 'calc(100% - 2rem)',
|
||||
'@media(prefers-reduced-motion: reduce)': {
|
||||
_motionReduce: {
|
||||
opacity: 1,
|
||||
},
|
||||
'@media(prefers-reduced-motion: no-preference)': {
|
||||
_motionSafe: {
|
||||
opacity: 0,
|
||||
animation: '.5s ease-in fade 0s forwards',
|
||||
},
|
||||
@@ -156,6 +157,18 @@ export const Home = () => {
|
||||
const { mutateAsync: createRoom } = useCreateRoom()
|
||||
const [laterRoomId, setLaterRoomId] = useState<null | string>(null)
|
||||
|
||||
const { user } = useUser()
|
||||
|
||||
/**
|
||||
* Used for SDK popup to close automatically.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
SdkReverseClient.broadcastAuthentication()
|
||||
}, [user])
|
||||
|
||||
return (
|
||||
<UserAware>
|
||||
<Screen>
|
||||
|
||||
@@ -1,30 +1,281 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PreJoin, type LocalUserChoices } from '@livekit/components-react'
|
||||
import {
|
||||
usePersistentUserChoices,
|
||||
usePreviewTracks,
|
||||
type LocalUserChoices,
|
||||
} from '@livekit/components-react'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { Screen } from '@/layout/Screen'
|
||||
import { CenteredContent } from '@/layout/CenteredContent'
|
||||
import { useUser } from '@/features/auth'
|
||||
import { useMemo, useEffect, useRef, useState } from 'react'
|
||||
import { LocalVideoTrack, Track } from 'livekit-client'
|
||||
import { H } from '@/primitives/H'
|
||||
import { SelectToggleDevice } from '../livekit/components/controls/SelectToggleDevice'
|
||||
import { Field } from '@/primitives/Field'
|
||||
import { Form } from '@/primitives'
|
||||
import { HStack, VStack } from '@/styled-system/jsx'
|
||||
|
||||
const onError = (e: Error) => console.error('ERROR', e)
|
||||
|
||||
export const Join = ({
|
||||
onSubmit,
|
||||
}: {
|
||||
onSubmit: (choices: LocalUserChoices) => void
|
||||
}) => {
|
||||
const { t } = useTranslation('rooms')
|
||||
const { user } = useUser()
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||
|
||||
const {
|
||||
userChoices: initialUserChoices,
|
||||
saveAudioInputDeviceId,
|
||||
saveVideoInputDeviceId,
|
||||
saveUsername,
|
||||
} = usePersistentUserChoices({})
|
||||
|
||||
const [audioDeviceId, setAudioDeviceId] = useState<string>(
|
||||
initialUserChoices.audioDeviceId
|
||||
)
|
||||
const [videoDeviceId, setVideoDeviceId] = useState<string>(
|
||||
initialUserChoices.videoDeviceId
|
||||
)
|
||||
const [username, setUsername] = useState<string>(initialUserChoices.username)
|
||||
|
||||
useEffect(() => {
|
||||
saveAudioInputDeviceId(audioDeviceId)
|
||||
}, [audioDeviceId, saveAudioInputDeviceId])
|
||||
|
||||
useEffect(() => {
|
||||
saveVideoInputDeviceId(videoDeviceId)
|
||||
}, [videoDeviceId, saveVideoInputDeviceId])
|
||||
|
||||
useEffect(() => {
|
||||
saveUsername(username)
|
||||
}, [username, saveUsername])
|
||||
|
||||
const [audioEnabled, setAudioEnabled] = useState(true)
|
||||
const [videoEnabled, setVideoEnabled] = useState(true)
|
||||
|
||||
const tracks = usePreviewTracks(
|
||||
{
|
||||
audio: { deviceId: initialUserChoices.audioDeviceId },
|
||||
video: { deviceId: initialUserChoices.videoDeviceId },
|
||||
},
|
||||
onError
|
||||
)
|
||||
|
||||
const videoTrack = useMemo(
|
||||
() =>
|
||||
tracks?.filter(
|
||||
(track) => track.kind === Track.Kind.Video
|
||||
)[0] as LocalVideoTrack,
|
||||
[tracks]
|
||||
)
|
||||
|
||||
const audioTrack = useMemo(
|
||||
() =>
|
||||
tracks?.filter(
|
||||
(track) => track.kind === Track.Kind.Audio
|
||||
)[0] as LocalVideoTrack,
|
||||
[tracks]
|
||||
)
|
||||
|
||||
const videoEl = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
const videoElement = videoEl.current as HTMLVideoElement | null
|
||||
|
||||
const handleVideoLoaded = () => {
|
||||
if (videoElement) {
|
||||
videoElement.style.opacity = '1'
|
||||
}
|
||||
}
|
||||
|
||||
if (videoElement && videoTrack && videoEnabled) {
|
||||
videoTrack.unmute()
|
||||
videoTrack.attach(videoElement)
|
||||
videoElement.addEventListener('loadedmetadata', handleVideoLoaded)
|
||||
}
|
||||
|
||||
return () => {
|
||||
videoTrack?.detach()
|
||||
videoElement?.removeEventListener('loadedmetadata', handleVideoLoaded)
|
||||
}
|
||||
}, [videoTrack, videoEnabled])
|
||||
|
||||
function handleSubmit() {
|
||||
onSubmit({
|
||||
audioEnabled,
|
||||
videoEnabled,
|
||||
audioDeviceId,
|
||||
videoDeviceId,
|
||||
username,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen layout="centered" footer={false}>
|
||||
<CenteredContent title={t('join.heading')}>
|
||||
<PreJoin
|
||||
persistUserChoices
|
||||
onSubmit={onSubmit}
|
||||
micLabel={t('join.audioinput.label')}
|
||||
camLabel={t('join.videoinput.label')}
|
||||
joinLabel={t('join.joinLabel')}
|
||||
userLabel={t('join.usernameLabel')}
|
||||
defaults={{ username: user?.full_name }}
|
||||
/>
|
||||
</CenteredContent>
|
||||
<Screen footer={false}>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
flexGrow: 1,
|
||||
lg: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
height: 'auto',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '2rem',
|
||||
padding: '0 2rem',
|
||||
flexDirection: 'column',
|
||||
minWidth: 0,
|
||||
width: '100%',
|
||||
lg: {
|
||||
flexDirection: 'row',
|
||||
width: 'auto',
|
||||
height: '570px',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
lg: {
|
||||
width: '740px',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
borderRadius: '1rem',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
aspectRatio: 16 / 9,
|
||||
'--tw-shadow':
|
||||
'0 10px 15px -5px #00000010, 0 4px 5px -6px #00000010',
|
||||
'--tw-shadow-colored':
|
||||
'0 10px 15px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color)',
|
||||
boxShadow:
|
||||
'var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)',
|
||||
backgroundColor: 'black',
|
||||
})}
|
||||
>
|
||||
{videoTrack && videoEnabled ? (
|
||||
// eslint-disable-next-line jsx-a11y/media-has-caption
|
||||
<video
|
||||
ref={videoEl}
|
||||
width="1280"
|
||||
height="720"
|
||||
className={css({
|
||||
display: 'block',
|
||||
width: '102%',
|
||||
height: '102%',
|
||||
objectFit: 'cover',
|
||||
transform: 'rotateY(180deg)',
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.3s ease-in-out',
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
})}
|
||||
>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: '24px',
|
||||
fontWeight: '300',
|
||||
})}
|
||||
>
|
||||
{!videoEnabled && t('cameraDisabled')}
|
||||
{videoEnabled && !videoTrack && t('cameraStarting')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<HStack justify="center" padding={1.5}>
|
||||
<SelectToggleDevice
|
||||
source={Track.Source.Microphone}
|
||||
initialState={audioEnabled}
|
||||
track={audioTrack}
|
||||
initialDeviceId={initialUserChoices.audioDeviceId}
|
||||
onChange={(enabled) => setAudioEnabled(enabled)}
|
||||
onDeviceError={(error) => console.error(error)}
|
||||
onActiveDeviceChange={(deviceId) =>
|
||||
setAudioDeviceId(deviceId ?? '')
|
||||
}
|
||||
variant="tertiary"
|
||||
/>
|
||||
<SelectToggleDevice
|
||||
source={Track.Source.Camera}
|
||||
initialState={videoEnabled}
|
||||
track={videoTrack}
|
||||
initialDeviceId={initialUserChoices.videoDeviceId}
|
||||
onChange={(enabled) => setVideoEnabled(enabled)}
|
||||
onDeviceError={(error) => console.error(error)}
|
||||
onActiveDeviceChange={(deviceId) =>
|
||||
setVideoDeviceId(deviceId ?? '')
|
||||
}
|
||||
variant="tertiary"
|
||||
/>
|
||||
</HStack>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
flexShrink: 0,
|
||||
padding: '0',
|
||||
sm: {
|
||||
width: '448px',
|
||||
padding: '0 3rem 9rem 3rem',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
submitLabel={t('joinLabel')}
|
||||
submitButtonProps={{
|
||||
fullWidth: true,
|
||||
}}
|
||||
>
|
||||
<VStack marginBottom={1}>
|
||||
<H lvl={1} margin={false}>
|
||||
{t('heading')}
|
||||
</H>
|
||||
<Field
|
||||
type="text"
|
||||
onChange={setUsername}
|
||||
label={t('usernameLabel')}
|
||||
aria-label={t('usernameLabel')}
|
||||
defaultValue={initialUserChoices?.username}
|
||||
validate={(value) => !value && t('errors.usernameEmpty')}
|
||||
wrapperProps={{
|
||||
noMargin: true,
|
||||
fullWidth: true,
|
||||
}}
|
||||
labelProps={{
|
||||
center: true,
|
||||
}}
|
||||
maxLength={50}
|
||||
/>
|
||||
</VStack>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Screen>
|
||||
)
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { RiAccountBoxLine } from '@remixicon/react'
|
||||
import { MenuItem } from 'react-aria-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { menuRecipe } from '@/primitives/menuRecipe'
|
||||
import { useSidePanel } from '../../../hooks/useSidePanel'
|
||||
|
||||
export const EffectsMenuItem = () => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
||||
const { toggleEffects } = useSidePanel()
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
onAction={() => toggleEffects()}
|
||||
className={menuRecipe({ icon: true, variant: 'dark' }).item}
|
||||
>
|
||||
<RiAccountBoxLine size={20} />
|
||||
{t('effects')}
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { RiMegaphoneLine } from '@remixicon/react'
|
||||
import { MenuItem } from 'react-aria-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { menuRecipe } from '@/primitives/menuRecipe'
|
||||
import { GRIST_FORM } from '@/utils/constants'
|
||||
|
||||
export const FeedbackMenuItem = () => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
href={GRIST_FORM}
|
||||
target="_blank"
|
||||
className={menuRecipe({ icon: true, variant: 'dark' }).item}
|
||||
>
|
||||
<RiMegaphoneLine size={20} />
|
||||
{t('feedback')}
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { RiFullscreenExitLine, RiFullscreenLine } from '@remixicon/react'
|
||||
import { MenuItem } from 'react-aria-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { menuRecipe } from '@/primitives/menuRecipe'
|
||||
import { useFullScreen } from '../../../hooks/useFullScreen'
|
||||
|
||||
export const FullScreenMenuItem = () => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
||||
const { toggleFullScreen, isCurrentlyFullscreen, isFullscreenAvailable } =
|
||||
useFullScreen()
|
||||
|
||||
if (!isFullscreenAvailable) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
onAction={() => toggleFullScreen()}
|
||||
className={menuRecipe({ icon: true, variant: 'dark' }).item}
|
||||
>
|
||||
{isCurrentlyFullscreen ? (
|
||||
<>
|
||||
<RiFullscreenExitLine size={20} />
|
||||
{t('fullscreen.exit')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RiFullscreenLine size={20} />
|
||||
{t('fullscreen.enter')}
|
||||
</>
|
||||
)}
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
+9
-36
@@ -1,21 +1,12 @@
|
||||
import {
|
||||
RiAccountBoxLine,
|
||||
RiMegaphoneLine,
|
||||
RiSettings3Line,
|
||||
} from '@remixicon/react'
|
||||
import { MenuItem, Menu as RACMenu, MenuSection } from 'react-aria-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Menu as RACMenu, MenuSection } from 'react-aria-components'
|
||||
import { Separator } from '@/primitives/Separator'
|
||||
import { useSidePanel } from '../../../hooks/useSidePanel'
|
||||
import { menuRecipe } from '@/primitives/menuRecipe.ts'
|
||||
import { useSettingsDialog } from '../SettingsDialogContext'
|
||||
import { GRIST_FORM } from '@/utils/constants'
|
||||
import { FullScreenMenuItem } from './FullScreenMenuItem'
|
||||
import { SettingsMenuItem } from './SettingsMenuItem'
|
||||
import { FeedbackMenuItem } from './FeedbackMenuItem'
|
||||
import { EffectsMenuItem } from './EffectsMenuItem'
|
||||
|
||||
// @todo try refactoring it to use MenuList component
|
||||
export const OptionsMenuItems = () => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
||||
const { toggleEffects } = useSidePanel()
|
||||
const { setDialogOpen } = useSettingsDialog()
|
||||
return (
|
||||
<RACMenu
|
||||
style={{
|
||||
@@ -24,31 +15,13 @@ export const OptionsMenuItems = () => {
|
||||
}}
|
||||
>
|
||||
<MenuSection>
|
||||
<MenuItem
|
||||
onAction={() => toggleEffects()}
|
||||
className={menuRecipe({ icon: true, variant: 'dark' }).item}
|
||||
>
|
||||
<RiAccountBoxLine size={20} />
|
||||
{t('effects')}
|
||||
</MenuItem>
|
||||
<FullScreenMenuItem />
|
||||
<EffectsMenuItem />
|
||||
</MenuSection>
|
||||
<Separator />
|
||||
<MenuSection>
|
||||
<MenuItem
|
||||
href={GRIST_FORM}
|
||||
target="_blank"
|
||||
className={menuRecipe({ icon: true, variant: 'dark' }).item}
|
||||
>
|
||||
<RiMegaphoneLine size={20} />
|
||||
{t('feedback')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className={menuRecipe({ icon: true, variant: 'dark' }).item}
|
||||
onAction={() => setDialogOpen(true)}
|
||||
>
|
||||
<RiSettings3Line size={20} />
|
||||
{t('settings')}
|
||||
</MenuItem>
|
||||
<FeedbackMenuItem />
|
||||
<SettingsMenuItem />
|
||||
</MenuSection>
|
||||
</RACMenu>
|
||||
)
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { RiSettings3Line } from '@remixicon/react'
|
||||
import { MenuItem } from 'react-aria-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { menuRecipe } from '@/primitives/menuRecipe'
|
||||
import { useSettingsDialog } from '../SettingsDialogContext'
|
||||
|
||||
export const SettingsMenuItem = () => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
||||
const { setDialogOpen } = useSettingsDialog()
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
className={menuRecipe({ icon: true, variant: 'dark' }).item}
|
||||
onAction={() => setDialogOpen(true)}
|
||||
>
|
||||
<RiSettings3Line size={20} />
|
||||
{t('settings')}
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
@@ -13,12 +13,14 @@ import {
|
||||
RiVideoOffLine,
|
||||
RiVideoOnLine,
|
||||
} from '@remixicon/react'
|
||||
import { Track } from 'livekit-client'
|
||||
import { LocalAudioTrack, LocalVideoTrack, Track } from 'livekit-client'
|
||||
|
||||
import { Shortcut } from '@/features/shortcuts/types'
|
||||
|
||||
import { ToggleDevice } from '@/features/rooms/livekit/components/controls/ToggleDevice.tsx'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export type ToggleSource = Exclude<
|
||||
Track.Source,
|
||||
@@ -65,16 +67,22 @@ const selectToggleDeviceConfig: SelectToggleDeviceConfigMap = {
|
||||
|
||||
type SelectToggleDeviceProps<T extends ToggleSource> =
|
||||
UseTrackToggleProps<T> & {
|
||||
track?: LocalAudioTrack | LocalVideoTrack | undefined
|
||||
initialDeviceId?: string
|
||||
onActiveDeviceChange: (deviceId: string) => void
|
||||
source: SelectToggleSource
|
||||
variant?: NonNullable<ButtonRecipeProps>['variant']
|
||||
menuVariant?: 'dark' | 'light'
|
||||
hideMenu?: boolean
|
||||
variant?: 'dark' | 'light'
|
||||
}
|
||||
|
||||
export const SelectToggleDevice = <T extends ToggleSource>({
|
||||
track,
|
||||
initialDeviceId,
|
||||
onActiveDeviceChange,
|
||||
hideMenu,
|
||||
variant = 'light',
|
||||
variant = 'primaryDark',
|
||||
menuVariant = 'light',
|
||||
...props
|
||||
}: SelectToggleDeviceProps<T>) => {
|
||||
const config = selectToggleDeviceConfig[props.source]
|
||||
@@ -85,7 +93,19 @@ export const SelectToggleDevice = <T extends ToggleSource>({
|
||||
const trackProps = useTrackToggle(props)
|
||||
|
||||
const { devices, activeDeviceId, setActiveMediaDevice } =
|
||||
useMediaDeviceSelect({ kind: config.kind })
|
||||
useMediaDeviceSelect({ kind: config.kind, track })
|
||||
|
||||
/**
|
||||
* When providing only track outside of a room context, activeDeviceId is undefined.
|
||||
* So we need to initialize it with the initialDeviceId.
|
||||
* nb: I don't understand why useMediaDeviceSelect cannot infer it from track device id.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (initialDeviceId !== undefined) {
|
||||
setActiveMediaDevice(initialDeviceId)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [setActiveMediaDevice])
|
||||
|
||||
const selectLabel = t('choose', { keyPrefix: `join.${config.kind}` })
|
||||
|
||||
@@ -99,6 +119,7 @@ export const SelectToggleDevice = <T extends ToggleSource>({
|
||||
<ToggleDevice
|
||||
{...trackProps}
|
||||
config={config}
|
||||
variant={variant}
|
||||
toggleButtonProps={{
|
||||
...(hideMenu
|
||||
? {
|
||||
@@ -108,13 +129,13 @@ export const SelectToggleDevice = <T extends ToggleSource>({
|
||||
}}
|
||||
/>
|
||||
{!hideMenu && (
|
||||
<Menu variant={variant}>
|
||||
<Menu variant={menuVariant}>
|
||||
<Button
|
||||
tooltip={selectLabel}
|
||||
aria-label={selectLabel}
|
||||
groupPosition="right"
|
||||
square
|
||||
variant={trackProps.enabled ? 'primaryDark' : 'error2'}
|
||||
variant={trackProps.enabled ? variant : 'error2'}
|
||||
>
|
||||
<RiArrowDownSLine />
|
||||
</Button>
|
||||
@@ -128,7 +149,7 @@ export const SelectToggleDevice = <T extends ToggleSource>({
|
||||
setActiveMediaDevice(value as string)
|
||||
onActiveDeviceChange(value as string)
|
||||
}}
|
||||
variant={variant}
|
||||
variant={menuVariant}
|
||||
/>
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
@@ -4,10 +4,14 @@ import { useTranslation } from 'react-i18next'
|
||||
import { Crisp } from 'crisp-sdk-web'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
import { useIsSupportEnabled } from '@/features/support/hooks/useSupport'
|
||||
|
||||
export const SupportToggle = ({ onPress, ...props }: ToggleButtonProps) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls' })
|
||||
const [isOpened, setIsOpened] = useState($crisp.is('chat:opened'))
|
||||
|
||||
const [isOpened, setIsOpened] = useState(() => {
|
||||
return window?.$crisp?.is?.('chat:opened') || false
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!Crisp) {
|
||||
@@ -26,6 +30,12 @@ export const SupportToggle = ({ onPress, ...props }: ToggleButtonProps) => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const isSupportEnabled = useIsSupportEnabled()
|
||||
|
||||
if (!isSupportEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
return (
|
||||
<ToggleButton
|
||||
square
|
||||
|
||||
@@ -6,13 +6,19 @@ import { useTranslation } from 'react-i18next'
|
||||
import { SelectToggleDeviceConfig } from './SelectToggleDevice'
|
||||
import useLongPress from '@/features/shortcuts/useLongPress'
|
||||
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
|
||||
import { useIsSpeaking, useLocalParticipant } from '@livekit/components-react'
|
||||
import {
|
||||
useIsSpeaking,
|
||||
useLocalParticipant,
|
||||
useMaybeRoomContext,
|
||||
} from '@livekit/components-react'
|
||||
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
|
||||
export type ToggleDeviceProps = {
|
||||
enabled: boolean
|
||||
toggle: () => void
|
||||
config: SelectToggleDeviceConfig
|
||||
variant?: NonNullable<ButtonRecipeProps>['variant']
|
||||
toggleButtonProps?: Partial<ToggleButtonProps>
|
||||
}
|
||||
|
||||
@@ -20,6 +26,7 @@ export const ToggleDevice = ({
|
||||
config,
|
||||
enabled,
|
||||
toggle,
|
||||
variant = 'primaryDark',
|
||||
toggleButtonProps,
|
||||
}: ToggleDeviceProps) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||
@@ -51,17 +58,15 @@ export const ToggleDevice = ({
|
||||
|
||||
const Icon = enabled ? iconOn : iconOff
|
||||
|
||||
const { localParticipant } = useLocalParticipant()
|
||||
const isSpeaking = useIsSpeaking(localParticipant)
|
||||
|
||||
if (kind === 'audioinput' && pushToTalk) {
|
||||
return <ActiveSpeaker isSpeaking={isSpeaking} pushToTalk />
|
||||
const context = useMaybeRoomContext()
|
||||
if (kind === 'audioinput' && pushToTalk && context) {
|
||||
return <ActiveSpeakerWrapper />
|
||||
}
|
||||
|
||||
return (
|
||||
<ToggleButton
|
||||
isSelected={!enabled}
|
||||
variant={enabled ? 'primaryDark' : 'error2'}
|
||||
variant={enabled ? variant : 'error2'}
|
||||
shySelected
|
||||
onPress={() => toggle()}
|
||||
aria-label={toggleLabel}
|
||||
@@ -73,3 +78,9 @@ export const ToggleDevice = ({
|
||||
</ToggleButton>
|
||||
)
|
||||
}
|
||||
|
||||
const ActiveSpeakerWrapper = () => {
|
||||
const { localParticipant } = useLocalParticipant()
|
||||
const isSpeaking = useIsSpeaking(localParticipant)
|
||||
return <ActiveSpeaker isSpeaking={isSpeaking} pushToTalk />
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { css } from '@/styled-system/css'
|
||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
|
||||
export const TranscriptToggle = ({
|
||||
variant = 'primaryDark',
|
||||
variant = 'primaryTextDark',
|
||||
onPress,
|
||||
...props
|
||||
}: ToggleButtonProps) => {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// We use vendor prefix properties
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
export function useFullScreen() {
|
||||
const getIsFullscreen = () => {
|
||||
return !!(
|
||||
document.fullscreenElement ||
|
||||
document.webkitFullscreenElement ||
|
||||
document.mozFullScreenElement ||
|
||||
document.msFullscreenElement
|
||||
)
|
||||
}
|
||||
|
||||
const [isFullscreenAvailable] = useState(
|
||||
() =>
|
||||
document.fullscreenEnabled ||
|
||||
document.webkitFullscreenEnabled ||
|
||||
document.mozFullScreenEnabled ||
|
||||
document.msFullscreenEnabled
|
||||
)
|
||||
|
||||
const enterFullscreen = async () => {
|
||||
try {
|
||||
const docEl = document.documentElement
|
||||
if (docEl.requestFullscreen) {
|
||||
await docEl.requestFullscreen()
|
||||
} else if (docEl.webkitRequestFullscreen) {
|
||||
await docEl.webkitRequestFullscreen()
|
||||
} else if (docEl.msRequestFullscreen) {
|
||||
await docEl.msRequestFullscreen()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error entering fullscreen:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const exitFullscreen = async () => {
|
||||
try {
|
||||
if (document.exitFullscreen) {
|
||||
await document.exitFullscreen()
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
await document.webkitExitFullscreen()
|
||||
} else if (document.msExitFullscreen) {
|
||||
await document.msExitFullscreen()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error exiting fullscreen:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleFullScreen = async () => {
|
||||
const isCurrentlyFullscreen = getIsFullscreen()
|
||||
if (isCurrentlyFullscreen) {
|
||||
await exitFullscreen()
|
||||
} else {
|
||||
await enterFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isCurrentlyFullscreen: getIsFullscreen(),
|
||||
isFullscreenAvailable,
|
||||
toggleFullScreen,
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,8 @@ import { HandToggle } from '../../components/controls/HandToggle'
|
||||
import { ScreenShareToggle } from '../../components/controls/ScreenShareToggle'
|
||||
import { OptionsButton } from '../../components/controls/Options/OptionsButton'
|
||||
import { StartMediaButton } from '../../components/controls/StartMediaButton'
|
||||
import { ChatToggle } from '../../components/controls/ChatToggle'
|
||||
import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle'
|
||||
import { SupportToggle } from '../../components/controls/SupportToggle'
|
||||
import { TranscriptToggle } from '../../components/controls/TranscriptToggle'
|
||||
import { MoreOptions } from './MoreOptions'
|
||||
import { useRef } from 'react'
|
||||
|
||||
export function DesktopControlBar({
|
||||
onDeviceError,
|
||||
@@ -21,9 +19,11 @@ export function DesktopControlBar({
|
||||
saveVideoInputDeviceId,
|
||||
}: ControlBarAuxProps) {
|
||||
const browserSupportsScreenSharing = supportsScreenSharing()
|
||||
const desktopControlBarEl = useRef<HTMLDivElement>(null)
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={desktopControlBarEl}
|
||||
className={css({
|
||||
width: '100vw',
|
||||
display: 'flex',
|
||||
@@ -62,7 +62,7 @@ export function DesktopControlBar({
|
||||
onActiveDeviceChange={(deviceId) =>
|
||||
saveAudioInputDeviceId(deviceId ?? '')
|
||||
}
|
||||
variant="dark"
|
||||
menuVariant="dark"
|
||||
/>
|
||||
<SelectToggleDevice
|
||||
source={Track.Source.Camera}
|
||||
@@ -73,7 +73,7 @@ export function DesktopControlBar({
|
||||
onActiveDeviceChange={(deviceId) =>
|
||||
saveVideoInputDeviceId(deviceId ?? '')
|
||||
}
|
||||
variant="dark"
|
||||
menuVariant="dark"
|
||||
/>
|
||||
{browserSupportsScreenSharing && (
|
||||
<ScreenShareToggle
|
||||
@@ -87,21 +87,7 @@ export function DesktopControlBar({
|
||||
<LeaveButton />
|
||||
<StartMediaButton />
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
flex: '1 1 33%',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
paddingRight: '0.25rem',
|
||||
})}
|
||||
>
|
||||
<ChatToggle />
|
||||
<ParticipantsToggle />
|
||||
<TranscriptToggle />
|
||||
<SupportToggle />
|
||||
</div>
|
||||
<MoreOptions parentElement={desktopControlBarEl} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { css } from '@/styled-system/css'
|
||||
import { ChatToggle } from '../../components/controls/ChatToggle'
|
||||
import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle'
|
||||
import { SupportToggle } from '../../components/controls/SupportToggle'
|
||||
import { TranscriptToggle } from '../../components/controls/TranscriptToggle'
|
||||
import { useSize } from '../../hooks/useResizeObserver'
|
||||
import { useState, RefObject } from 'react'
|
||||
import { Dialog, DialogTrigger, Popover } from 'react-aria-components'
|
||||
import { Button } from '@/primitives'
|
||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const CONTROL_BAR_BREAKPOINT = 750
|
||||
|
||||
const NavigationControls = ({ onPress }: Partial<ToggleButtonProps>) => (
|
||||
<>
|
||||
<ChatToggle onPress={onPress} />
|
||||
<ParticipantsToggle onPress={onPress} />
|
||||
<TranscriptToggle onPress={onPress} />
|
||||
<SupportToggle onPress={onPress} />
|
||||
</>
|
||||
)
|
||||
|
||||
export const LateralMenu = () => {
|
||||
const { t } = useTranslation('rooms')
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const handlePress = () => setIsOpen(!isOpen)
|
||||
const handleClose = () => setIsOpen(false)
|
||||
|
||||
return (
|
||||
<DialogTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
|
||||
<Button
|
||||
square
|
||||
variant="secondaryDark"
|
||||
aria-label={t('controls.moreOptions')}
|
||||
tooltip={t('controls.moreOptions')}
|
||||
onPress={handlePress}
|
||||
>
|
||||
{isOpen ? <RiArrowDownSLine /> : <RiArrowUpSLine />}
|
||||
</Button>
|
||||
<Popover>
|
||||
<Dialog
|
||||
className={css({
|
||||
width: '65px',
|
||||
backgroundColor: 'primaryDark.50',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
borderRadius: '4px',
|
||||
paddingTop: '10px',
|
||||
gap: '0.5rem',
|
||||
})}
|
||||
>
|
||||
<NavigationControls onPress={handleClose} />
|
||||
</Dialog>
|
||||
</Popover>
|
||||
</DialogTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
export const MoreOptions = ({
|
||||
parentElement,
|
||||
}: {
|
||||
parentElement: RefObject<HTMLDivElement>
|
||||
}) => {
|
||||
const { width: parentWidth } = useSize(parentElement)
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
flex: '1 1 33%',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
paddingRight: '0.25rem',
|
||||
})}
|
||||
>
|
||||
{parentWidth > CONTROL_BAR_BREAKPOINT ? (
|
||||
<NavigationControls />
|
||||
) : (
|
||||
<LateralMenu />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
export enum ClientMessageType {
|
||||
ROOM_CREATED = 'ROOM_CREATED',
|
||||
}
|
||||
|
||||
export class SdkReverseClient {
|
||||
/**
|
||||
* IDEA: Use API Key. Must be based on some sort of credentials ? No needs for now as now security
|
||||
* issues are plausible.
|
||||
*/
|
||||
static getAllowTargetOrigin() {
|
||||
return '*'
|
||||
}
|
||||
|
||||
static post(type: ClientMessageType, data: unknown = {}) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type,
|
||||
data,
|
||||
},
|
||||
SdkReverseClient.getAllowTargetOrigin()
|
||||
)
|
||||
}
|
||||
|
||||
static broadcastAuthentication() {
|
||||
const bc = new BroadcastChannel('APP_CHANNEL')
|
||||
bc.postMessage({ type: 'AUTHENTICATED' })
|
||||
|
||||
/**
|
||||
* This means the parent window has authenticated has successfully refetched user, then we can close the popup.
|
||||
*/
|
||||
bc.onmessage = (event) => {
|
||||
if (event.data.type === 'AUTHENTICATED_ACK') {
|
||||
window.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static waitForAuthenticationAck() {
|
||||
return new Promise<void>((resolve) => {
|
||||
const bc = new BroadcastChannel('APP_CHANNEL')
|
||||
bc.onmessage = async (event) => {
|
||||
if (event.data.type === 'AUTHENTICATED') {
|
||||
resolve()
|
||||
bc.postMessage({ type: 'AUTHENTICATED_ACK' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static ensureAuth() {
|
||||
// TODO: ...
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { Button } from '@/primitives/Button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePersistentUserChoices } from '@livekit/components-react'
|
||||
import { useState } from 'react'
|
||||
import { getRouteUrl } from '@/navigation/getRouteUrl'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { RiCheckLine, RiFileCopyLine } from '@remixicon/react'
|
||||
import { VisioIcon } from '@/assets/VisioIcon'
|
||||
import { generateRoomId, useCreateRoom } from '../../rooms'
|
||||
import { ClientMessageType, SdkReverseClient } from '../SdkReverseClient'
|
||||
import { authUrl, useUser } from '@/features/auth'
|
||||
|
||||
export const SdkCreateButton = () => {
|
||||
const { isLoggedIn, ...other } = useUser()
|
||||
const { t } = useTranslation('sdk', { keyPrefix: 'createButton' })
|
||||
const [roomUrl, setRoomUrl] = useState<string>()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const {
|
||||
userChoices: { username },
|
||||
} = usePersistentUserChoices()
|
||||
|
||||
const { mutateAsync: createRoom } = useCreateRoom()
|
||||
|
||||
const startSSO = () => {
|
||||
SdkReverseClient.waitForAuthenticationAck().then(async () => {
|
||||
await other.refetch()
|
||||
submitCreateRoom()
|
||||
})
|
||||
const params = `scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,
|
||||
width=400,height=900,left=100,top=100`
|
||||
window.open(new URL('authenticate/', authUrl()).href, '', params)
|
||||
}
|
||||
|
||||
const submitCreateRoom = () => {
|
||||
setIsLoading(true)
|
||||
setTimeout(() => {
|
||||
const slug = generateRoomId()
|
||||
createRoom({ slug, username }).then((data) => {
|
||||
const roomUrlTmp = getRouteUrl('room', data.slug)
|
||||
setRoomUrl(roomUrlTmp)
|
||||
setIsLoading(false)
|
||||
SdkReverseClient.post(ClientMessageType.ROOM_CREATED, {
|
||||
url: roomUrlTmp,
|
||||
})
|
||||
})
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
if (isLoggedIn) {
|
||||
submitCreateRoom()
|
||||
} else {
|
||||
startSSO()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
paddingTop: '3px',
|
||||
paddingLeft: '3px',
|
||||
})}
|
||||
>
|
||||
{roomUrl ? (
|
||||
<RoomUrl roomUrl={roomUrl} />
|
||||
) : (
|
||||
<Button
|
||||
variant="primaryDark"
|
||||
aria-label={t('label')}
|
||||
onPress={submit}
|
||||
data-attr="sdk-create"
|
||||
loading={isLoading}
|
||||
icon={<VisioIcon />}
|
||||
>
|
||||
{t('label')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const RoomUrl = ({ roomUrl }: { roomUrl: string }) => {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
|
||||
const copy = () => {
|
||||
navigator.clipboard.writeText(roomUrl!)
|
||||
setIsCopied(true)
|
||||
setTimeout(() => setIsCopied(false), 1000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
color: 'greyscale.600',
|
||||
})}
|
||||
>
|
||||
{roomUrl}
|
||||
</span>
|
||||
<Button
|
||||
variant={isCopied ? 'success' : 'quaternaryText'}
|
||||
data-attr="sdk-create-copy"
|
||||
onPress={copy}
|
||||
square
|
||||
>
|
||||
{isCopied ? <RiCheckLine /> : <RiFileCopyLine />}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react'
|
||||
import { Crisp } from 'crisp-sdk-web'
|
||||
import { ApiUser } from '@/features/auth/api/ApiUser'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
|
||||
export const initializeSupportSession = (user: ApiUser) => {
|
||||
if (!Crisp.isCrispInjected()) return
|
||||
@@ -29,3 +30,8 @@ export const useSupport = ({ id }: useSupportProps) => {
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const useIsSupportEnabled = () => {
|
||||
const { data } = useConfig()
|
||||
return !!data?.support?.id
|
||||
}
|
||||
|
||||
@@ -24,7 +24,12 @@
|
||||
"toggleOff": "",
|
||||
"toggleOn": "",
|
||||
"usernameHint": "",
|
||||
"usernameLabel": ""
|
||||
"usernameLabel": "",
|
||||
"errors": {
|
||||
"usernameEmpty": ""
|
||||
},
|
||||
"cameraDisabled": "",
|
||||
"cameraStarting": ""
|
||||
},
|
||||
"leaveRoomPrompt": "",
|
||||
"shareDialog": {
|
||||
@@ -75,7 +80,8 @@
|
||||
"open": "",
|
||||
"closed": ""
|
||||
},
|
||||
"support": ""
|
||||
"support": "",
|
||||
"moreOptions": ""
|
||||
},
|
||||
"options": {
|
||||
"buttonLabel": "",
|
||||
@@ -84,7 +90,11 @@
|
||||
"settings": "",
|
||||
"username": "",
|
||||
"effects": "",
|
||||
"switchCamera": ""
|
||||
"switchCamera": "",
|
||||
"fullscreen": {
|
||||
"enter": "",
|
||||
"exit": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"effects": {
|
||||
|
||||
@@ -18,13 +18,18 @@
|
||||
"enable": "Enable microphone",
|
||||
"label": "Microphone"
|
||||
},
|
||||
"heading": "Verify your settings",
|
||||
"heading": "Join the meeting",
|
||||
"joinLabel": "Join",
|
||||
"joinMeeting": "Join meeting",
|
||||
"toggleOff": "Click to turn off",
|
||||
"toggleOn": "Click to turn on",
|
||||
"usernameHint": "Shown to other participants",
|
||||
"usernameLabel": "Your name"
|
||||
"usernameLabel": "Your name",
|
||||
"errors": {
|
||||
"usernameEmpty": "Your name cannot be empty"
|
||||
},
|
||||
"cameraDisabled": "Camera is disabled.",
|
||||
"cameraStarting": "Camera is starting."
|
||||
},
|
||||
"leaveRoomPrompt": "This will make you leave the meeting.",
|
||||
"shareDialog": {
|
||||
@@ -74,7 +79,8 @@
|
||||
"open": "Hide AI assistant",
|
||||
"closed": "Show AI assistant"
|
||||
},
|
||||
"support": "Support"
|
||||
"support": "Support",
|
||||
"moreOptions": "More options"
|
||||
},
|
||||
"options": {
|
||||
"buttonLabel": "More Options",
|
||||
@@ -83,7 +89,11 @@
|
||||
"settings": "Settings",
|
||||
"username": "Update Your Name",
|
||||
"effects": "Apply effects",
|
||||
"switchCamera": "Switch camera"
|
||||
"switchCamera": "Switch camera",
|
||||
"fullscreen": {
|
||||
"enter": "Fullscreen",
|
||||
"exit": "Exit fullscreen mode"
|
||||
}
|
||||
}
|
||||
},
|
||||
"effects": {
|
||||
|
||||
@@ -18,13 +18,18 @@
|
||||
"enable": "Activer le micro",
|
||||
"label": "Microphone"
|
||||
},
|
||||
"heading": "Vérifiez vos paramètres",
|
||||
"heading": "Rejoindre la réunion",
|
||||
"joinLabel": "Rejoindre",
|
||||
"joinMeeting": "Rejoindre la réjoindre",
|
||||
"toggleOff": "Cliquez pour désactiver",
|
||||
"toggleOn": "Cliquez pour activer",
|
||||
"usernameHint": "Affiché aux autres participants",
|
||||
"usernameLabel": "Votre nom"
|
||||
"usernameLabel": "Votre nom",
|
||||
"errors": {
|
||||
"usernameEmpty": "Votre nom ne peut pas être vide"
|
||||
},
|
||||
"cameraDisabled": "La caméra est désactivée.",
|
||||
"cameraStarting": "La caméra va démarrer."
|
||||
},
|
||||
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
|
||||
"shareDialog": {
|
||||
@@ -74,7 +79,8 @@
|
||||
"open": "Masquer l'assistant IA",
|
||||
"closed": "Afficher l'assistant IA"
|
||||
},
|
||||
"support": "Support"
|
||||
"support": "Support",
|
||||
"moreOptions": "Plus d'options"
|
||||
},
|
||||
"options": {
|
||||
"buttonLabel": "Plus d'options",
|
||||
@@ -83,7 +89,11 @@
|
||||
"settings": "Paramètres",
|
||||
"username": "Choisir votre nom",
|
||||
"effects": "Appliquer des effets",
|
||||
"switchCamera": "Changer de caméra"
|
||||
"switchCamera": "Changer de caméra",
|
||||
"fullscreen": {
|
||||
"enter": "Plein écran",
|
||||
"exit": "Quitter le mode plein écran"
|
||||
}
|
||||
}
|
||||
},
|
||||
"effects": {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"createButton": {
|
||||
"label": "Créer un lien Visio"
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,15 @@ import { type RecipeVariantProps } from '@/styled-system/css'
|
||||
import { buttonRecipe, type ButtonRecipe } from './buttonRecipe'
|
||||
import { TooltipWrapper, type TooltipWrapperProps } from './TooltipWrapper'
|
||||
import { ReactNode } from 'react'
|
||||
import { Loader } from './Loader'
|
||||
|
||||
export type ButtonProps = RecipeVariantProps<ButtonRecipe> &
|
||||
RACButtonsProps &
|
||||
TooltipWrapperProps & {
|
||||
// Use tooltip as description below the button.
|
||||
description?: boolean
|
||||
} & {
|
||||
icon?: ReactNode
|
||||
}
|
||||
|
||||
export const Button = ({
|
||||
@@ -20,13 +23,16 @@ export const Button = ({
|
||||
...props
|
||||
}: ButtonProps) => {
|
||||
const [variantProps, componentProps] = buttonRecipe.splitVariantProps(props)
|
||||
const { className, ...remainingComponentProps } = componentProps
|
||||
|
||||
return (
|
||||
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
||||
<RACButton
|
||||
className={buttonRecipe(variantProps)}
|
||||
{...(componentProps as RACButtonsProps)}
|
||||
className={[buttonRecipe(variantProps), className].join(' ')}
|
||||
{...(remainingComponentProps as RACButtonsProps)}
|
||||
>
|
||||
{!props.loading && props.icon}
|
||||
{props.loading && <Loader />}
|
||||
{componentProps.children as ReactNode}
|
||||
{props.description && <span>{tooltip}</span>}
|
||||
</RACButton>
|
||||
|
||||
@@ -25,11 +25,31 @@ const FieldWrapper = styled('div', {
|
||||
marginBottom: 'textfield',
|
||||
minWidth: 0,
|
||||
},
|
||||
variants: {
|
||||
noMargin: {
|
||||
true: {
|
||||
marginBottom: 0,
|
||||
},
|
||||
},
|
||||
fullWidth: {
|
||||
true: {
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const StyledLabel = styled(Label, {
|
||||
base: {
|
||||
display: 'block',
|
||||
fontSize: '0.75rem',
|
||||
},
|
||||
variants: {
|
||||
center: {
|
||||
true: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -80,6 +100,8 @@ type FieldProps<T extends object> = (
|
||||
) & {
|
||||
label: string
|
||||
description?: string
|
||||
wrapperProps?: React.ComponentProps<typeof FieldWrapper>
|
||||
labelProps?: React.ComponentProps<typeof StyledLabel>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,7 +126,7 @@ export const Field = <T extends object>({
|
||||
}: FieldProps<T>) => {
|
||||
const LabelAndDescription = (
|
||||
<>
|
||||
<StyledLabel>{label}</StyledLabel>
|
||||
<StyledLabel {...props.labelProps}>{label}</StyledLabel>
|
||||
<FieldDescription slot="description">{description}</FieldDescription>
|
||||
</>
|
||||
)
|
||||
@@ -118,7 +140,7 @@ export const Field = <T extends object>({
|
||||
|
||||
if (type === 'text') {
|
||||
return (
|
||||
<FieldWrapper>
|
||||
<FieldWrapper {...props.wrapperProps}>
|
||||
<RACTextField
|
||||
validate={validate as unknown as TextFieldProps['validate']}
|
||||
{...(props as PartialTextFieldProps)}
|
||||
@@ -133,7 +155,7 @@ export const Field = <T extends object>({
|
||||
|
||||
if (type === 'checkbox') {
|
||||
return (
|
||||
<FieldWrapper>
|
||||
<FieldWrapper {...props.wrapperProps}>
|
||||
<Checkbox
|
||||
validate={validate as unknown as CheckboxProps['validate']}
|
||||
description={description}
|
||||
@@ -147,7 +169,7 @@ export const Field = <T extends object>({
|
||||
|
||||
if (type === 'checkboxGroup') {
|
||||
return (
|
||||
<FieldWrapper>
|
||||
<FieldWrapper {...props.wrapperProps}>
|
||||
<CheckboxGroup
|
||||
validate={validate as unknown as CheckboxGroupProps['validate']}
|
||||
{...(props as PartialCheckboxGroupProps)}
|
||||
@@ -170,7 +192,7 @@ export const Field = <T extends object>({
|
||||
|
||||
if (type === 'radioGroup') {
|
||||
return (
|
||||
<FieldWrapper>
|
||||
<FieldWrapper {...props.wrapperProps}>
|
||||
<RadioGroup
|
||||
validate={validate as unknown as RadioGroupProps['validate']}
|
||||
{...(props as PartialRadioGroupProps)}
|
||||
@@ -189,7 +211,7 @@ export const Field = <T extends object>({
|
||||
|
||||
if (type === 'select') {
|
||||
return (
|
||||
<FieldWrapper>
|
||||
<FieldWrapper {...props.wrapperProps}>
|
||||
<Select
|
||||
validate={validate as unknown as SelectProps<T>['validate']}
|
||||
{...(props as PartialSelectProps<T>)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Form as RACForm, type FormProps } from 'react-aria-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { Button, useCloseDialog } from '@/primitives'
|
||||
import { ButtonProps } from '@/primitives/Button'
|
||||
|
||||
/**
|
||||
* From wrapper that exposes form data on submit and adds submit/cancel buttons
|
||||
@@ -13,6 +14,7 @@ import { Button, useCloseDialog } from '@/primitives'
|
||||
export const Form = ({
|
||||
onSubmit,
|
||||
submitLabel,
|
||||
submitButtonProps,
|
||||
withCancelButton = true,
|
||||
onCancelButtonPress,
|
||||
children,
|
||||
@@ -25,6 +27,7 @@ export const Form = ({
|
||||
event: FormEvent<HTMLFormElement>
|
||||
) => void
|
||||
submitLabel: string
|
||||
submitButtonProps?: ButtonProps
|
||||
withCancelButton?: boolean
|
||||
onCancelButtonPress?: () => void
|
||||
}) => {
|
||||
@@ -47,7 +50,7 @@ export const Form = ({
|
||||
>
|
||||
{children}
|
||||
<HStack gap="gutter">
|
||||
<Button type="submit" variant="primary">
|
||||
<Button type="submit" variant="primary" {...submitButtonProps}>
|
||||
{submitLabel}
|
||||
</Button>
|
||||
{!!onCancel && (
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Text } from './Text'
|
||||
import { Text, TextProps } from './Text'
|
||||
|
||||
export const H = ({
|
||||
children,
|
||||
lvl,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLHeadingElement> & { lvl: 1 | 2 | 3 }) => {
|
||||
}: React.HTMLAttributes<HTMLHeadingElement> & {
|
||||
lvl: 1 | 2 | 3
|
||||
} & TextProps) => {
|
||||
const tag = `h${lvl}` as const
|
||||
return (
|
||||
<Text as={tag} variant={tag} {...props}>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { cva } from '@/styled-system/css'
|
||||
|
||||
const loader = cva({
|
||||
base: {
|
||||
borderRadius: '50%',
|
||||
position: 'relative',
|
||||
animation: 'rotate 1s linear infinite',
|
||||
'&:before, &:after': {
|
||||
content: '""',
|
||||
boxSizing: 'border-box',
|
||||
position: 'absolute',
|
||||
inset: '0',
|
||||
borderRadius: '50%',
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'white',
|
||||
},
|
||||
_before: {
|
||||
animation: 'prixClipFix 2s linear infinite',
|
||||
},
|
||||
_after: {
|
||||
borderColor: 'white',
|
||||
animation:
|
||||
'prixClipFix 2s linear infinite, rotate 0.5s linear infinite reverse',
|
||||
inset: 6,
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
small: {
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
'&:before, &:after': {
|
||||
borderWidth: '2px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'small',
|
||||
},
|
||||
})
|
||||
|
||||
export const Loader = () => {
|
||||
return <div className={loader()}></div>
|
||||
}
|
||||
@@ -8,6 +8,7 @@ export const buttonRecipe = cva({
|
||||
transition: 'background 200ms, outline 200ms, border-color 200ms',
|
||||
cursor: 'pointer',
|
||||
border: '1px solid transparent',
|
||||
gap: '0.5rem',
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
@@ -109,6 +110,21 @@ export const buttonRecipe = cva({
|
||||
color: 'primaryDark.100 !important',
|
||||
},
|
||||
},
|
||||
secondaryDark: {
|
||||
backgroundColor: 'primaryDark.50',
|
||||
color: 'white',
|
||||
'&[data-pressed]': {
|
||||
backgroundColor: 'primaryDark.200',
|
||||
},
|
||||
'&[data-hovered]': {
|
||||
backgroundColor: 'primaryDark.100',
|
||||
color: 'white',
|
||||
},
|
||||
'&[data-selected]': {
|
||||
backgroundColor: 'primaryDark.700 !important',
|
||||
color: 'primaryDark.100 !important',
|
||||
},
|
||||
},
|
||||
primaryTextDark: {
|
||||
backgroundColor: 'transparent',
|
||||
color: 'white',
|
||||
@@ -124,6 +140,18 @@ export const buttonRecipe = cva({
|
||||
color: 'primaryDark.100',
|
||||
},
|
||||
},
|
||||
quaternaryText: {
|
||||
backgroundColor: 'transparent',
|
||||
color: 'greyscale.600',
|
||||
'&[data-hovered]': {
|
||||
backgroundColor: 'greyscale.100',
|
||||
color: 'greyscale.700',
|
||||
},
|
||||
'&[data-pressed]': {
|
||||
backgroundColor: 'greyscale.100',
|
||||
color: 'greyscale.700',
|
||||
},
|
||||
},
|
||||
greyscale: {
|
||||
backgroundColor: 'transparent',
|
||||
color: 'greyscale.400',
|
||||
@@ -214,6 +242,9 @@ export const buttonRecipe = cva({
|
||||
width: 'full',
|
||||
},
|
||||
},
|
||||
loading: {
|
||||
true: {},
|
||||
},
|
||||
// some toggle buttons make more sense without a "pushed button" style when selected because their content changes to mark the state
|
||||
shySelected: {
|
||||
true: {},
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FeedbackRoute, RoomRoute, roomIdPattern } from '@/features/rooms'
|
||||
import { HomeRoute } from '@/features/home'
|
||||
|
||||
export const routes: Record<
|
||||
'home' | 'room' | 'feedback',
|
||||
'home' | 'room' | 'feedback' | 'sdkCreateButton',
|
||||
{
|
||||
name: RouteName
|
||||
path: RegExp | string
|
||||
|
||||
@@ -38,3 +38,34 @@ body:has(.lk-video-conference) #crisp-chatbox > div > a {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loader animations.
|
||||
*/
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes prixClipFix {
|
||||
0% {
|
||||
clip-path: polygon(50% 50%, 0 0, 0 0, 0 0, 0 0, 0 0);
|
||||
}
|
||||
25% {
|
||||
clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 0, 100% 0, 100% 0);
|
||||
}
|
||||
50% {
|
||||
clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 100% 100%, 100% 100%);
|
||||
}
|
||||
75% {
|
||||
clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 100%);
|
||||
}
|
||||
100% {
|
||||
clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export default defineConfig(({ mode }) => {
|
||||
server: {
|
||||
port: parseInt(env.VITE_PORT) || 3000,
|
||||
host: env.VITE_HOST || 'localhost',
|
||||
allowedHosts: ['.nip.io'],
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"build": "npm run build-mjml-to-html && npm run build-html-to-plain-text"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.13.0"
|
||||
"node": "22.13.1"
|
||||
},
|
||||
"repository": "https://github.com/numerique-gouv/meet",
|
||||
"author": "DINUM",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,50 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
```js
|
||||
export default tseslint.config({
|
||||
languageOptions: {
|
||||
// other options...
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
||||
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import react from 'eslint-plugin-react'
|
||||
|
||||
export default tseslint.config({
|
||||
// Set the react version
|
||||
settings: { react: { version: '18.3' } },
|
||||
plugins: {
|
||||
// Add the react plugin
|
||||
react,
|
||||
},
|
||||
rules: {
|
||||
// other rules...
|
||||
// Enable its recommended rules
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Visio SDK Demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+3231
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "consumer",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.17.0",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@vitejs/plugin-react": "4.3.4",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-react-hooks": "5.1.0",
|
||||
"eslint-plugin-react-refresh": "0.4.18",
|
||||
"globals": "15.14.0",
|
||||
"typescript": "5.7.3",
|
||||
"typescript-eslint": "8.18.2",
|
||||
"vite": "6.0.11"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,5 @@
|
||||
#root {
|
||||
margin: 0 auto;
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { VisioCreateButton } from "@gouvfr-lasuite/visio-sdk";
|
||||
import "./App.css";
|
||||
import { useState } from "react";
|
||||
|
||||
function App() {
|
||||
const [roomUrl, setRoomUrl] = useState("");
|
||||
|
||||
return (
|
||||
<form className="form">
|
||||
<div className="header">
|
||||
<h2>Create event</h2>
|
||||
<span>Visio demo app</span>
|
||||
</div>
|
||||
<div className="group">
|
||||
<label>Subject</label>
|
||||
<input type="text" />
|
||||
</div>
|
||||
<div className="group">
|
||||
<label>Place</label>
|
||||
<input type="text" />
|
||||
</div>
|
||||
<div className="group">
|
||||
<label>Visioconference</label>
|
||||
<VisioCreateButton onRoomCreated={setRoomUrl} />
|
||||
</div>
|
||||
<div className="group">
|
||||
<label>Description</label>
|
||||
<textarea />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
alert("Room url: " + roomUrl);
|
||||
}}
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,74 @@
|
||||
* {
|
||||
font-family: 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";
|
||||
box-sizing: border-box;
|
||||
color: rgb(9, 9, 11);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: oklch(0.985 0 0);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: oklch(0.372 0.044 257.287);
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 500px;
|
||||
gap: 1rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
background-color: white;
|
||||
border-radius: 1rem;
|
||||
padding: 3rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
label {
|
||||
color: rgb(9, 9, 11);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
border: 1px rgb(228, 228, 231) solid;
|
||||
border-radius: 6px;
|
||||
height: 36px;
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: rgb(45, 45, 70);
|
||||
color: white;
|
||||
height: 46px;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
VITE_VISIO_SDK_URL=https://visio.numerique.gouv.fr/sdk
|
||||
@@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:@tanstack/eslint-plugin-query/recommended",
|
||||
"plugin:jsx-a11y/recommended",
|
||||
"prettier",
|
||||
],
|
||||
ignorePatterns: ["dist", ".eslintrc.cjs", "styled-system"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["react-refresh"],
|
||||
rules: {
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"semi": false,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# @gouvfr-lasuite/visio-sdk
|
||||
|
||||
## 0.0.1
|
||||
|
||||
### Major Changes
|
||||
|
||||
- Create library with VisioCreateButton
|
||||
@@ -0,0 +1,17 @@
|
||||
# Contributing
|
||||
|
||||
## Release
|
||||
|
||||
1. Run `npm run build`
|
||||
|
||||
2. Update CHANGELOG.md according to the Major / Minor / Patch semver convention.
|
||||
|
||||
3. Based on semver upgrade the package version in `package.json`.
|
||||
|
||||
4. Commit the changes and create a PR named "🔖(release) version packages".
|
||||
|
||||
5. Run `npx @changesets/cli publish`. It will publish the new version of the package to NPM and create a git tag.
|
||||
|
||||
6. Run `git push origin tag @gouvfr-lasuite/visio-sdk@<VERSION>`
|
||||
|
||||
7. Tell everyone 🎉 !
|
||||
@@ -0,0 +1,30 @@
|
||||
<div align="center">
|
||||
|
||||
# 🎥 Visio SDK
|
||||
|
||||
**Easily open a widget to browse user files on any of your project**
|
||||
|
||||
<img src="docs/demo.gif" alt="" />
|
||||
|
||||
**As simple as**
|
||||
|
||||
</div>
|
||||
|
||||
```ts
|
||||
import { VisioCreateButton } from "@gouvfr-lasuite/visio-sdk";
|
||||
|
||||
function App() {
|
||||
const [roomUrl, setRoomUrl] = useState();
|
||||
|
||||
return <VisioCreateButton onRoomCreated={setRoomUrl} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
To install, you can use npm or yarn:
|
||||
|
||||
```
|
||||
$ npm install --save @gouvfr-lasuite/visio-sdk
|
||||
$ yarn add @gouvfr-lasuite/visio-sdk
|
||||
```
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 708 KiB |
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "@gouvfr-lasuite/visio-sdk",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"main": "./dist/visio-sdk.umd.cjs",
|
||||
"module": "./dist/visio-sdk.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/visio-sdk.js",
|
||||
"require": "./dist/visio-sdk.umd.cjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite build -w --mode development",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint . --ext ts,tsx",
|
||||
"format": "prettier --write ./src",
|
||||
"check": "prettier --check ./src"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/eslint-plugin-query": "5.64.2",
|
||||
"@types/node": "20.14.2",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.15.0",
|
||||
"@typescript-eslint/parser": "7.15.0",
|
||||
"@vitejs/plugin-react-swc": "3.5.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "10.0.1",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-react-refresh": "0.4.7",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"sass": "1.83.4",
|
||||
"prettier": "3.4.2",
|
||||
"typescript": "5.4.5",
|
||||
"vite": "5.3.4",
|
||||
"vite-plugin-dts": "3.9.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,3 @@
|
||||
export const DEFAULT_CONFIG = {
|
||||
url: import.meta.env.VITE_VISIO_SDK_URL,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DEFAULT_CONFIG } from '@/Config'
|
||||
|
||||
export type ConfigType = typeof DEFAULT_CONFIG
|
||||
|
||||
export enum ClientMessageType {
|
||||
ROOM_CREATED = 'ROOM_CREATED',
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { DEFAULT_CONFIG } from '@/Config'
|
||||
import { ClientMessageType } from '@/Types'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const VisioCreateButton = ({
|
||||
onRoomCreated,
|
||||
}: {
|
||||
onRoomCreated: (roomUrl: string) => void
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
// Make sure it is the correct origin.
|
||||
if (event.origin !== new URL(DEFAULT_CONFIG.url).origin) {
|
||||
return
|
||||
}
|
||||
if (event.data.type === ClientMessageType.ROOM_CREATED) {
|
||||
const data = event.data.data
|
||||
const roomUrl = data.url
|
||||
onRoomCreated(roomUrl)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', onMessage)
|
||||
return () => {
|
||||
window.removeEventListener('message', onMessage)
|
||||
}
|
||||
}, [onRoomCreated])
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/iframe-has-title
|
||||
<iframe
|
||||
allow="clipboard-read; clipboard-write"
|
||||
src={DEFAULT_CONFIG.url + '/create-button'}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '52px',
|
||||
border: 'none',
|
||||
}}
|
||||
></iframe>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
export * from '@/Types'
|
||||
export * from '@/create/VisioCreateButton'
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_VISIO_SDK_URL: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable", "WebWorker"],
|
||||
"module": "esnext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Local */
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import path, { resolve } from "path";
|
||||
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import { AliasOptions, defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
dts({ tsconfigPath: "./tsconfig.app.json", rollupTypes: true }),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
} as AliasOptions,
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, "src/index.tsx"),
|
||||
name: "visio-sdk",
|
||||
fileName: "visio-sdk",
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ["react", "react-dom"],
|
||||
output: {
|
||||
globals: {
|
||||
react: "React",
|
||||
"react-dom": "ReactDOM",
|
||||
},
|
||||
},
|
||||
},
|
||||
copyPublicDir: false,
|
||||
},
|
||||
});
|
||||
Generated
+7603
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "0.0.0",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"workspaces": [
|
||||
"./library",
|
||||
"./consumer"
|
||||
]
|
||||
}
|
||||
@@ -10,14 +10,14 @@ dependencies = [
|
||||
"celery==5.4.0",
|
||||
"redis==5.2.1",
|
||||
"minio==7.2.15",
|
||||
"openai==1.59.8",
|
||||
"openai==1.60.2",
|
||||
"requests==2.32.3",
|
||||
"sentry-sdk[fastapi, celery]==2.20.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"ruff==0.9.2",
|
||||
"ruff==0.9.3",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
Reference in New Issue
Block a user