From e981bc160d009c970c84755ba2f3235eb3aeceb1 Mon Sep 17 00:00:00 2001 From: Hans Kristian Flaatten Date: Wed, 11 Feb 2026 21:19:44 +0100 Subject: [PATCH 01/24] feat: add contract templates and onboarding functionality - Implemented contract templates management with CRUD operations. - Added contract variables and context for dynamic contract generation. - Created onboarding process for sponsors, including token generation and validation. - Enhanced sponsor data model to include onboarding status and contract template references. - Introduced new schemas for contract templates and onboarding submissions. - Updated sponsor router to handle contract template operations and onboarding processes. --- .../index.ts | 21 + package.json | 1 + pnpm-lock.yaml | 345 +++++++++++ sanity/schema.ts | 2 + sanity/schemaTypes/conference.ts | 16 + sanity/schemaTypes/contractTemplate.ts | 163 +++++ sanity/schemaTypes/sponsor.ts | 14 + sanity/schemaTypes/sponsorForConference.ts | 84 +++ .../admin/sponsors/contracts/[id]/page.tsx | 34 ++ .../admin/sponsors/contracts/new/page.tsx | 28 + .../(admin)/admin/sponsors/contracts/page.tsx | 28 + .../sponsor/onboarding/[token]/page.tsx | 17 + src/app/(main)/sponsor/terms/page.tsx | 113 ++++ .../sponsor-crm/OnboardingLinkButton.tsx | 109 ++++ .../admin/sponsor-crm/SponsorCRMForm.tsx | 8 +- .../admin/sponsor-crm/form/constants.ts | 35 ++ src/components/admin/sponsor-crm/utils.ts | 47 ++ .../sponsor/ContractTemplateEditorPage.tsx | 572 ++++++++++++++++++ .../sponsor/ContractTemplateListPage.tsx | 205 +++++++ .../sponsor/SponsorOnboardingForm.tsx | 410 +++++++++++++ src/lib/conference/types.ts | 2 + src/lib/sponsor-crm/action-items.ts | 52 ++ src/lib/sponsor-crm/activity.ts | 51 ++ src/lib/sponsor-crm/contract-pdf.tsx | 462 ++++++++++++++ src/lib/sponsor-crm/contract-templates.ts | 270 +++++++++ src/lib/sponsor-crm/contract-variables.ts | 169 ++++++ src/lib/sponsor-crm/onboarding.ts | 137 +++++ src/lib/sponsor-crm/sanity.ts | 60 +- src/lib/sponsor-crm/types.ts | 55 ++ src/server/_app.ts | 2 + src/server/routers/onboarding.ts | 81 +++ src/server/routers/sponsor.ts | 257 ++++++++ src/server/schemas/contractTemplate.ts | 60 ++ src/server/schemas/onboarding.ts | 34 ++ src/server/schemas/sponsorForConference.ts | 19 + 35 files changed, 3951 insertions(+), 12 deletions(-) create mode 100644 migrations/033-add-signature-onboarding-fields/index.ts create mode 100644 sanity/schemaTypes/contractTemplate.ts create mode 100644 src/app/(admin)/admin/sponsors/contracts/[id]/page.tsx create mode 100644 src/app/(admin)/admin/sponsors/contracts/new/page.tsx create mode 100644 src/app/(admin)/admin/sponsors/contracts/page.tsx create mode 100644 src/app/(main)/sponsor/onboarding/[token]/page.tsx create mode 100644 src/app/(main)/sponsor/terms/page.tsx create mode 100644 src/components/admin/sponsor-crm/OnboardingLinkButton.tsx create mode 100644 src/components/admin/sponsor/ContractTemplateEditorPage.tsx create mode 100644 src/components/admin/sponsor/ContractTemplateListPage.tsx create mode 100644 src/components/sponsor/SponsorOnboardingForm.tsx create mode 100644 src/lib/sponsor-crm/contract-pdf.tsx create mode 100644 src/lib/sponsor-crm/contract-templates.ts create mode 100644 src/lib/sponsor-crm/contract-variables.ts create mode 100644 src/lib/sponsor-crm/onboarding.ts create mode 100644 src/server/routers/onboarding.ts create mode 100644 src/server/schemas/contractTemplate.ts create mode 100644 src/server/schemas/onboarding.ts diff --git a/migrations/033-add-signature-onboarding-fields/index.ts b/migrations/033-add-signature-onboarding-fields/index.ts new file mode 100644 index 00000000..65f3bb45 --- /dev/null +++ b/migrations/033-add-signature-onboarding-fields/index.ts @@ -0,0 +1,21 @@ +import { defineMigration, at, setIfMissing } from 'sanity/migrate' + +export default defineMigration({ + title: 'Add signature and onboarding fields to sponsorForConference', + description: + 'Backfills default values for new signatureStatus, reminderCount, and onboardingComplete fields ' + + 'on all sponsorForConference documents to support contract signing and sponsor onboarding features.', + documentTypes: ['sponsorForConference'], + + migrate: { + document(_doc) { + const operations = [] + + operations.push(at('signatureStatus', setIfMissing('not-started'))) + operations.push(at('reminderCount', setIfMissing(0))) + operations.push(at('onboardingComplete', setIfMissing(false))) + + return operations + }, + }, +}) diff --git a/package.json b/package.json index 5d366587..9b1ee7a4 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@portabletext/to-html": "^5.0.1", "@portabletext/types": "^4.0.1", "@react-email/render": "^2.0.4", + "@react-pdf/renderer": "^4.3.2", "@sanity/image-url": "^2.0.3", "@sanity/types": "^5.8.1", "@tailwindcss/forms": "^0.5.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13655e21..a1b23780 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: '@react-email/render': specifier: ^2.0.4 version: 2.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@react-pdf/renderer': + specifier: ^4.3.2 + version: 4.3.2(react@19.2.4) '@sanity/image-url': specifier: ^2.0.3 version: 2.0.3 @@ -2430,6 +2433,49 @@ packages: react: ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-pdf/fns@3.1.2': + resolution: {integrity: sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g==} + + '@react-pdf/font@4.0.4': + resolution: {integrity: sha512-8YtgGtL511txIEc9AjiilpZ7yjid8uCd8OGUl6jaL3LIHnrToUupSN4IzsMQpVTCMYiDLFnDNQzpZsOYtRS/Pg==} + + '@react-pdf/image@3.0.4': + resolution: {integrity: sha512-z0ogVQE0bKqgXQ5smgzIU857rLV7bMgVdrYsu3UfXDDLSzI7QPvzf6MFTFllX6Dx2rcsF13E01dqKPtJEM799g==} + + '@react-pdf/layout@4.4.2': + resolution: {integrity: sha512-gNu2oh8MiGR+NJZYTJ4c4q0nWCESBI6rKFiodVhE7OeVAjtzZzd6l65wsN7HXdWJqOZD3ttD97iE+tf5SOd/Yg==} + + '@react-pdf/pdfkit@4.1.0': + resolution: {integrity: sha512-Wm/IOAv0h/U5Ra94c/PltFJGcpTUd/fwVMVeFD6X9tTTPCttIwg0teRG1Lqq617J8K4W7jpL/B0HTH0mjp3QpQ==} + + '@react-pdf/png-js@3.0.0': + resolution: {integrity: sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA==} + + '@react-pdf/primitives@4.1.1': + resolution: {integrity: sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ==} + + '@react-pdf/reconciler@2.0.0': + resolution: {integrity: sha512-7zaPRujpbHSmCpIrZ+b9HSTJHthcVZzX0Wx7RzvQGsGBUbHP4p6s5itXrAIOuQuPvDepoHGNOvf6xUuMVvdoyw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@react-pdf/render@4.3.2': + resolution: {integrity: sha512-el5KYM1sH/PKcO4tRCIm8/AIEmhtraaONbwCrBhFdehoGv6JtgnXiMxHGAvZbI5kEg051GbyP+XIU6f6YbOu6Q==} + + '@react-pdf/renderer@4.3.2': + resolution: {integrity: sha512-EhPkj35gO9rXIyyx29W3j3axemvVY5RigMmlK4/6Ku0pXB8z9PEE/sz4ZBOShu2uot6V4xiCR3aG+t9IjJJlBQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@react-pdf/stylesheet@6.1.2': + resolution: {integrity: sha512-E3ftGRYUQGKiN3JOgtGsLDo0hGekA6dmkmi/MYACytmPTKxQRBSO3126MebmCq+t1rgU9uRlREIEawJ+8nzSbw==} + + '@react-pdf/textkit@6.1.0': + resolution: {integrity: sha512-sFlzDC9CDFrJsnL3B/+NHrk9+Advqk7iJZIStiYQDdskbow8GF/AGYrpIk+vWSnh35YxaGbHkqXq53XOxnyrjQ==} + + '@react-pdf/types@2.9.2': + resolution: {integrity: sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g==} + '@react-stately/flags@3.1.2': resolution: {integrity: sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==} @@ -3720,6 +3766,9 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + abs-svg-path@0.1.1: + resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -3997,6 +4046,10 @@ packages: resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} engines: {node: '>= 0.6.0'} + base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -4038,9 +4091,15 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + browserify-zlib@0.1.4: resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==} + browserify-zlib@0.2.0: + resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} + browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -4206,6 +4265,10 @@ packages: resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} engines: {node: '>=6'} + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -4233,6 +4296,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color2k@2.0.3: resolution: {integrity: sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==} @@ -4306,6 +4372,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + crypto-random-string@2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} @@ -4473,6 +4542,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + diff@4.0.4: resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} engines: {node: '>=0.3.1'} @@ -4552,6 +4624,9 @@ packages: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -4935,6 +5010,9 @@ packages: debug: optional: true + fontkit@2.0.4: + resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==} + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -5175,6 +5253,12 @@ packages: hotscript@1.0.13: resolution: {integrity: sha512-C++tTF1GqkGYecL+2S1wJTfoH6APGAsbb7PAWQ3iVIwgG/EFseAfEVOKFgAFq4yK3+6j1EjUD4UQ9dRJHX/sSQ==} + hsl-to-hex@1.0.0: + resolution: {integrity: sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==} + + hsl-to-rgb-for-reals@1.1.1: + resolution: {integrity: sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -5215,6 +5299,9 @@ packages: humanize-list@1.0.1: resolution: {integrity: sha512-4+p3fCRF21oUqxhK0yZ6yaSP/H5/wZumc7q1fH99RkW7Q13aAxDeP78BKjoR+6y+kaHqKF/JWuQhsNuuI2NKtA==} + hyphen@1.14.1: + resolution: {integrity: sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==} + i18next@23.16.8: resolution: {integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==} @@ -5312,6 +5399,9 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} @@ -5480,6 +5570,9 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} + is-url@1.2.4: + resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -5553,6 +5646,9 @@ packages: engines: {node: '>=10'} hasBin: true + jay-peg@1.1.1: + resolution: {integrity: sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==} + jest-changed-files@30.2.0: resolution: {integrity: sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -5945,6 +6041,9 @@ packages: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -6063,6 +6162,9 @@ packages: media-chrome@4.17.2: resolution: {integrity: sha512-o/IgiHx0tdSVwRxxqF5H12FK31A/A8T71sv3KdAvh7b6XeBS9dXwqvIFwlR9kdEuqg3n7xpmRIuL83rmYq8FTg==} + media-engine@1.0.3: + resolution: {integrity: sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==} + media-tracks@0.3.4: resolution: {integrity: sha512-5SUElzGMYXA7bcyZBL1YzLTxH9Iyw1AeYNJxzByqbestrrtB0F3wfiWUr7aROpwodO4fwnxOt78Xjb3o3ONNQg==} @@ -6278,6 +6380,9 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + normalize-svg-path@1.1.0: + resolution: {integrity: sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==} + npm-run-path@3.1.0: resolution: {integrity: sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==} engines: {node: '>=8'} @@ -6424,6 +6529,9 @@ packages: pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -6443,6 +6551,9 @@ packages: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} + parse-svg-path@0.1.2: + resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -6720,6 +6831,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} @@ -6950,6 +7064,9 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + restructure@3.0.2: + resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} + retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -7021,6 +7138,9 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + scheduler@0.25.0-rc-603e6108-20241029: + resolution: {integrity: sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -7115,6 +7235,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + simple-wcswidth@1.1.2: resolution: {integrity: sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==} @@ -7345,6 +7468,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svg-arc-to-cubic-bezier@3.2.0: + resolution: {integrity: sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==} + svix@1.84.1: resolution: {integrity: sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==} @@ -7398,6 +7524,9 @@ packages: through2@4.0.2: resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tiny-invariant@1.3.1: resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} @@ -7627,10 +7756,16 @@ packages: resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} engines: {node: '>=4'} + unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + unicode-property-aliases-ecmascript@2.2.0: resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} engines: {node: '>=4'} + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -7759,6 +7894,10 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + vite-compatible-readable-stream@3.6.1: + resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==} + engines: {node: '>= 6'} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -8027,6 +8166,9 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} + yoga-layout@3.2.1: + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + zip-stream@6.0.1: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} @@ -10344,6 +10486,107 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + '@react-pdf/fns@3.1.2': {} + + '@react-pdf/font@4.0.4': + dependencies: + '@react-pdf/pdfkit': 4.1.0 + '@react-pdf/types': 2.9.2 + fontkit: 2.0.4 + is-url: 1.2.4 + + '@react-pdf/image@3.0.4': + dependencies: + '@react-pdf/png-js': 3.0.0 + jay-peg: 1.1.1 + + '@react-pdf/layout@4.4.2': + dependencies: + '@react-pdf/fns': 3.1.2 + '@react-pdf/image': 3.0.4 + '@react-pdf/primitives': 4.1.1 + '@react-pdf/stylesheet': 6.1.2 + '@react-pdf/textkit': 6.1.0 + '@react-pdf/types': 2.9.2 + emoji-regex-xs: 1.0.0 + queue: 6.0.2 + yoga-layout: 3.2.1 + + '@react-pdf/pdfkit@4.1.0': + dependencies: + '@babel/runtime': 7.28.6 + '@react-pdf/png-js': 3.0.0 + browserify-zlib: 0.2.0 + crypto-js: 4.2.0 + fontkit: 2.0.4 + jay-peg: 1.1.1 + linebreak: 1.1.0 + vite-compatible-readable-stream: 3.6.1 + + '@react-pdf/png-js@3.0.0': + dependencies: + browserify-zlib: 0.2.0 + + '@react-pdf/primitives@4.1.1': {} + + '@react-pdf/reconciler@2.0.0(react@19.2.4)': + dependencies: + object-assign: 4.1.1 + react: 19.2.4 + scheduler: 0.25.0-rc-603e6108-20241029 + + '@react-pdf/render@4.3.2': + dependencies: + '@babel/runtime': 7.28.6 + '@react-pdf/fns': 3.1.2 + '@react-pdf/primitives': 4.1.1 + '@react-pdf/textkit': 6.1.0 + '@react-pdf/types': 2.9.2 + abs-svg-path: 0.1.1 + color-string: 1.9.1 + normalize-svg-path: 1.1.0 + parse-svg-path: 0.1.2 + svg-arc-to-cubic-bezier: 3.2.0 + + '@react-pdf/renderer@4.3.2(react@19.2.4)': + dependencies: + '@babel/runtime': 7.28.6 + '@react-pdf/fns': 3.1.2 + '@react-pdf/font': 4.0.4 + '@react-pdf/layout': 4.4.2 + '@react-pdf/pdfkit': 4.1.0 + '@react-pdf/primitives': 4.1.1 + '@react-pdf/reconciler': 2.0.0(react@19.2.4) + '@react-pdf/render': 4.3.2 + '@react-pdf/types': 2.9.2 + events: 3.3.0 + object-assign: 4.1.1 + prop-types: 15.8.1 + queue: 6.0.2 + react: 19.2.4 + + '@react-pdf/stylesheet@6.1.2': + dependencies: + '@react-pdf/fns': 3.1.2 + '@react-pdf/types': 2.9.2 + color-string: 1.9.1 + hsl-to-hex: 1.0.0 + media-engine: 1.0.3 + postcss-value-parser: 4.2.0 + + '@react-pdf/textkit@6.1.0': + dependencies: + '@react-pdf/fns': 3.1.2 + bidi-js: 1.0.3 + hyphen: 1.14.1 + unicode-properties: 1.4.1 + + '@react-pdf/types@2.9.2': + dependencies: + '@react-pdf/font': 4.0.4 + '@react-pdf/primitives': 4.1.1 + '@react-pdf/stylesheet': 6.1.2 + '@react-stately/flags@3.1.2': dependencies: '@swc/helpers': 0.5.18 @@ -11903,6 +12146,8 @@ snapshots: dependencies: event-target-shim: 5.0.1 + abs-svg-path@0.1.1: {} + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -12231,6 +12476,8 @@ snapshots: base64-arraybuffer@1.0.2: {} + base64-js@0.0.8: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.9.19: {} @@ -12277,10 +12524,18 @@ snapshots: dependencies: fill-range: 7.1.1 + brotli@1.3.3: + dependencies: + base64-js: 1.5.1 + browserify-zlib@0.1.4: dependencies: pako: 0.2.9 + browserify-zlib@0.2.0: + dependencies: + pako: 1.0.11 + browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.19 @@ -12440,6 +12695,8 @@ snapshots: kind-of: 6.0.3 shallow-clone: 3.0.1 + clone@2.1.2: {} + clsx@2.1.1: {} co@4.6.0: {} @@ -12468,6 +12725,11 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + color2k@2.0.3: {} combined-stream@1.0.8: @@ -12544,6 +12806,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-js@4.2.0: {} + crypto-random-string@2.0.0: {} css-color-keywords@1.0.0: {} @@ -12692,6 +12956,8 @@ snapshots: detect-node-es@1.1.0: {} + dfa@1.2.0: {} + diff@4.0.4: {} dijkstrajs@1.0.3: {} @@ -12767,6 +13033,8 @@ snapshots: emittery@0.13.1: {} + emoji-regex-xs@1.0.0: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -13350,6 +13618,18 @@ snapshots: optionalDependencies: debug: 4.4.3(supports-color@8.1.1) + fontkit@2.0.4: + dependencies: + '@swc/helpers': 0.5.18 + brotli: 1.3.3 + clone: 2.1.2 + dfa: 1.2.0 + fast-deep-equal: 3.1.3 + restructure: 3.0.2 + tiny-inflate: 1.0.3 + unicode-properties: 1.4.1 + unicode-trie: 2.0.0 + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -13613,6 +13893,12 @@ snapshots: hotscript@1.0.13: {} + hsl-to-hex@1.0.0: + dependencies: + hsl-to-rgb-for-reals: 1.1.1 + + hsl-to-rgb-for-reals@1.1.1: {} + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -13667,6 +13953,8 @@ snapshots: humanize-list@1.0.1: {} + hyphen@1.14.1: {} + i18next@23.16.8: dependencies: '@babel/runtime': 7.28.6 @@ -13759,6 +14047,8 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.4: {} + is-async-function@2.1.1: dependencies: async-function: 1.0.0 @@ -13902,6 +14192,8 @@ snapshots: is-unicode-supported@2.1.0: {} + is-url@1.2.4: {} + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -13998,6 +14290,10 @@ snapshots: filelist: 1.0.4 picocolors: 1.1.1 + jay-peg@1.1.1: + dependencies: + restructure: 3.0.2 + jest-changed-files@30.2.0: dependencies: execa: 5.1.1 @@ -14594,6 +14890,11 @@ snapshots: lilconfig@3.1.3: {} + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + lines-and-columns@1.2.4: {} linkify-it@5.0.0: @@ -14731,6 +15032,8 @@ snapshots: transitivePeerDependencies: - react + media-engine@1.0.3: {} + media-tracks@0.3.4: {} mendoza@3.0.8: {} @@ -14913,6 +15216,10 @@ snapshots: normalize-path@3.0.0: {} + normalize-svg-path@1.1.0: + dependencies: + svg-arc-to-cubic-bezier: 3.2.0 + npm-run-path@3.1.0: dependencies: path-key: 3.1.1 @@ -15091,6 +15398,8 @@ snapshots: pako@0.2.9: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -15116,6 +15425,8 @@ snapshots: parse-ms@4.0.0: {} + parse-svg-path@0.1.2: {} + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -15326,6 +15637,10 @@ snapshots: queue-microtask@1.2.3: {} + queue@6.0.2: + dependencies: + inherits: 2.0.4 + quick-lru@5.1.1: {} quick-lru@7.3.0: {} @@ -15577,6 +15892,8 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + restructure@3.0.2: {} + retry@0.13.1: {} reusify@1.1.0: {} @@ -15845,6 +16162,8 @@ snapshots: dependencies: xmlchars: 2.2.0 + scheduler@0.25.0-rc-603e6108-20241029: {} + scheduler@0.27.0: {} scroll-into-view-if-needed@3.1.0: @@ -15971,6 +16290,10 @@ snapshots: signal-exit@4.1.0: {} + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + simple-wcswidth@1.1.2: {} slash@3.0.0: {} @@ -16226,6 +16549,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svg-arc-to-cubic-bezier@3.2.0: {} + svix@1.84.1: dependencies: standardwebhooks: 1.0.0 @@ -16296,6 +16621,8 @@ snapshots: dependencies: readable-stream: 3.6.2 + tiny-inflate@1.0.3: {} + tiny-invariant@1.3.1: {} tinyglobby@0.2.15: @@ -16519,8 +16846,18 @@ snapshots: unicode-match-property-value-ecmascript@2.2.1: {} + unicode-properties@1.4.1: + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 + unicode-property-aliases-ecmascript@2.2.0: {} + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + unicorn-magic@0.3.0: {} unique-string@2.0.0: @@ -16650,6 +16987,12 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + vite-compatible-readable-stream@3.6.1: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + vite-node@3.2.4(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 @@ -16921,6 +17264,8 @@ snapshots: yoctocolors@2.1.2: {} + yoga-layout@3.2.1: {} + zip-stream@6.0.1: dependencies: archiver-utils: 5.0.2 diff --git a/sanity/schema.ts b/sanity/schema.ts index 6d6bc6bd..9dfb9226 100644 --- a/sanity/schema.ts +++ b/sanity/schema.ts @@ -3,6 +3,7 @@ import { type SchemaTypeDefinition } from 'sanity' import { fileAttachment, urlAttachment } from './schemaTypes/attachment' import blockContent from './schemaTypes/blockContent' import conference from './schemaTypes/conference' +import contractTemplate from './schemaTypes/contractTemplate' import coSpeakerInvitation from './schemaTypes/coSpeakerInvitation' import dashboardConfig from './schemaTypes/dashboardConfig' import dataProcessingConsent from './schemaTypes/dataProcessingConsent' @@ -57,5 +58,6 @@ export const schema: { types: SchemaTypeDefinition[] } = { sponsorForConference, sponsorActivity, sponsorEmailTemplate, + contractTemplate, ], } diff --git a/sanity/schemaTypes/conference.ts b/sanity/schemaTypes/conference.ts index 2fa2f4ac..b74d131a 100644 --- a/sanity/schemaTypes/conference.ts +++ b/sanity/schemaTypes/conference.ts @@ -81,6 +81,22 @@ export default defineType({ fieldset: 'basicInfo', validation: (Rule) => Rule.required(), }), + defineField({ + name: 'organizerOrgNumber', + title: 'Organiser Org Number', + type: 'string', + fieldset: 'basicInfo', + description: + 'Organization number of the organiser (used in contracts and invoices)', + }), + defineField({ + name: 'organizerAddress', + title: 'Organiser Address', + type: 'string', + fieldset: 'basicInfo', + description: + 'Registered address of the organiser (used in contracts and invoices)', + }), defineField({ name: 'city', title: 'City', diff --git a/sanity/schemaTypes/contractTemplate.ts b/sanity/schemaTypes/contractTemplate.ts new file mode 100644 index 00000000..84320d45 --- /dev/null +++ b/sanity/schemaTypes/contractTemplate.ts @@ -0,0 +1,163 @@ +import { defineField, defineType } from 'sanity' +import { CURRENCY_OPTIONS } from './constants' + +export default defineType({ + name: 'contractTemplate', + title: 'Contract Template', + type: 'document', + fields: [ + defineField({ + name: 'title', + title: 'Title', + type: 'string', + description: 'Internal name for this contract template', + validation: (Rule) => Rule.required(), + }), + defineField({ + name: 'conference', + title: 'Conference', + type: 'reference', + to: [{ type: 'conference' }], + description: 'Conference this template belongs to', + validation: (Rule) => Rule.required(), + }), + defineField({ + name: 'tier', + title: 'Default Tier', + type: 'reference', + to: [{ type: 'sponsorTier' }], + description: + 'Optional: associate this template with a specific sponsor tier', + options: { + filter: ({ document }: { document: any }) => { + if (!document?.conference?._ref) return {} + + return { + filter: 'conference._ref == $conferenceId', + params: { conferenceId: document.conference._ref }, + } + }, + }, + }), + defineField({ + name: 'language', + title: 'Language', + type: 'string', + options: { + list: [ + { title: 'Norwegian (Bokmål)', value: 'nb' }, + { title: 'English', value: 'en' }, + ], + layout: 'radio', + }, + initialValue: 'nb', + validation: (Rule) => Rule.required(), + }), + defineField({ + name: 'currency', + title: 'Default Currency', + type: 'string', + options: { + list: [...CURRENCY_OPTIONS], + layout: 'dropdown', + }, + initialValue: 'NOK', + }), + defineField({ + name: 'sections', + title: 'Contract Sections', + type: 'array', + description: + 'Ordered list of sections that make up the contract document', + of: [ + { + type: 'object', + name: 'contractSection', + title: 'Section', + fields: [ + { + name: 'heading', + title: 'Section Heading', + type: 'string', + validation: (Rule) => Rule.required(), + }, + { + name: 'body', + title: 'Section Body', + type: 'blockContent', + description: + 'Use {{{VARIABLE_NAME}}} for dynamic values (e.g. {{{SPONSOR_NAME}}}, {{{TIER_NAME}}}, {{{CONTRACT_VALUE}}})', + }, + ], + preview: { + select: { + title: 'heading', + }, + }, + }, + ], + validation: (Rule) => Rule.required().min(1), + }), + defineField({ + name: 'headerText', + title: 'Header Text', + type: 'string', + description: 'Text shown in the PDF header (e.g. organization name)', + initialValue: 'Cloud Native Days Norway', + }), + defineField({ + name: 'footerText', + title: 'Footer Text', + type: 'string', + description: + 'Text shown in the PDF footer (e.g. org number, contact info)', + }), + defineField({ + name: 'terms', + title: 'General Terms & Conditions', + type: 'blockContent', + description: + 'General terms and conditions included as Appendix 1 in the contract PDF and displayed on the public sponsor terms page', + }), + defineField({ + name: 'isDefault', + title: 'Default Template', + type: 'boolean', + description: + 'Use this template as the default when no tier-specific template exists', + initialValue: false, + }), + defineField({ + name: 'isActive', + title: 'Active', + type: 'boolean', + description: 'Whether this template is available for use', + initialValue: true, + }), + ], + preview: { + select: { + title: 'title', + conferenceName: 'conference.title', + tierName: 'tier.title', + language: 'language', + isActive: 'isActive', + }, + prepare({ title, conferenceName, tierName, language, isActive }) { + const lang = language === 'nb' ? '🇳🇴' : '🇬🇧' + const status = isActive === false ? ' (inactive)' : '' + const tier = tierName ? ` — ${tierName}` : '' + return { + title: `${lang} ${title}${status}`, + subtitle: `${conferenceName || 'No Conference'}${tier}`, + } + }, + }, + orderings: [ + { + title: 'Title', + name: 'title', + by: [{ field: 'title', direction: 'asc' }], + }, + ], +}) diff --git a/sanity/schemaTypes/sponsor.ts b/sanity/schemaTypes/sponsor.ts index 49a58cf2..0ba037f4 100644 --- a/sanity/schemaTypes/sponsor.ts +++ b/sanity/schemaTypes/sponsor.ts @@ -43,6 +43,20 @@ export default defineType({ ) }, }), + defineField({ + name: 'address', + title: 'Address', + type: 'string', + description: 'Registered company address (used in contracts)', + hidden: ({ currentUser }) => { + return !( + currentUser != null && + currentUser.roles.find( + ({ name }) => name === 'administrator' || name === 'editor', + ) + ) + }, + }), ], preview: { select: { diff --git a/sanity/schemaTypes/sponsorForConference.ts b/sanity/schemaTypes/sponsorForConference.ts index 04988313..92355ffe 100644 --- a/sanity/schemaTypes/sponsorForConference.ts +++ b/sanity/schemaTypes/sponsorForConference.ts @@ -87,6 +87,68 @@ export default defineType({ initialValue: 'none', validation: (Rule) => Rule.required(), }), + defineField({ + name: 'signatureStatus', + title: 'Signature Status', + type: 'string', + description: 'Digital signature status from e-signing provider', + options: { + list: [ + { title: 'Not Started', value: 'not-started' }, + { title: 'Pending', value: 'pending' }, + { title: 'Signed', value: 'signed' }, + { title: 'Rejected', value: 'rejected' }, + { title: 'Expired', value: 'expired' }, + ], + layout: 'dropdown', + }, + initialValue: 'not-started', + }), + defineField({ + name: 'signatureId', + title: 'Signature ID', + type: 'string', + description: 'External ID from e-signing provider (e.g. Posten.no)', + readOnly: true, + }), + defineField({ + name: 'signerEmail', + title: 'Signer Email', + type: 'string', + description: 'Email of the person who should sign the contract', + }), + defineField({ + name: 'contractSentAt', + title: 'Contract Sent Date', + type: 'datetime', + description: 'When the contract was sent for signing', + readOnly: true, + }), + defineField({ + name: 'contractDocument', + title: 'Contract Document', + type: 'file', + description: 'Generated PDF contract document', + options: { + accept: 'application/pdf', + }, + }), + defineField({ + name: 'reminderCount', + title: 'Reminder Count', + type: 'number', + description: 'Number of contract signing reminders sent', + initialValue: 0, + readOnly: true, + validation: (Rule) => Rule.min(0), + }), + defineField({ + name: 'contractTemplate', + title: 'Contract Template', + type: 'reference', + to: [{ type: 'contractTemplate' }], + description: 'Template used to generate the contract', + }), defineField({ name: 'status', title: 'Status', @@ -317,6 +379,28 @@ export default defineType({ ) }, }), + defineField({ + name: 'onboardingToken', + title: 'Onboarding Token', + type: 'string', + description: 'Unique token for sponsor self-service onboarding portal', + readOnly: true, + }), + defineField({ + name: 'onboardingComplete', + title: 'Onboarding Complete', + type: 'boolean', + description: 'Whether the sponsor has completed onboarding', + initialValue: false, + readOnly: true, + }), + defineField({ + name: 'onboardingCompletedAt', + title: 'Onboarding Completed At', + type: 'datetime', + description: 'When the sponsor completed onboarding', + readOnly: true, + }), ], preview: { select: { diff --git a/src/app/(admin)/admin/sponsors/contracts/[id]/page.tsx b/src/app/(admin)/admin/sponsors/contracts/[id]/page.tsx new file mode 100644 index 00000000..c90977a5 --- /dev/null +++ b/src/app/(admin)/admin/sponsors/contracts/[id]/page.tsx @@ -0,0 +1,34 @@ +import { getConferenceForCurrentDomain } from '@/lib/conference/sanity' +import { ErrorDisplay } from '@/components/admin' +import { ContractTemplateEditorPage } from '@/components/admin/sponsor/ContractTemplateEditorPage' + +export default async function EditContractTemplatePage({ + params, +}: { + params: Promise<{ id: string }> +}) { + const { id } = await params + + const { conference, error: conferenceError } = + await getConferenceForCurrentDomain({}) + + if (conferenceError) { + return ( + + ) + } + + if (!conference) { + return ( + + ) + } + + return +} diff --git a/src/app/(admin)/admin/sponsors/contracts/new/page.tsx b/src/app/(admin)/admin/sponsors/contracts/new/page.tsx new file mode 100644 index 00000000..7e69d813 --- /dev/null +++ b/src/app/(admin)/admin/sponsors/contracts/new/page.tsx @@ -0,0 +1,28 @@ +import { getConferenceForCurrentDomain } from '@/lib/conference/sanity' +import { ErrorDisplay } from '@/components/admin' +import { ContractTemplateEditorPage } from '@/components/admin/sponsor/ContractTemplateEditorPage' + +export default async function NewContractTemplatePage() { + const { conference, error: conferenceError } = + await getConferenceForCurrentDomain({}) + + if (conferenceError) { + return ( + + ) + } + + if (!conference) { + return ( + + ) + } + + return +} diff --git a/src/app/(admin)/admin/sponsors/contracts/page.tsx b/src/app/(admin)/admin/sponsors/contracts/page.tsx new file mode 100644 index 00000000..c75c992d --- /dev/null +++ b/src/app/(admin)/admin/sponsors/contracts/page.tsx @@ -0,0 +1,28 @@ +import { getConferenceForCurrentDomain } from '@/lib/conference/sanity' +import { ErrorDisplay } from '@/components/admin' +import { ContractTemplateListPage } from '@/components/admin/sponsor/ContractTemplateListPage' + +export default async function AdminContractTemplates() { + const { conference, error: conferenceError } = + await getConferenceForCurrentDomain({}) + + if (conferenceError) { + return ( + + ) + } + + if (!conference) { + return ( + + ) + } + + return +} diff --git a/src/app/(main)/sponsor/onboarding/[token]/page.tsx b/src/app/(main)/sponsor/onboarding/[token]/page.tsx new file mode 100644 index 00000000..808e240e --- /dev/null +++ b/src/app/(main)/sponsor/onboarding/[token]/page.tsx @@ -0,0 +1,17 @@ +import { SponsorOnboardingForm } from '@/components/sponsor/SponsorOnboardingForm' + +interface OnboardingPageProps { + params: Promise<{ token: string }> +} + +export default async function SponsorOnboardingPage({ + params, +}: OnboardingPageProps) { + const { token } = await params + + return ( +
+ +
+ ) +} diff --git a/src/app/(main)/sponsor/terms/page.tsx b/src/app/(main)/sponsor/terms/page.tsx new file mode 100644 index 00000000..0d9abb82 --- /dev/null +++ b/src/app/(main)/sponsor/terms/page.tsx @@ -0,0 +1,113 @@ +import { Container } from '@/components/Container' +import { Button } from '@/components/Button' +import { getConferenceForDomain } from '@/lib/conference/sanity' +import { getTermsForConference } from '@/lib/sponsor-crm/contract-templates' +import { PortableText } from '@portabletext/react' +import { portableTextComponents } from '@/lib/portabletext/components' +import { cacheLife, cacheTag } from 'next/cache' +import { headers } from 'next/headers' + +export const metadata = { + title: 'Sponsorship Terms & Conditions - Cloud Native Days Norway', + description: + 'General terms and conditions for sponsorship of Cloud Native Days Norway', +} + +async function CachedTermsContent({ domain }: { domain: string }) { + 'use cache' + cacheLife('days') + cacheTag('content:sponsor-terms') + + const { conference, error: confError } = await getConferenceForDomain(domain) + + if (confError || !conference) { + return ( +
+

+ Unable to load terms +

+

+ We're experiencing technical difficulties. Please try again + later. +

+ +
+ ) + } + + const { + terms, + conferenceName, + error: termsError, + } = await getTermsForConference(conference._id) + + if (termsError || !terms) { + return ( +
+ +
+

+ Sponsorship Terms & Conditions +

+

+ Terms and conditions for {conference.title} sponsorship are not + yet available. Please contact us at{' '} + + {conference.sponsorEmail} + {' '} + for more information. +

+
+ +
+
+
+
+ ) + } + + return ( +
+ +
+

+ General Terms & Conditions +

+

+ {conferenceName || conference.title} Sponsorship Agreement +

+ +
+ +
+ +
+

+ For questions about these terms, please contact{' '} + + {conference.sponsorEmail} + +

+
+
+
+
+ ) +} + +export default async function SponsorTermsPage() { + const headersList = await headers() + const domain = headersList.get('host') || '' + + return +} diff --git a/src/components/admin/sponsor-crm/OnboardingLinkButton.tsx b/src/components/admin/sponsor-crm/OnboardingLinkButton.tsx new file mode 100644 index 00000000..8cffee70 --- /dev/null +++ b/src/components/admin/sponsor-crm/OnboardingLinkButton.tsx @@ -0,0 +1,109 @@ +'use client' + +import { useState } from 'react' +import { api } from '@/lib/trpc/client' +import { + LinkIcon, + ClipboardDocumentIcon, + CheckIcon, +} from '@heroicons/react/24/outline' + +interface OnboardingLinkButtonProps { + sponsorForConferenceId: string + existingToken?: string + onboardingComplete?: boolean +} + +export function OnboardingLinkButton({ + sponsorForConferenceId, + existingToken, + onboardingComplete, +}: OnboardingLinkButtonProps) { + const [showLink, setShowLink] = useState(false) + const [generatedUrl, setGeneratedUrl] = useState(null) + const [copied, setCopied] = useState(false) + + const generateMutation = api.onboarding.generateToken.useMutation({ + onSuccess: (data) => { + setGeneratedUrl(data.url) + setShowLink(true) + }, + }) + + const handleGenerate = () => { + generateMutation.mutate({ sponsorForConferenceId }) + } + + const handleCopy = async () => { + if (!generatedUrl) return + await navigator.clipboard.writeText(generatedUrl) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + if (onboardingComplete) { + return ( + + + Onboarded + + ) + } + + if (showLink && generatedUrl) { + return ( +
+ + + +
+ ) + } + + return ( + + ) +} diff --git a/src/components/admin/sponsor-crm/SponsorCRMForm.tsx b/src/components/admin/sponsor-crm/SponsorCRMForm.tsx index 0a91a086..16bf1728 100644 --- a/src/components/admin/sponsor-crm/SponsorCRMForm.tsx +++ b/src/components/admin/sponsor-crm/SponsorCRMForm.tsx @@ -42,6 +42,7 @@ import { SponsorLogoEditor } from '../sponsor/SponsorLogoEditor' import { SponsorActivityTimeline } from '../sponsor/SponsorActivityTimeline' import { SponsorTier } from '@/lib/sponsor/types' import { useSponsorCRMFormMutations } from '@/hooks/useSponsorCRMFormMutations' +import { OnboardingLinkButton } from './OnboardingLinkButton' type FormView = 'pipeline' | 'contacts' | 'logo' | 'history' @@ -278,6 +279,11 @@ export function SponsorCRMForm({
{sponsor && ( <> + + {showVariables && ( +
+

+ Use{' '} + + {'{{{VARIABLE_NAME}}}'} + {' '} + syntax in section headings and body text. +

+
+ {Object.entries(CONTRACT_VARIABLE_DESCRIPTIONS).map( + ([key, desc]) => ( +
+ + {key} + + + {desc} + +
+ ), + )} +
+
+ )} +
+ + {/* Contract Sections */} +
+
+

+ Contract Sections +

+ +
+ +
+ {sections.map((section, index) => ( +
+
+ + Section {index + 1} + +
+ + + +
+
+
+
+ + + updateSection(index, 'heading', e.target.value) + } + className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white" + placeholder="e.g. 1. Parties" + /> +
+
+ +

+ Rich text editing is available through the Sanity Studio. + Enter plain text here for initial setup. +

+