mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-13 07:46:10 +00:00
merged main
This commit is contained in:
commit
6281ab0b49
10
.github/workflows/test.build.lib.yml
vendored
10
.github/workflows/test.build.lib.yml
vendored
@ -1,6 +1,12 @@
|
||||
name: build:lib
|
||||
|
||||
on: push
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
@ -37,7 +43,7 @@ jobs:
|
||||
build-examples:
|
||||
if: needs.files-changed.outputs.build == 'true'
|
||||
name: Test Example Apps
|
||||
needs: [build, files-changed]
|
||||
needs: [files-changed, build]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
5
app/.env
5
app/.env
@ -1 +1,4 @@
|
||||
VITE_OPEN_COLLECTIVE_API_KEY=your_key
|
||||
VITE_OPEN_COLLECTIVE_API_KEY=your_key
|
||||
VITE_API_URL=https://api.utopia-lab.org
|
||||
VITE_VALIDATE_INVITE_FLOW_ID=01d61db0-25aa-4bfa-bc24-c6a8f208a455
|
||||
VITE_REDEEM_INVITE_FLOW_ID=cc80ec73-ecf5-4789-bee5-1127fb1a6ed4
|
||||
|
||||
@ -82,7 +82,7 @@ module.exports = {
|
||||
'import/no-relative-parent-imports': [
|
||||
'error',
|
||||
{
|
||||
ignore: ['#[src,types,root,components,utils,assets]/*'],
|
||||
ignore: ['#[src,types,root,components,utils,assets]/*', '@/config/*'],
|
||||
},
|
||||
],
|
||||
'import/no-self-import': 'error',
|
||||
|
||||
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
|
||||
50
app/package-lock.json
generated
50
app/package-lock.json
generated
@ -18,7 +18,8 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-rnd": "^10.4.1",
|
||||
"react-router-dom": "^6.23.0",
|
||||
"utopia-ui": "^3.0.111"
|
||||
"utopia-ui": "^3.0.111",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "^4.4.1",
|
||||
@ -6775,6 +6776,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/globrex": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
|
||||
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@ -10662,6 +10669,26 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfck": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz",
|
||||
"integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"tsconfck": "bin/tsconfck.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || >=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
||||
@ -10825,7 +10852,7 @@
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@ -11302,6 +11329,25 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-tsconfig-paths": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz",
|
||||
"integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"globrex": "^0.1.2",
|
||||
"tsconfck": "^3.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/fdir": {
|
||||
"version": "6.4.4",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-rnd": "^10.4.1",
|
||||
"react-router-dom": "^6.23.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"utopia-ui": "^3.0.111"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
Content,
|
||||
AuthProvider,
|
||||
Modal,
|
||||
InvitePage,
|
||||
LoginPage,
|
||||
SignupPage,
|
||||
Quests,
|
||||
@ -33,8 +34,8 @@ import {
|
||||
MarketView,
|
||||
SVG,
|
||||
LoadingMapOverlay,
|
||||
ProfileView,
|
||||
ProfileForm,
|
||||
ProfileView,
|
||||
UserSettings,
|
||||
} from 'utopia-ui'
|
||||
|
||||
@ -48,11 +49,16 @@ import { itemsApi } from './api/itemsApi'
|
||||
import { layersApi } from './api/layersApi'
|
||||
import { mapApi } from './api/mapApi'
|
||||
import { permissionsApi } from './api/permissionsApi'
|
||||
import { userApi } from './api/userApi'
|
||||
import { UserApi } from './api/userApi'
|
||||
import { ModalContent } from './ModalContent'
|
||||
import { Landingpage } from './pages/Landingpage'
|
||||
import MapContainer from './pages/MapContainer'
|
||||
import { getBottomRoutes, routes } from './routes/sidebar'
|
||||
import { config } from '@/config'
|
||||
import { InviteApi } from './api/inviteApi'
|
||||
|
||||
const userApi = new UserApi()
|
||||
const inviteApi = new InviteApi(userApi)
|
||||
|
||||
function App() {
|
||||
const [permissionsApiInstance, setPermissionsApiInstance] = useState<permissionsApi>()
|
||||
@ -140,12 +146,12 @@ function App() {
|
||||
if (map && layers)
|
||||
return (
|
||||
<div className='App tw:overflow-x-hidden'>
|
||||
<AuthProvider userApi={new userApi()}>
|
||||
<AuthProvider userApi={userApi} inviteApi={inviteApi}>
|
||||
<AppShell
|
||||
assetsApi={new assetsApi('https://api.utopia-lab.org/assets/')}
|
||||
appName={map.name}
|
||||
embedded={embedded}
|
||||
openCollectiveApiKey={import.meta.env.VITE_OPEN_COLLECTIVE_API_KEY}
|
||||
openCollectiveApiKey={config.openCollectiveApiKey}
|
||||
>
|
||||
<Permissions
|
||||
api={permissionsApiInstance}
|
||||
@ -157,7 +163,8 @@ function App() {
|
||||
<Quests />
|
||||
<Routes>
|
||||
<Route path='/*' element={<MapContainer map={map} layers={layers} />}>
|
||||
<Route path='login' element={<LoginPage />} />
|
||||
<Route path='invite/:id' element={<InvitePage inviteApi={inviteApi} />} />
|
||||
<Route path='login' element={<LoginPage inviteApi={inviteApi} />} />
|
||||
<Route path='signup' element={<SignupPage />} />
|
||||
<Route
|
||||
path='reset-password'
|
||||
|
||||
@ -17,6 +17,14 @@ export function Welcome1({ clickAction1, map }: ChapterProps) {
|
||||
{map.custom_text ? (
|
||||
<>
|
||||
<TextView rawText={map.custom_text}></TextView>
|
||||
<div className='tw:grid'>
|
||||
<label
|
||||
className='tw:btn tw:btn-primary tw:place-self-end tw:mt-4'
|
||||
onClick={() => clickAction1()}
|
||||
>
|
||||
Close
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className='tw:relative'>
|
||||
|
||||
@ -51,6 +51,11 @@ interface CustomUserFields {
|
||||
position: Point
|
||||
}
|
||||
|
||||
interface ItemSecret {
|
||||
secret: string
|
||||
item: string
|
||||
}
|
||||
|
||||
export interface MyCollections {
|
||||
places: Place[]
|
||||
events: Event[]
|
||||
@ -58,6 +63,7 @@ export interface MyCollections {
|
||||
tags: Tag[]
|
||||
projects: Project[]
|
||||
directus_users: CustomUserFields[]
|
||||
item_secrets: ItemSecret[]
|
||||
items: Item[]
|
||||
team: any[]
|
||||
features: any[]
|
||||
|
||||
79
app/src/api/inviteApi.ts
Normal file
79
app/src/api/inviteApi.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/* @eslint-disable-next-line import/no-relative-parent-imports */
|
||||
import { config } from '@/config'
|
||||
|
||||
import type { UserApi } from 'utopia-ui'
|
||||
|
||||
type InvitingProfileResponse = [
|
||||
{
|
||||
item: string
|
||||
},
|
||||
]
|
||||
|
||||
export class InviteApi {
|
||||
userApi: UserApi
|
||||
|
||||
constructor(userApi: UserApi) {
|
||||
this.userApi = userApi
|
||||
}
|
||||
|
||||
async validateInvite(inviteId: string): Promise<string | null> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${config.apiUrl}/flows/trigger/${config.validateInviteFlowId}?secret=${inviteId}`,
|
||||
{
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) return null
|
||||
|
||||
const data = (await response.json()) as InvitingProfileResponse
|
||||
|
||||
return data[0].item
|
||||
} catch (error: unknown) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error fetching inviting profile:', error)
|
||||
if (error instanceof Error && error.message) {
|
||||
throw new Error(error.message)
|
||||
} else {
|
||||
throw new Error('An unknown error occurred while fetching the inviting profile.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async redeemInvite(inviteId: string, itemId: string): Promise<string | null> {
|
||||
try {
|
||||
const token = await this.userApi.getToken()
|
||||
|
||||
if (!token) {
|
||||
throw new Error('User is not authenticated. Cannot redeem invite.')
|
||||
}
|
||||
|
||||
const response = await fetch(`${config.apiUrl}/flows/trigger/${config.redeemInviteFlowId}`, {
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ secret: inviteId, item: itemId }),
|
||||
})
|
||||
|
||||
if (!response.ok) return null
|
||||
|
||||
return (await response.json()) as string
|
||||
} catch (error: unknown) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error fetching inviting profile:', error)
|
||||
if (error instanceof Error && error.message) {
|
||||
throw new Error(error.message)
|
||||
} else {
|
||||
throw new Error('An unknown error occurred while fetching the inviting profile.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -45,6 +45,7 @@ export class itemsApi<T> implements ItemsApi<T> {
|
||||
readItems(this.collectionName as never, {
|
||||
fields: [
|
||||
'*',
|
||||
'secrets.*',
|
||||
'to.*',
|
||||
'relations.*',
|
||||
'user_created.*',
|
||||
|
||||
@ -8,7 +8,7 @@ import { createUser, passwordRequest, passwordReset, readMe, updateMe } from '@d
|
||||
|
||||
import { directusClient } from './directus'
|
||||
|
||||
import type { UserApi, UserItem } from 'utopia-ui'
|
||||
import type { UserItem } from 'utopia-ui'
|
||||
|
||||
interface DirectusError {
|
||||
errors: {
|
||||
@ -17,7 +17,7 @@ interface DirectusError {
|
||||
}[]
|
||||
}
|
||||
|
||||
export class userApi implements UserApi {
|
||||
export class UserApi {
|
||||
async register(email: string, password: string, userName: string): Promise<any> {
|
||||
try {
|
||||
return await directusClient.request(createUser({ email, password, first_name: userName }))
|
||||
|
||||
12
app/src/config/index.ts
Normal file
12
app/src/config/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export const config = {
|
||||
apiUrl: String(import.meta.env.VITE_API_URL ?? 'https://api.utopia-lab.org'),
|
||||
validateInviteFlowId: String(
|
||||
import.meta.env.VITE_VALIDATE_INVITE_FLOW_ID ?? '01d61db0-25aa-4bfa-bc24-c6a8f208a455',
|
||||
),
|
||||
redeemInviteFlowId: String(
|
||||
import.meta.env.VITE_REDEEM_INVITE_FLOW_ID ?? 'cc80ec73-ecf5-4789-bee5-1127fb1a6ed4',
|
||||
),
|
||||
openCollectiveApiKey: String(import.meta.env.VITE_OPEN_COLLECTIVE_API_KEY ?? ''),
|
||||
}
|
||||
|
||||
export type Config = typeof config
|
||||
@ -84,6 +84,8 @@ function MapContainer({ layers, map }: { layers: LayerProps[]; map: any }) {
|
||||
defaultTheme={map.default_theme}
|
||||
showZoomControl={map.show_zoom_control}
|
||||
expandLayerControl={map.expand_layer_control}
|
||||
tileServerUrl={map.tile_server_url}
|
||||
tileServerAttribution={map.tile_server_attribution}
|
||||
>
|
||||
{layers &&
|
||||
apis &&
|
||||
|
||||
@ -10,16 +10,40 @@
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"utopia-ui": ["../lib/src"],
|
||||
"#components/*": ["../lib/src/Components/*"],
|
||||
"#utils/*": ["../lib/src/Utils/*"],
|
||||
"#types/*": ["../lib/src/types/*"],
|
||||
"#assets/*": ["../lib/src/assets/*"],
|
||||
"#src/*": ["../lib/src/*"],
|
||||
"#root/*": ["../lib/*"]
|
||||
"@/*": [
|
||||
"src/*"
|
||||
],
|
||||
"utopia-ui": [
|
||||
"../lib/src"
|
||||
],
|
||||
"#components/*": [
|
||||
"../lib/src/Components/*"
|
||||
],
|
||||
"#utils/*": [
|
||||
"../lib/src/Utils/*"
|
||||
],
|
||||
"#types/*": [
|
||||
"../lib/src/types/*"
|
||||
],
|
||||
"#assets/*": [
|
||||
"../lib/src/assets/*"
|
||||
],
|
||||
"#src/*": [
|
||||
"../lib/src/*"
|
||||
],
|
||||
"#root/*": [
|
||||
"../lib/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import tsConfigPaths from 'vite-tsconfig-paths'
|
||||
|
||||
// __dirname-Ersatz für ESModules
|
||||
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
||||
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
@ -18,21 +19,9 @@ export default defineConfig({
|
||||
* },
|
||||
*/
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
],
|
||||
plugins: [react(), tailwindcss(), tsConfigPaths()],
|
||||
resolve: {
|
||||
dedupe: ['react', 'react-dom', 'react-router-dom'],
|
||||
alias: {
|
||||
'utopia-ui': path.resolve(__dirname, '../lib/src'),
|
||||
'#components': path.resolve(__dirname, '../lib/src/Components'),
|
||||
'#utils': path.resolve(__dirname, '../lib/src/Utils'),
|
||||
'#types': path.resolve(__dirname, '../lib/src/types'),
|
||||
'#assets': path.resolve(__dirname, '../lib/src/assets'),
|
||||
'#src': path.resolve(__dirname, '../lib/src'),
|
||||
'#root': path.resolve(__dirname, '../lib'),
|
||||
}
|
||||
},
|
||||
build: {
|
||||
sourcemap: true,
|
||||
@ -44,21 +33,20 @@ export default defineConfig({
|
||||
}
|
||||
if (id.includes('node_modules')) {
|
||||
if (id.includes('react')) {
|
||||
return 'react';
|
||||
return 'react'
|
||||
}
|
||||
if (id.includes('tiptap')) {
|
||||
return 'tiptap';
|
||||
return 'tiptap'
|
||||
}
|
||||
if (id.includes('leaflet')) {
|
||||
return 'leaflet';
|
||||
return 'leaflet'
|
||||
}
|
||||
if (id.includes('lib/node_modules')) {
|
||||
return 'utopia-ui-vendor'
|
||||
}
|
||||
else return 'vendor';
|
||||
} else return 'vendor'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
20
lib/package-lock.json
generated
20
lib/package-lock.json
generated
@ -53,6 +53,7 @@
|
||||
"react-leaflet-cluster": "^2.1.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-photo-album": "^3.0.2",
|
||||
"react-qr-code": "^2.0.16",
|
||||
"react-router-dom": "^6.23.0",
|
||||
"react-toastify": "^9.1.3",
|
||||
"rehype-raw": "^7.0.0",
|
||||
@ -11658,6 +11659,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qr.js": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz",
|
||||
"integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz",
|
||||
@ -11875,6 +11882,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-qr-code": {
|
||||
"version": "2.0.16",
|
||||
"resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.16.tgz",
|
||||
"integrity": "sha512-8f54aTOo7DxYr1LB47pMeclV5SL/zSbJxkXHIS2a+QnAIa4XDVIdmzYRC+CBCJeDLSCeFHn8gHtltwvwZGJD/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1",
|
||||
"qr.js": "0.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||
|
||||
@ -141,6 +141,7 @@
|
||||
"react-leaflet-cluster": "^2.1.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-photo-album": "^3.0.2",
|
||||
"react-qr-code": "^2.0.16",
|
||||
"react-router-dom": "^6.23.0",
|
||||
"react-toastify": "^9.1.3",
|
||||
"rehype-raw": "^7.0.0",
|
||||
|
||||
@ -48,6 +48,7 @@ export default [
|
||||
/node_modules\/markdown-it-task-lists/,
|
||||
/node_modules\/classnames/,
|
||||
/node_modules\/html-truncate/,
|
||||
/node_modules\/react-qr-code/,
|
||||
],
|
||||
requireReturnsDefault: 'auto',
|
||||
}),
|
||||
|
||||
@ -1,28 +1,65 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import { useMyProfile } from '#components/Map/hooks/useMyProfile'
|
||||
import { MapOverlayPage } from '#components/Templates/MapOverlayPage'
|
||||
|
||||
import { useAuth } from './useAuth'
|
||||
|
||||
import type { InviteApi } from '#types/InviteApi'
|
||||
|
||||
interface Props {
|
||||
inviteApi: InviteApi
|
||||
}
|
||||
|
||||
/**
|
||||
* @category Auth
|
||||
*/
|
||||
export function LoginPage() {
|
||||
export function LoginPage({ inviteApi }: Props) {
|
||||
const [email, setEmail] = useState<string>('')
|
||||
const [password, setPassword] = useState<string>('')
|
||||
|
||||
const { login, loading } = useAuth()
|
||||
|
||||
const { myProfile, isMyProfileLoaded } = useMyProfile()
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const onLogin = async () => {
|
||||
const redeemInvite = useCallback(
|
||||
async (inviteCode: string): Promise<string | null> => {
|
||||
if (!isMyProfileLoaded) return null
|
||||
|
||||
if (!myProfile) {
|
||||
toast.error('Could not find your profile to redeem the invite.')
|
||||
return null
|
||||
}
|
||||
|
||||
const invitingProfileId = await inviteApi.redeemInvite(inviteCode, myProfile.id)
|
||||
localStorage.removeItem('inviteCode') // Clear invite code after redeeming
|
||||
return invitingProfileId
|
||||
},
|
||||
[inviteApi, isMyProfileLoaded, myProfile],
|
||||
)
|
||||
|
||||
const handleSuccess = useCallback(async () => {
|
||||
const inviteCode = localStorage.getItem('inviteCode')
|
||||
let invitingProfileId: string | null = null
|
||||
if (inviteCode) {
|
||||
invitingProfileId = await redeemInvite(inviteCode)
|
||||
}
|
||||
if (invitingProfileId) {
|
||||
navigate(`/item/${invitingProfileId}`)
|
||||
} else {
|
||||
navigate('/')
|
||||
}
|
||||
}, [navigate, redeemInvite])
|
||||
|
||||
const onLogin = useCallback(async () => {
|
||||
await toast.promise(login({ email, password }), {
|
||||
success: {
|
||||
render({ data }) {
|
||||
navigate('/')
|
||||
void handleSuccess()
|
||||
return `Hi ${data?.first_name ? data.first_name : 'Traveler'}`
|
||||
},
|
||||
// other options
|
||||
@ -36,7 +73,7 @@ export function LoginPage() {
|
||||
},
|
||||
pending: 'logging in ...',
|
||||
})
|
||||
}
|
||||
}, [email, handleSuccess, login, password])
|
||||
|
||||
useEffect(() => {
|
||||
const keyDownHandler = (event: KeyboardEvent) => {
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { createContext, useState, useContext, useEffect } from 'react'
|
||||
import { createContext, useState, useContext, useEffect, useCallback } from 'react'
|
||||
|
||||
import type { InviteApi } from '#types/InviteApi'
|
||||
import type { UserApi } from '#types/UserApi'
|
||||
import type { UserItem } from '#types/UserItem'
|
||||
|
||||
interface AuthProviderProps {
|
||||
userApi: UserApi
|
||||
inviteApi: InviteApi
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
@ -16,6 +18,7 @@ interface AuthCredentials {
|
||||
|
||||
interface AuthContextProps {
|
||||
isAuthenticated: boolean
|
||||
isInitialized: boolean
|
||||
user: UserItem | null
|
||||
login: (credentials: AuthCredentials) => Promise<UserItem | undefined>
|
||||
register: (credentials: AuthCredentials, userName: string) => Promise<UserItem | undefined>
|
||||
@ -29,6 +32,7 @@ interface AuthContextProps {
|
||||
|
||||
const AuthContext = createContext<AuthContextProps>({
|
||||
isAuthenticated: false,
|
||||
isInitialized: false,
|
||||
user: null,
|
||||
login: () => Promise.reject(Error('Unimplemented')),
|
||||
register: () => Promise.reject(Error('Unimplemented')),
|
||||
@ -47,17 +51,10 @@ export const AuthProvider = ({ userApi, children }: AuthProviderProps) => {
|
||||
const [user, setUser] = useState<UserItem | null>(null)
|
||||
const [token, setToken] = useState<string>()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [isInitialized, setIsInitialized] = useState<boolean>(false)
|
||||
const isAuthenticated = !!user
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
loadUser()
|
||||
setLoading(false)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
async function loadUser(): Promise<UserItem | undefined> {
|
||||
const loadUser: () => Promise<UserItem | undefined> = useCallback(async () => {
|
||||
try {
|
||||
const token = await userApi.getToken()
|
||||
setToken(token)
|
||||
@ -66,20 +63,30 @@ export const AuthProvider = ({ userApi, children }: AuthProviderProps) => {
|
||||
setUser(me)
|
||||
setLoading(false)
|
||||
return me
|
||||
} else return undefined
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
// eslint-disable-next-line no-catch-all/no-catch-all
|
||||
} catch (error) {
|
||||
setLoading(false)
|
||||
return undefined
|
||||
} finally {
|
||||
setIsInitialized(true)
|
||||
}
|
||||
}
|
||||
}, [userApi])
|
||||
|
||||
useEffect(() => {
|
||||
void loadUser()
|
||||
}, [loadUser])
|
||||
|
||||
const login = async (credentials: AuthCredentials): Promise<UserItem | undefined> => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const user = await userApi.login(credentials.email, credentials.password)
|
||||
setToken(user?.access_token)
|
||||
return await loadUser()
|
||||
const fullUser = await loadUser()
|
||||
|
||||
return fullUser
|
||||
} catch (error) {
|
||||
setLoading(false)
|
||||
throw error
|
||||
@ -150,6 +157,7 @@ export const AuthProvider = ({ userApi, children }: AuthProviderProps) => {
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
isAuthenticated,
|
||||
isInitialized,
|
||||
user,
|
||||
login,
|
||||
register,
|
||||
|
||||
9
lib/src/Components/Input/InputLabel.tsx
Normal file
9
lib/src/Components/Input/InputLabel.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
export const InputLabel = ({ label }: { label: string }) => {
|
||||
return (
|
||||
<label className='tw:label tw:pb-1'>
|
||||
<span className='tw:block tw:text-sm tw:font-medium tw:text-base-content/50 tw:mb-1'>
|
||||
{label}:
|
||||
</span>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
@ -1,8 +1,9 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { InputLabel } from './InputLabel'
|
||||
|
||||
interface TextAreaProps {
|
||||
labelTitle?: string
|
||||
labelStyle?: string
|
||||
containerStyle?: string
|
||||
dataField?: string
|
||||
inputStyle?: string
|
||||
@ -18,7 +19,6 @@ interface TextAreaProps {
|
||||
export function TextAreaInput({
|
||||
labelTitle,
|
||||
dataField,
|
||||
labelStyle,
|
||||
containerStyle,
|
||||
inputStyle,
|
||||
defaultValue,
|
||||
@ -43,13 +43,7 @@ export function TextAreaInput({
|
||||
|
||||
return (
|
||||
<div className={`tw:form-control tw:w-full ${containerStyle ?? ''}`}>
|
||||
{labelTitle ? (
|
||||
<label className='tw:label'>
|
||||
<span className={`tw:label-text tw:text-base-content ${labelStyle ?? ''}`}>
|
||||
{labelTitle}
|
||||
</span>
|
||||
</label>
|
||||
) : null}
|
||||
{labelTitle ? <InputLabel label={labelTitle} /> : null}
|
||||
<textarea
|
||||
required={required}
|
||||
ref={ref}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { InputLabel } from './InputLabel'
|
||||
|
||||
interface InputTextProps {
|
||||
labelTitle?: string
|
||||
labelStyle?: string
|
||||
type?: string
|
||||
dataField?: string
|
||||
containerStyle?: string
|
||||
@ -20,7 +21,6 @@ interface InputTextProps {
|
||||
*/
|
||||
export function TextInput({
|
||||
labelTitle,
|
||||
labelStyle,
|
||||
type,
|
||||
dataField,
|
||||
containerStyle,
|
||||
@ -48,13 +48,7 @@ export function TextInput({
|
||||
|
||||
return (
|
||||
<div className={`tw:form-control ${containerStyle ?? ''}`}>
|
||||
{labelTitle ? (
|
||||
<label className='tw:label'>
|
||||
<span className={`tw:label-text tw:text-base-content ${labelStyle ?? ''}`}>
|
||||
{labelTitle}
|
||||
</span>
|
||||
</label>
|
||||
) : null}
|
||||
{labelTitle ? <InputLabel label={labelTitle} /> : null}
|
||||
<input
|
||||
required={required}
|
||||
pattern={pattern}
|
||||
|
||||
@ -5,12 +5,13 @@ exports[`<TextAreaInput /> > labelTitle > sets label 1`] = `
|
||||
class="tw:form-control tw:w-full "
|
||||
>
|
||||
<label
|
||||
class="tw:label"
|
||||
class="tw:label tw:pb-1"
|
||||
>
|
||||
<span
|
||||
class="tw:label-text tw:text-base-content "
|
||||
class="tw:block tw:text-sm tw:font-medium tw:text-base-content/50 tw:mb-1"
|
||||
>
|
||||
My Title
|
||||
:
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
|
||||
@ -5,12 +5,13 @@ exports[`<TextInput /> > labelTitle > sets label 1`] = `
|
||||
class="tw:form-control "
|
||||
>
|
||||
<label
|
||||
class="tw:label"
|
||||
class="tw:label tw:pb-1"
|
||||
>
|
||||
<span
|
||||
class="tw:label-text tw:text-base-content "
|
||||
class="tw:block tw:text-sm tw:font-medium tw:text-base-content/50 tw:mb-1"
|
||||
>
|
||||
My Title
|
||||
:
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
export { TextAreaInput } from './TextAreaInput'
|
||||
export { TextInput } from './TextInput'
|
||||
export { InputLabel } from './InputLabel'
|
||||
export { RichTextEditor } from './RichTextEditor/RichTextEditor'
|
||||
|
||||
@ -6,7 +6,6 @@ import type { Item } from '#types/Item'
|
||||
export interface StartEndInputProps {
|
||||
item?: Item
|
||||
showLabels?: boolean
|
||||
labelStyle?: string
|
||||
updateStartValue?: (value: string) => void
|
||||
updateEndValue?: (value: string) => void
|
||||
containerStyle?: string
|
||||
@ -18,7 +17,6 @@ export interface StartEndInputProps {
|
||||
export const PopupStartEndInput = ({
|
||||
item,
|
||||
showLabels = true,
|
||||
labelStyle,
|
||||
updateStartValue,
|
||||
updateEndValue,
|
||||
containerStyle,
|
||||
@ -31,7 +29,6 @@ export const PopupStartEndInput = ({
|
||||
dataField='start'
|
||||
inputStyle='tw:text-sm tw:px-2'
|
||||
labelTitle={showLabels ? 'Start' : ''}
|
||||
labelStyle={labelStyle}
|
||||
defaultValue={item && item.start ? item.start.substring(0, 10) : ''}
|
||||
autocomplete='one-time-code'
|
||||
updateFormValue={updateStartValue}
|
||||
@ -42,7 +39,6 @@ export const PopupStartEndInput = ({
|
||||
dataField='end'
|
||||
inputStyle='tw:text-sm tw:px-2'
|
||||
labelTitle={showLabels ? 'End' : ''}
|
||||
labelStyle={labelStyle}
|
||||
defaultValue={item && item.end ? item.end.substring(0, 10) : ''}
|
||||
autocomplete='one-time-code'
|
||||
updateFormValue={updateEndValue}
|
||||
|
||||
@ -56,6 +56,8 @@ function UtopiaMap({
|
||||
defaultTheme,
|
||||
donationWidget,
|
||||
expandLayerControl,
|
||||
tileServerUrl,
|
||||
tileServerAttribution,
|
||||
}: {
|
||||
/** height of the map (default '500px') */
|
||||
height?: string
|
||||
@ -85,6 +87,10 @@ function UtopiaMap({
|
||||
donationWidget?: boolean
|
||||
/** open layer control on map initialisation */
|
||||
expandLayerControl?: boolean
|
||||
/** configure a custom tile server */
|
||||
tileServerUrl?: string
|
||||
/** configure a custom tile server attribution */
|
||||
tileServerAttribution?: string
|
||||
}) {
|
||||
return (
|
||||
<ContextWrapper>
|
||||
@ -104,6 +110,8 @@ function UtopiaMap({
|
||||
showThemeControl={showThemeControl}
|
||||
defaultTheme={defaultTheme}
|
||||
expandLayerControl={expandLayerControl}
|
||||
tileServerUrl={tileServerUrl}
|
||||
tileServerAttribution={tileServerAttribution}
|
||||
>
|
||||
{children}
|
||||
</UtopiaMapInner>
|
||||
|
||||
@ -55,6 +55,8 @@ export function UtopiaMapInner({
|
||||
defaultTheme = '',
|
||||
donationWidget,
|
||||
expandLayerControl,
|
||||
tileServerUrl,
|
||||
tileServerAttribution,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
geo?: GeoJsonObject
|
||||
@ -65,6 +67,8 @@ export function UtopiaMapInner({
|
||||
showThemeControl?: boolean
|
||||
defaultTheme?: string
|
||||
expandLayerControl?: boolean
|
||||
tileServerUrl?: string
|
||||
tileServerAttribution?: string
|
||||
}) {
|
||||
const selectNewItemPosition = useSelectPosition()
|
||||
const setSelectNewItemPosition = useSetSelectPosition()
|
||||
@ -277,8 +281,11 @@ export function UtopiaMapInner({
|
||||
</Control>
|
||||
<TileLayer
|
||||
maxZoom={19}
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
url='https://tile.osmand.net/hd/{z}/{x}/{y}.png'
|
||||
attribution={
|
||||
tileServerAttribution ??
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}
|
||||
url={tileServerUrl ?? 'https://tile.osmand.net/hd/{z}/{x}/{y}.png'}
|
||||
/>
|
||||
<MarkerClusterGroup
|
||||
ref={(r) => setClusterRef(r as any)}
|
||||
|
||||
20
lib/src/Components/Map/hooks/useMyProfile.ts
Normal file
20
lib/src/Components/Map/hooks/useMyProfile.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { useAuth } from '#components/Auth/useAuth'
|
||||
|
||||
import { useItems, useAllItemsLoaded } from './useItems'
|
||||
|
||||
export const useMyProfile = () => {
|
||||
const items = useItems()
|
||||
const allItemsLoaded = useAllItemsLoaded()
|
||||
|
||||
const user = useAuth().user
|
||||
|
||||
// allItemsLoaded is not reliable, so we check if items.length > 0
|
||||
const isMyProfileLoaded = allItemsLoaded && items.length > 0 && !!user
|
||||
|
||||
// Find the user's profile item
|
||||
const myProfile = items.find(
|
||||
(item) => item.layer?.userProfileLayer && item.user_created?.id === user?.id,
|
||||
)
|
||||
|
||||
return { myProfile, isMyProfileLoaded }
|
||||
}
|
||||
75
lib/src/Components/Onboarding/InvitePage.tsx
Normal file
75
lib/src/Components/Onboarding/InvitePage.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import { useAuth } from '#components/Auth/useAuth'
|
||||
import { useMyProfile } from '#components/Map/hooks/useMyProfile'
|
||||
import { MapOverlayPage } from '#components/Templates/MapOverlayPage'
|
||||
|
||||
import type { InviteApi } from '#types/InviteApi'
|
||||
|
||||
interface Props {
|
||||
inviteApi: InviteApi
|
||||
}
|
||||
|
||||
/**
|
||||
* @category Onboarding
|
||||
*/
|
||||
export function InvitePage({ inviteApi }: Props) {
|
||||
const { isAuthenticated, isInitialized: isAuthenticationInitialized } = useAuth()
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { myProfile, isMyProfileLoaded } = useMyProfile()
|
||||
|
||||
if (!id) throw new Error('Invite ID is required')
|
||||
|
||||
useEffect(() => {
|
||||
async function redeemInvite() {
|
||||
if (!id) throw new Error('Invite ID is required')
|
||||
|
||||
if (!isMyProfileLoaded) return
|
||||
|
||||
if (!myProfile) {
|
||||
toast.error('Could not find your profile to redeem the invite.')
|
||||
return
|
||||
}
|
||||
|
||||
const invitingProfileId = await inviteApi.redeemInvite(id, myProfile.id)
|
||||
|
||||
if (invitingProfileId) {
|
||||
toast.success('Invite redeemed successfully!')
|
||||
navigate(`/item/${invitingProfileId}`)
|
||||
} else {
|
||||
toast.error('Failed to redeem invite')
|
||||
navigate('/')
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAuthenticationInitialized) return
|
||||
|
||||
if (isAuthenticated) {
|
||||
void redeemInvite()
|
||||
} else {
|
||||
// Save invite code in local storage
|
||||
localStorage.setItem('inviteCode', id)
|
||||
|
||||
// Redirect to login page
|
||||
navigate('/login')
|
||||
}
|
||||
}, [
|
||||
id,
|
||||
isAuthenticated,
|
||||
inviteApi,
|
||||
navigate,
|
||||
isAuthenticationInitialized,
|
||||
myProfile,
|
||||
isMyProfileLoaded,
|
||||
])
|
||||
|
||||
return (
|
||||
<MapOverlayPage backdrop className='tw:max-w-xs tw:h-fit'>
|
||||
<h2 className='tw:text-2xl tw:font-semibold tw:mb-2 tw:text-center'>Invitation</h2>
|
||||
</MapOverlayPage>
|
||||
)
|
||||
}
|
||||
1
lib/src/Components/Onboarding/index.ts
Normal file
1
lib/src/Components/Onboarding/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { InvitePage } from './InvitePage'
|
||||
@ -1,4 +1,4 @@
|
||||
import { TextInput } from '#components/Input'
|
||||
import { InputLabel, TextInput } from '#components/Input'
|
||||
|
||||
import type { FormState } from '#types/FormState'
|
||||
|
||||
@ -12,12 +12,7 @@ export const ContactInfoForm = ({
|
||||
return (
|
||||
<div className='tw:mt-2 tw:space-y-2'>
|
||||
<div>
|
||||
<label
|
||||
htmlFor='email'
|
||||
className='tw:block tw:text-sm tw:font-medium tw:text-gray-500 tw:mb-1'
|
||||
>
|
||||
Email-Adresse (Kontakt):
|
||||
</label>
|
||||
<InputLabel label='Email-Adresse (Kontakt)' />
|
||||
<TextInput
|
||||
placeholder='Email'
|
||||
type='email'
|
||||
@ -33,12 +28,7 @@ export const ContactInfoForm = ({
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor='telephone'
|
||||
className='tw:block tw:text-sm tw:font-medium tw:text-gray-500 tw:mb-1'
|
||||
>
|
||||
Telefonnummer (Kontakt):
|
||||
</label>
|
||||
<InputLabel label='Telefonnummer (Kontakt)' />
|
||||
<TextInput
|
||||
placeholder='Telefonnummer'
|
||||
type='tel'
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { TextInput } from '#components/Input'
|
||||
import { TextInput, InputLabel } from '#components/Input'
|
||||
|
||||
import type { FormState } from '#types/FormState'
|
||||
|
||||
@ -12,12 +12,7 @@ export const CrowdfundingForm = ({
|
||||
return (
|
||||
<div className='tw:mt-4 tw:space-y-4'>
|
||||
<div>
|
||||
<label
|
||||
htmlFor='OpenCollectiveSlug'
|
||||
className='tw:block tw:text-sm tw:font-medium tw:text-gray-500 tw:mb-1'
|
||||
>
|
||||
Open Collective Slug:
|
||||
</label>
|
||||
<InputLabel label='Open Collective Slug' />
|
||||
<TextInput
|
||||
placeholder='Open Collective Slug'
|
||||
type='text'
|
||||
|
||||
@ -5,6 +5,7 @@ import { useDropzone } from 'react-dropzone'
|
||||
import { BiSolidImage } from 'react-icons/bi'
|
||||
|
||||
import { useAppState } from '#components/AppShell/hooks/useAppState'
|
||||
import { InputLabel } from '#components/Input/InputLabel'
|
||||
import DialogModal from '#components/Templates/DialogModal'
|
||||
import { getImageDimensions } from '#utils/getImageDimensions'
|
||||
|
||||
@ -13,6 +14,7 @@ import type { FormState } from '#types/FormState'
|
||||
interface Props {
|
||||
state: FormState
|
||||
setState: React.Dispatch<React.SetStateAction<FormState>>
|
||||
hideInputLabel?: boolean
|
||||
}
|
||||
|
||||
const compressionOptions = {
|
||||
@ -21,7 +23,7 @@ const compressionOptions = {
|
||||
useWebWorker: true,
|
||||
}
|
||||
|
||||
export const GalleryForm = ({ state, setState }: Props) => {
|
||||
export const GalleryForm = ({ state, setState, hideInputLabel = false }: Props) => {
|
||||
const appState = useAppState()
|
||||
|
||||
const [imageSelectedToDelete, setImageSelectedToDelete] = useState<number | null>(null)
|
||||
@ -96,8 +98,9 @@ export const GalleryForm = ({ state, setState }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='tw:grid tw:grid-cols-2 tw:@md:grid-cols-3 tw:@lg:grid-cols-4 tw:gap-4 tw:my-4'>
|
||||
<div className='tw:mt-3'>
|
||||
{!hideInputLabel && <InputLabel label='Media' />}
|
||||
<div className='tw:grid tw:grid-cols-2 tw:@md:grid-cols-3 tw:@lg:grid-cols-4 tw:gap-4'>
|
||||
{images.map((image, index) => (
|
||||
<div key={index} className='tw:relative'>
|
||||
<img
|
||||
@ -161,6 +164,6 @@ export const GalleryForm = ({ state, setState }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
</DialogModal>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { InputLabel } from '#components/Input'
|
||||
import ComboBoxInput from '#components/Input/ComboBoxInput'
|
||||
|
||||
import type { FormState } from '#types/FormState'
|
||||
@ -51,12 +52,7 @@ export const GroupSubheaderForm = ({
|
||||
return (
|
||||
<div className='tw:grid tw:grid-cols-1 tw:@sm:grid-cols-2 tw:gap-2'>
|
||||
<div>
|
||||
<label
|
||||
htmlFor='status'
|
||||
className='tw:block tw:text-sm tw:font-medium tw:text-gray-500 tw:mb-1'
|
||||
>
|
||||
Gruppenstatus:
|
||||
</label>
|
||||
<InputLabel label='Gruppenstatus' />
|
||||
<ComboBoxInput
|
||||
id='status'
|
||||
options={groupStates || []}
|
||||
@ -70,12 +66,7 @@ export const GroupSubheaderForm = ({
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor='groupType'
|
||||
className='tw:block tw:text-sm tw:font-medium tw:text-gray-500 tw:mb-1'
|
||||
>
|
||||
Gruppenart:
|
||||
</label>
|
||||
<InputLabel label='Gruppenart' />
|
||||
<ComboBoxInput
|
||||
id='groupType'
|
||||
options={groupTypes?.map((gt) => gt.groupTypes_id.name) || []}
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
import { render, fireEvent } from '@testing-library/react'
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
|
||||
import { InviteLinkView } from './InviteLinkView'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
|
||||
const itemWithSecret: Item = {
|
||||
secrets: [
|
||||
{
|
||||
secret: 'secret1',
|
||||
},
|
||||
],
|
||||
id: '1',
|
||||
name: 'Test Item',
|
||||
}
|
||||
|
||||
const itemWithoutSecret: Item = {
|
||||
secrets: [],
|
||||
id: '2',
|
||||
name: 'Test Item Without Secret',
|
||||
}
|
||||
|
||||
const itemWithUndefinedSecrets: Item = {
|
||||
id: '3',
|
||||
name: 'Test Item With Undefined Secrets',
|
||||
}
|
||||
|
||||
describe('<InviteLinkView />', () => {
|
||||
let wrapper: ReturnType<typeof render>
|
||||
|
||||
const Wrapper = ({ item }: { item: Item }) => {
|
||||
return render(<InviteLinkView item={item} />)
|
||||
}
|
||||
|
||||
describe('when item does not have secrets', () => {
|
||||
it('does not render anything', () => {
|
||||
wrapper = Wrapper({ item: itemWithoutSecret })
|
||||
expect(wrapper.container.firstChild).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when item has secrets undefined', () => {
|
||||
it('does not render anything', () => {
|
||||
wrapper = Wrapper({ item: itemWithUndefinedSecrets })
|
||||
expect(wrapper.container.firstChild).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when item has secrets', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper({ item: itemWithSecret })
|
||||
})
|
||||
|
||||
it('renders the secret', () => {
|
||||
expect(wrapper.getByDisplayValue('secret1', { exact: false })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('matches the snapshot', () => {
|
||||
expect(wrapper.container.firstChild).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('copies the secret to clipboard when button is clicked', () => {
|
||||
const copyButton = wrapper.getByRole('button')
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
|
||||
const clipboardSpy = vi.spyOn(navigator.clipboard, 'writeText')
|
||||
|
||||
fireEvent.click(copyButton)
|
||||
|
||||
// TODO Implement in a way that the URL stays consistent on CI
|
||||
expect(clipboardSpy).toHaveBeenCalledWith('http://localhost:3000/invite/secret1')
|
||||
})
|
||||
})
|
||||
})
|
||||
38
lib/src/Components/Profile/Subcomponents/InviteLinkView.tsx
Normal file
38
lib/src/Components/Profile/Subcomponents/InviteLinkView.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { ClipboardIcon } from '@heroicons/react/24/outline'
|
||||
import QRCode from 'react-qr-code'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
|
||||
export const InviteLinkView = ({ item }: { item: Item }) => {
|
||||
// Only show if user has permission to view secrets.
|
||||
if (!item.secrets || item.secrets.length === 0) return
|
||||
|
||||
const link = `${window.location.origin}/invite/${item.secrets[0].secret}`
|
||||
|
||||
const copyToClipboard = () => {
|
||||
void navigator.clipboard
|
||||
.writeText(link)
|
||||
.then(() => toast.success('Invite link copied to clipboard!'))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='tw:my-10 tw:mt-2 tw:px-6'>
|
||||
<h2 className='tw:text-lg tw:font-semibold'>Invite</h2>
|
||||
<div className='tw:mt-2 tw:text-sm tw:flex tw:gap-2 tw:mb-2'>
|
||||
<input
|
||||
type='text'
|
||||
value={link}
|
||||
readOnly
|
||||
className='tw:w-full tw:p-2 tw:border tw:rounded'
|
||||
/>
|
||||
<button onClick={copyToClipboard} className='btn btn-circle btn-primary'>
|
||||
<ClipboardIcon className='w-6 h-6' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='tw:bg-white tw:p-2 tw:w-fit'>
|
||||
<QRCode value={link} size={128} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { RichTextEditor } from '#components/Input/RichTextEditor/RichTextEditor'
|
||||
import { InputLabel, RichTextEditor } from '#components/Input'
|
||||
|
||||
import { MarkdownHint } from './MarkdownHint'
|
||||
|
||||
@ -13,11 +13,9 @@ import type { FormState } from '#types/FormState'
|
||||
export const ProfileTextForm = ({
|
||||
state,
|
||||
setState,
|
||||
// Is this really used?
|
||||
dataField,
|
||||
heading,
|
||||
size,
|
||||
hideInputLabel,
|
||||
}: {
|
||||
state: FormState
|
||||
setState: React.Dispatch<React.SetStateAction<FormState>>
|
||||
@ -36,15 +34,10 @@ export const ProfileTextForm = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`tw:max-h-124 tw:md:max-h-full tw:flex tw:flex-col tw:mt-2 ${size === 'full' ? 'tw:flex-1 tw:min-h-42' : 'tw:h-28 tw:flex-none'}`}
|
||||
className={`tw:max-h-124 tw:md:max-h-full tw:flex tw:flex-col tw:mt-3 ${size === 'full' ? 'tw:flex-1 tw:min-h-42' : 'tw:h-30 tw:flex-none'}`}
|
||||
>
|
||||
<div className='tw:flex tw:justify-between tw:items-center'>
|
||||
<label
|
||||
htmlFor='nextAppointment'
|
||||
className='tw:block tw:text-sm tw:font-medium tw:text-base-content/50 tw:mb-1'
|
||||
>
|
||||
{heading || 'Text'}:
|
||||
</label>
|
||||
<InputLabel label={heading || 'Text'} />
|
||||
<MarkdownHint />
|
||||
</div>
|
||||
<RichTextEditor
|
||||
@ -58,7 +51,6 @@ export const ProfileTextForm = ({
|
||||
}))
|
||||
}
|
||||
showMenu={size === 'full'}
|
||||
labelStyle={hideInputLabel ? 'tw:hidden' : ''}
|
||||
containerStyle={size === 'full' ? 'tw:flex-1' : 'tw:h-24 tw:flex-none'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
37
lib/src/Components/Profile/Subcomponents/RelationsView.tsx
Normal file
37
lib/src/Components/Profile/Subcomponents/RelationsView.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { useItems } from '#components/Map/hooks/useItems'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
|
||||
interface Props {
|
||||
item: Item
|
||||
relation: string
|
||||
}
|
||||
|
||||
export const RelationsView = ({ item, relation }: Props) => {
|
||||
const items = useItems()
|
||||
|
||||
if (!item.relations) return
|
||||
|
||||
const relationsOfRightType = item.relations.filter((r) => r.type === relation)
|
||||
|
||||
const relatedItems = items.filter((i) => relationsOfRightType.some((r) => r.id === i.id))
|
||||
|
||||
const hasRelatedItems = relatedItems.length > 0
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>{relation}</h2>
|
||||
{hasRelatedItems ? (
|
||||
<ul>
|
||||
{relatedItems.map((relatedItem) => (
|
||||
<li key={relatedItem.id}>
|
||||
<a href={`/item/${relatedItem.id}`}>{relatedItem.name}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>No related items found.</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -3,99 +3,113 @@
|
||||
exports[`GalleryForm > with previous images > renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="tw:grid tw:grid-cols-2 tw:@md:grid-cols-3 tw:@lg:grid-cols-4 tw:gap-4 tw:my-4"
|
||||
class="tw:mt-3"
|
||||
>
|
||||
<div
|
||||
class="tw:relative"
|
||||
<label
|
||||
class="tw:label tw:pb-1"
|
||||
>
|
||||
<img
|
||||
alt="Gallery image 1"
|
||||
class="tw:w-full tw:h-full tw:object-cover tw:rounded-lg "
|
||||
src="undefined1.jpg"
|
||||
/>
|
||||
<button
|
||||
class="tw:m-2 tw:bg-red-500 tw:text-white tw:p-2 tw:rounded-full tw:absolute tw:top-0 tw:right-0 tw:hover:bg-red-600 tw:cursor-pointer"
|
||||
type="button"
|
||||
<span
|
||||
class="tw:block tw:text-sm tw:font-medium tw:text-base-content/50 tw:mb-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="tw:h-5 tw:w-5"
|
||||
data-slot="icon"
|
||||
data-testid="trash"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
Media
|
||||
:
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="tw:relative"
|
||||
class="tw:grid tw:grid-cols-2 tw:@md:grid-cols-3 tw:@lg:grid-cols-4 tw:gap-4"
|
||||
>
|
||||
<img
|
||||
alt="Gallery image 2"
|
||||
class="tw:w-full tw:h-full tw:object-cover tw:rounded-lg "
|
||||
src="undefined2.jpg"
|
||||
/>
|
||||
<button
|
||||
class="tw:m-2 tw:bg-red-500 tw:text-white tw:p-2 tw:rounded-full tw:absolute tw:top-0 tw:right-0 tw:hover:bg-red-600 tw:cursor-pointer"
|
||||
type="button"
|
||||
<div
|
||||
class="tw:relative"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="tw:h-5 tw:w-5"
|
||||
data-slot="icon"
|
||||
data-testid="trash"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<img
|
||||
alt="Gallery image 1"
|
||||
class="tw:w-full tw:h-full tw:object-cover tw:rounded-lg "
|
||||
src="undefined1.jpg"
|
||||
/>
|
||||
<button
|
||||
class="tw:m-2 tw:bg-red-500 tw:text-white tw:p-2 tw:rounded-full tw:absolute tw:top-0 tw:right-0 tw:hover:bg-red-600 tw:cursor-pointer"
|
||||
type="button"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="tw:flex tw:flex-col tw:items-center tw:justify-center tw:text-base-content/50 tw:w-full tw:h-full tw:cursor-pointer tw:card tw:card-body tw:border tw:border-current/50 tw:border-dashed tw:bg-base-200"
|
||||
role="presentation"
|
||||
tabindex="0"
|
||||
>
|
||||
<input
|
||||
accept="image/jpeg,image/png"
|
||||
data-testid="gallery-upload-input"
|
||||
multiple=""
|
||||
style="border: 0px; clip: rect(0, 0, 0, 0); clip-path: inset(50%); height: 1px; margin: 0px -1px -1px 0px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"
|
||||
tabindex="-1"
|
||||
type="file"
|
||||
/>
|
||||
<div>
|
||||
<svg
|
||||
class="tw:h-16 tw:w-16 tw:m-auto tw:mb-2"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
stroke="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="tw:h-5 tw:w-5"
|
||||
data-slot="icon"
|
||||
data-testid="trash"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="tw:relative"
|
||||
>
|
||||
<img
|
||||
alt="Gallery image 2"
|
||||
class="tw:w-full tw:h-full tw:object-cover tw:rounded-lg "
|
||||
src="undefined2.jpg"
|
||||
/>
|
||||
<button
|
||||
class="tw:m-2 tw:bg-red-500 tw:text-white tw:p-2 tw:rounded-full tw:absolute tw:top-0 tw:right-0 tw:hover:bg-red-600 tw:cursor-pointer"
|
||||
type="button"
|
||||
>
|
||||
<path
|
||||
d="M19.999 4h-16c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm-13.5 3a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm5.5 10h-7l4-5 1.5 2 3-4 5.5 7h-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="tw:text-center"
|
||||
>
|
||||
Upload Image
|
||||
</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="tw:h-5 tw:w-5"
|
||||
data-slot="icon"
|
||||
data-testid="trash"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="tw:flex tw:flex-col tw:items-center tw:justify-center tw:text-base-content/50 tw:w-full tw:h-full tw:cursor-pointer tw:card tw:card-body tw:border tw:border-current/50 tw:border-dashed tw:bg-base-200"
|
||||
role="presentation"
|
||||
tabindex="0"
|
||||
>
|
||||
<input
|
||||
accept="image/jpeg,image/png"
|
||||
data-testid="gallery-upload-input"
|
||||
multiple=""
|
||||
style="border: 0px; clip: rect(0, 0, 0, 0); clip-path: inset(50%); height: 1px; margin: 0px -1px -1px 0px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"
|
||||
tabindex="-1"
|
||||
type="file"
|
||||
/>
|
||||
<div>
|
||||
<svg
|
||||
class="tw:h-16 tw:w-16 tw:m-auto tw:mb-2"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
stroke="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19.999 4h-16c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm-13.5 3a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm5.5 10h-7l4-5 1.5 2 3-4 5.5 7h-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="tw:text-center"
|
||||
>
|
||||
Upload Image
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -105,77 +119,91 @@ exports[`GalleryForm > with previous images > renders 1`] = `
|
||||
exports[`GalleryForm > with uploading images > renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="tw:grid tw:grid-cols-2 tw:@md:grid-cols-3 tw:@lg:grid-cols-4 tw:gap-4 tw:my-4"
|
||||
class="tw:mt-3"
|
||||
>
|
||||
<div
|
||||
class="tw:relative"
|
||||
<label
|
||||
class="tw:label tw:pb-1"
|
||||
>
|
||||
<img
|
||||
alt="Gallery image 1"
|
||||
class="tw:w-full tw:h-full tw:object-cover tw:rounded-lg tw:opacity-50"
|
||||
src="blob-url-placeholder"
|
||||
/>
|
||||
<span
|
||||
class="tw:loading tw:loading-spinner tw:absolute tw:inset-0 tw:m-auto"
|
||||
/>
|
||||
</div>
|
||||
class="tw:block tw:text-sm tw:font-medium tw:text-base-content/50 tw:mb-1"
|
||||
>
|
||||
Media
|
||||
:
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="tw:relative"
|
||||
class="tw:grid tw:grid-cols-2 tw:@md:grid-cols-3 tw:@lg:grid-cols-4 tw:gap-4"
|
||||
>
|
||||
<img
|
||||
alt="Gallery image 2"
|
||||
class="tw:w-full tw:h-full tw:object-cover tw:rounded-lg tw:opacity-50"
|
||||
src="blob-url-placeholder"
|
||||
/>
|
||||
<span
|
||||
class="tw:loading tw:loading-spinner tw:absolute tw:inset-0 tw:m-auto"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tw:relative"
|
||||
>
|
||||
<img
|
||||
alt="Gallery image 3"
|
||||
class="tw:w-full tw:h-full tw:object-cover tw:rounded-lg tw:opacity-50"
|
||||
src="blob-url-placeholder"
|
||||
/>
|
||||
<span
|
||||
class="tw:loading tw:loading-spinner tw:absolute tw:inset-0 tw:m-auto"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tw:flex tw:flex-col tw:items-center tw:justify-center tw:text-base-content/50 tw:w-full tw:h-full tw:cursor-pointer tw:card tw:card-body tw:border tw:border-current/50 tw:border-dashed tw:bg-base-200"
|
||||
role="presentation"
|
||||
tabindex="0"
|
||||
>
|
||||
<input
|
||||
accept="image/jpeg,image/png"
|
||||
data-testid="gallery-upload-input"
|
||||
multiple=""
|
||||
style="border: 0px; clip: rect(0, 0, 0, 0); clip-path: inset(50%); height: 1px; margin: 0px -1px -1px 0px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"
|
||||
tabindex="-1"
|
||||
type="file"
|
||||
/>
|
||||
<div>
|
||||
<svg
|
||||
class="tw:h-16 tw:w-16 tw:m-auto tw:mb-2"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
stroke="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19.999 4h-16c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm-13.5 3a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm5.5 10h-7l4-5 1.5 2 3-4 5.5 7h-7z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="tw:relative"
|
||||
>
|
||||
<img
|
||||
alt="Gallery image 1"
|
||||
class="tw:w-full tw:h-full tw:object-cover tw:rounded-lg tw:opacity-50"
|
||||
src="blob-url-placeholder"
|
||||
/>
|
||||
<span
|
||||
class="tw:text-center"
|
||||
>
|
||||
Upload Image
|
||||
</span>
|
||||
class="tw:loading tw:loading-spinner tw:absolute tw:inset-0 tw:m-auto"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tw:relative"
|
||||
>
|
||||
<img
|
||||
alt="Gallery image 2"
|
||||
class="tw:w-full tw:h-full tw:object-cover tw:rounded-lg tw:opacity-50"
|
||||
src="blob-url-placeholder"
|
||||
/>
|
||||
<span
|
||||
class="tw:loading tw:loading-spinner tw:absolute tw:inset-0 tw:m-auto"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tw:relative"
|
||||
>
|
||||
<img
|
||||
alt="Gallery image 3"
|
||||
class="tw:w-full tw:h-full tw:object-cover tw:rounded-lg tw:opacity-50"
|
||||
src="blob-url-placeholder"
|
||||
/>
|
||||
<span
|
||||
class="tw:loading tw:loading-spinner tw:absolute tw:inset-0 tw:m-auto"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="tw:flex tw:flex-col tw:items-center tw:justify-center tw:text-base-content/50 tw:w-full tw:h-full tw:cursor-pointer tw:card tw:card-body tw:border tw:border-current/50 tw:border-dashed tw:bg-base-200"
|
||||
role="presentation"
|
||||
tabindex="0"
|
||||
>
|
||||
<input
|
||||
accept="image/jpeg,image/png"
|
||||
data-testid="gallery-upload-input"
|
||||
multiple=""
|
||||
style="border: 0px; clip: rect(0, 0, 0, 0); clip-path: inset(50%); height: 1px; margin: 0px -1px -1px 0px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"
|
||||
tabindex="-1"
|
||||
type="file"
|
||||
/>
|
||||
<div>
|
||||
<svg
|
||||
class="tw:h-16 tw:w-16 tw:m-auto tw:mb-2"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
stroke="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19.999 4h-16c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm-13.5 3a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm5.5 10h-7l4-5 1.5 2 3-4 5.5 7h-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="tw:text-center"
|
||||
>
|
||||
Upload Image
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -185,41 +213,55 @@ exports[`GalleryForm > with uploading images > renders 1`] = `
|
||||
exports[`GalleryForm > without previous images > renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="tw:grid tw:grid-cols-2 tw:@md:grid-cols-3 tw:@lg:grid-cols-4 tw:gap-4 tw:my-4"
|
||||
class="tw:mt-3"
|
||||
>
|
||||
<div
|
||||
class="tw:flex tw:flex-col tw:items-center tw:justify-center tw:text-base-content/50 tw:w-full tw:h-full tw:cursor-pointer tw:card tw:card-body tw:border tw:border-current/50 tw:border-dashed tw:bg-base-200"
|
||||
role="presentation"
|
||||
tabindex="0"
|
||||
<label
|
||||
class="tw:label tw:pb-1"
|
||||
>
|
||||
<input
|
||||
accept="image/jpeg,image/png"
|
||||
data-testid="gallery-upload-input"
|
||||
multiple=""
|
||||
style="border: 0px; clip: rect(0, 0, 0, 0); clip-path: inset(50%); height: 1px; margin: 0px -1px -1px 0px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"
|
||||
tabindex="-1"
|
||||
type="file"
|
||||
/>
|
||||
<div>
|
||||
<svg
|
||||
class="tw:h-16 tw:w-16 tw:m-auto tw:mb-2"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
stroke="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19.999 4h-16c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm-13.5 3a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm5.5 10h-7l4-5 1.5 2 3-4 5.5 7h-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="tw:text-center"
|
||||
>
|
||||
Upload Image
|
||||
</span>
|
||||
<span
|
||||
class="tw:block tw:text-sm tw:font-medium tw:text-base-content/50 tw:mb-1"
|
||||
>
|
||||
Media
|
||||
:
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="tw:grid tw:grid-cols-2 tw:@md:grid-cols-3 tw:@lg:grid-cols-4 tw:gap-4"
|
||||
>
|
||||
<div
|
||||
class="tw:flex tw:flex-col tw:items-center tw:justify-center tw:text-base-content/50 tw:w-full tw:h-full tw:cursor-pointer tw:card tw:card-body tw:border tw:border-current/50 tw:border-dashed tw:bg-base-200"
|
||||
role="presentation"
|
||||
tabindex="0"
|
||||
>
|
||||
<input
|
||||
accept="image/jpeg,image/png"
|
||||
data-testid="gallery-upload-input"
|
||||
multiple=""
|
||||
style="border: 0px; clip: rect(0, 0, 0, 0); clip-path: inset(50%); height: 1px; margin: 0px -1px -1px 0px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"
|
||||
tabindex="-1"
|
||||
type="file"
|
||||
/>
|
||||
<div>
|
||||
<svg
|
||||
class="tw:h-16 tw:w-16 tw:m-auto tw:mb-2"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
stroke="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19.999 4h-16c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm-13.5 3a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm5.5 10h-7l4-5 1.5 2 3-4 5.5 7h-7z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="tw:text-center"
|
||||
>
|
||||
Upload Image
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -17,6 +17,7 @@ const componentMap = {
|
||||
startEnd: ProfileStartEndForm,
|
||||
crowdfundings: CrowdfundingForm,
|
||||
gallery: GalleryForm,
|
||||
inviteLinks: () => null, // Not needed for now
|
||||
// weitere Komponenten hier
|
||||
}
|
||||
|
||||
|
||||
@ -4,8 +4,10 @@ import { ContactInfoView } from '#components/Profile/Subcomponents/ContactInfoVi
|
||||
import { CrowdfundingView } from '#components/Profile/Subcomponents/CrowdfundingView'
|
||||
import { GalleryView } from '#components/Profile/Subcomponents/GalleryView'
|
||||
import { GroupSubHeaderView } from '#components/Profile/Subcomponents/GroupSubHeaderView'
|
||||
import { InviteLinkView } from '#components/Profile/Subcomponents/InviteLinkView'
|
||||
import { ProfileStartEndView } from '#components/Profile/Subcomponents/ProfileStartEndView'
|
||||
import { ProfileTextView } from '#components/Profile/Subcomponents/ProfileTextView'
|
||||
import { RelationsView } from '#components/Profile/Subcomponents/RelationsView'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
import type { Key } from 'react'
|
||||
@ -17,6 +19,8 @@ const componentMap = {
|
||||
startEnd: ProfileStartEndView,
|
||||
gallery: GalleryView,
|
||||
crowdfundings: CrowdfundingView,
|
||||
inviteLinks: InviteLinkView,
|
||||
relations: RelationsView,
|
||||
// weitere Komponenten hier
|
||||
}
|
||||
|
||||
|
||||
@ -44,7 +44,6 @@ export const TabsForm = ({
|
||||
<PopupStartEndInput
|
||||
item={item}
|
||||
showLabels={true}
|
||||
labelStyle={'tw:text-base-content/50'}
|
||||
updateEndValue={(e) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
@ -62,7 +61,6 @@ export const TabsForm = ({
|
||||
|
||||
<RichTextEditor
|
||||
labelTitle='About'
|
||||
labelStyle={'tw:text-base-content/50'}
|
||||
placeholder='about ...'
|
||||
defaultValue={item?.text ? item.text : ''}
|
||||
updateFormValue={(v) =>
|
||||
|
||||
@ -1,6 +1,29 @@
|
||||
.leaflet-control-attribution {
|
||||
display: none;
|
||||
.leaflet-control-attribution{
|
||||
color: #000 !important;
|
||||
background-color: white/50 !important;
|
||||
border-radius: 4px;
|
||||
padding: 0 4px;
|
||||
z-index: 400 !important;
|
||||
font-size: 10px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.leaflet-control-attribution a{
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
.leaflet-attribution-flag {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.leaflet-control-attribution a:not([href*="openstreetmap"]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 2) Entfernt den Trenner “|” (liegt im span[aria-hidden]) */
|
||||
.leaflet-control-attribution span[aria-hidden="true"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.leaflet-control-locate {
|
||||
display: none;
|
||||
|
||||
@ -8,6 +8,7 @@ export * from './Components/Gaming'
|
||||
export * from './Components/Templates'
|
||||
export * from './Components/Input'
|
||||
export * from './Components/Item'
|
||||
export * from './Components/Onboarding'
|
||||
export * from './Components/Profile'
|
||||
|
||||
declare global {
|
||||
|
||||
4
lib/src/types/InviteApi.d.ts
vendored
Normal file
4
lib/src/types/InviteApi.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
export interface InviteApi {
|
||||
validateInvite(inviteId: string): Promise<string | null>
|
||||
redeemInvite(inviteId: string, itemId: string): Promise<string | null>
|
||||
}
|
||||
5
lib/src/types/Item.d.ts
vendored
5
lib/src/types/Item.d.ts
vendored
@ -18,6 +18,10 @@ interface GalleryItem {
|
||||
| string
|
||||
}
|
||||
|
||||
interface ItemSecret {
|
||||
secret: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @category Types
|
||||
*/
|
||||
@ -55,6 +59,7 @@ export interface Item {
|
||||
next_appointment?: string
|
||||
gallery?: GalleryItem[]
|
||||
openCollectiveSlug?: string
|
||||
secrets?: ItemSecret[]
|
||||
|
||||
// {
|
||||
// coordinates: [number, number]
|
||||
|
||||
2
lib/src/types/UtopiaMapProps.d.ts
vendored
2
lib/src/types/UtopiaMapProps.d.ts
vendored
@ -18,4 +18,6 @@ export interface UtopiaMapProps {
|
||||
donationWidget?: boolean
|
||||
defaultTheme?: string
|
||||
expandLayerControl?: boolean
|
||||
tileServerUrl?: string
|
||||
tileServerAttribution?: string
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user