diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index fc2c714d..0de176a9 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -10,12 +10,12 @@ jobs: outputs: has_changes: ${{ steps.check_for_changes.outputs.has_changes }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 3 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: '22.14.0' + node-version: 22 - name: Install turbo run: npm install -g turbo@2.4.4 && npm install -g turbo-ignore - name: Check for changes @@ -29,6 +29,9 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 3 + - uses: actions/setup-node@v4 + with: + node-version: '22' - name: Install turbo run: npm install -g turbo@2.4.4 - name: Login to Docker diff --git a/.github/workflows/dashboard.yml b/.github/workflows/dashboard.yml index d2d4d40a..f392a52b 100644 --- a/.github/workflows/dashboard.yml +++ b/.github/workflows/dashboard.yml @@ -42,6 +42,7 @@ jobs: echo NEXT_PUBLIC_API_URL="https://api.buildtheearth.net/api/v1" >> apps/dashboard/.env echo NEXT_PUBLIC_SMYLER_API_URL="https://smybteapi.buildtheearth.net" >> apps/dashboard/.env echo NEXT_PUBLIC_FRONTEND_URL="https://buildtheearth.net" >> apps/dashboard/.env + # echo DATABASE_URL="${{ secrets.DATABASE_URL }}" >> apps/dashboard/.env - name: Build the Docker image run: docker build . --file apps/dashboard/Dockerfile --tag ghcr.io/buildtheearth/dashboard-website:$(git rev-parse --short HEAD) --tag ghcr.io/buildtheearth/dashboard-website:latest - name: Docker push tag diff --git a/.vscode/settings.json b/.vscode/settings.json index eb5d4a82..27da17c9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,6 +29,13 @@ "frontend/seo", "api/applications", "api/claims", - "frontend/legal" + "frontend/legal", + "frontend/gallery", + "dash/editor", + "db", + "dash/responsive", + "frontend/i18n", + "frontend/stats", + "frontend/join" ] } diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index ce4441ae..0ac137de 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -1,4 +1,4 @@ -FROM node:21-alpine AS base +FROM node:22-alpine AS base FROM base AS builder RUN apk update @@ -18,6 +18,9 @@ WORKDIR /app # Install dependencies COPY --from=builder /app/out/json/ . +# Enable corepack to use the correct Yarn version +RUN corepack enable +RUN corepack prepare yarn@4.9.1 --activate RUN yarn install # Build the project diff --git a/apps/api/src/util/package.ts b/apps/api/src/util/package.ts index ba5ca96a..82dad612 100644 --- a/apps/api/src/util/package.ts +++ b/apps/api/src/util/package.ts @@ -1 +1,2 @@ -export const LIB_VERSION = "1.1.0";export const LIB_LICENSE = undefined; +export const LIB_VERSION = '1.1.0'; +export const LIB_LICENSE = undefined; diff --git a/apps/dashboard/.env.example b/apps/dashboard/.env.example index 19db7d6c..218ee5cc 100644 --- a/apps/dashboard/.env.example +++ b/apps/dashboard/.env.example @@ -1,26 +1,44 @@ # # Keycloak # -NEXT_PUBLIC_KEYCLOAK_URL="https://yourkeycloak.net/realms/yourrealm" -NEXT_PUBLIC_KEYCLOAK_ID="yourclient" -KEYCLOAK_SECRET="topsecret" +NEXT_PUBLIC_KEYCLOAK_URL="https://.../realms/..." +NEXT_PUBLIC_KEYCLOAK_ID="..." +KEYCLOAK_SECRET="..." +KEYCLOAK_ADMIN_CLIENT_ID="..." +KEYCLOAK_ADMIN_CLIENT_SECRET="..." # -# NextAuth +# Auth # -NEXTAUTH_URL="http://localhost:3000" -NEXTAUTH_SECRET="secondtopsecret" +NEXTAUTH_URL="http://localhost:3001" +NEXTAUTH_SECRET="..." +INTERNAL_API_KEY="..." # -# BuildTheEarth +# APIs # -NEXT_PUBLIC_API_URL="https://api.yourserver.net/api/v1" -NEXT_PUBLIC_SMYLER_API_URL="https://smybteapi.yourserver.net" -NEXT_PUBLIC_FRONTEND_URL="https://yourserver.net" -FRONTEND_KEY="thirdtopsecret" +NEXT_PUBLIC_API_URL="https://.../api/v1" +NEXT_PUBLIC_SMYLER_API_URL="https://..." +# +# Main Website # -# Other Confirguration +NEXT_PUBLIC_FRONTEND_URL="https://..." +FRONTEND_KEY="..." + +# +# Mapbox +# +NEXT_PUBLIC_MAPBOX_TOKEN="..." + +# +# Database +# +DATABASE_URL="postgresql://user:password@server:5432/database?pool_timeout=0" + +# +# DISCORD # REPORTS_WEBHOOK="https://discord.com/api/webhooks/..." -NEXT_PUBLIC_MAPBOX_TOKEN="fourthtopsecret" \ No newline at end of file +DISCORD_BOT_URL="https://..." +DISCORD_BOT_SECRET="..." \ No newline at end of file diff --git a/apps/dashboard/Dockerfile b/apps/dashboard/Dockerfile index dec2aedd..373aded7 100644 --- a/apps/dashboard/Dockerfile +++ b/apps/dashboard/Dockerfile @@ -1,8 +1,8 @@ -FROM node:21-alpine AS base +FROM node:22-alpine AS base +RUN corepack enable FROM base AS builder -RUN apk update -RUN apk add --no-cache libc6-compat +RUN apk update && apk add --no-cache libc6-compat openssl WORKDIR /app # Run turbo (will prune the lockfile to only include target dependencies) @@ -12,12 +12,14 @@ RUN turbo prune dashboard --docker # Add lockfile and package.json's of isolated subworkspace FROM base AS installer -RUN apk update -RUN apk add --no-cache libc6-compat +RUN apk update && apk add --no-cache libc6-compat openssl +# This is only for prisma v5 because it only looks in /lib for openssl libaries +RUN ln -s /usr/lib/libssl.so.3 /lib/libssl.so.3 WORKDIR /app # First install the dependencies (as they change less often) COPY --from=builder /app/out/json/ . +# RUN ln -s /usr/lib/libssl.so.3 /lib/libssl.so.3 RUN yarn install # Build the project @@ -25,7 +27,6 @@ COPY --from=builder /app/out/full/ . COPY --from=builder /app/apps/dashboard/.env ./apps/dashboard/.env ENV NEXT_TELEMETRY_DISABLED 1 - RUN yarn turbo run build --filter=dashboard... FROM base AS runner @@ -35,8 +36,8 @@ ENV NODE_ENV production ENV NEXT_TELEMETRY_DISABLED 1 # Create a runner user -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs USER nextjs # Reduce image size diff --git a/apps/dashboard/README.md b/apps/dashboard/README.md index 64a39b46..eb53998a 100644 --- a/apps/dashboard/README.md +++ b/apps/dashboard/README.md @@ -22,16 +22,22 @@ See the global Readme file. # Envoriment Variables -| Variable | Example Value | Description | -| -------------------------- | ----------------------------------------- | ----------------------------------------------------------------- | -| NEXT_PUBLIC_KEYCLOAK_URL | https://yourkeycloak.net/realms/yourrealm | The Keycloak SSO URL, including the realm | -| NEXT_PUBLIC_KEYCLOAK_ID | yourclient | A client ID for your Keycloak Installation | -| KEYCLOAK_SECRET | topsecret | The client secret of your client | -| NEXTAUTH_URL | http://localhost:3000 | The URL NextAuth should use for redirections back to your website | -| NEXTAUTH_SECRET | secondtopsecret | A secret used by NextAuth to encrypt session information | -| NEXT_PUBLIC_API_URL | https://api.yourserver.net/api/v1 | The URL of your deployed or local BuildTheEarth API | -| NEXT_PUBLIC_SMYLER_API_URL | https://smybteapi.yourserver.net | The URL of your deployed or local SmyBTE API | -| NEXT_PUBLIC_MAPBOX_TOKEN | fourthtopsecret | Your personal mapbox studio token | -| NEXT_PUBLIC_FRONTEND_URL | https://yourserver.net | The URL to your local or deployed BuildTheEarth Website | -| FRONTEND_KEY | thirdtopsecret | The Key used to Authenticate against the BuildTheEarth Website | -| REPORTS_WEBHOOK | https://discord.com/api/webhooks/... | A discord webhook to send reports to | +| Variable | Example Value | Description | +| ---------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------ | +| NEXT_PUBLIC_KEYCLOAK_URL | https://yourkeycloak.net/realms/yourrealm | The Keycloak SSO URL, including the realm | +| NEXT_PUBLIC_KEYCLOAK_ID | yourclient | A client ID for your Keycloak Installation | +| KEYCLOAK_SECRET | topsecret | The client secret of your client | +| KEYCLOAK_ADMIN_CLIENT_ID | yourclient | A client ID for your Keycloak Installation (Admin users client) | +| KEYCLOAK_ADMIN_CLIENT_SECRET | topsecret | The client secret of your admin client | +| NEXTAUTH_URL | http://localhost:3000 | The URL NextAuth should use for redirections back to your website | +| NEXTAUTH_SECRET | secondtopsecret | A secret used by NextAuth to encrypt session information | +| INTERNAL_API_KEY | internalsecret | A secret used by the website to send custom api requests to itself | +| NEXT_PUBLIC_API_URL | https://api.yourserver.net/api/v1 | The URL of your deployed or local BuildTheEarth API | +| NEXT_PUBLIC_SMYLER_API_URL | https://smybteapi.yourserver.net | The URL of your deployed or local SmyBTE API | +| NEXT_PUBLIC_FRONTEND_URL | https://yourserver.net | The URL to your local or deployed BuildTheEarth Website | +| FRONTEND_KEY | thirdtopsecret | The Key used to Authenticate against the BuildTheEarth Website | +| NEXT_PUBLIC_MAPBOX_TOKEN | fourthtopsecret | Your personal mapbox studio token | +| DATABASE_URL | postgresql://user:password@server:5432/database?pool_timeout=0 | Your Database connection string | +| REPORTS_WEBHOOK | https://discord.com/api/webhooks/... | A discord webhook to send reports to | +| DISCORD_BOT_URL | https://bot.yourserver.net/... | The URL to your local or delpolyed BuildTheEarth Main Bot | +| DISCORD_BOT_SECRET | fifthtopsecret | The secret key to your Main Bot instance | diff --git a/apps/dashboard/next.config.ts b/apps/dashboard/next.config.ts index 20ce639b..8136ea61 100644 --- a/apps/dashboard/next.config.ts +++ b/apps/dashboard/next.config.ts @@ -19,7 +19,20 @@ const nextConfig: NextConfig = { poweredByHeader: false, outputFileTracingRoot: path.join(__dirname, '../../'), images: { - domains: ['cdn.buildtheearth.net'], + remotePatterns: [ + { + protocol: 'https', + hostname: 'cdn.buildtheearth.net', + port: '', + pathname: '/uploads/**', + }, + { + protocol: 'https', + hostname: 'cdn.buildtheearth.net', + port: '', + pathname: '/static/**', + }, + ], }, }; diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 6eb83c3c..6b63478e 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -14,18 +14,19 @@ }, "dependencies": { "@bte-germany/terraconvert": "^1.1.2", - "@mantine/charts": "^7.17.4", - "@mantine/code-highlight": "^7.17.4", - "@mantine/core": "^7.17.4", - "@mantine/dates": "^7.17.4", - "@mantine/form": "^7.17.4", - "@mantine/hooks": "^7.17.4", - "@mantine/modals": "^7.17.4", - "@mantine/notifications": "^7.17.4", - "@mantine/nprogress": "^7.17.4", - "@mantine/spotlight": "^7.17.4", - "@mantine/tiptap": "^7.17.4", - "@mapbox/mapbox-gl-draw": "^1.4.3", + "@keycloak/keycloak-admin-client": "^26.2.0", + "@mantine/charts": "^8.2.7", + "@mantine/code-highlight": "^8.2.7", + "@mantine/core": "^8.2.7", + "@mantine/dates": "^8.2.7", + "@mantine/form": "^8.2.7", + "@mantine/hooks": "^8.2.7", + "@mantine/modals": "^8.2.7", + "@mantine/notifications": "^8.2.7", + "@mantine/nprogress": "^8.2.7", + "@mantine/spotlight": "^8.2.7", + "@mantine/tiptap": "^8.2.7", + "@mapbox/mapbox-gl-draw": "^1.5.0", "@repo/db": "*", "@tabler/icons-react": "^3.9.0", "@tiptap/core": "^2.11.7", @@ -39,27 +40,32 @@ "@tiptap/pm": "^2.11.7", "@tiptap/react": "^2.11.7", "@tiptap/starter-kit": "^2.11.7", - "@types/mapbox__mapbox-gl-draw": "1.4.4", + "@turf/helpers": "^7.2.0", + "@turf/turf": "^7.2.0", + "axios": "^1.9.0", "clsx": "^2.1.1", "dayjs": "^1.11.11", "mantine-contextmenu": "^7.11.0", "mantine-datatable": "^7.12.4", "mapbox-gl": "2.13.0", - "mapbox-gl-draw-snap-mode": "^0.2.0", + "mapbox-gl-draw-snap-mode": "^0.4.0", "mapbox-gl-style-switcher": "^1.0.11", "moment": "^2.30.1", "moment-timezone": "^0.5.45", - "next": "15.3.0", + "next": "^15.3.4", "next-auth": "^4.24.7", "next-transpile-modules": "^10.0.1", "react": "19.1.0", "react-dom": "19.1.0", "recharts": "^2.13.3", - "swr": "^2.2.5" + "swr": "^2.2.5", + "zustand": "^5.0.4" }, "devDependencies": { "@repo/prettier-config": "*", "@repo/typescript-config": "*", + "@types/mapbox-gl": "^3.4.1", + "@types/mapbox__mapbox-gl-draw": "^1.4.8", "@types/node": "^20", "@types/react": "19.1.1", "@types/react-dom": "19.1.2", diff --git a/apps/dashboard/src/actions/buildTeams.ts b/apps/dashboard/src/actions/buildTeams.ts index ad893109..07c50190 100644 --- a/apps/dashboard/src/actions/buildTeams.ts +++ b/apps/dashboard/src/actions/buildTeams.ts @@ -45,7 +45,7 @@ export const adminTransferTeam = async ( select: { id: true }, }); const transaction = await prisma.$transaction( - members.map((m) => + members.map((m: { id: any }) => prisma.user.update({ where: { id: m.id }, data: { joinedBuildTeams: { connect: { id: destinationId } } } }), ), ); diff --git a/apps/dashboard/src/actions/claimEditor.ts b/apps/dashboard/src/actions/claimEditor.ts new file mode 100644 index 00000000..c8c6ceb0 --- /dev/null +++ b/apps/dashboard/src/actions/claimEditor.ts @@ -0,0 +1,358 @@ +'use server'; + +import { constructClaimGeoJSONQuery } from '@/app/(sideNavbar)/api/data/claims.geojson/query'; +import turf, { toPolygon } from '@/util/coordinates'; +import prisma from '@/util/db'; +import { updateClaimBuildingCount, updateClaimOSMDetails } from '@/util/geojsonHelpers'; +import { Prisma } from '@repo/db'; +import { revalidatePath } from 'next/cache'; + +export const getPersonalClaims = async (userId: string) => { + const claims = await prisma.claim.findMany(constructClaimGeoJSONQuery({ user: userId, extended: true })); + return claims; +}; +export const getAllowedBuildTeams = async (userId: string) => { + const buildTeams = await prisma.buildTeam.findMany({ + where: { + members: { + some: { + ssoId: userId, + }, + }, + allowBuilderClaim: true, + }, + select: { + id: true, + }, + }); + return buildTeams.map((bt: { id: string }) => bt.id); +}; + +export const saveClaim = async (data: { id: string; userId: string; area?: string[] }): Promise => { + try { + const claim = await prisma.claim.findFirst({ + where: { id: data.id, owner: { ssoId: data.userId } }, + }); + + if (!claim) { + return Promise.reject('Claim not found or you do not have permission to edit this claim.'); + } + + let center = undefined; + if (data.area && data.area.length > 0) { + center = turf.center(toPolygon(data.area)).geometry.coordinates.join(', '); + } + + const buildingCount = data.area && (await updateClaimBuildingCount({ area: data.area })); + + if (typeof buildingCount !== 'number') { + if (buildingCount && typeof (buildingCount as { message?: string }).message === 'string') { + return Promise.reject((buildingCount as { message: string }).message); + } + return Promise.reject('Failed to update building count for claim.'); + } + + let osmDetails = undefined; + + if (center) { + osmDetails = await updateClaimOSMDetails({ id: data.id, name: claim.name, center }); + if (!osmDetails) { + return Promise.reject('Failed to update OSM details for claim.'); + } + } + + const claim2 = await prisma.claim.update({ + where: { id: data.id, owner: { ssoId: data.userId } }, + data: { + area: data.area, + center: center, + buildings: buildingCount, + ...osmDetails, + }, + }); + + revalidatePath('/editor'); + return; + } catch (e) { + let msg = 'Unknown error'; + if (e instanceof Prisma.PrismaClientKnownRequestError) { + msg = e.code; + if (e.code === 'P2025') { + msg = 'Claim not found or you do not have permission to edit this claim.'; + } + } + return Promise.reject(msg); + } +}; +export const saveAdvancedClaim = async (data: { + id: string; + userId: string; + name?: string; + description?: string; + city?: string; + finished?: boolean; + active?: boolean; + builders?: { id: string }[]; +}): Promise => { + try { + const claim = await prisma.claim.findFirst({ + where: { id: data.id, owner: { ssoId: data.userId } }, + }); + + if (!claim) { + return Promise.reject('Claim not found or you do not have permission to edit this claim.'); + } + + const claim2 = await prisma.claim.update({ + where: { id: data.id, owner: { ssoId: data.userId } }, + data: { + name: data.name, + description: data.description, + city: data.city, + finished: data.finished, + active: data.active, + builders: data.builders ? { set: data.builders.map((b) => ({ id: b.id })) } : undefined, + }, + }); + + revalidatePath(`/editor/${data.id}`); + return; + } catch (e) { + let msg = 'Unknown error'; + if (e instanceof Prisma.PrismaClientKnownRequestError) { + msg = e.code; + if (e.code === 'P2025') { + msg = 'Claim not found or you do not have permission to edit this claim.'; + } + } + return Promise.reject(msg); + } +}; +export const createClaim = async (data: { + id: string; + userId: string; + area: string[]; + buildTeamId: string; +}): Promise => { + try { + const buildTeam = await prisma.buildTeam.findFirst({ + where: { id: data.buildTeamId, members: { some: { ssoId: data.userId } }, allowBuilderClaim: true }, + }); + + if (!buildTeam) { + return Promise.reject('You do not have permission to create a claim in this BuildTeam.'); + } + + let center = undefined; + if (data.area?.length > 0) { + center = turf.center(toPolygon(data.area)).geometry.coordinates.join(', '); + } + + const buildingCount = await updateClaimBuildingCount({ area: data.area }); + + if (typeof buildingCount !== 'number') { + if (buildingCount && typeof (buildingCount as { message?: string }).message === 'string') { + return Promise.reject((buildingCount as { message: string }).message); + } + return Promise.reject('Failed to set building count for claim.'); + } + + let osmDetails = undefined; + + if (center) { + osmDetails = await updateClaimOSMDetails({ id: data.id, center }); + if (!osmDetails) { + return Promise.reject('Failed to set OSM details for claim.'); + } + } + + const claim = await prisma.claim.create({ + data: { + id: data.id, + owner: { connect: { ssoId: data.userId } }, + buildTeam: { connect: { id: data.buildTeamId } }, + area: data.area, + center: center, + buildings: buildingCount, + active: true, + finished: false, + ...osmDetails, + }, + }); + + revalidatePath('/editor'); + return; + } catch (e) { + let msg = 'Unknown error'; + if (e instanceof Error) { + msg = e.message; + throw e; + } + if (e instanceof Prisma.PrismaClientKnownRequestError) { + msg = e.code; + if (e.code === 'P2025') { + msg = 'Claim not found or you do not have permission to edit this claim.'; + } + } + return Promise.reject(msg); + } +}; +export const deleteClaim = async (data: { id: string; userId: string }): Promise => { + try { + const claim = await prisma.claim.findFirst({ + where: { id: data.id, owner: { ssoId: data.userId } }, + }); + + if (!claim) { + return Promise.reject('Claim not found or you do not have permission to delete this claim.'); + } + + await prisma.claim.delete({ + where: { id: data.id, owner: { ssoId: data.userId } }, + }); + + revalidatePath('/editor'); + return; + } catch (e) { + let msg = 'Unknown error'; + if (e instanceof Prisma.PrismaClientKnownRequestError) { + msg = e.code; + if (e.code === 'P2025') { + msg = 'Claim not found or you do not have permission to delete this claim.'; + } + } + return Promise.reject(msg); + } +}; +export const transferClaim = async (data: { id: string; userId: string; newUserId: string }): Promise => { + try { + const claim = await prisma.claim.findFirst({ + where: { id: data.id, owner: { ssoId: data.userId } }, + include: { builders: { select: { id: true } } }, + }); + + if (!claim) { + return Promise.reject('Claim not found or you do not have permission to edit this claim.'); + } + + await prisma.claim.update({ + where: { id: data.id, owner: { ssoId: data.userId } }, + data: { + owner: { connect: { id: data.newUserId } }, + builders: { + set: [ + ...(claim.builders.filter((b: { id: string }) => b.id != data.newUserId) || []), + ...(claim.ownerId ? [{ id: claim.ownerId }] : []), + ], + }, + }, + }); + + revalidatePath('/editor'); + return; + } catch (e) { + let msg = 'Unknown error'; + if (e instanceof Prisma.PrismaClientKnownRequestError) { + msg = e.code; + if (e.code === 'P2025') { + msg = 'Claim not found or you do not have permission to delete this claim.'; + } + } + return Promise.reject(msg); + } +}; + +// export const createClaim = async (data: { +// id: string; +// userId: string; +// area: string[]; +// finished?: boolean; +// active?: boolean; +// description?: string; +// buildTeamId: string; +// city?: string; +// name?: string; +// }): Promise => { +// try { +// if (!data.area || data.area.length == 0) { +// return Promise.reject('Claim area is required.'); +// } + +// const buildteam = await prisma.buildTeam.findUnique({ +// where: { id: data.buildTeamId }, +// select: { +// allowBuilderClaim: true, +// id: true, +// members: { where: { ssoId: data.userId } }, +// }, +// }); + +// if (!buildteam) { +// return Promise.reject('BuildTeam not found.'); +// } + +// if (buildteam.allowBuilderClaim === false) { +// return Promise.reject('BuildTeam does not allow claims.'); +// } + +// if (buildteam.members.length <= 0) { +// return Promise.reject('You are not a member of this BuildTeam.'); +// } + +// let center = turf.center(toPolygon(data.area)).geometry.coordinates.join(', '); + +// const buildingCount = data.area && (await updateClaimBuildingCount({ area: data.area })); + +// if (typeof buildingCount !== 'number') { +// if (buildingCount && typeof (buildingCount as { message?: string }).message === 'string') { +// return Promise.reject((buildingCount as { message: string }).message); +// } +// return Promise.reject('Failed to get building count for claim.'); +// } + +// let osmDetails = await updateClaimOSMDetails({ id: data.id, name: data.name, center }); + +// if (!osmDetails) { +// return Promise.reject('Failed to update OSM details for claim.'); +// } + +// const claim = await prisma.claim.create({ +// data: { +// buildTeam: { connect: { id: data.buildTeamId } }, +// id: data.id, +// owner: { connect: { ssoId: data.userId } }, +// area: data.area, +// center: center, +// finished: data.finished, +// active: data.active, +// description: data.description, +// buildings: buildingCount, +// ...osmDetails, +// }, +// include: { +// buildTeam: { +// select: { +// webhook: true, +// }, +// }, +// }, +// }); + +// await sendBtWebhook(claim.buildTeam.webhook, WebhookType.CLAIM_CREATE, { +// ...claim, +// buildTeam: undefined, +// }); + +// revalidatePath('/editor'); +// return; +// } catch (e) { +// let msg = 'Unknown error'; +// if (e instanceof Prisma.PrismaClientKnownRequestError) { +// msg = e.code; +// if (e.code === 'P2025') { +// msg = 'Claim not found or you do not have permission to edit this claim.'; +// } +// } +// return Promise.reject(msg); +// } +// }; diff --git a/apps/dashboard/src/actions/user.ts b/apps/dashboard/src/actions/user.ts new file mode 100644 index 00000000..34a4a89d --- /dev/null +++ b/apps/dashboard/src/actions/user.ts @@ -0,0 +1,21 @@ +'use server'; +import prisma from '@/util/db'; +import keycloakAdmin from '@/util/keycloak'; + +export const editOwnProfile = async ( + prevState: any, + data: { email: string; username: string; ssoId: string }, +): Promise => { + try { + const user = await prisma.user.update({ + where: { ssoId: data.ssoId }, + data: { username: data.username }, + }); + await keycloakAdmin.users.update({ id: user.ssoId }, { username: data.username, email: data.email }); + const kcUser = await keycloakAdmin.users.findOne({ id: user.ssoId }); + return { status: 'success', user }; + } catch (error) { + console.error('Error updating user:', error); + return { status: 'error', error: 'Failed to update user' }; + } +}; diff --git a/apps/dashboard/src/app/(editorNavbar)/editor/[id]/interactivity.tsx b/apps/dashboard/src/app/(editorNavbar)/editor/[id]/interactivity.tsx new file mode 100644 index 00000000..e4324f84 --- /dev/null +++ b/apps/dashboard/src/app/(editorNavbar)/editor/[id]/interactivity.tsx @@ -0,0 +1,240 @@ +'use client'; +import { UserDisplay } from '@/components/data/User'; +import { UserSelect } from '@/components/input/UserSelect'; +import { + ActionIcon, + Badge, + Box, + Button, + Code, + Menu, + MenuDivider, + MenuDropdown, + MenuItem, + MenuTarget, + rem, + SimpleGrid, + Skeleton, + Switch, + Table, + Textarea, + TextInput, + Title, +} from '@mantine/core'; +import { IconDeviceFloppy, IconDots, IconTransfer, IconTrash } from '@tabler/icons-react'; +import { useSession } from 'next-auth/react'; +import { useEffect } from 'react'; +import { AdvancedClaimEditorClaim, useAdvancedClaimEditorStore } from './store'; + +export function AdvancedEditor({ initialClaim }: { initialClaim: AdvancedClaimEditorClaim | null }) { + const { claim, setClaim, setUserId, updateClaim, saveChanges, transferOwnership } = useAdvancedClaimEditorStore(); + const session = useSession(); + + useEffect(() => { + if (initialClaim && initialClaim.id != claim?.id) { + setClaim(initialClaim); + setUserId(session?.data?.user.id || 'XXXXX'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initialClaim]); + + return ( + + + + Claim Details + + updateClaim({ name: e.currentTarget.value })} + mb="md" + /> +