From 55b7607ee2eab51791d415e7a23f9356a91b40e4 Mon Sep 17 00:00:00 2001 From: Sandino Scheidegger Date: Sun, 5 Jan 2025 10:43:14 +0100 Subject: [PATCH 01/15] Website: Nav bar optimization (#1000) --- shared/locales/fr/website-common.json | 2 +- .../src/components/navbar/navbar-client.tsx | 68 ++++++++++--------- website/src/components/navbar/navbar.tsx | 8 +-- 3 files changed, 41 insertions(+), 37 deletions(-) diff --git a/shared/locales/fr/website-common.json b/shared/locales/fr/website-common.json index 47210196f..1fc1d6e19 100644 --- a/shared/locales/fr/website-common.json +++ b/shared/locales/fr/website-common.json @@ -8,7 +8,7 @@ "twitter-image": "/assets/metadata/twitter/default.jpg" }, "navigation": { - "our-promise": "Modèle à 100 %", + "our-promise": "Notre promesse", "about-us": "Qui nous sommes", "contact": "Contact", "contributors": "Donateurs et donatrices", diff --git a/website/src/components/navbar/navbar-client.tsx b/website/src/components/navbar/navbar-client.tsx index 307b6292c..af90fdd57 100644 --- a/website/src/components/navbar/navbar-client.tsx +++ b/website/src/components/navbar/navbar-client.tsx @@ -276,13 +276,18 @@ const DesktopNavigation = ({ lang, region, languages, regions, currencies, navig const transparency = navigation![2]; return ( -
-
+
+
- - +
+ +
+ -
+
{translations.myProfile}
@@ -292,17 +297,12 @@ const DesktopNavigation = ({ lang, region, languages, regions, currencies, navig
-
-
- {ourWork.title} -
+
+
+ + {ourWork.title} + +
{ourWork.links?.map((link: any, index: number) => ( {link.title} @@ -310,9 +310,11 @@ const DesktopNavigation = ({ lang, region, languages, regions, currencies, navig ))}
-
- {aboutUs.title} -
+
+ + {aboutUs.title} + +
{aboutUs.links?.map((link, index) => ( {link.title} @@ -320,9 +322,11 @@ const DesktopNavigation = ({ lang, region, languages, regions, currencies, navig ))}
-
- {transparency.title} -
+
+ + {transparency.title} + +
{transparency.links?.map((link: any, index: number) => ( {link.title} @@ -331,14 +335,14 @@ const DesktopNavigation = ({ lang, region, languages, regions, currencies, navig
-
+
{(!isIntRegion || (isIntRegion && country)) && ( Country flag{languages.find((l) => l.code === lang)?.translation}
-
-
+
+
{regions .sort((a, b) => a.translation.localeCompare(b.translation)) .map((reg, index) => ( setRegion(reg.code)} @@ -363,13 +367,13 @@ const DesktopNavigation = ({ lang, region, languages, regions, currencies, navig ))}
-
+
{languages .sort((a, b) => a.translation.localeCompare(b.translation)) .map((l, index) => ( setLanguage(l.code)} @@ -378,14 +382,14 @@ const DesktopNavigation = ({ lang, region, languages, regions, currencies, navig ))}
-
+
{currencies .sort((a, b) => a.code.localeCompare(b.code)) .map((curr, index) => ( setCurrency(curr.code)} diff --git a/website/src/components/navbar/navbar.tsx b/website/src/components/navbar/navbar.tsx index b256301a9..def1e6431 100644 --- a/website/src/components/navbar/navbar.tsx +++ b/website/src/components/navbar/navbar.tsx @@ -70,10 +70,6 @@ async function Navbar({ lang, region }: DefaultParams) { title: translator.t('navigation.recipient-selection'), href: `/${lang}/${region}/transparency/recipient-selection`, }, - { - title: translator.t('navigation.faq'), - href: `/${lang}/${region}/faq`, - }, { title: translator.t('navigation.evidence'), href: `/${lang}/${region}/transparency/evidence`, @@ -82,6 +78,10 @@ async function Navbar({ lang, region }: DefaultParams) { title: translator.t('navigation.reporting'), href: `/${lang}/${region}/transparency/reporting`, }, + { + title: translator.t('navigation.faq'), + href: `/${lang}/${region}/faq`, + }, ], }, ]} From cf72cee0537ebc2108659878adde784a993e028e Mon Sep 17 00:00:00 2001 From: mkue Date: Fri, 10 Jan 2025 23:38:31 +0100 Subject: [PATCH 02/15] Deployment: Install vercel during repo initialization step --- .github/workflows/actions/init/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/actions/init/action.yml b/.github/workflows/actions/init/action.yml index ed481273c..beef12df8 100644 --- a/.github/workflows/actions/init/action.yml +++ b/.github/workflows/actions/init/action.yml @@ -24,10 +24,10 @@ runs: cache: npm cache-dependency-path: package-lock.json - - name: Install and set up firebase-tools + - name: Install and set up vercel & firebase-tools shell: bash run: | - npm -g install firebase-tools@13.29.1 && \ + npm -g install firebase-tools@13.29.1 vercel@39.2.6 && \ firebase setup:emulators:firestore && \ firebase setup:emulators:storage && \ firebase setup:emulators:ui From 9f2ecec1e05bb421e9dba825e34a0966b16ba09a Mon Sep 17 00:00:00 2001 From: Karin Berg Date: Sat, 11 Jan 2025 21:53:37 +0100 Subject: [PATCH 03/15] [Mobile App] Add CodeMagic yaml workflow file (#1004) --- codemagic.yaml | 484 ++++++++++++++++++++++++++++++++ recipients_app/clean_build.sh | 2 +- recipients_app/ios/Podfile.lock | 20 +- 3 files changed, 495 insertions(+), 11 deletions(-) create mode 100644 codemagic.yaml diff --git a/codemagic.yaml b/codemagic.yaml new file mode 100644 index 000000000..f6b0e2bae --- /dev/null +++ b/codemagic.yaml @@ -0,0 +1,484 @@ +definitions: + instance_mac_mini_m2: &instance_mac_mini_m2 + instance_type: mac_mini_m2 + max_build_duration: 30 + + env_versions: &env_versions + flutter: 3.22.3 + xcode: 15.4 + cocoapods: 1.16.2 + java: 17 + + scripts: + - &verify_flutter_version + name: Verify Flutter version + script: | + # Verify Flutter version + echo "Verify Flutter version..." + + REQUIRED_VERSION=$(grep flutter .tool-versions | awk '{print $2}') + echo "REQUIRED_VERSION: $REQUIRED_VERSION" + CURRENT_VERSION=$(flutter --version | grep -m 1 "^Flutter" | awk '{print $2}') + echo "CURRENT_VERSION: $CURRENT_VERSION" + + if [ "$CURRENT_VERSION" != "$REQUIRED_VERSION" ]; then + echo "Error: Flutter version $REQUIRED_VERSION is required (current: $CURRENT_VERSION)" + exit 1 + else + echo "Required version: $REQUIRED_VERSION == Current version: $CURRENT_VERSION" + echo "Flutter version is correct" + fi + working_directory: recipients_app + +workflows: + android-staging-firebase-app-distribution-workflow: + name: Android Staging (Firebase App Distribution) + <<: *instance_mac_mini_m2 + + environment: + android_signing: + - social_income_upload_keystore.jks + groups: + - slack + - app-config-stage + - firebase + #- google_play # <-- (Includes GCLOUD_SERVICE_ACCOUNT_CREDENTIALS) + <<: *env_versions + + cache: + cache_paths: + - $HOME/.gradle/caches + - $FLUTTER_ROOT/.pub-cache + + scripts: + - name: Set up key.properties + script: | + #!/usr/bin/env sh + set -e # exit on first failed command + + cat >> "$CM_BUILD_DIR/recipients_app/android/key.properties" < $GOOGLE_APPLICATION_CREDENTIALS + # Define reusable section 'verify_flutter_version' + - *verify_flutter_version + - name: Get Flutter packages + script: | + #!/usr/bin/env sh + set -e # exit on first failed command + + flutter pub get + working_directory: recipients_app + - name: Build AAB with Flutter + script: | + #!/usr/bin/env sh + set -e # exit on first failed command + + # Build the Flutter APK app + flutter build apk --release \ + --flavor stage \ + -t lib/main.dart \ + --dart-define=SURVEY_BASE_URL=$SURVEY_BASE_URL \ + --dart-define=SENTRY_URL=$SENTRY_URL + working_directory: recipients_app + + artifacts: + - recipients_app/build/**/outputs/**/*.apk + - recipients_app/build/**/outputs/**/*.aab + - recipients_app/build/**/outputs/**/mapping.txt + - flutter_drive.log + + publishing: + #google_play: + # credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS + # track: internal + # submit_as_draft: true + firebase: + firebase_service_account: $FIREBASE_APP_DISTRIBUTION_SERVICE_ACCOUNT + android: + app_id: 1:51495651779:android:08ef862a962bc041185576 + groups: + - internal-testers + artifact_type: "apk" + + # Notifications + email: # See the following link for details about email publishing - https://docs.codemagic.io/publishing-yaml/distribution/#email + recipients: + - sandino@socialincome.org + notify: + success: true # To receive a notification when a build succeeds + failure: false # To not receive a notification when a build fails + slack: # See the following link about how to connect your Slack account - https://docs.codemagic.io/publishing-yaml/distribution/#slack + channel: $SLACK_CHANNEL_NAME + notify_on_build_start: false + notify: + success: true # To receive a notification when a build succeeds + failure: true # To not receive a notification when a build fails + + android-production-workflow: + name: Android Production + <<: *instance_mac_mini_m2 + + environment: + android_signing: + - social_income_upload_keystore.jks + groups: + - slack + - app-config-prod + #- google_play # <-- (Includes GCLOUD_SERVICE_ACCOUNT_CREDENTIALS) + <<: *env_versions + + cache: + cache_paths: + - $HOME/.gradle/caches + - $FLUTTER_ROOT/.pub-cache + + scripts: + - name: Set up key.properties + script: | + #!/usr/bin/env sh + set -e # exit on first failed command + + cat >> "$CM_BUILD_DIR/recipients_app/android/key.properties" < google-services.json + - *verify_flutter_version + - name: Get Flutter packages + script: | + #!/usr/bin/env sh + set -e # exit on first failed command + + flutter pub get + working_directory: recipients_app + - name: Build AAB with Flutter + script: | + #!/usr/bin/env sh + set -e # exit on first failed command + + # Build the Flutter AAB app + flutter build appbundle --release \ + --flavor prod \ + -t lib/main.dart \ + --dart-define=SURVEY_BASE_URL=$SURVEY_BASE_URL \ + --dart-define=SENTRY_URL=$SENTRY_URL + working_directory: recipients_app + + artifacts: + - recipients_app/build/**/outputs/**/*.apk + - recipients_app/build/**/outputs/**/*.aab + - recipients_app/build/**/outputs/**/mapping.txt + - flutter_drive.log + + publishing: + #google_play: + # credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS + # track: internal + # submit_as_draft: true + + # Notifications + email: # See the following link for details about email publishing - https://docs.codemagic.io/publishing-yaml/distribution/#email + recipients: + #- sandino@socialincome.org + - karin@karinberg.de + notify: + success: true # To receive a notification when a build succeeds + failure: false # To not receive a notification when a build fails + slack: # See the following link about how to connect your Slack account - https://docs.codemagic.io/publishing-yaml/distribution/#slack + channel: $SLACK_CHANNEL_NAME + notify_on_build_start: false + notify: + success: true # To receive a notification when a build succeeds + failure: true # To not receive a notification when a build fails + + ios-staging-testflight-workflow: + name: iOS Staging (Test Flight) + <<: *instance_mac_mini_m2 + + integrations: + app_store_connect: Social Income Recipient App + + environment: + ios_signing: + distribution_type: app_store + bundle_identifier: org.socialincome.app.stage + groups: + - slack + - app-config-stage + vars: + APP_ID: 6464113329 # Apple ID of App "Stage Social Income" + <<: *env_versions + + cache: + cache_paths: + - $FLUTTER_ROOT/.pub-cache + - $HOME/Library/Caches/CocoaPods + + scripts: + - name: Set up code signing settings on Xcode project + script: | + #!/usr/bin/env sh + set -e # exit on first failed command + + xcode-project use-profiles \ + --archive-method=app-store \ + --export-options-plist=$CM_BUILD_DIR/recipients_app/ios/export_options.plist + + # Print the export options plist + more $CM_BUILD_DIR/recipients_app/ios/export_options.plist + - *verify_flutter_version + - name: Get Flutter packages & Install pods + script: | + #!/usr/bin/env sh + set -e # exit on first failed command + + flutter pub get + find . -name "Podfile" -execdir pod install \; + working_directory: recipients_app + - name: Flutter build ipa + script: | + #!/usr/bin/env sh + set -e # exit on first failed command + + # Build the Flutter iOS app + flutter build ipa --release \ + --flavor stage \ + -t lib/main.dart \ + --dart-define=SURVEY_BASE_URL=$SURVEY_BASE_URL \ + --dart-define=SENTRY_URL=$SENTRY_URL \ + --export-options-plist=$CM_BUILD_DIR/recipients_app/ios/export_options.plist + working_directory: recipients_app + + artifacts: + - recipients_app/build/ios/ipa/*.ipa + - /tmp/xcodebuild_logs/*.log + - flutter_drive.log + + publishing: + # Publish to Apple's Testflight via AppStore Connect + app_store_connect: + # Use referenced App Store Connect API key from above to authenticate binary upload + auth: integration + # Whether or not to submit the uploaded build to TestFlight beta review. + # Note: This action is performed during post-processing. + submit_to_testflight: true + + # Notifications + email: # See the following link for details about email publishing - https://docs.codemagic.io/publishing-yaml/distribution/#email + recipients: + - sandino@socialincome.org + notify: + success: true # To receive a notification when a build succeeds + failure: false # To not receive a notification when a build fails + slack: # See the following link about how to connect your Slack account - https://docs.codemagic.io/publishing-yaml/distribution/#slack + channel: $SLACK_CHANNEL_NAME + notify_on_build_start: false + notify: + success: true # To receive a notification when a build succeeds + failure: true # To not receive a notification when a build fails + + ios-staging-firebase-app-distribution-workflow: + name: iOS Staging (Firebase App Distribution) + <<: *instance_mac_mini_m2 + + integrations: + app_store_connect: Social Income Recipient App + + environment: + ios_signing: + distribution_type: ad_hoc + bundle_identifier: org.socialincome.app.stage + groups: + - slack + - app-config-stage + - firebase + vars: + APP_ID: 6464113329 # Apple ID of App "Stage Social Income" + <<: *env_versions + + cache: + cache_paths: + - $FLUTTER_ROOT/.pub-cache + - $HOME/Library/Caches/CocoaPods + + scripts: + # - name: Set up keychain to be used for code signing using Codemagic CLI 'keychain' command + # script: keychain initialize + # - name: Fetch signing files + # script: | + # app-store-connect fetch-signing-files "org.socialincome.app.stage" \ + # --type IOS_APP_ADHOC \ + # --create + # - name: Set up signing certificate + # script: keychain add-certificates + - name: Set up code signing settings on Xcode project + script: | + #!/usr/bin/env sh + set -e # exit on first failed command + + xcode-project use-profiles \ + --archive-method=ad-hoc \ + --export-options-plist=$CM_BUILD_DIR/recipients_app/ios/export_options.plist + + # Print the export options plist + more $CM_BUILD_DIR/recipients_app/ios/export_options.plist + - *verify_flutter_version + - name: Get Flutter packages & Install pods + script: | + #!/usr/bin/env sh + set -e # exit on first failed command + + flutter pub get + find . -name "Podfile" -execdir pod install \; + working_directory: recipients_app + - name: Flutter build ipa + script: | + #!/usr/bin/env sh + set -e # exit on first failed command + + # Build the Flutter iOS app + flutter build ipa --release \ + --flavor stage \ + -t lib/main.dart \ + --dart-define=SURVEY_BASE_URL=$SURVEY_BASE_URL \ + --dart-define=SENTRY_URL=$SENTRY_URL \ + --export-options-plist=$CM_BUILD_DIR/recipients_app/ios/export_options.plist + working_directory: recipients_app + + artifacts: + - recipients_app/build/ios/ipa/*.ipa + - /tmp/xcodebuild_logs/*.log + - flutter_drive.log + + publishing: + # Publish to Firebase App Distribution + firebase: + firebase_service_account: $FIREBASE_APP_DISTRIBUTION_SERVICE_ACCOUNT + ios: + app_id: 1:51495651779:ios:d4d28e75065983fb185576 + groups: + - internal-testers + + # Notifications + email: # See the following link for details about email publishing - https://docs.codemagic.io/publishing-yaml/distribution/#email + recipients: + - sandino@socialincome.org + notify: + success: true # To receive a notification when a build succeeds + failure: false # To not receive a notification when a build fails + slack: # See the following link about how to connect your Slack account - https://docs.codemagic.io/publishing-yaml/distribution/#slack + channel: $SLACK_CHANNEL_NAME + notify_on_build_start: false + notify: + success: true # To receive a notification when a build succeeds + failure: true # To not receive a notification when a build fails + + ios-production-workflow: + name: iOS Production + <<: *instance_mac_mini_m2 + + integrations: + app_store_connect: Social Income Recipient App + + environment: + ios_signing: + distribution_type: app_store + bundle_identifier: org.socialincome.app + groups: + - slack + - app-config-prod + vars: + APP_ID: 6444860109 # Apple ID of Prod App "Social Income" + <<: *env_versions + + cache: + cache_paths: + - $FLUTTER_ROOT/.pub-cache + - $HOME/Library/Caches/CocoaPods + + scripts: + - name: Load Prod Firebase configuration + script: | + #!/usr/bin/env sh + set -e # exit on first failed command + + cd $CM_BUILD_DIR/recipients_app/ios/GoogleServicesConfig + echo $IOS_FIREBASE_SECRET > GoogleService-Info-Prod.plist + - name: Set up code signing settings on Xcode project + script: | + #!/usr/bin/env sh + set -e # exit on first failed command + + xcode-project use-profiles \ + --archive-method=app-store \ + --export-options-plist=$CM_BUILD_DIR/recipients_app/ios/export_options.plist + + # Print the export options plist + more $CM_BUILD_DIR/recipients_app/ios/export_options.plist + - *verify_flutter_version + - name: Get Flutter packages & Install pods + script: | + #!/usr/bin/env sh + set -e # exit on first failed command + + flutter pub get + find . -name "Podfile" -execdir pod install \; + working_directory: recipients_app + - name: Flutter build ipa + script: | + #!/usr/bin/env sh + set -e # exit on first failed command + + # Build the Flutter iOS app + flutter build ipa --release \ + --flavor prod \ + -t lib/main.dart \ + --dart-define=SURVEY_BASE_URL=$SURVEY_BASE_URL \ + --dart-define=SENTRY_URL=$SENTRY_URL \ + --export-options-plist=$CM_BUILD_DIR/recipients_app/ios/export_options.plist + working_directory: recipients_app + + artifacts: + - recipients_app/build/ios/ipa/*.ipa + - /tmp/xcodebuild_logs/*.log + - flutter_drive.log + + publishing: + # Publish to Apple's Testflight via AppStore Connect + app_store_connect: + # Use referenced App Store Connect API key from above to authenticate binary upload + auth: integration + # Whether or not to submit the uploaded build to TestFlight beta review. + # Note: This action is performed during post-processing. + submit_to_testflight: true + + # Notifications + email: # See the following link for details about email publishing - https://docs.codemagic.io/publishing-yaml/distribution/#email + recipients: + - sandino@socialincome.org + notify: + success: true # To receive a notification when a build succeeds + failure: false # To not receive a notification when a build fails + slack: # See the following link about how to connect your Slack account - https://docs.codemagic.io/publishing-yaml/distribution/#slack + channel: $SLACK_CHANNEL_NAME + notify_on_build_start: false + notify: + success: true # To receive a notification when a build succeeds + failure: true # To not receive a notification when a build fails diff --git a/recipients_app/clean_build.sh b/recipients_app/clean_build.sh index 2134d69f9..01bf85e71 100755 --- a/recipients_app/clean_build.sh +++ b/recipients_app/clean_build.sh @@ -13,7 +13,7 @@ fi # Verify Flutter version echo "Verify Flutter version..." REQUIRED_VERSION=$(grep flutter .tool-versions | awk '{print $2}') -CURRENT_VERSION=$(flutter --version | head -n 1 | awk '{print $2}') +CURRENT_VERSION=$(flutter --version | grep -m 1 "^Flutter" | awk '{print $2}') if [ "$CURRENT_VERSION" != "$REQUIRED_VERSION" ]; then echo "Error: Flutter version $REQUIRED_VERSION is required (current: $CURRENT_VERSION)" exit 1 diff --git a/recipients_app/ios/Podfile.lock b/recipients_app/ios/Podfile.lock index ba6dd0339..76837dde9 100644 --- a/recipients_app/ios/Podfile.lock +++ b/recipients_app/ios/Podfile.lock @@ -222,12 +222,12 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: AppCheckCore: 9feb4300caa596a36416cde10674dc5bec1e022e - cloud_firestore: 003d53b6b8b392600b7769acf9421cc4f23e5911 + cloud_firestore: 28753092b326c43b17251b50aa6c5b3247c939e5 Firebase: 0312a2352584f782ea56f66d91606891d4607f06 - firebase_app_check: 717089a2df93747620263dd6c8e444bde5699b7e - firebase_auth: 5719ddc9f654b813405899480e84971bd8e61235 - firebase_core: a626d00494efa398e7c54f25f1454a64c8abf197 - firebase_messaging: 06391e8f35dc65a00c56580266285263d2861f10 + firebase_app_check: d2513b120608284733805c98e80b5b11c1bf705e + firebase_auth: f17bd6bfc0bc3d83c6df9dc3829e5b6fee10b147 + firebase_core: 3b49a055ff54114cae400581c13671fe53936c36 + firebase_messaging: 30fa3ec8cd0dc8a860b7817548911b97345a0875 FirebaseAppCheck: 148fa858b8070710c8f83d3f545b5f1e85d104a4 FirebaseAppCheckInterop: 6a1757cfd4067d8e00fccd14fcc1b8fd78cfac07 FirebaseAuth: c0f93dcc570c9da2bffb576969d793e95c344fbb @@ -245,19 +245,19 @@ SPEC CHECKSUMS: FirebaseMessaging: 88950ba9485052891ebe26f6c43a52bb62248952 FirebaseSharedSwift: 0274086954b1b2d5fd7e829eccc587044d72a4ba Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 + flutter_native_splash: 35ddbc7228eafcb3969dcc5f1fbbe27c1145a4f0 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19 nanopb: 438bc412db1928dac798aa6fd75726007be04262 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21 Sentry: e9215d7b17f7902692b4f8700e061e4f853e3521 - sentry_flutter: 927eed60d66951d1b0f1db37fe94ff5cb7c80231 - url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 - webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36 + sentry_flutter: ba644c90ccccb202066a5218a593d1c35584cd37 + url_launcher_ios: c14c42eb16c26cfb6c43c2e40ce35a4085431791 + webview_flutter_wkwebview: 3a409e0c27995945d2dabd90079927fba5c34492 PODFILE CHECKSUM: f727177ee8bd2fa0ad40e98c3b4299dfade495ac From 7fcc09de03b862b3215321e050982ce54974ed13 Mon Sep 17 00:00:00 2001 From: mkue Date: Sun, 12 Jan 2025 12:13:48 +0100 Subject: [PATCH 04/15] Website: Bump next to 14.2.23 --- package-lock.json | 88 ++++++++++++++++++++++---------------------- website/package.json | 2 +- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index f6c74f2cc..a331e6443 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6009,9 +6009,9 @@ } }, "node_modules/@next/env": { - "version": "14.2.19", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.19.tgz", - "integrity": "sha512-8yWSNi1p+AOsd1QsxZMMkXtdrz8wvYoRxoUa9olmHBspHVLnxpptyKoI574ZF90yq1gXv/CqpchVLBoK8RcN7w==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.23.tgz", + "integrity": "sha512-CysUC9IO+2Bh0omJ3qrb47S8DtsTKbFidGm6ow4gXIG6reZybqxbkH2nhdEm1tC8SmgzDdpq3BIML0PWsmyUYA==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -6093,9 +6093,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.19", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.19.tgz", - "integrity": "sha512-mL0Nn2PNKV0L++F1l69wY3ySCg9ryw9NtvPhAXm952wpxLVWCCDkVb6XTtOeicF8EdstbjZyLRBflEHtv/Wk2w==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.23.tgz", + "integrity": "sha512-WhtEntt6NcbABA8ypEoFd3uzq5iAnrl9AnZt9dXdO+PZLACE32z3a3qA5OoV20JrbJfSJ6Sd6EqGZTrlRnGxQQ==", "cpu": [ "arm64" ], @@ -6109,9 +6109,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.19", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.19.tgz", - "integrity": "sha512-2HWOTmk+qNa5R+NW8+752jye5JXlQVkGTf6IV+cT2+q3YeRYQPoqYcIr9KSlB8pvgqRqtIGHcioZFvWSeXjxVA==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.23.tgz", + "integrity": "sha512-vwLw0HN2gVclT/ikO6EcE+LcIN+0mddJ53yG4eZd0rXkuEr/RnOaMH8wg/sYl5iz5AYYRo/l6XX7FIo6kwbw1Q==", "cpu": [ "x64" ], @@ -6125,9 +6125,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.19", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.19.tgz", - "integrity": "sha512-tlbHT+Chnjqh9gSrNNrDAnqFHkoA++wI829bTawoLwAPemji57/qhDp88YteTuUN3rd4U3FcV0f9qiZbyMStFQ==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.23.tgz", + "integrity": "sha512-uuAYwD3At2fu5CH1wD7FpP87mnjAv4+DNvLaR9kiIi8DLStWSW304kF09p1EQfhcbUI1Py2vZlBO2VaVqMRtpg==", "cpu": [ "arm64" ], @@ -6141,9 +6141,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.19", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.19.tgz", - "integrity": "sha512-v0FD7DDS1yapnJw8JuvvePlqxyNY+OKrfoVDvibc+9ADVVzbINkHTCXIDshMXy/rBKheUpkycgS1lOaovgZQ5Q==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.23.tgz", + "integrity": "sha512-Mm5KHd7nGgeJ4EETvVgFuqKOyDh+UMXHXxye6wRRFDr4FdVRI6YTxajoV2aHE8jqC14xeAMVZvLqYqS7isHL+g==", "cpu": [ "arm64" ], @@ -6157,9 +6157,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.19", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.19.tgz", - "integrity": "sha512-S+DneEj0Knv8+cilSn8ZP+xU/926eeeueZ4DjQcFy3hZT+2R29wTYBnDeUk+FCVchjzD9s0dvWff6eHDYrJoSA==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.23.tgz", + "integrity": "sha512-Ybfqlyzm4sMSEQO6lDksggAIxnvWSG2cDWnG2jgd+MLbHYn2pvFA8DQ4pT2Vjk3Cwrv+HIg7vXJ8lCiLz79qoQ==", "cpu": [ "x64" ], @@ -6173,9 +6173,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.19", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.19.tgz", - "integrity": "sha512-RwczHg2q4n4Ls9PJtBDlBNkfl6G7Fd4uvSRIXPM6Inw52q8R+oWduvuqdzlvcygnv78wt1yrQI14ZRiU0BjgVw==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.23.tgz", + "integrity": "sha512-OSQX94sxd1gOUz3jhhdocnKsy4/peG8zV1HVaW6DLEbEmRRtUCUQZcKxUD9atLYa3RZA+YJx+WZdOnTkDuNDNA==", "cpu": [ "x64" ], @@ -6189,9 +6189,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.19", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.19.tgz", - "integrity": "sha512-42YlHVSq3q1nTxLkikG2dRUsKM6vq8v0jrxbR1M6vlgprWlIoXme31g3SpJvVp52v1SfE9WbyAegQxfSIENDSQ==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.23.tgz", + "integrity": "sha512-ezmbgZy++XpIMTcTNd0L4k7+cNI4ET5vMv/oqNfTuSXkZtSA9BURElPFyarjjGtRgZ9/zuKDHoMdZwDZIY3ehQ==", "cpu": [ "arm64" ], @@ -6205,9 +6205,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.19", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.19.tgz", - "integrity": "sha512-xMEXItVFcT7fV6ndcfnT4ZHm3R0C8398tiC3KInsK+511Or9Jq5G7zagz8aonNRKZkw15zdM1txRslSMRHe4mA==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.23.tgz", + "integrity": "sha512-zfHZOGguFCqAJ7zldTKg4tJHPJyJCOFhpoJcVxKL9BSUHScVDnMdDuOU1zPPGdOzr/GWxbhYTjyiEgLEpAoFPA==", "cpu": [ "ia32" ], @@ -6221,9 +6221,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.19", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.19.tgz", - "integrity": "sha512-bOkmujDRcqbHO2Mxun7SogL1fwzGT/PYqFZ0+aTBjmkhGhx7V/Dun4MNjnxJEGByGNg2EcwdWzsYcRUnHs8Ivg==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.23.tgz", + "integrity": "sha512-xCtq5BD553SzOgSZ7UH5LH+OATQihydObTrCTvVzOro8QiWYKdBVwcB2Mn2MLMo6DGW9yH1LSPw7jS7HhgJgjw==", "cpu": [ "x64" ], @@ -24720,12 +24720,12 @@ } }, "node_modules/next": { - "version": "14.2.19", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.19.tgz", - "integrity": "sha512-YTOWj8MDofNLfSeHhDgFJK6koeMOrbzEZIL2SQ7yeSA8WWGgfoajI4V21Wn2bqVtM7D3QYWSIK/Sdvqi0ptfQQ==", + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.23.tgz", + "integrity": "sha512-mjN3fE6u/tynneLiEg56XnthzuYw+kD7mCujgVqioxyPqbmiotUCGJpIZGS/VaPg3ZDT1tvWxiVyRzeqJFm/kw==", "license": "MIT", "dependencies": { - "@next/env": "14.2.19", + "@next/env": "14.2.23", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -24740,15 +24740,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.19", - "@next/swc-darwin-x64": "14.2.19", - "@next/swc-linux-arm64-gnu": "14.2.19", - "@next/swc-linux-arm64-musl": "14.2.19", - "@next/swc-linux-x64-gnu": "14.2.19", - "@next/swc-linux-x64-musl": "14.2.19", - "@next/swc-win32-arm64-msvc": "14.2.19", - "@next/swc-win32-ia32-msvc": "14.2.19", - "@next/swc-win32-x64-msvc": "14.2.19" + "@next/swc-darwin-arm64": "14.2.23", + "@next/swc-darwin-x64": "14.2.23", + "@next/swc-linux-arm64-gnu": "14.2.23", + "@next/swc-linux-arm64-musl": "14.2.23", + "@next/swc-linux-x64-gnu": "14.2.23", + "@next/swc-linux-x64-musl": "14.2.23", + "@next/swc-win32-arm64-msvc": "14.2.23", + "@next/swc-win32-ia32-msvc": "14.2.23", + "@next/swc-win32-x64-msvc": "14.2.23" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -36961,7 +36961,7 @@ "js-cookie": "^3.0.5", "lodash": "^4.17.21", "luxon": "^3.5.0", - "next": "14.2.19", + "next": "14.2.23", "react": "18.3.1", "react-dom": "18.3.1", "react-hot-toast": "^2.4.1", diff --git a/website/package.json b/website/package.json index 55dba2c09..d90c2663e 100644 --- a/website/package.json +++ b/website/package.json @@ -38,7 +38,7 @@ "js-cookie": "^3.0.5", "lodash": "^4.17.21", "luxon": "^3.5.0", - "next": "14.2.19", + "next": "14.2.23", "react": "18.3.1", "react-dom": "18.3.1", "react-hot-toast": "^2.4.1", From 656d2423b88282397475d50cf71eed359178d923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=BCndig?= Date: Sun, 12 Jan 2025 12:17:34 +0100 Subject: [PATCH 05/15] Functions: Handle "charge.updated" events to process updated balance transactions from Stripe (#1007) --- functions/src/webhooks/stripe/index.ts | 9 +++++---- shared/.env.sample | 5 +++++ shared/src/stripe/StripeEventHandler.test.ts | 13 +++++++++---- shared/src/stripe/StripeEventHandler.ts | 16 ++++++++-------- shared/tests/utils.ts | 11 +++++++++++ 5 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 shared/tests/utils.ts diff --git a/functions/src/webhooks/stripe/index.ts b/functions/src/webhooks/stripe/index.ts index 97efcc295..c48f4629d 100644 --- a/functions/src/webhooks/stripe/index.ts +++ b/functions/src/webhooks/stripe/index.ts @@ -1,7 +1,6 @@ import { DocumentReference, DocumentSnapshot } from 'firebase-admin/firestore'; import { logger } from 'firebase-functions'; import { onRequest } from 'firebase-functions/v2/https'; -import Stripe from 'stripe'; import { FirestoreAdmin } from '../../../../shared/src/firebase/admin/FirestoreAdmin'; import { SendgridSubscriptionClient } from '../../../../shared/src/sendgrid/SendgridSubscriptionClient'; import { StripeEventHandler } from '../../../../shared/src/stripe/StripeEventHandler'; @@ -43,12 +42,14 @@ export default onRequest(async (request, response) => { try { const sig = request.headers['stripe-signature']!; const event = stripeEventHandler.constructWebhookEvent(request.rawBody, sig, STRIPE_WEBHOOK_SECRET); - const charge = event.data.object as Stripe.Charge; switch (event.type) { case 'charge.succeeded': + // The charge.succeeded events do sometimes not contain the balance_transaction, so we need to listen to charge.updated event + // to get the final balance_transaction and update the contribution document with the final amount. + case 'charge.updated': case 'charge.failed': { - const contributionRef = await stripeEventHandler.handleChargeEvent(charge); - logger.info(`Charge event ${event.type} handled for charge ${charge.id}.`); + const contributionRef = await stripeEventHandler.handleChargeEvent(event.data.object); + logger.info(`Charge event ${event.type} handled for charge ${event.data.object.id}.`); if (contributionRef) { logger.info(`Created contribution ${contributionRef.id}. Adding contributor to newsletter.`); try { diff --git a/shared/.env.sample b/shared/.env.sample index a2d028109..05781c806 100644 --- a/shared/.env.sample +++ b/shared/.env.sample @@ -1,3 +1,8 @@ +# These environment variables are required for runnning the tests against the local firebase environment +GCLOUD_PROJECT="demo-social-income-local" +FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 +FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099 + # For the Twilio related tests to work locally, configure the test credentials from Twilio in your .env file TWILIO_SID=ACXXXXXXXXXXXXXXXXXXXX TWILIO_TOKEN=yyyyyyyyyyyyyyyyyyyyy diff --git a/shared/src/stripe/StripeEventHandler.test.ts b/shared/src/stripe/StripeEventHandler.test.ts index 99af6b0e6..0bb6e4364 100644 --- a/shared/src/stripe/StripeEventHandler.test.ts +++ b/shared/src/stripe/StripeEventHandler.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from '@jest/globals'; import { DateTime } from 'luxon'; import Stripe from 'stripe'; +import { clearFirestoreData } from '../../tests/utils'; import { FirestoreAdmin } from '../firebase/admin/FirestoreAdmin'; import { getOrInitializeFirebaseAdmin } from '../firebase/admin/app'; import { toFirebaseAdminTimestamp } from '../firebase/admin/utils'; @@ -14,6 +15,10 @@ describe('stripeWebhook', () => { const firestoreAdmin = new FirestoreAdmin(getOrInitializeFirebaseAdmin({ projectId: projectId })); const stripeWebhook = new StripeEventHandler('DUMMY_KEY', firestoreAdmin); + beforeEach(async () => { + await clearFirestoreData(firestoreAdmin); + }); + // Mock the Stripe API call jest.spyOn(stripeWebhook.stripe.customers, 'retrieve').mockImplementation(async () => { return { @@ -27,7 +32,7 @@ describe('stripeWebhook', () => { expect(initialUser).toBeUndefined(); const ref = await stripeWebhook.storeCharge(testCharge, null); - const contribution = await ref!.get(); + const contribution = await ref.get(); expect(contribution.data()).toEqual(expectedContribution); const createdUser = (await stripeWebhook.findFirestoreUser(testCustomer))!.data(); @@ -48,7 +53,7 @@ describe('stripeWebhook', () => { }); const ref = await stripeWebhook.storeCharge(testCharge, null); - const contribution = await ref!.get(); + const contribution = await ref.get(); expect(contribution.data()).toEqual(expectedContribution); }); @@ -58,7 +63,7 @@ describe('stripeWebhook', () => { }); const ref = await stripeWebhook.storeCharge(testCharge, { campaignId: 'xyz' }); - const contribution = await ref!.get(); + const contribution = await ref.get(); expect(contribution.data()?.campaign_path).toEqual( firestoreAdmin.firestore.collection(CAMPAIGN_FIRESTORE_PATH).doc('xyz'), ); @@ -70,7 +75,7 @@ describe('stripeWebhook', () => { }); const ref = await stripeWebhook.storeCharge(testCharge, null); - const contribution = await ref!.get(); + const contribution = await ref.get(); expect(contribution.data()).toEqual(expectedContribution); }); diff --git a/shared/src/stripe/StripeEventHandler.ts b/shared/src/stripe/StripeEventHandler.ts index bc6927d09..d1c755d7f 100644 --- a/shared/src/stripe/StripeEventHandler.ts +++ b/shared/src/stripe/StripeEventHandler.ts @@ -33,12 +33,12 @@ export class StripeEventHandler { const checkoutMetadata = await this.getCheckoutMetadata(charge); - // We only store non-successful charges if the user already exists. - // This prevents us from having users in the database that never made a successful contribution. - if ( - fullCharge.status === 'succeeded' || - (await this.findFirestoreUser(await this.retrieveStripeCustomer(fullCharge.customer as string))) - ) { + const firestoreUser = await this.findFirestoreUser( + await this.retrieveStripeCustomer(fullCharge.customer as string), + ); + if (fullCharge.status === 'succeeded' || firestoreUser) { + // We only store non-successful charges if the user already exists. + // This prevents us from having users in the database that never made a successful contribution. return await this.storeCharge(fullCharge, checkoutMetadata); } return null; @@ -205,8 +205,8 @@ export class StripeEventHandler { const contributionRef = ( userRef.collection(CONTRIBUTION_FIRESTORE_PATH) as CollectionReference ).doc(charge.id); - await contributionRef.set(contribution); - console.info(`Ingested ${charge.id} into firestore for user ${userRef.id}`); + await contributionRef.set(contribution, { merge: true }); + console.info(`Updated contribution document: ${contributionRef.path}`); await this.maybeUpdateCampaign(contribution); return contributionRef; }; diff --git a/shared/tests/utils.ts b/shared/tests/utils.ts new file mode 100644 index 000000000..11cb593d3 --- /dev/null +++ b/shared/tests/utils.ts @@ -0,0 +1,11 @@ +import { FirestoreAdmin } from '../src/firebase/admin/FirestoreAdmin'; + +export async function clearFirestoreData(firestoreAdmin: FirestoreAdmin) { + const collections = await firestoreAdmin.firestore.listCollections(); + const deletePromises = collections.map(async (collection) => { + const documents = await collection.listDocuments(); + const deleteDocPromises = documents.map((doc) => firestoreAdmin.firestore.recursiveDelete(doc)); + await Promise.all(deleteDocPromises); + }); + await Promise.all(deletePromises); +} From 0974c41c182ee16d21f5821be11d694595998803 Mon Sep 17 00:00:00 2001 From: mkue Date: Sun, 12 Jan 2025 21:22:44 +0100 Subject: [PATCH 06/15] Website: Specify SENTRY_AUTH_TOKEN in next.config.js --- website/next.config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/next.config.js b/website/next.config.js index 03dd0e1f4..190b1a343 100644 --- a/website/next.config.js +++ b/website/next.config.js @@ -14,6 +14,8 @@ module.exports = withSentryConfig(module.exports, { org: 'social-income', project: 'website', + authToken: process.env.SENTRY_AUTH_TOKEN, + // Only print logs for uploading source maps in CI silent: !process.env.CI, From 36330070d5820512104882921c51c062da21b516 Mon Sep 17 00:00:00 2001 From: mkue Date: Sun, 12 Jan 2025 21:24:16 +0100 Subject: [PATCH 07/15] Website: Use SENTRY_AUTH_TOKEN in production deployment action --- .github/workflows/production-deployment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/production-deployment.yml b/.github/workflows/production-deployment.yml index e1df34d24..5ba5a71f8 100644 --- a/.github/workflows/production-deployment.yml +++ b/.github/workflows/production-deployment.yml @@ -34,6 +34,7 @@ jobs: env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} steps: - name: Checkout repository uses: actions/checkout@v3 From f4572cee6c1fbfa0c159ebd1130224db030a6b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=BCndig?= Date: Sun, 12 Jan 2025 22:06:26 +0100 Subject: [PATCH 08/15] Bugfix: Make sure amount displayed on campaign page is dynamically calculated (#1008) --- admin/src/collections/Campaigns.ts | 22 +++++-------------- shared/src/stripe/StripeEventHandler.ts | 21 ------------------ shared/src/types/campaign.ts | 2 -- shared/src/types/contribution.ts | 9 +++++--- .../(website)/campaign/[campaign]/page.tsx | 12 ++++++---- 5 files changed, 20 insertions(+), 46 deletions(-) diff --git a/admin/src/collections/Campaigns.ts b/admin/src/collections/Campaigns.ts index 41d3fbf56..6d1fe6617 100644 --- a/admin/src/collections/Campaigns.ts +++ b/admin/src/collections/Campaigns.ts @@ -69,45 +69,35 @@ export const campaignsCollection = buildAuditedCollection({ multiline: true, }, link_website: { - title: 'Website Link', + name: 'Website Link', dataType: 'string', validation: { required: false }, description: 'The link to the website (optional)', }, link_instagram: { - title: 'Instagram Link', + name: 'Instagram Link', dataType: 'string', validation: { required: false }, description: 'The link to the Instagram profile (optional)', }, link_tiktok: { - title: 'TikTok Link', + name: 'TikTok Link', dataType: 'string', validation: { required: false }, description: 'The link to the TikTok profile (optional)', }, link_facebook: { - title: 'Facebook Link', + name: 'Facebook Link', dataType: 'string', validation: { required: false }, description: 'The link to the Facebook profile (optional)', }, link_x: { - title: 'X (formerly Twitter) Link', + name: 'X (formerly Twitter) Link', dataType: 'string', validation: { required: false }, description: 'The link to the X profile (optional)', }, - amount_collected_chf: { - dataType: 'number', - name: 'Collected amount in CHF', - readOnly: true, - }, - contributions: { - dataType: 'number', - name: 'Contributions', - readOnly: true, - }, goal: { dataType: 'number', name: 'Optional Fundraising Goal', @@ -152,7 +142,7 @@ export const campaignsCollection = buildAuditedCollection({ validation: { required: true, matches: /^[a-z0-9]+(?:-[a-z0-9]+)*$/, - matchMessage: 'Slug must contain only lowercase letters, numbers, and hyphens', + matchesMessage: 'Slug must contain only lowercase letters, numbers, and hyphens', }, description: 'URL-friendly version of the title. Must be unique and contain only lowercase letters, numbers, and hyphens.', diff --git a/shared/src/stripe/StripeEventHandler.ts b/shared/src/stripe/StripeEventHandler.ts index d1c755d7f..33c3a97da 100644 --- a/shared/src/stripe/StripeEventHandler.ts +++ b/shared/src/stripe/StripeEventHandler.ts @@ -139,26 +139,6 @@ export class StripeEventHandler { } }; - /** - * Increments the total donations of a campaign if the charge is associated with a campaignId. - */ - maybeUpdateCampaign = async (contribution: StripeContribution): Promise => { - if (contribution.campaign_path) { - try { - const campaign = await contribution.campaign_path.get(); - const current_contributions = campaign.data()?.contributions ?? 0; - const current_amount_chf = campaign.data()?.amount_collected_chf ?? 0; - await contribution.campaign_path.update({ - contributions: current_contributions + 1, - amount_collected_chf: current_amount_chf + contribution.amount_chf, - }); - console.log(`Campaign amount ${contribution.campaign_path} updated.`); - } catch (error) { - console.error(`Error updating campaign amount ${contribution.campaign_path}.`, error); - } - } - }; - constructStatus = (status: Stripe.Charge.Status) => { switch (status) { case 'succeeded': @@ -207,7 +187,6 @@ export class StripeEventHandler { ).doc(charge.id); await contributionRef.set(contribution, { merge: true }); console.info(`Updated contribution document: ${contributionRef.path}`); - await this.maybeUpdateCampaign(contribution); return contributionRef; }; } diff --git a/shared/src/types/campaign.ts b/shared/src/types/campaign.ts index 90c98ef8c..cdac19c9f 100644 --- a/shared/src/types/campaign.ts +++ b/shared/src/types/campaign.ts @@ -17,8 +17,6 @@ export type Campaign = { link_tiktok?: string; link_facebook?: string; link_x?: string; - amount_collected_chf: number; // automatically updated by incoming payments. - contributions: number; // automatically updated by incoming payments. goal?: number; goal_currency?: Currency; end_date: Timestamp; diff --git a/shared/src/types/contribution.ts b/shared/src/types/contribution.ts index 56b70a6b7..0a1818a76 100644 --- a/shared/src/types/contribution.ts +++ b/shared/src/types/contribution.ts @@ -22,13 +22,16 @@ export enum StatusKey { export type Contribution = StripeContribution | BankWireContribution; +/** + * Represents a contribution to Social Income. The amount that ends up on our account is amount_chf - fees_chf. + */ type BaseContribution = { source: ContributionSourceKey; status: StatusKey; created: Timestamp; amount: number; - amount_chf: number; - fees_chf: number; + amount_chf: number; // Amount donated in CHF, including fees + fees_chf: number; // Transaction fees in CHF currency: Currency; campaign_path?: DocumentReference; }; @@ -36,7 +39,7 @@ type BaseContribution = { export type StripeContribution = BaseContribution & { source: ContributionSourceKey.STRIPE; monthly_interval: number; - reference_id: string; // stripe charge id + reference_id: string; // The stripe charge id, see: https://docs.stripe.com/api/charges }; export type BankWireContribution = BaseContribution & { diff --git a/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx b/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx index ec209e8ec..b3fe7ed36 100644 --- a/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx @@ -6,6 +6,7 @@ import { firestoreAdmin } from '@/firebase-admin'; import { WebsiteLanguage, WebsiteRegion } from '@/i18n'; import { getMetadata } from '@/metadata'; import { Campaign, CAMPAIGN_FIRESTORE_PATH, CampaignStatus } from '@socialincome/shared/src/types/campaign'; +import { Contribution, CONTRIBUTION_FIRESTORE_PATH } from '@socialincome/shared/src/types/contribution'; import { daysUntilTs } from '@socialincome/shared/src/utils/date'; import { getLatestExchangeRate } from '@socialincome/shared/src/utils/exchangeRates'; import { Translator } from '@socialincome/shared/src/utils/i18n'; @@ -85,8 +86,11 @@ export default async function Page({ params }: CampaignPageProps) { ? await getLatestExchangeRate(firestoreAdmin, campaign.goal_currency) : 1.0; - const contributions = campaign.contributions ?? 0; - const amountCollected = Math.round((campaign.amount_collected_chf ?? 0) * exchangeRate); + const contributions = await firestoreAdmin + .collectionGroup(CONTRIBUTION_FIRESTORE_PATH) + .where('campaign_path', '==', firestoreAdmin.firestore.doc([CAMPAIGN_FIRESTORE_PATH, params.campaign].join('/'))) + .get(); + const amountCollected = (contributions.docs.reduce((sum, c) => sum + c.data().amount_chf, 0) ?? 0) * exchangeRate; const percentageCollected = campaign.goal ? Math.round((amountCollected / campaign.goal) * 100) : undefined; const daysLeft = daysUntilTs(campaign.end_date.toDate()); @@ -122,7 +126,7 @@ export default async function Page({ params }: CampaignPageProps) { {translator?.t('campaign.without-goal.collected', { context: { - count: contributions, + count: contributions.docs.length, amount: amountCollected, currency: campaign.goal_currency, total: campaign.goal, @@ -157,7 +161,7 @@ export default async function Page({ params }: CampaignPageProps) { {translator.t('campaign.with-goal.collected-amount', { context: { - count: contributions, + count: contributions.docs.length, amount: amountCollected, currency: campaign.goal_currency, }, From be8179cc907d2e6085155c3f14b77035964c2474 Mon Sep 17 00:00:00 2001 From: mkue Date: Sun, 12 Jan 2025 23:26:09 +0100 Subject: [PATCH 09/15] Admin: Allow specifying an amount in CHF that is added to campaign total --- admin/src/collections/Campaigns.ts | 7 ++++++- shared/src/types/campaign.ts | 1 + .../[lang]/[region]/(website)/campaign/[campaign]/page.tsx | 5 ++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/admin/src/collections/Campaigns.ts b/admin/src/collections/Campaigns.ts index 6d1fe6617..b1403cc0f 100644 --- a/admin/src/collections/Campaigns.ts +++ b/admin/src/collections/Campaigns.ts @@ -111,6 +111,12 @@ export const campaignsCollection = buildAuditedCollection({ EUR: 'EUR', }, }, + additional_amount_chf: { + dataType: 'number', + name: 'Additional Amount CHF', + description: + 'Amount that is added to the amount raised (e.g. estimated amount that was donated because of the campaign, but not through the campaign page)', + }, end_date: { // @ts-ignore dataType: 'date', @@ -140,7 +146,6 @@ export const campaignsCollection = buildAuditedCollection({ dataType: 'string', name: 'Url Slug', validation: { - required: true, matches: /^[a-z0-9]+(?:-[a-z0-9]+)*$/, matchesMessage: 'Slug must contain only lowercase letters, numbers, and hyphens', }, diff --git a/shared/src/types/campaign.ts b/shared/src/types/campaign.ts index cdac19c9f..0bc39ebca 100644 --- a/shared/src/types/campaign.ts +++ b/shared/src/types/campaign.ts @@ -19,6 +19,7 @@ export type Campaign = { link_x?: string; goal?: number; goal_currency?: Currency; + additional_amount_chf?: number; end_date: Timestamp; status: CampaignStatus; public?: boolean; diff --git a/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx b/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx index b3fe7ed36..423756918 100644 --- a/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/campaign/[campaign]/page.tsx @@ -90,7 +90,10 @@ export default async function Page({ params }: CampaignPageProps) { .collectionGroup(CONTRIBUTION_FIRESTORE_PATH) .where('campaign_path', '==', firestoreAdmin.firestore.doc([CAMPAIGN_FIRESTORE_PATH, params.campaign].join('/'))) .get(); - const amountCollected = (contributions.docs.reduce((sum, c) => sum + c.data().amount_chf, 0) ?? 0) * exchangeRate; + let amountCollected = contributions.docs.reduce((sum, c) => sum + c.data().amount_chf, 0); + amountCollected += campaign.additional_amount_chf || 0; + amountCollected *= exchangeRate; + const percentageCollected = campaign.goal ? Math.round((amountCollected / campaign.goal) * 100) : undefined; const daysLeft = daysUntilTs(campaign.end_date.toDate()); From 2d28b076aa3457886bb2964710e6a14767038468 Mon Sep 17 00:00:00 2001 From: Pranav Chatur Date: Mon, 13 Jan 2025 22:25:46 +0530 Subject: [PATCH 10/15] Website: Display contributions dynamically on selections page (instead of hardcoding in shared/locales) (#1001) * Display contributions dynamically instead of hardcoding in shared/locales * Debug ill-functioning of currency-redirect.tsx, revert changes in shared locales * Experiment adding "../" to Draws path * Use process.cwd to get the current nextjs folder * Remove extra "/transparency" route in currency * Clean TODOs and ready for review * Move currency-redirect.tsx to (components) directory in transparency folder --------- Co-authored-by: Sandino Scheidegger --- shared/locales/de/website-selection.json | 1 - shared/locales/en/website-selection.json | 1 - shared/locales/fr/website-selection.json | 1 - shared/locales/it/website-selection.json | 1 - .../currency-redirect.tsx | 0 .../transparency/finances/[currency]/page.tsx | 2 +- .../{ => [currency]}/(assets)/globe.svg | 0 .../(assets)/transparency.svg | 0 .../(components)/draw-card.tsx | 2 +- .../(components)/scroll-to-chevron.tsx | 0 .../{ => [currency]}/(sections)/faq.tsx | 0 .../(sections)/hero-section.tsx | 2 +- .../(sections)/past-rounds.tsx | 4 +-- .../{ => [currency]}/(sections)/resources.tsx | 24 ++++++++++++++--- .../(sections)/selection-process.tsx | 0 .../{ => [currency]}/(sections)/state.ts | 2 +- .../recipient-selection/[currency]/page.tsx | 27 +++++++++++++++++++ .../transparency/recipient-selection/page.tsx | 27 ++++++++----------- 18 files changed, 64 insertions(+), 30 deletions(-) rename website/src/app/[lang]/[region]/(website)/transparency/{finances/[currency] => (components)}/currency-redirect.tsx (100%) rename website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/{ => [currency]}/(assets)/globe.svg (100%) rename website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/{ => [currency]}/(assets)/transparency.svg (100%) rename website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/{ => [currency]}/(components)/draw-card.tsx (95%) rename website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/{ => [currency]}/(components)/scroll-to-chevron.tsx (100%) rename website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/{ => [currency]}/(sections)/faq.tsx (100%) rename website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/{ => [currency]}/(sections)/hero-section.tsx (94%) rename website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/{ => [currency]}/(sections)/past-rounds.tsx (89%) rename website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/{ => [currency]}/(sections)/resources.tsx (67%) rename website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/{ => [currency]}/(sections)/selection-process.tsx (100%) rename website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/{ => [currency]}/(sections)/state.ts (95%) create mode 100644 website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/page.tsx diff --git a/shared/locales/de/website-selection.json b/shared/locales/de/website-selection.json index dd90b9e5f..e9476f635 100644 --- a/shared/locales/de/website-selection.json +++ b/shared/locales/de/website-selection.json @@ -42,7 +42,6 @@ "text": "die die Unterstützung am meisten benötigen.”" } ], - "amount": "USD 300,000+", "amount-context": "Spenden erhalten ↗", "scope": "Mehr ", "continue-1": "Auswahlverfahren" diff --git a/shared/locales/en/website-selection.json b/shared/locales/en/website-selection.json index eb9194885..a2e0a7b7b 100644 --- a/shared/locales/en/website-selection.json +++ b/shared/locales/en/website-selection.json @@ -39,7 +39,6 @@ "text": "who need Social Income the most." } ], - "amount": "USD 300,000+", "amount-context": "raised to date ↗", "scope": "visit our ", "continue-1": "Selection Process" diff --git a/shared/locales/fr/website-selection.json b/shared/locales/fr/website-selection.json index cbef6c272..645d90e51 100644 --- a/shared/locales/fr/website-selection.json +++ b/shared/locales/fr/website-selection.json @@ -39,7 +39,6 @@ "text": "sur les personnes qui ont le plus besoin de Social Income." } ], - "amount": "USD 300,000+", "amount-context": "reçus à ce jour ↗", "scope": "Plus de ", "continue-1": "Processus de sélection" diff --git a/shared/locales/it/website-selection.json b/shared/locales/it/website-selection.json index 4e1b65509..af75cd5da 100644 --- a/shared/locales/it/website-selection.json +++ b/shared/locales/it/website-selection.json @@ -39,7 +39,6 @@ "text": "che hanno più bisogno di Social Income." } ], - "amount": "USD 300,000+", "amount-context": "raccolti fino a oggi ↗", "scope": "Più ", "continue-1": "Processo di selezione" diff --git a/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/currency-redirect.tsx b/website/src/app/[lang]/[region]/(website)/transparency/(components)/currency-redirect.tsx similarity index 100% rename from website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/currency-redirect.tsx rename to website/src/app/[lang]/[region]/(website)/transparency/(components)/currency-redirect.tsx diff --git a/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/page.tsx b/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/page.tsx index 4d750b628..37d5fc068 100644 --- a/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/transparency/finances/[currency]/page.tsx @@ -1,5 +1,5 @@ import { DefaultPageProps, DefaultParams } from '@/app/[lang]/[region]'; -import { CurrencyRedirect } from '@/app/[lang]/[region]/(website)/transparency/finances/[currency]/currency-redirect'; +import { CurrencyRedirect } from '@/app/[lang]/[region]/(website)/transparency/(components)/currency-redirect'; import { firestoreAdmin } from '@/firebase-admin'; import { WebsiteCurrency, WebsiteLanguage, WebsiteRegion, websiteCurrencies } from '@/i18n'; import { Currency } from '@socialincome/shared/src/types/currency'; diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(assets)/globe.svg b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(assets)/globe.svg similarity index 100% rename from website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(assets)/globe.svg rename to website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(assets)/globe.svg diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(assets)/transparency.svg b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(assets)/transparency.svg similarity index 100% rename from website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(assets)/transparency.svg rename to website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(assets)/transparency.svg diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(components)/draw-card.tsx b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(components)/draw-card.tsx similarity index 95% rename from website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(components)/draw-card.tsx rename to website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(components)/draw-card.tsx index b3427ec85..57a04db55 100644 --- a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(components)/draw-card.tsx +++ b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(components)/draw-card.tsx @@ -1,10 +1,10 @@ 'use client'; -import { CompletedDraw } from '@/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/state'; import { WebsiteLanguage } from '@/i18n'; import { Card, Collapsible, CollapsibleContent, CollapsibleTrigger, Typography } from '@socialincome/ui'; import { DateTime } from 'luxon'; import Link from 'next/link'; +import { CompletedDraw } from '../(sections)/state'; type DrawCardProps = { lang: WebsiteLanguage; diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(components)/scroll-to-chevron.tsx b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(components)/scroll-to-chevron.tsx similarity index 100% rename from website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(components)/scroll-to-chevron.tsx rename to website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(components)/scroll-to-chevron.tsx diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/faq.tsx b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/faq.tsx similarity index 100% rename from website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/faq.tsx rename to website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/faq.tsx diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/hero-section.tsx b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/hero-section.tsx similarity index 94% rename from website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/hero-section.tsx rename to website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/hero-section.tsx index 49c4ffc67..73a901ab2 100644 --- a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/hero-section.tsx +++ b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/hero-section.tsx @@ -1,10 +1,10 @@ import { DefaultParams } from '@/app/[lang]/[region]'; -import ScrollToChevron from '@/app/[lang]/[region]/(website)/transparency/recipient-selection/(components)/scroll-to-chevron'; import { Translator } from '@socialincome/shared/src/utils/i18n'; import { Typography } from '@socialincome/ui'; import { FontColor } from '@socialincome/ui/src/interfaces/color'; import Image from 'next/image'; import globeRotating from '../(assets)/globe.svg'; +import ScrollToChevron from '../(components)/scroll-to-chevron'; export async function HeroSection({ lang }: DefaultParams) { const translator = await Translator.getInstance({ diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/past-rounds.tsx b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/past-rounds.tsx similarity index 89% rename from website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/past-rounds.tsx rename to website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/past-rounds.tsx index a29f3b42d..12009a52b 100644 --- a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/past-rounds.tsx +++ b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/past-rounds.tsx @@ -1,9 +1,9 @@ import { DefaultParams } from '@/app/[lang]/[region]'; -import { DrawCard } from '@/app/[lang]/[region]/(website)/transparency/recipient-selection/(components)/draw-card'; -import { loadPastDraws } from '@/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/state'; import { Translator } from '@socialincome/shared/src/utils/i18n'; import { BaseContainer, Typography } from '@socialincome/ui'; import { FontColor } from '@socialincome/ui/src/interfaces/color'; +import { DrawCard } from '../(components)/draw-card'; +import { loadPastDraws } from './state'; export const revalidate = 3600 * 24; // update once a day diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/resources.tsx b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/resources.tsx similarity index 67% rename from website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/resources.tsx rename to website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/resources.tsx index af9d027e5..e9ca92028 100644 --- a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/resources.tsx +++ b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/resources.tsx @@ -1,17 +1,32 @@ import { DefaultParams } from '@/app/[lang]/[region]'; -import ScrollToChevron from '@/app/[lang]/[region]/(website)/transparency/recipient-selection/(components)/scroll-to-chevron'; +import { firestoreAdmin } from '@/firebase-admin'; +import { WebsiteCurrency } from '@/i18n'; import { Translator } from '@socialincome/shared/src/utils/i18n'; +import { ContributionStatsCalculator } from '@socialincome/shared/src/utils/stats/ContributionStatsCalculator'; import { Button, Typography } from '@socialincome/ui'; import { FontColor } from '@socialincome/ui/src/interfaces/color'; import Image from 'next/image'; import transparency from '../(assets)/transparency.svg'; +import ScrollToChevron from '../(components)/scroll-to-chevron'; -export async function Resources({ lang }: DefaultParams) { +type ResourcePageProps = { + currency: string; +} & DefaultParams; + +const roundAmount = (amount: number) => (amount ? Math.round(amount / 10) * 10 : 0); + +export async function Resources({ lang, currency }: ResourcePageProps) { const translator = await Translator.getInstance({ language: lang, namespaces: ['website-selection'], }); + const contributionCalculator = await ContributionStatsCalculator.build( + firestoreAdmin, + currency.toUpperCase() as WebsiteCurrency, + ); + const totalContributionsAmount = contributionCalculator.totalContributionsAmount(); + return (
@@ -39,9 +54,10 @@ export async function Resources({ lang }: DefaultParams) {
diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/selection-process.tsx b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/selection-process.tsx similarity index 100% rename from website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/selection-process.tsx rename to website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/selection-process.tsx diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/state.ts b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/state.ts similarity index 95% rename from website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/state.ts rename to website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/state.ts index c338481f7..cf727b688 100644 --- a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/(sections)/state.ts +++ b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/state.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'fs'; import * as fs from 'fs/promises'; -const DRAWS_PATH = '../recipients_selection/draws'; +const DRAWS_PATH = process.cwd() + '/../recipients_selection/draws'; export type Draw = { name: string; diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/page.tsx b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/page.tsx new file mode 100644 index 000000000..16dac6041 --- /dev/null +++ b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/page.tsx @@ -0,0 +1,27 @@ +import { DefaultParams } from '@/app/[lang]/[region]'; +import { CurrencyRedirect } from '@/app/[lang]/[region]/(website)/transparency/(components)/currency-redirect'; +import { WebsiteCurrency } from '@/i18n'; +import { SelectionFaq } from './(sections)/faq'; +import { HeroSection } from './(sections)/hero-section'; +import { PastRounds } from './(sections)/past-rounds'; +import { Resources } from './(sections)/resources'; +import { SelectionProcess } from './(sections)/selection-process'; + +type RecipientSelectionPageProps = { + params: { + currency: string; + } & DefaultParams; +}; + +export default async function Page({ params: { lang, region, currency } }: RecipientSelectionPageProps) { + return ( +
+ + + + + + +
+ ); +} diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/page.tsx b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/page.tsx index 71400c9a5..a9bdd5987 100644 --- a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/page.tsx +++ b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/page.tsx @@ -1,18 +1,13 @@ -import { DefaultPageProps } from '@/app/[lang]/[region]'; -import { SelectionFaq } from './(sections)/faq'; -import { HeroSection } from './(sections)/hero-section'; -import { PastRounds } from './(sections)/past-rounds'; -import { Resources } from './(sections)/resources'; -import { SelectionProcess } from './(sections)/selection-process'; +'use client'; -export default async function Page({ params: { lang, region } }: DefaultPageProps) { - return ( -
- - - - - -
- ); +import { useI18n } from '@/components/providers/context-providers'; +import { redirect } from 'next/navigation'; +import { useEffect } from 'react'; + +export default function Page() { + const { currency } = useI18n(); + + useEffect(() => { + redirect('./recipient-selection/' + currency?.toLowerCase()); + }, [currency]); } From b2706b08fd95aab969a3fd8d949c9010a85576fe Mon Sep 17 00:00:00 2001 From: PM <3749956+CluEleSsUK@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:50:43 +0700 Subject: [PATCH 11/15] Hashed list of receipients (#1013) --- .../lists/20-slaes-2025-01-13.txt | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 recipients_selection/lists/20-slaes-2025-01-13.txt diff --git a/recipients_selection/lists/20-slaes-2025-01-13.txt b/recipients_selection/lists/20-slaes-2025-01-13.txt new file mode 100644 index 000000000..b33261d3a --- /dev/null +++ b/recipients_selection/lists/20-slaes-2025-01-13.txt @@ -0,0 +1,135 @@ +3c549479e1b4c690e1997e611f52f407ecf717bde073e9896c63a2759ab572d0 +cdadd22ccb0b34c5894637fbce986150ca782bcb5976ec8fe7d8e05f8df32ba5 +75023b721eb0809f27c22338c3f3dc489b35bd37c951c3efa4532c38f4983e70 +05e6278cd4bf87280ab2806d2b4b0ca69c108c55ca2777428fffdfc16885b429 +5a9d9ef89680f7d74010a4a3e0e6acd21811abddce420ce939a5d2a98c17d81e +f3a091521e60aa73a193cf8ba1486d386a963c288ff45ee88aeec667bf558115 +6951732a1edc56d7f4ffdd9d30c6bff6e0839b938f4d614d19d74f28505fb3e9 +fbb38011414f0844c53a5ee63d995ab1915950d68cd8890e498596b40382d9dc +9d4ba4896443cf1a09dae41893cb292bd142ff755536ff3e67be152465e2d7a7 +cbbeabf8270fade78f5716a4bd4bb05aee4b9df3bedb329f66fc5780acc07c2a +cefce9a39f387a367b09d6987eb1357afb0806e8a2a6510edb67946515ab5b0a +aee3258d23767baf3443aaad74cbd1e752652cdcd4c5b14f2660bfc8d8015664 +8fe43ad77d2a1562ab59ad0645378efc50f9062b8b0cccd3810dc0002843185c +359ae84b8854f56920334fe6377a7b1330eca6e2217fd90c231a87da8644e9a6 +81b43a69a7c437e13a7c90e6133dbb815917a63a93c52017ad9dbf1573186819 +d90d4234442b1284817017552fee5570f9ab011435c0314eb6584a6dad7fc489 +70ecebb5090cf8b4e6d819a626660af2f0e66eefcc8dbcb7e748cde18758159c +5a07ac03f5ec409997d20d1e63ead54c4878c43e960cf5f45e6defe70a383225 +3e69041b012a70754ef4b87987a583be76d7b57ed94a17a2b50bc85d363a1066 +12b2ed5d857812f87fbd85c4600b96a9dc6dc1716433ac369dc3a05f39fc0e43 +99d8af55134a49cbfd7bddf5c57f56e301d69e5940827157a4eefeafe552cc28 +74df800c1da8c70f0a6477e19f94a55b1903a67479d7e4de0acf86bf598cde7b +1697d5f056895ba71c025aa0badbef1802888a9f0c514d20fa78ec973e16823f +9a5c31ff7bdf21e62845088314fd4219861d8a8c2396f2b50a9b92e8bfa37e76 +412a889e716c571a056a605568f1af18c3808d69e95a651175b2a5b241b17237 +4093ba2c82d1bb8c2d1ce1008d62a6edc9bfc33e416695dfd3f00978dd0dd422 +f3464b42c9872dd2b42013cb1ae6021f63dc970617c616b1a48a0bfe26733577 +0c761b93d75dbd8747823a7db60b6dd5636ca12debd5cfa238224360e9943b57 +fd91ce193087696784ebd2ad7def21c5cd8df70fd7b1184921004c19573322bb +84a4d5f67a3c34010ae7c3e4be7d1c857d47e792c830bbea6391ef369a455392 +d346b42880d5dee1ce6607fc3fff7c07bf02b388f04f6af4b312fff6cc8e48fc +c4aaf6e53113c44ccc9d4b74ebbdfb4db2ec505e61a7f81418694906bfb263ae +1aeb306054e6633331feaa0f36a7ea32c31f575c3cd4a14a2144083c8c4262aa +4de933e7e5b71fb260aad156e2f1d0e1a1f34b5e74ac369129806233c4723b38 +cf35fcf394016ab0a602cf5d897aeb64de9fce669a5e54f0369a2a4e28802e88 +9902a832af01bbec0cef90e456f081ff96f57666cbd0bcb35f9ff0e0ccd8d158 +432473f1a053f4cf519677338bea0a99d37358c615ea18fa11dd69cb4ada2c24 +cd831096ae49cec2fa9cb7a2fe02fb7b7ecc593b279106cca1dd5ec736c1d1ed +16fa404e77fb5495487285803ad224efbfb24cc7a0c25ed0198d8023c37e5001 +ccac63b7013cffe262e20ce9c4fbbbd54f8badeceecc0de7a60b6ae1a74422b9 +f1915749f472dcdfeac8012117d654aa5e3916396400ccf72e5b5028762dc759 +48a5048d3ebddf3254c3c76d0401c5f693dd41a34f902606931877fbfc6267bf +036401accd14a9776b407db04a9359ae3d66eeb6a55cfee7948fd8724f388bbd +619da5ec2bf0964708a9ba6d729f5c9a0022ecb8110ebad4721b78867493a6cc +60e42d225421967867d7b4f09553436b03f7d8d6bd212ed7fcf866b05c1f5e50 +fe95b437cda56d587dd3091d3da9760a6dca7ce9fd17edd5fe17653527fcc74a +e5b1471200bd779c385a6161794f293982ee6e61936760ae48401e73c7d7d9f9 +a1bb579aea2198292b0250a08ac3a3dd03ed496978295221d22b7869123af4b0 +74ef83131aac4b3d9e2948984d9defa55ec703109c07b67b9f42d59de80adc50 +648ce03ab4b8dbc154c9dec8c162096ac7a9a31474e4f087c7a9b1ce156a9d4e +fca4df93db2e2482aa51ac00636e740019318c440c1b4a20965caf8973dc00a5 +190d0556b69a8166ae7c9b109129b78b7f5b652136c2d89e80ee4e1dd3d89362 +a603b7a77ffe131ac7f080c1c9560773aab1f099779572fcb5d6418dbc48b13a +45eb431ef2096a1852b01f40b0f83659c7cf7be90adcbbf6f6c30b253875f256 +c4c594aaf128658de0596dca9cbbab297439c137ddeed1c41b113047fbaee33c +67c26c6c48e45f890fa2fae9a7622f841a1e844378b0f161fe4dcdf8912893d7 +ce8786a52eff1e628f2cf028859a9998841f69b1ac69153752c25bb15dee5a85 +6c3fd48ce3fc404e8db9026ae0a3959522c59ce287b679dd2b63619b3253147c +24b9b34f427d3eb5472a17169988e56c9d49c58d9f3d5fe167087685a974314d +fb84b405284ef0972fb4655e8107e4c872b84845ef6c6f4895321e206791d5cf +635753525f84bd833a267844e7101575562883785a96c33dc521bdb3e55580c6 +337e166ea259aa1567b19eb1e7a55e2b52bcf481fd3ebea60986ffc9b3fedfc8 +771d90d89ec9e463e77b62f10ab45a9e75d7a7a5f759cd947b9fd7dc0316068b +079b382fc8d8f958ad3c67f0b91ddc12ad79b25fa230bdbed67ff608106e8f48 +cfcd75de02828427e96d81ded8d03688b74ed4097727b8b20907d6db7c5cc0ed +b09101cd2acc9aba7aba9049380d85a08c45e1210822ec5e40eee1c0da3f423d +c99a05f791c93449519e63b40cfb7be79e7a2ce63a05803ff06b2dad2d1f6208 +de14e1d69d17c4b382cd718f50a35c2976abbe5069364a4eb54f70e2eb11f3e0 +43ee907f2c3454f9f33b89ddeedd1294a62cf24acc937b115ae8f6a2055d9dce +76f412d226b9a2723ed677ae6a96f48808f5ce0495aa358dbaeafa4743d74900 +a2e64e40a017868900d79d82f13180d9928c701c75fafd6903191f5eb37b3d9f +876069cc2611f3b95dd2dd972901197fda537b8613794b52e396629d42b6c9f9 +66b904e1f7187224f83709f79049eb554b5a7ced11ebb8ee4ad0fec3e8ff34a6 +a1be4bb62fc2646e418f37f59bde841ac510b3ff8821a3186278cb56e161d305 +5292ead78dd2e613948d451ed84a929fd5f7a68810054b5bf9d261d1ee09b2c0 +56a66590a816cca5c2f5e29cb7775e38f81a14752015223374e26610bd47232f +c1dd7ea86d6dec5d8993f5072b4c10f214b0a670adc17d5343533f905e030210 +67e16977cfaa3814103c3d460a9873923df301c833279b059c87022adbe14811 +56860088e07a9d08d44544bcfb83271dae61c9864bab0d8b5c4ee01890be3548 +387810e87d088dbf56d9fb998e4c43e748840ff3ac1dc9207f9046051e4830cd +6f8f4d0c3573f2b4ae9b683af8d9cbb8ce1ad898c3ab9c3425c40b273b3be25f +c52462c4ccb1f28ce2063d5c08edaf1a129af100906b173649f64711677cdd36 +616d960ef8deaf46df4faa0c08a725f1a7b31b83bfb881e9de452d01f1c74470 +97cd700e394c130de613306a3bd7f4ea125b759f880a7859342c0e4e0d04ceee +92724b0ffd873f7b18a80ffdfc97e9944075c23f83c1387174d788badffb9614 +2270de6bbb1b1fe34fc068d2524574ca400dc586ff2b4986224694438c1ca026 +18e67ff4feb0a87c8eac38c2a9c8cee047ce815590e5ab85ab5642c3762a94ea +e61049c76b9f1e936bdcf750090b6ac3b29ccd528c2ace90babb582d51a2347d +786a078488661e122b691b6b0a2b5eb36815691b9f5ac08ef06ef2e97dca8bf7 +65ad8ddf9093f2db2cffea134b431ed4d44a17310cc3ba52e71d024ae127e62c +f3732f8a7a3224cce8623508551222d2e82241653e0634d61dac5eb067c74df8 +e4ac123f7258a0847b5de95d0ae760f37ecfe29291b1c71f03444294eaedd3fd +109b040e4e2dc0b76f6695191dd7143c79267c70f7b8f5bec6601196c43e7948 +f7d7635e994031d1b6868d339241e38c52f7b07ea0144f58701af302df9827db +af3bb7d51ff33c5ca365bcf27c8f2ba1cee06df9d5537e7d6df001ba46405996 +fde170873fce7ac1d5462713806528af64dc56baa367149101e5dfd9368615b9 +03e341580f74f1e9dc0a764ac7e9be694e53433a80b9a5af180acd696f65d107 +cfd3071770969900d1290bed244adae77a56140782a6a6edd60d4bd2f52d544b +2d6d46c2d93741b1a74e1bed36b9089a82af90c0b0b3267e02a1074e308981fb +29283365c26b800643e2c881e8f0e95180c0d59c59186df129bb3fe476c1c3bb +d1c8a33726ae82a854f9d69f7ba3ba42d94b4d2cbd54d9634fb10b36bd7dcfbe +326211513743a6c85abf526644542e32a0ffc6f72289b43512d3bb47f6591ae5 +a0ee521bd6256fb6750a4e41f4001cfc0629f38f50e56b4b519684f5e39b5a20 +8f1c025e16d779d7cecb64607ac7ee6113c96fed0c11e637b454ccd84de87a4a +8dc6a6106d897be9309fa46f9fd63b7f6fe757699c7e16fe785886c3eee92895 +967f6117de7a9199cb58c94958a0ffa0df5541286d980c54ebe197f388974f39 +a30612a1fa41cf238b1f41e260f240f9129cc41d2c6fb34cf781e0163b42c0d5 +1166251b9bbe0ee73e66e2ad2c83185f8d6a93ffd55c66359fea65e42cd95574 +a0dee8126fb5d4d3ae19f7eb13fe23d068e25eb6fe53505fbf34f87c2fae5aa4 +1b19ffb4a4f78ba3c6f2b07242687321f132e8a9a5066861cb36e1995c2f9b95 +d08371e76f200c3c82a68d57b2b34738325e4c7a8fb4ccade4d1197bce880c2e +17467511f11f451aa5b3467b5f02a1be59deb081b8ade7b90b4098f92e6d450c +f4d5838f339a9cd93393b41a7a7ca7091ee342e2b375f4a7463386ce7799d8c7 +e7b9825a9ec3ab928b6b13513acc9461e1a4129caab2a6c07808f0511b82b089 +8210f2b4f3020d020d47861d72543f4ec71b4dddd64eb61956951f1001cb2d75 +ab54045373352df27b563a5973e1847945c0e2da4f15194a6ae7b539e45e2979 +c9436062d839ca9df12dfa7c7073ed35bd44c9042bd86fe45d2902f3b7a23742 +5a7604bdf28dbf85b55bc867734b2217d5f918e266f2b30f49cec97ed9558875 +126c40a2e9c524d87abcbb3613eac45f78ff4bf06f59b40be8055c16114a28f5 +c172d9c8119bb6b473986ac1e6f63244e272712e579d70091b7ad37a5a51f18b +ddabc22c6c0ae68ffb3d3ea12e46bb0ce6cd850c7979d1cb18093754e19daaf1 +3b408a89731cefe079f6f0bfc4361a87e5f40e3a85ffe62fd8a3e36e20adb064 +548d1e1fff2af60983418ff66b00414c97b3c6002cb763f64b6628109d047a44 +7ec52e1d419b56ece5e5ae2b5a22446f2c7ef74691b21a4d0943b8e7cb63bd83 +8592bd54e90a6cad8a627c27491e067eb6e15155606ebf8b091ef22071f29ff2 +cb5e0afa7a54b4e38ba2022e132ee9f7514c2c24a0714222ed5e58223fac23ca +5776cb14cee26542b72055e651fedc0290c9650642b575028f45e20aae8b99a8 +4ac7761582b039c7ec1abe215bc8063c40d6db76f7d7df53cb85dcde7916bc63 +c9f0ba31b621001bc1e3682a3d9e9b5d02c816b7826f2fe8666d918fcaae08ed +e22f5b825af597e7e5d8de5987f74abe4e0fef2ac797c580d711bc842f9f0c22 +731aa97ff5cdef7b036b7898a084e9211a1c20ffde5b66b780f3a5b92f4ab66e +23b28544b57fef04546ab45d86b966468d4fcd12e2a5a8c89344a96e3c4921d9 +ab63334c7aaa058f60e9179a5ecb36015d2757806ae5d94e3827aadf0f1dabb6 +587926de78d2bb92f30172ff9ba4b0b38f5fa162bc3bb85e4a4e39ada87de68b +dff56cee8aa47f8a356d95b8fdcf556a0324f7a20871c8730893ccabbd2d4348 From ae54aec7c2bef1ae1fc7b3f1a5e5ef9943fb88f4 Mon Sep 17 00:00:00 2001 From: PM <3749956+CluEleSsUK@users.noreply.github.com> Date: Thu, 23 Jan 2025 17:31:04 +0100 Subject: [PATCH 12/15] completed a recipient draw (#1018) Co-authored-by: ssandino --- recipients_selection/draws/20-slaes-2025-01-13.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 recipients_selection/draws/20-slaes-2025-01-13.txt diff --git a/recipients_selection/draws/20-slaes-2025-01-13.txt b/recipients_selection/draws/20-slaes-2025-01-13.txt new file mode 100644 index 000000000..1724984f8 --- /dev/null +++ b/recipients_selection/draws/20-slaes-2025-01-13.txt @@ -0,0 +1 @@ +{"time":1737449464417,"round":14882034,"randomness":"524379580b9af81e6c8e1e859590831c50cca493eb2a86ebb86811c38fe08a7a","totalCount":135,"winners":["786a078488661e122b691b6b0a2b5eb36815691b9f5ac08ef06ef2e97dca8bf7","60e42d225421967867d7b4f09553436b03f7d8d6bd212ed7fcf866b05c1f5e50","fde170873fce7ac1d5462713806528af64dc56baa367149101e5dfd9368615b9","ab54045373352df27b563a5973e1847945c0e2da4f15194a6ae7b539e45e2979","05e6278cd4bf87280ab2806d2b4b0ca69c108c55ca2777428fffdfc16885b429","6c3fd48ce3fc404e8db9026ae0a3959522c59ce287b679dd2b63619b3253147c","e5b1471200bd779c385a6161794f293982ee6e61936760ae48401e73c7d7d9f9","4093ba2c82d1bb8c2d1ce1008d62a6edc9bfc33e416695dfd3f00978dd0dd422","5a07ac03f5ec409997d20d1e63ead54c4878c43e960cf5f45e6defe70a383225","4de933e7e5b71fb260aad156e2f1d0e1a1f34b5e74ac369129806233c4723b38","3e69041b012a70754ef4b87987a583be76d7b57ed94a17a2b50bc85d363a1066","fca4df93db2e2482aa51ac00636e740019318c440c1b4a20965caf8973dc00a5","a1bb579aea2198292b0250a08ac3a3dd03ed496978295221d22b7869123af4b0","f3464b42c9872dd2b42013cb1ae6021f63dc970617c616b1a48a0bfe26733577","f4d5838f339a9cd93393b41a7a7ca7091ee342e2b375f4a7463386ce7799d8c7","190d0556b69a8166ae7c9b109129b78b7f5b652136c2d89e80ee4e1dd3d89362","619da5ec2bf0964708a9ba6d729f5c9a0022ecb8110ebad4721b78867493a6cc","8210f2b4f3020d020d47861d72543f4ec71b4dddd64eb61956951f1001cb2d75","876069cc2611f3b95dd2dd972901197fda537b8613794b52e396629d42b6c9f9","8dc6a6106d897be9309fa46f9fd63b7f6fe757699c7e16fe785886c3eee92895"]} From 2b387bda018148fb543c58dbbc3aa3de72d521e7 Mon Sep 17 00:00:00 2001 From: Alexey Shestakov Date: Thu, 23 Jan 2025 20:34:31 +0400 Subject: [PATCH 13/15] Add Storybook to store components (#1012) And move icons to ui folder --- .github/workflows/deploy-storybook.yml | 55 + package-lock.json | 1493 ++++++++++++++++- package.json | 4 +- ui/.gitignore | 2 + ui/.storybook/main.js | 40 + ui/.storybook/preview.js | 17 + ui/package.json | 13 + ui/src/components/button.tsx | 2 +- ui/src/components/dropdown-menu.tsx | 180 -- ui/src/components/menubar.tsx | 208 --- ui/src/components/navigation-menu.tsx | 121 -- .../src/icons/donate.tsx | 3 + .../logos/si-icon.tsx => ui/src/icons/si.tsx | 4 +- .../src/icons/spinner.tsx | 3 +- ui/src/index.ts | 7 +- ui/src/stories/accordion.stories.tsx | 67 + ui/src/stories/alert.stories.tsx | 86 + ui/src/stories/avatar.stories.tsx | 77 + ui/src/stories/badge.stories.tsx | 86 + ui/src/stories/button.stories.tsx | 128 ++ ui/src/stories/card.stories.tsx | 111 ++ ui/src/stories/carousel.stories.tsx | 158 ++ ui/src/stories/checkbox.stories.tsx | 102 ++ ui/src/stories/collapsible.stories.tsx | 138 ++ ui/src/stories/dialog.stories.tsx | 146 ++ ui/src/stories/form.stories.tsx | 215 +++ ui/src/stories/hover-card.stories.tsx | 161 ++ ui/src/stories/icons.stories.tsx | 64 + ui/src/stories/input.stories.tsx | 107 ++ ui/src/stories/label.stories.tsx | 116 ++ ui/src/stories/popover.stories.tsx | 136 ++ ui/src/stories/progress.stories.tsx | 132 ++ ui/src/stories/radio-group.stories.tsx | 134 ++ ui/src/stories/select.stories.tsx | 160 ++ ui/src/stories/separator.stories.tsx | 101 ++ ui/src/stories/switch.stories.tsx | 114 ++ ui/src/stories/table.stories.tsx | 189 +++ ui/src/stories/tabs.stories.tsx | 157 ++ ui/src/stories/toggle.stories.tsx | 180 ++ ui/src/stories/tooltip.stories.tsx | 187 +++ ui/src/stories/typography.stories.tsx | 145 ++ .../me/contributions/contributions-table.tsx | 12 +- .../donation-certificates-table.tsx | 13 +- .../me/subscriptions/subscriptions-client.tsx | 13 +- .../(website)/me/work-info/employers-list.tsx | 3 +- .../newsletter/subscription-info-form.tsx | 13 +- .../src/components/navbar/navbar-client.tsx | 4 +- 47 files changed, 5076 insertions(+), 531 deletions(-) create mode 100644 .github/workflows/deploy-storybook.yml create mode 100644 ui/.storybook/main.js create mode 100644 ui/.storybook/preview.js delete mode 100644 ui/src/components/dropdown-menu.tsx delete mode 100644 ui/src/components/menubar.tsx delete mode 100644 ui/src/components/navigation-menu.tsx rename website/src/components/logos/donate-icon.tsx => ui/src/icons/donate.tsx (96%) rename website/src/components/logos/si-icon.tsx => ui/src/icons/si.tsx (88%) rename website/src/components/logos/spinner-icon.tsx => ui/src/icons/spinner.tsx (87%) create mode 100644 ui/src/stories/accordion.stories.tsx create mode 100644 ui/src/stories/alert.stories.tsx create mode 100644 ui/src/stories/avatar.stories.tsx create mode 100644 ui/src/stories/badge.stories.tsx create mode 100644 ui/src/stories/button.stories.tsx create mode 100644 ui/src/stories/card.stories.tsx create mode 100644 ui/src/stories/carousel.stories.tsx create mode 100644 ui/src/stories/checkbox.stories.tsx create mode 100644 ui/src/stories/collapsible.stories.tsx create mode 100644 ui/src/stories/dialog.stories.tsx create mode 100644 ui/src/stories/form.stories.tsx create mode 100644 ui/src/stories/hover-card.stories.tsx create mode 100644 ui/src/stories/icons.stories.tsx create mode 100644 ui/src/stories/input.stories.tsx create mode 100644 ui/src/stories/label.stories.tsx create mode 100644 ui/src/stories/popover.stories.tsx create mode 100644 ui/src/stories/progress.stories.tsx create mode 100644 ui/src/stories/radio-group.stories.tsx create mode 100644 ui/src/stories/select.stories.tsx create mode 100644 ui/src/stories/separator.stories.tsx create mode 100644 ui/src/stories/switch.stories.tsx create mode 100644 ui/src/stories/table.stories.tsx create mode 100644 ui/src/stories/tabs.stories.tsx create mode 100644 ui/src/stories/toggle.stories.tsx create mode 100644 ui/src/stories/tooltip.stories.tsx create mode 100644 ui/src/stories/typography.stories.tsx diff --git a/.github/workflows/deploy-storybook.yml b/.github/workflows/deploy-storybook.yml new file mode 100644 index 000000000..6e0c134c7 --- /dev/null +++ b/.github/workflows/deploy-storybook.yml @@ -0,0 +1,55 @@ +name: Deploy Storybook to GitHub Pages + +on: + push: + branches: + - main # Or your default branch name + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + build-and-deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Read .nvmrc + id: nvm + run: echo "NVMRC=$(cat '.nvmrc')" >> $GITHUB_OUTPUT + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ steps.nvm.outputs.NVMRC }} + cache: "npm" + + - name: Install Dependencies + run: npm ci + + - name: Build Storybook + run: npm run ui:build + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ui/storybook-static + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/package-lock.json b/package-lock.json index a331e6443..927c1f6c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -135,6 +135,13 @@ "url": "https://dotenvx.com" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.1.tgz", + "integrity": "sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@algolia/cache-browser-local-storage": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.24.0.tgz", @@ -5441,6 +5448,39 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.4.2.tgz", + "integrity": "sha512-feQ+ntr+8hbVudnsTUapiMN9q8T90XA1d5jn9QzY09sNoj4iD9wi0PY1vsBFTda4ZjEaxRK9S81oarR2nj7TFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.27.0", + "react-docgen-typescript": "^2.2.2" + }, + "peerDependencies": { + "typescript": ">= 4.3.x", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -5535,6 +5575,24 @@ "react": "16 - 18" } }, + "node_modules/@mdx-js/react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz", + "integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, "node_modules/@mui/base": { "version": "5.0.0-beta.40-0", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40-0.tgz", @@ -9755,6 +9813,747 @@ "resolved": "website", "link": true }, + "node_modules/@storybook/addon-actions": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.5.1.tgz", + "integrity": "sha512-oBBSpOJ6/rCdbdU1JxGCLernaCxALLWDIeZk6tLoQbtbsx/czD1sodqjcujjKwbQwNyZTf8xR8zsCSzG06dWDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@types/uuid": "^9.0.1", + "dequal": "^2.0.2", + "polished": "^4.2.2", + "uuid": "^9.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.5.1" + } + }, + "node_modules/@storybook/addon-actions/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@storybook/addon-backgrounds": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.5.1.tgz", + "integrity": "sha512-4NFRFblPbRP3D4o4sSbJ1x9SMncP4+SHdSqKIovTjb+zOhqYPFYWMTinzEndUnBSDGREldHUvHjROuxrD/0qzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "memoizerific": "^1.11.3", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.5.1" + } + }, + "node_modules/@storybook/addon-controls": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.5.1.tgz", + "integrity": "sha512-RA/SPXW1chfsWaV8Lv/aXJNZJ8hasDEXQ1C5xRCt+T8DFvPqRZGgUfIpsiZ80AKp5RzufT9KL+39piPMljhKXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "dequal": "^2.0.2", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.5.1" + } + }, + "node_modules/@storybook/addon-docs": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.5.1.tgz", + "integrity": "sha512-XhELkuNFOa8q2rF/AXTwnKZth7lCFqkfR5VuEAQ+g9hv2p6I/VGlTddylzjdaZKhiy4p8O9DrzGdLFj+oxOpMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mdx-js/react": "^3.0.0", + "@storybook/blocks": "8.5.1", + "@storybook/csf-plugin": "8.5.1", + "@storybook/react-dom-shim": "8.5.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.5.1" + } + }, + "node_modules/@storybook/addon-essentials": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-8.5.1.tgz", + "integrity": "sha512-jPGrZ7j+RWistrsgpvjUBvLpWRuOeDNdV014ggHBxDMNX9GWb1GSubWW2Tlo7BfOuUvjICVAjI4KMp/IC/jwZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/addon-actions": "8.5.1", + "@storybook/addon-backgrounds": "8.5.1", + "@storybook/addon-controls": "8.5.1", + "@storybook/addon-docs": "8.5.1", + "@storybook/addon-highlight": "8.5.1", + "@storybook/addon-measure": "8.5.1", + "@storybook/addon-outline": "8.5.1", + "@storybook/addon-toolbars": "8.5.1", + "@storybook/addon-viewport": "8.5.1", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.5.1" + } + }, + "node_modules/@storybook/addon-highlight": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-8.5.1.tgz", + "integrity": "sha512-nhwx39DuWy2OFP+AQg8EzYP3giM+rQ0OIdAXgAjDVdKk2sGj43gwNYS9wQzXeczEUiSEjQk0JJwBqjF+GtSrag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.5.1" + } + }, + "node_modules/@storybook/addon-interactions": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-8.5.1.tgz", + "integrity": "sha512-tXCKBIWjwhVuSRRoEiPx+u0D4oqMkctTzysfoCw2sqftIT8t2yHyviX29s87z2NH+DNqzBGGDG1UUaLe5qq3Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/instrumenter": "8.5.1", + "@storybook/test": "8.5.1", + "polished": "^4.2.2", + "ts-dedent": "^2.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.5.1" + } + }, + "node_modules/@storybook/addon-measure": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.5.1.tgz", + "integrity": "sha512-Goc/IRh0aYT7zfDP9fgwL+DFX52DylanoBf0uGf59IQ7sEJHbwWm0OpiSEDo+NbtytbG83UOQamT7aQxhQo7Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "tiny-invariant": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.5.1" + } + }, + "node_modules/@storybook/addon-onboarding": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-onboarding/-/addon-onboarding-8.5.1.tgz", + "integrity": "sha512-ooa59cAz9sJHFbStIL3/kpBpLM9xZGQa2zmSn8TMCMUq1TM/rJARYD4SNj4BeBsLH24wFHI9SXf15Cln8EhASg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.5.1" + } + }, + "node_modules/@storybook/addon-outline": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.5.1.tgz", + "integrity": "sha512-LM3wG5bUgAAEgDS4MD1dw2VStduSYTMc/rNgaTExVVr7pPeuAgkfyIUriP3P0i7x5jweSb2aGzaTuy3PUHAWfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.5.1" + } + }, + "node_modules/@storybook/addon-toolbars": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.5.1.tgz", + "integrity": "sha512-01Odzujfq/g9u1ZTmH/X3I9cCnsNzG/wuyhzFr/T99jerx8QG/U45iYYph2Ytw6A5AtYyCnPYmsTsI+phjUvuA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.5.1" + } + }, + "node_modules/@storybook/addon-viewport": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-8.5.1.tgz", + "integrity": "sha512-kKCXZT3keUEQulv2tOzRSl/GdFA2JeFjHmks/n7qQLY0zDqdx/C7K9jUECcrOJiLclZwTJvHA3YXrglVJoa6Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "memoizerific": "^1.11.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.5.1" + } + }, + "node_modules/@storybook/blocks": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.5.1.tgz", + "integrity": "sha512-xUjnOa9udmHhlBTZ+bmMHeU1M9a5OnvnX8urQ0TrNpSyHH7HoPd3xZC4fzz73nSJNMVHIYMZYsz2pj/WfeA/hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/csf": "0.1.12", + "@storybook/icons": "^1.2.12", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.5.1" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@storybook/builder-vite": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-8.5.1.tgz", + "integrity": "sha512-m7nzMmXL8ySRDp3AWsd18xB/mRVFdGnCbXeC2HREQVsu1WFkvcHtksvF4x1BOeeL73eokD2/GzgpCjAS0xVvbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/csf-plugin": "8.5.1", + "browser-assert": "^1.2.1", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.5.1", + "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/@storybook/components": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.5.1.tgz", + "integrity": "sha512-dgZfIIRdI7yA9bYb1rhWzbvU4AnbndAeNhLouxHJkUR5r2Ycp9mJba5UNynN1slgDOxB+VMnq1fWKyfWQrBqnw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" + } + }, + "node_modules/@storybook/core": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.5.1.tgz", + "integrity": "sha512-4zxjclENpZYuNY1fZJE4a7hd8Ho/SiOSN2B57fsIi1qCpKax3JU3J59ZcAWT0iidy5qgM2qMcWbrl0Bl/tWamA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/csf": "0.1.12", + "better-opn": "^3.0.2", + "browser-assert": "^1.2.1", + "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", + "esbuild-register": "^3.5.0", + "jsdoc-type-pratt-parser": "^4.0.0", + "process": "^0.11.10", + "recast": "^0.23.5", + "semver": "^7.6.2", + "util": "^0.12.5", + "ws": "^8.2.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "prettier": "^2 || ^3" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, + "node_modules/@storybook/core/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@storybook/csf": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.12.tgz", + "integrity": "sha512-9/exVhabisyIVL0VxTCxo01Tdm8wefIXKXfltAPTSr8cbLn5JAxGQ6QV3mjdecLGEOucfoVhAKtJfVHxEK1iqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^2.19.0" + } + }, + "node_modules/@storybook/csf-plugin": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-8.5.1.tgz", + "integrity": "sha512-8GFrQgJ+/hzWAj9o4XK8m7UFPLxf0w3RwX0ZMPeb6zDhq/1BUE97AjKFb4Oexkh4I67Pycv4gRUOY9+tXF/1DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "unplugin": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.5.1" + } + }, + "node_modules/@storybook/csf-plugin/node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@storybook/csf-plugin/node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@storybook/csf/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/global": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", + "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@storybook/icons": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.3.1.tgz", + "integrity": "sha512-tgiD2v9v/4sjGOliemoP/8bUe4+ZFpehcqdCVQcPiGZfV0kSBv34Ge+MafeKqM7SLwvGesrbOEOakaogSqGxiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" + } + }, + "node_modules/@storybook/instrumenter": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-8.5.1.tgz", + "integrity": "sha512-wMAhsIzwOh/xXKANAP3IbtXxRWFAZtpRisB0sy8WVTPS3a1L1cA6X+U80Ex/omek6L0FZwKZSKmmfkDeZkYnCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@vitest/utils": "^2.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.5.1" + } + }, + "node_modules/@storybook/manager-api": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.5.1.tgz", + "integrity": "sha512-Oj9kPYbp/82LRQ+rsc0ZH0fkzeiT2U1kvubmNiRjtopQHCP3UTVnvWIXC9zSRFKmS+NaAdd0JYsIBvE8fjnoqQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" + } + }, + "node_modules/@storybook/preview-api": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.5.1.tgz", + "integrity": "sha512-fLR7nvAbjHVLazDA6CLy9O/bpBzKDKqxyBp6SybTBPYa76IzsX8ITSMMt1YcP6rOGhVgcKNA9iBNxRddjLIV0Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" + } + }, + "node_modules/@storybook/react": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-8.5.1.tgz", + "integrity": "sha512-wKhR9SZUbpYUxRDAYUHH4fZHVxiNG43PxT1uvLfX/i7TPMw+wW+G3Q2yrgms1oHmqqRCvlnGHwT5/t9FFxN31w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/components": "8.5.1", + "@storybook/global": "^5.0.0", + "@storybook/manager-api": "8.5.1", + "@storybook/preview-api": "8.5.1", + "@storybook/react-dom-shim": "8.5.1", + "@storybook/theming": "8.5.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@storybook/test": "8.5.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.5.1", + "typescript": ">= 4.2.x" + }, + "peerDependenciesMeta": { + "@storybook/test": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/react-dom-shim": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.5.1.tgz", + "integrity": "sha512-peDiT6A1zyODKd7tVQIiFNU42Iolca67h3kkOQPb7nm/Czf2yIa/BHw+yiNDZx82eCIEvBy1Xf7lnjH8PD61xA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.5.1" + } + }, + "node_modules/@storybook/react-vite": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-8.5.1.tgz", + "integrity": "sha512-ccsPJXjR7WMS/t7R5nJpPtqRzJxjsllqVMNGk9xxoLasWDf3vOLohgyCgt63ws8iOMh26lqZsFyPyWFcpKW/hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@joshwooding/vite-plugin-react-docgen-typescript": "0.4.2", + "@rollup/pluginutils": "^5.0.2", + "@storybook/builder-vite": "8.5.1", + "@storybook/react": "8.5.1", + "find-up": "^5.0.0", + "magic-string": "^0.30.0", + "react-docgen": "^7.0.0", + "resolve": "^1.22.8", + "tsconfig-paths": "^4.2.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@storybook/test": "8.5.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.5.1", + "vite": "^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "@storybook/test": { + "optional": true + } + } + }, + "node_modules/@storybook/react-vite/node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@storybook/react-vite/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@storybook/react-vite/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/react-vite/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/react-vite/node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/@storybook/react-vite/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/react-vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@storybook/react-vite/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@storybook/react-vite/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@storybook/test": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/test/-/test-8.5.1.tgz", + "integrity": "sha512-V0sEXqL5kS0YKugCqWgmCpNODdlCCiVlPqm3i+E2+G97DR980BwXf8J6VPscQDRS9ZG39BrM83Aau6Anxrt1Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/csf": "0.1.12", + "@storybook/global": "^5.0.0", + "@storybook/instrumenter": "8.5.1", + "@testing-library/dom": "10.4.0", + "@testing-library/jest-dom": "6.5.0", + "@testing-library/user-event": "14.5.2", + "@vitest/expect": "2.0.5", + "@vitest/spy": "2.0.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.5.1" + } + }, + "node_modules/@storybook/theming": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.5.1.tgz", + "integrity": "sha512-sg61vY1gM8w42CIi28vo//6E1gHgHLNBNaRhkfvLFpu9PuhAcVWLwBDZq0BoKmDMxRxbSPV2gvIKeXdOtbSCJw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" + } + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -10120,6 +10919,137 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz", + "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -10180,6 +11110,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -10338,6 +11275,13 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/doctrine": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", + "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "8.56.12", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz", @@ -10550,6 +11494,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -10906,6 +11857,13 @@ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/vimeo__player": { "version": "2.18.3", "resolved": "https://registry.npmjs.org/@types/vimeo__player/-/vimeo__player-2.18.3.tgz", @@ -11349,6 +12307,102 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/@vitest/expect": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/@vitest/utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.0.5", + "estree-walker": "^3.0.3", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", + "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", + "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.8", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -12170,6 +13224,16 @@ "safer-buffer": "~2.1.0" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", @@ -12683,6 +13747,50 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/better-opn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", + "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "open": "^8.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/better-opn/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/better-opn/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bfj": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.1.0.tgz", @@ -12901,6 +14009,12 @@ "base64-js": "^1.1.2" } }, + "node_modules/browser-assert": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/browser-assert/-/browser-assert-1.2.1.tgz", + "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==", + "dev": true + }, "node_modules/browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -13206,6 +14320,23 @@ "node": ">=4" } }, + "node_modules/chai": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", + "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -13238,6 +14369,16 @@ "dev": true, "license": "MIT" }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/check-types": { "version": "11.2.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", @@ -14677,6 +15818,13 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssdb": { "version": "7.11.2", "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz", @@ -15177,6 +16325,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-equal": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", @@ -15363,6 +16521,16 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -15501,6 +16669,13 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -16215,6 +17390,44 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/esbuild-register/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/esbuild-register/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -19746,7 +20959,6 @@ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=8" } @@ -22566,6 +23778,16 @@ "dev": true, "license": "MIT" }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", + "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/jsdom": { "version": "16.7.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", @@ -23478,6 +24700,13 @@ "integrity": "sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==", "license": "MIT" }, + "node_modules/loupe": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", + "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "dev": true, + "license": "MIT" + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -23544,6 +24773,16 @@ "node": ">=12" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -23618,6 +24857,13 @@ "tmpl": "1.0.5" } }, + "node_modules/map-or-similar": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz", + "integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==", + "dev": true, + "license": "MIT" + }, "node_modules/markdown-it": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz", @@ -23752,6 +24998,16 @@ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", "license": "MIT" }, + "node_modules/memoizerific": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", + "integrity": "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==", + "dev": true, + "license": "MIT", + "dependencies": { + "map-or-similar": "^1.5.0" + } + }, "node_modules/mensch": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", @@ -23846,6 +25102,16 @@ "node": ">=6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/mini-css-extract-plugin": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", @@ -25685,6 +26951,16 @@ "node": ">=8" } }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/pdfkit": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.15.2.tgz", @@ -25971,6 +27247,19 @@ "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" }, + "node_modules/polished": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", + "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -28466,6 +29755,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/react-docgen": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-7.1.0.tgz", + "integrity": "sha512-APPU8HB2uZnpl6Vt/+0AFoVYgSRtfiP6FLrZgPPTDmqSb2R4qZRbgd0A3VzIFxDt5e+Fozjx79WjLWnF69DK8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.18.9", + "@babel/traverse": "^7.18.9", + "@babel/types": "^7.18.9", + "@types/babel__core": "^7.18.0", + "@types/babel__traverse": "^7.18.0", + "@types/doctrine": "^0.0.9", + "@types/resolve": "^1.20.2", + "doctrine": "^3.0.0", + "resolve": "^1.22.1", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": ">=16.14.0" + } + }, + "node_modules/react-docgen-typescript": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz", + "integrity": "sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 4.3.x" + } + }, + "node_modules/react-docgen/node_modules/@types/resolve": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", + "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", + "dev": true, + "license": "MIT" + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -30142,6 +31470,46 @@ "node": ">=8.10.0" } }, + "node_modules/recast": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.9.tgz", + "integrity": "sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/recast/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/recharts": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz", @@ -30192,6 +31560,33 @@ "node": ">=6.0.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redent/node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/redux": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", @@ -32150,6 +33545,33 @@ "node": ">= 0.4" } }, + "node_modules/storybook": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.5.1.tgz", + "integrity": "sha512-HuaAFA97j2w4i/1EHKj6X4iDiVzPrXzQpmTEE1tLD1QXzqrQKKHse+Ggc8AGMuLTAzxA6xmrX9xibgMNWCgvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/core": "8.5.1" + }, + "bin": { + "getstorybook": "bin/index.cjs", + "sb": "bin/index.cjs", + "storybook": "bin/index.cjs" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "prettier": "^2 || ^3" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, "node_modules/stream-chain": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", @@ -32476,6 +33898,22 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", + "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -33627,6 +35065,26 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -33752,6 +35210,16 @@ "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", "license": "MIT" }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/ts-deepmerge": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-2.0.7.tgz", @@ -34651,6 +36119,20 @@ "integrity": "sha512-QXo+O/QkLP/x1nyi54uQiG0XrODxdysuQvE5dtVqv7F5K2Qb6FsN+qbr6KhF5wQ20tfcV3VQp0/2x1e1MRSPWg==", "license": "MIT" }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -36251,10 +37733,18 @@ "react": ">=18.3.1", "react-dom": ">=18.3.1", "react-hook-form": "^7.52.1", + "tailwind-merge": "^2.5.3", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8" }, "devDependencies": { + "@storybook/addon-essentials": "^8.5.1", + "@storybook/addon-interactions": "^8.5.1", + "@storybook/addon-onboarding": "^8.5.1", + "@storybook/blocks": "^8.5.1", + "@storybook/react": "^8.5.1", + "@storybook/react-vite": "^8.5.1", + "@storybook/test": "^8.5.1", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.13", "@types/react": "^18.3.18", @@ -36266,6 +37756,7 @@ "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "storybook": "^8.5.1", "tailwind-merge": "^2.4.0", "tailwindcss": "^3.4.4" } diff --git a/package.json b/package.json index 2e97eaef7..187f9f5d8 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,9 @@ "website:test:e2e:update": "npm --workspace=@socialincome/website run test:e2e:update", "website:test:e2e:update:emulator": "npm --workspace=@socialincome/website run test:e2e:update:emulator", "website:extract-translations": "npm --workspace=@socialincome/website run extract-translations", - "website:check-translations": "npm --workspace=@socialincome/website run check-translations" + "website:check-translations": "npm --workspace=@socialincome/website run check-translations", + "ui:serve": "npm --workspace=@socialincome/ui run storybook", + "ui:build": "npm --workspace=@socialincome/ui run build-storybook" }, "devDependencies": { "firebase-tools": "13.29.1", diff --git a/ui/.gitignore b/ui/.gitignore index bcc318b81..dc0ff257b 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -1,2 +1,4 @@ storybook-static dist + +*storybook.log diff --git a/ui/.storybook/main.js b/ui/.storybook/main.js new file mode 100644 index 000000000..78408f9d1 --- /dev/null +++ b/ui/.storybook/main.js @@ -0,0 +1,40 @@ +import { dirname, join } from 'path'; + +/** + * This function is used to resolve the absolute path of a package. + * It is needed in projects that use Yarn PnP or are set up within a monorepo. + */ +function getAbsolutePath(value) { + return dirname(require.resolve(join(value, 'package.json'))); +} + +/** @type { import('@storybook/react-vite').StorybookConfig } */ +const config = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + + addons: [ + getAbsolutePath('@storybook/addon-onboarding'), + getAbsolutePath('@storybook/addon-essentials'), + getAbsolutePath('@storybook/addon-interactions'), + ], + + framework: { + name: getAbsolutePath('@storybook/react-vite'), + options: {}, + }, + + viteFinal: async (config) => { + // Use PostCSS config from postcss.config.js + config.css = { + postcss: true, + }; + return config; + }, + + docs: {}, + + typescript: { + reactDocgen: 'react-docgen-typescript', + }, +}; +export default config; diff --git a/ui/.storybook/preview.js b/ui/.storybook/preview.js new file mode 100644 index 000000000..731568931 --- /dev/null +++ b/ui/.storybook/preview.js @@ -0,0 +1,17 @@ +import '../src/globals.css'; + +/** @type { import('@storybook/react').Preview } */ +const preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, + + tags: ['autodocs'], +}; + +export default preview; diff --git a/ui/package.json b/ui/package.json index 91bc6931b..5a5c94ee8 100644 --- a/ui/package.json +++ b/ui/package.json @@ -40,9 +40,17 @@ "react-dom": ">=18.3.1", "react-hook-form": "^7.52.1", "tailwindcss-animate": "^1.0.7", + "tailwind-merge": "^2.5.3", "zod": "^3.23.8" }, "devDependencies": { + "@storybook/addon-essentials": "^8.5.1", + "@storybook/addon-interactions": "^8.5.1", + "@storybook/addon-onboarding": "^8.5.1", + "@storybook/blocks": "^8.5.1", + "@storybook/react": "^8.5.1", + "@storybook/react-vite": "^8.5.1", + "@storybook/test": "^8.5.1", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.13", "@types/react": "^18.3.18", @@ -54,7 +62,12 @@ "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "storybook": "^8.5.1", "tailwind-merge": "^2.4.0", "tailwindcss": "^3.4.4" + }, + "scripts": { + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" } } diff --git a/ui/src/components/button.tsx b/ui/src/components/button.tsx index 8499b6c1f..3384bfa4f 100644 --- a/ui/src/components/button.tsx +++ b/ui/src/components/button.tsx @@ -1,8 +1,8 @@ import { Slot } from '@radix-ui/react-slot'; -import { SpinnerIcon } from '@socialincome/website/src/components/logos/spinner-icon'; import { cva, type VariantProps } from 'class-variance-authority'; import * as React from 'react'; import { ComponentType } from 'react'; +import { SpinnerIcon } from '../icons/spinner'; import { cn } from '../lib/utils'; const buttonVariants = cva( diff --git a/ui/src/components/dropdown-menu.tsx b/ui/src/components/dropdown-menu.tsx deleted file mode 100644 index 38a9ac392..000000000 --- a/ui/src/components/dropdown-menu.tsx +++ /dev/null @@ -1,180 +0,0 @@ -'use client'; - -import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; -import { Check, ChevronRight, Circle } from 'lucide-react'; -import * as React from 'react'; -import { cn } from '../lib/utils'; - -const DropdownMenu = DropdownMenuPrimitive.Root; - -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; - -const DropdownMenuGroup = DropdownMenuPrimitive.Group; - -const DropdownMenuPortal = DropdownMenuPrimitive.Portal; - -const DropdownMenuSub = DropdownMenuPrimitive.Sub; - -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; - -const DropdownMenuSubTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean; - } ->(({ className, inset, children, ...props }, ref) => ( - - {children} - - -)); -DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; - -const DropdownMenuSubContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; - -const DropdownMenuContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, sideOffset = 4, ...props }, ref) => ( - - - -)); -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; - -const DropdownMenuItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - -)); -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; - -const DropdownMenuCheckboxItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, checked, ...props }, ref) => ( - - - - - - - {children} - -)); -DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; - -const DropdownMenuRadioItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - - - - {children} - -)); -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; - -const DropdownMenuLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - -)); -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; - -const DropdownMenuSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; - -const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { - return ; -}; -DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'; - -export { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuPortal, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuTrigger, -}; diff --git a/ui/src/components/menubar.tsx b/ui/src/components/menubar.tsx deleted file mode 100644 index 80010fd45..000000000 --- a/ui/src/components/menubar.tsx +++ /dev/null @@ -1,208 +0,0 @@ -'use client'; - -import * as MenubarPrimitive from '@radix-ui/react-menubar'; -import { Check, ChevronRight, Circle } from 'lucide-react'; -import * as React from 'react'; -import { cn } from '../lib/utils'; - -const MenubarMenu = MenubarPrimitive.Menu; - -const MenubarGroup = MenubarPrimitive.Group; - -const MenubarPortal = MenubarPrimitive.Portal; - -const MenubarSub = MenubarPrimitive.Sub; - -const MenubarRadioGroup = MenubarPrimitive.RadioGroup; - -const Menubar = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -Menubar.displayName = MenubarPrimitive.Root.displayName; - -const MenubarTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName; - -const MenubarSubTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean; - } ->(({ className, inset, children, ...props }, ref) => ( - - {children} - - -)); -MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName; - -const MenubarSubContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName; - -const MenubarContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, align = 'start', alignOffset = -4, sideOffset = 8, ...props }, ref) => ( - - - -)); -MenubarContent.displayName = MenubarPrimitive.Content.displayName; - -const MenubarItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - -)); -MenubarItem.displayName = MenubarPrimitive.Item.displayName; - -const MenubarCheckboxItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, checked, ...props }, ref) => ( - - - - - - - {children} - -)); -MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName; - -const MenubarRadioItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - - - - {children} - -)); -MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName; - -const MenubarLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - -)); -MenubarLabel.displayName = MenubarPrimitive.Label.displayName; - -const MenubarSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName; - -const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes) => { - return ; -}; -MenubarShortcut.displayname = 'MenubarShortcut'; - -export { - Menubar, - MenubarCheckboxItem, - MenubarContent, - MenubarGroup, - MenubarItem, - MenubarLabel, - MenubarMenu, - MenubarPortal, - MenubarRadioGroup, - MenubarRadioItem, - MenubarSeparator, - MenubarShortcut, - MenubarSub, - MenubarSubContent, - MenubarSubTrigger, - MenubarTrigger, -}; diff --git a/ui/src/components/navigation-menu.tsx b/ui/src/components/navigation-menu.tsx deleted file mode 100644 index cb8554310..000000000 --- a/ui/src/components/navigation-menu.tsx +++ /dev/null @@ -1,121 +0,0 @@ -'use client'; - -import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu'; -import { cva } from 'class-variance-authority'; -import { ChevronDown } from 'lucide-react'; -import * as React from 'react'; -import { cn } from '../lib/utils'; - -const NavigationMenu = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - {children} - - -)); -NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName; - -const NavigationMenuList = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName; - -const NavigationMenuItem = NavigationMenuPrimitive.Item; - -const navigationMenuTriggerStyle = cva( - 'group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-lg font-medium transition-colors hover:bg-muted hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50', -); - -const NavigationMenuTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - {children}{' '} - -)); -NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName; - -const NavigationMenuContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName; - -const NavigationMenuLink = NavigationMenuPrimitive.Link; - -const NavigationMenuViewport = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( -
- -
-)); -NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName; - -const NavigationMenuIndicator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -
- -)); -NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName; - -export { - NavigationMenu, - NavigationMenuContent, - NavigationMenuIndicator, - NavigationMenuItem, - NavigationMenuLink, - NavigationMenuList, - NavigationMenuTrigger, - navigationMenuTriggerStyle, - NavigationMenuViewport, -}; diff --git a/website/src/components/logos/donate-icon.tsx b/ui/src/icons/donate.tsx similarity index 96% rename from website/src/components/logos/donate-icon.tsx rename to ui/src/icons/donate.tsx index 75b7a774e..7d4d6e0d2 100644 --- a/website/src/components/logos/donate-icon.tsx +++ b/ui/src/icons/donate.tsx @@ -8,9 +8,12 @@ export function DonateIcon({ className, ...props }: HTMLAttributes) height="19" viewBox="0 0 19 19" fill="currentColor" + aria-labelledby="donate-icon-title" xmlns="http://www.w3.org/2000/svg" className={twMerge('text-accent', className)} + {...props} > + ) { xmlSpace="preserve" fill="currentColor" aria-hidden={true} - aria-labelledby="si-icon" + aria-labelledby="si-icon-title" viewBox="0 0 816 815.8" className={twMerge('text-accent', className)} {...props} > - Social Income Icon + Social Income Icon diff --git a/website/src/components/logos/spinner-icon.tsx b/ui/src/icons/spinner.tsx similarity index 87% rename from website/src/components/logos/spinner-icon.tsx rename to ui/src/icons/spinner.tsx index 85c5845bf..5b4087321 100644 --- a/website/src/components/logos/spinner-icon.tsx +++ b/ui/src/icons/spinner.tsx @@ -9,11 +9,12 @@ export function SpinnerIcon({ className, ...props }: HTMLAttributes) xmlSpace="preserve" fill="none" aria-hidden={true} - aria-labelledby="si-logo-title" + aria-labelledby="spinner-icon-title" viewBox="0 0 24 24" className={twMerge('h-5 w-5 animate-spin', className)} {...props} > + Loading spinner ; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + type: 'single', + collapsible: true, + className: 'w-[400px]', + }, + render: () => ( + + + Is it accessible? + Yes. It adheres to the WAI-ARIA design pattern. + + + + Is it styled? + + Yes. It comes with default styles that match the other components' aesthetic. + + + + + Is it animated? + Yes. It's animated by default, but you can disable it if you prefer. + + + ), +}; + +export const Multiple: Story = { + args: { + type: 'multiple', + className: 'w-[400px]', + }, + render: () => ( + + + Can I open multiple items? + Yes. Just set the type prop to "multiple" on the Accordion component. + + + + Is it responsive? + Yes. The accordion adjusts to different screen sizes. + + + + Can I customize the styles? + Yes. You can use the className prop to override the default styles. + + + ), +}; diff --git a/ui/src/stories/alert.stories.tsx b/ui/src/stories/alert.stories.tsx new file mode 100644 index 000000000..49c6d7db9 --- /dev/null +++ b/ui/src/stories/alert.stories.tsx @@ -0,0 +1,86 @@ +import { ExclamationTriangleIcon, InformationCircleIcon } from '@heroicons/react/24/outline'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Alert, AlertDescription, AlertTitle } from '../components/alert'; + +const meta: Meta = { + title: 'Components/Alert', + component: Alert, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => ( + + Default Alert + This is a default alert message. + + ), +}; + +export const Primary: Story = { + render: () => ( + + + Primary Alert + This is a primary alert with an icon. + + ), +}; + +export const Secondary: Story = { + render: () => ( + + Secondary Alert + This is a secondary alert message. + + ), +}; + +export const Accent: Story = { + render: () => ( + + Accent Alert + This is an accent alert message. + + ), +}; + +export const Destructive: Story = { + render: () => ( + + + Destructive Alert + This is a destructive alert with a warning icon. + + ), +}; + +export const WithoutTitle: Story = { + render: () => ( + + This is an alert without a title. + + ), +}; + +export const WithoutDescription: Story = { + render: () => ( + + Alert without description + + ), +}; + +export const CustomContent: Story = { + render: () => ( + + Custom Styled Alert + + This alert has custom styling applied through className props. + + + ), +}; diff --git a/ui/src/stories/avatar.stories.tsx b/ui/src/stories/avatar.stories.tsx new file mode 100644 index 000000000..1edd13379 --- /dev/null +++ b/ui/src/stories/avatar.stories.tsx @@ -0,0 +1,77 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Avatar, AvatarFallback, AvatarImage } from '../components/avatar'; + +const meta = { + title: 'Components/Avatar', + component: Avatar, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Avatar with Image +export const WithImage: Story = { + render: () => ( + + + CN + + ), +}; + +// Avatar with Fallback +export const WithFallback: Story = { + render: () => ( + + JD + + ), +}; + +// Avatar with Failed Image Load +export const WithFailedImage: Story = { + render: () => ( + + + 404 + + ), +}; + +// Custom Sized Avatar +export const CustomSize: Story = { + render: () => ( +
+ + + LG + + + + MD + + + + SM + +
+ ), +}; + +// Custom Styled Avatar +export const CustomStyled: Story = { + render: () => ( +
+ + P + + + S + + + A + +
+ ), +}; diff --git a/ui/src/stories/badge.stories.tsx b/ui/src/stories/badge.stories.tsx new file mode 100644 index 000000000..55059394d --- /dev/null +++ b/ui/src/stories/badge.stories.tsx @@ -0,0 +1,86 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Badge } from '../components/badge'; + +const meta = { + title: 'Components/Badge', + component: Badge, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Default Badge +export const Default: Story = { + render: () => Default Badge, +}; + +// All Variants +export const Variants: Story = { + render: () => ( +
+ Default + Secondary + Destructive + Muted + Accent + Outline +
+ ), +}; + +// Interactive States +export const Interactive: Story = { + render: () => ( +
+ Clickable Badge + + Clickable Secondary + + + Clickable Destructive + +
+ ), +}; + +// With Icons +export const WithIcons: Story = { + render: () => ( +
+ + Online + + + New + + + + Critical + +
+ ), +}; + +// Custom Styled +export const CustomStyled: Story = { + render: () => ( +
+ Custom Blue + Custom Green + Custom Border + Custom Rounded +
+ ), +}; + +// Different Sizes +export const Sizes: Story = { + render: () => ( +
+ Small + Default + Large + Extra Large +
+ ), +}; diff --git a/ui/src/stories/button.stories.tsx b/ui/src/stories/button.stories.tsx new file mode 100644 index 000000000..3a2b5478b --- /dev/null +++ b/ui/src/stories/button.stories.tsx @@ -0,0 +1,128 @@ +import { ArrowRightIcon, ChevronRightIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from '../components/button'; + +const meta = { + title: 'Components/Button', + component: Button, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Variants +export const Variants: Story = { + render: () => ( +
+ + + + + + +
+ ), +}; + +// Sizes +export const Sizes: Story = { + render: () => ( +
+ + + + +
+ ), +}; + +// With Icons +export const WithIcons: Story = { + render: () => ( +
+ + + + +
+ ), +}; + +// Loading State +export const Loading: Story = { + render: () => ( +
+ + + +
+ ), +}; + +// Disabled State +export const Disabled: Story = { + render: () => ( +
+ + + + +
+ ), +}; + +// Icon Only Buttons +export const IconOnly: Story = { + render: () => ( +
+ + + + +
+ ), +}; + +// Combined Features +export const Combined: Story = { + render: () => ( +
+ + + +
+ ), +}; diff --git a/ui/src/stories/card.stories.tsx b/ui/src/stories/card.stories.tsx new file mode 100644 index 000000000..078a3d136 --- /dev/null +++ b/ui/src/stories/card.stories.tsx @@ -0,0 +1,111 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from '../components/button'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../components/card'; + +const meta = { + title: 'Components/Card', + component: Card, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Card +export const Basic: Story = { + render: () => ( + + + Card Title + Card Description + + +

Card Content

+
+
+ ), +}; + +// Card with Footer +export const WithFooter: Story = { + render: () => ( + + + Newsletter + Get updates on our latest features. + + +

Subscribe to our newsletter to stay updated with our latest news and features.

+
+ + + + +
+ ), +}; + +// Interactive Card +export const Interactive: Story = { + render: () => ( + + + Interactive Card + This card has hover and click effects + + +

Click or hover over this card to see the effects.

+
+
+ ), +}; + +// Custom Styled Card +export const CustomStyled: Story = { + render: () => ( + + + Custom Theme + Custom styled card with primary theme + + +

This card uses custom background and text colors.

+
+ + + +
+ ), +}; + +// Multiple Cards Layout +export const GridLayout: Story = { + render: () => ( +
+ + + Card 1 + + Content 1 + + + + Card 2 + + Content 2 + + + + Card 3 + + Content 3 + + + + Card 4 + + Content 4 + +
+ ), +}; diff --git a/ui/src/stories/carousel.stories.tsx b/ui/src/stories/carousel.stories.tsx new file mode 100644 index 000000000..55838daaf --- /dev/null +++ b/ui/src/stories/carousel.stories.tsx @@ -0,0 +1,158 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { Card, CardContent } from '../components/card'; +import { Carousel, CarouselContent } from '../components/carousel'; + +const meta = { + title: 'Components/Carousel', + component: Carousel, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Helper component for demo slides +const DemoSlide = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + +); + +// Basic Carousel +export const Basic: Story = { + render: () => ( + + + 1 + + + 2 + + + 3 + + + ), +}; + +// With Controls +export const WithControls: Story = { + render: () => ( + + + 1 + + + 2 + + + 3 + + + ), +}; + +// With Dots +export const WithDots: Story = { + render: () => ( + + + 1 + + + 2 + + + 3 + + + ), +}; + +// With Both Controls and Dots +export const WithControlsAndDots: Story = { + render: () => ( + + + 1 + + + 2 + + + 3 + + + ), +}; + +// Multiple Slides +export const MultipleSlides: Story = { + render: () => ( + + {[1, 2, 3, 4, 5, 6].map((num) => ( + + {num} + + ))} + + ), +}; + +// Autoplay +export const Autoplay: Story = { + render: () => ( + + + 1 + + + 2 + + + 3 + + + ), +}; + +// Custom Content +export const CustomContent: Story = { + render: () => ( + + +
+
+ Custom 1 +
+
+
+ +
+
+ Custom 2 +
+
+
+ +
+
+ Custom 3 +
+
+
+
+ ), +}; diff --git a/ui/src/stories/checkbox.stories.tsx b/ui/src/stories/checkbox.stories.tsx new file mode 100644 index 000000000..be8d84abe --- /dev/null +++ b/ui/src/stories/checkbox.stories.tsx @@ -0,0 +1,102 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { Checkbox } from '../components/checkbox'; +import { Label } from '../components/label'; + +const meta = { + title: 'Components/Checkbox', + component: Checkbox, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Checkbox +export const Basic: Story = { + render: () => , +}; + +// With Label +export const WithLabel: Story = { + render: () => ( +
+ + +
+ ), +}; + +// Checked State +export const Checked: Story = { + render: () => , +}; + +// Disabled States +export const Disabled: Story = { + render: () => ( +
+
+ + +
+
+ + +
+
+ ), +}; + +// Form Example +export const FormExample: Story = { + render: () => ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ), +}; + +// Custom Styled +export const CustomStyled: Story = { + render: () => ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ), +}; + +// Interactive Example +export const Interactive: Story = { + render: () => { + const [checked, setChecked] = React.useState(false); + return ( +
+ setChecked(Boolean(checkedState))} /> + +
+ ); + }, +}; diff --git a/ui/src/stories/collapsible.stories.tsx b/ui/src/stories/collapsible.stories.tsx new file mode 100644 index 000000000..eb2222bbb --- /dev/null +++ b/ui/src/stories/collapsible.stories.tsx @@ -0,0 +1,138 @@ +import { ChevronDownIcon } from '@heroicons/react/24/outline'; +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { Button } from '../components/button'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../components/collapsible'; + +const meta = { + title: 'Components/Collapsible', + component: Collapsible, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Collapsible +export const Basic: Story = { + render: () => { + const [isOpen, setIsOpen] = React.useState(false); + return ( + +
+

What is a Collapsible?

+ + + +
+ +
+ A collapsible is a component that can be expanded or collapsed to show or hide content. +
+
+
+ ); + }, +}; + +// Multiple Sections +export const MultipleSections: Story = { + render: () => { + const [openSections, setOpenSections] = React.useState([]); + + const toggleSection = (index: number) => { + setOpenSections((current) => + current.includes(index) ? current.filter((i) => i !== index) : [...current, index], + ); + }; + + return ( +
+ {[1, 2, 3].map((section) => ( + toggleSection(section)} + className="rounded-lg border" + > +
+

Section {section}

+ + + +
+ +
Content for section {section}
+
+
+ ))} +
+ ); + }, +}; + +// Custom Styled +export const CustomStyled: Story = { + render: () => { + const [isOpen, setIsOpen] = React.useState(false); + return ( + +
+

Custom Styled Collapsible

+ + + +
+ +
+ This is a custom styled collapsible with primary color theme. +
+
+
+ ); + }, +}; + +// With Animation +export const WithAnimation: Story = { + render: () => { + const [isOpen, setIsOpen] = React.useState(false); + return ( + +
+

Animated Collapsible

+ + + +
+ +
+
+ This collapsible has smooth animations for both the chevron and content. +
+
+
+
+ ); + }, +}; diff --git a/ui/src/stories/dialog.stories.tsx b/ui/src/stories/dialog.stories.tsx new file mode 100644 index 000000000..a623e9271 --- /dev/null +++ b/ui/src/stories/dialog.stories.tsx @@ -0,0 +1,146 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from '../components/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '../components/dialog'; + +const meta = { + title: 'Components/Dialog', + component: Dialog, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Dialog +export const Basic: Story = { + render: () => ( + + + + + + + Dialog Title + This is a basic dialog with a title and description. + + + + ), +}; + +// Dialog with Footer Actions +export const WithActions: Story = { + render: () => ( + + + + + + + Confirm Action + Are you sure you want to perform this action? This cannot be undone. + + + + + + + + ), +}; + +// Form Dialog +export const FormDialog: Story = { + render: () => ( + + + + + + + Edit Profile + Make changes to your profile here. Click save when you're done. + +
+
+ + +
+
+ + +
+
+ + + +
+
+ ), +}; + +// Custom Styled Dialog +export const CustomStyled: Story = { + render: () => ( + + + + + + + Custom Theme + + This dialog uses custom background and text colors. + + +
+

Custom styled content goes here.

+
+ + + +
+
+ ), +}; + +// Alert Dialog +export const AlertDialog: Story = { + render: () => ( + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your account and remove your data from our + servers. + + + + + + + + + ), +}; diff --git a/ui/src/stories/form.stories.tsx b/ui/src/stories/form.stories.tsx new file mode 100644 index 000000000..324765e3a --- /dev/null +++ b/ui/src/stories/form.stories.tsx @@ -0,0 +1,215 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import type { Meta, StoryObj } from '@storybook/react'; +import { useForm } from 'react-hook-form'; +import * as z from 'zod'; +import { Button } from '../components/button'; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '../components/form'; +import { Input } from '../components/input'; + +const meta = { + title: 'Components/Form', + component: Form, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Form Schema +const formSchema = z.object({ + username: z.string().min(2, { + message: 'Username must be at least 2 characters.', + }), + email: z.string().email({ + message: 'Please enter a valid email address.', + }), +}); + +// Basic Form +export const Basic: Story = { + render: () => { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + username: '', + email: '', + }, + }); + + function onSubmit(values: z.infer) { + console.log(values); + } + + return ( +
+ + ( + + Username + + + + This is your public display name. + + + )} + /> + ( + + Email + + + + We'll never share your email with anyone else. + + + )} + /> + + + + ); + }, +}; + +// Form with Validation +export const WithValidation: Story = { + render: () => { + const form = useForm>({ + resolver: zodResolver(formSchema), + mode: 'onChange', + }); + + function onSubmit(values: z.infer) { + console.log(values); + } + + return ( +
+ + ( + + Username + + + + + + )} + /> + ( + + Email + + + + + + )} + /> + + + + ); + }, +}; + +// Form with Custom Styling +export const CustomStyled: Story = { + render: () => { + const form = useForm>(); + + return ( +
+ + ( + + Username + + + + Custom styled form field + + + )} + /> + + + + ); + }, +}; + +// Form with Different States +export const DifferentStates: Story = { + render: () => { + const form = useForm(); + + return ( +
+
+ ( + + Default State + + + + + )} + /> + + +
+ ( + + Disabled State + + + + + )} + /> + + +
+ ( + + Error State + + + + This is an error message + + )} + /> + +
+ ); + }, +}; diff --git a/ui/src/stories/hover-card.stories.tsx b/ui/src/stories/hover-card.stories.tsx new file mode 100644 index 000000000..ffd4741b7 --- /dev/null +++ b/ui/src/stories/hover-card.stories.tsx @@ -0,0 +1,161 @@ +import { CalendarIcon, UserIcon } from '@heroicons/react/24/outline'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from '../components/button'; +import { HoverCard, HoverCardContent, HoverCardTrigger } from '../components/hover-card'; + +const meta = { + title: 'Components/HoverCard', + component: HoverCard, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Hover Card +export const Basic: Story = { + render: () => ( +
+ + + + + +
+
+
+ +
+
+

@username

+

Full Name

+
+
+

Frontend developer and UI designer. Love creating beautiful user interfaces.

+
+
+
+
+ ), +}; + +// With Rich Content +export const RichContent: Story = { + render: () => ( +
+ + + + + +
+
+

@johndoe

+

The quick brown fox jumps over the lazy dog.

+
+ + Joined December 2021 +
+
+
+
+
+
+ + +
+ ), +}; + +// Different Positions +export const Positions: Story = { + render: () => ( +
+ + + + + +

This card appears on top

+
+
+ + + + + + +

This card appears at the bottom

+
+
+ + + + + + +

This card appears on the left

+
+
+ + + + + + +

This card appears on the right

+
+
+
+ ), +}; + +// Custom Styled +export const CustomStyled: Story = { + render: () => ( +
+ + + + + +
+

Custom Styled Card

+

This hover card has custom background and text colors.

+
+
+
+
+ ), +}; + +// With Interactive Content +export const WithInteractiveContent: Story = { + render: () => ( +
+ + + + + +
+
+

Interactive Content

+

This card contains interactive elements.

+
+
+ + +
+
+
+
+
+ ), +}; diff --git a/ui/src/stories/icons.stories.tsx b/ui/src/stories/icons.stories.tsx new file mode 100644 index 000000000..89e9071bd --- /dev/null +++ b/ui/src/stories/icons.stories.tsx @@ -0,0 +1,64 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { DonateIcon } from '../icons/donate'; +import { SIIcon } from '../icons/si'; +import { SpinnerIcon } from '../icons/spinner'; + +const meta = { + title: 'Components/Icons', + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const AllIcons: Story = { + render: () => ( +
+
+ + DonateIcon +
+
+ + SIIcon +
+
+ + SpinnerIcon +
+
+ ), +}; + +export const Sizes: Story = { + render: () => ( +
+ + + + +
+ ), +}; + +export const Colors: Story = { + render: () => ( +
+ + + + +
+ ), +}; + +export const AnimatedSpinner: Story = { + render: () => ( +
+ + + + +
+ ), +}; diff --git a/ui/src/stories/input.stories.tsx b/ui/src/stories/input.stories.tsx new file mode 100644 index 000000000..a6ac391da --- /dev/null +++ b/ui/src/stories/input.stories.tsx @@ -0,0 +1,107 @@ +import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Input } from '../components/input'; + +const meta = { + title: 'Components/Input', + component: Input, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Input +export const Basic: Story = { + render: () => , +}; + +// Input Types +export const Types: Story = { + render: () => ( +
+ + + + + + + +
+ ), +}; + +// States +export const States: Story = { + render: () => ( +
+ + + + +
+ ), +}; + +// With Icon +export const WithIcon: Story = { + render: () => ( +
+ + +
+ ), +}; + +// Sizes +export const Sizes: Story = { + render: () => ( +
+ + + +
+ ), +}; + +// Custom Styled +export const CustomStyled: Story = { + render: () => ( +
+ + + +
+ ), +}; + +// With Label and Error +export const WithLabelAndError: Story = { + render: () => ( +
+
+ + +
+
+ + +

Please enter a valid email address

+
+
+ ), +}; + +// With Button +export const WithButton: Story = { + render: () => ( +
+ + +
+ ), +}; diff --git a/ui/src/stories/label.stories.tsx b/ui/src/stories/label.stories.tsx new file mode 100644 index 000000000..f6f73ae43 --- /dev/null +++ b/ui/src/stories/label.stories.tsx @@ -0,0 +1,116 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Checkbox } from '../components/checkbox'; +import { Input } from '../components/input'; +import { Label } from '../components/label'; +import { Switch } from '../components/switch'; + +const meta = { + title: 'Components/Label', + component: Label, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Label +export const Basic: Story = { + render: () => , +}; + +// With Input +export const WithInput: Story = { + render: () => ( +
+ + +
+ ), +}; + +// With Required Field +export const Required: Story = { + render: () => ( +
+ + +
+ ), +}; + +// With Checkbox +export const WithCheckbox: Story = { + render: () => ( +
+ + +
+ ), +}; + +// With Switch +export const WithSwitch: Story = { + render: () => ( +
+ + +
+ ), +}; + +// Different States +export const States: Story = { + render: () => ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ), +}; + +// Custom Styled +export const CustomStyled: Story = { + render: () => ( +
+
+ + +
+
+ + +
+
+ ), +}; + +// With Description +export const WithDescription: Story = { + render: () => ( +
+ + +

Accepted formats: .jpg, .png, .gif

+
+ ), +}; diff --git a/ui/src/stories/popover.stories.tsx b/ui/src/stories/popover.stories.tsx new file mode 100644 index 000000000..36a42026b --- /dev/null +++ b/ui/src/stories/popover.stories.tsx @@ -0,0 +1,136 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from '../components/button'; +import { Popover, PopoverContent, PopoverTrigger } from '../components/popover'; + +const meta = { + title: 'Components/Popover', + component: Popover, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Popover +export const Basic: Story = { + render: () => ( + + + + + +
+
+

Dimensions

+

Set the dimensions for the layer.

+
+
+
+
+ ), +}; + +// With Form Elements +export const WithFormElements: Story = { + render: () => ( + + + + + +
+
+

Profile

+

Update your profile information.

+
+
+
+ + +
+
+ + +
+
+
+
+
+ ), +}; + +// With Custom Delay +export const WithCustomDelay: Story = { + render: () => ( + + + + + +

This popover has a custom open delay of 500ms and close delay of 300ms.

+
+
+ ), +}; + +// With Rich Content +export const WithRichContent: Story = { + render: () => ( + + + + + +
+
+

Product Details

+
+
+
+

Premium Package

+

Access to all premium features

+

$99/month

+
+
+
+
+ + +
+
+ + + ), +}; + +// With Custom Positioning +export const WithCustomPositioning: Story = { + render: () => ( +
+ + + + + +
+ + + +
+
+
+
+ ), +}; diff --git a/ui/src/stories/progress.stories.tsx b/ui/src/stories/progress.stories.tsx new file mode 100644 index 000000000..cd55bc223 --- /dev/null +++ b/ui/src/stories/progress.stories.tsx @@ -0,0 +1,132 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { Progress } from '../components/progress'; + +const meta = { + title: 'Components/Progress', + component: Progress, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Progress +export const Basic: Story = { + render: () => ( +
+ +
+ ), +}; + +// Different Values +export const DifferentValues: Story = { + render: () => ( +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ ), +}; + +// Animated Progress +export const Animated: Story = { + render: () => { + const [progress, setProgress] = React.useState(0); + + React.useEffect(() => { + const timer = setInterval(() => { + setProgress((prevProgress) => (prevProgress >= 100 ? 0 : prevProgress + 10)); + }, 1000); + + return () => { + clearInterval(timer); + }; + }, []); + + return ( +
+ +
+ ); + }, +}; + +// Custom Styled +export const CustomStyled: Story = { + render: () => ( +
+
+ +
+
+ +
+
+ ), +}; + +// With Label +export const WithLabel: Story = { + render: () => { + const value = 75; + return ( +
+
+ Progress + {value}% +
+
+ +
+
+ ); + }, +}; + +// Different Sizes +export const DifferentSizes: Story = { + render: () => ( +
+
+ +
+
+ +
+
+ +
+
+ ), +}; diff --git a/ui/src/stories/radio-group.stories.tsx b/ui/src/stories/radio-group.stories.tsx new file mode 100644 index 000000000..993040faf --- /dev/null +++ b/ui/src/stories/radio-group.stories.tsx @@ -0,0 +1,134 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Label } from '../components/label'; +import { RadioGroup, RadioGroupItem } from '../components/radio-group'; + +const meta = { + title: 'Components/RadioGroup', + component: RadioGroup, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Radio Group +export const Basic: Story = { + render: () => ( + +
+ + +
+
+ + +
+
+ + +
+
+ ), +}; + +// With Description +export const WithDescription: Story = { + render: () => ( + +
+ +
+ +

System default spacing for controls and content.

+
+
+
+ +
+ +

Additional spacing for better readability.

+
+
+
+ +
+ +

Reduced spacing to show more content.

+
+
+
+ ), +}; + +// Disabled State +export const DisabledState: Story = { + render: () => ( + +
+ + +
+
+ + +
+
+ ), +}; + +// With Form +export const WithForm: Story = { + render: () => ( +
{ + e.preventDefault(); + // Handle form submission + }} + > +
+
+

Notification Preferences

+ +
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ ), +}; + +// Custom Styled +export const CustomStyled: Story = { + render: () => ( + +
+ + +
+
+ + +
+
+ ), +}; diff --git a/ui/src/stories/select.stories.tsx b/ui/src/stories/select.stories.tsx new file mode 100644 index 000000000..0f6a93513 --- /dev/null +++ b/ui/src/stories/select.stories.tsx @@ -0,0 +1,160 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectSeparator, + SelectTrigger, + SelectValue, +} from '../components/select'; + +const meta = { + title: 'Components/Select', + component: Select, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Select +export const Basic: Story = { + render: () => ( + + ), +}; + +// With Groups and Labels +export const WithGroupsAndLabels: Story = { + render: () => ( + + ), +}; + +// Disabled State +export const DisabledState: Story = { + render: () => ( + + ), +}; + +// With Form +export const WithForm: Story = { + render: () => ( +
{ + e.preventDefault(); + // Handle form submission + }} + > +
+
+ + +
+ +
+
+ ), +}; + +// Custom Styled +export const CustomStyled: Story = { + render: () => ( + + ), +}; + +// With Error State +export const WithErrorState: Story = { + render: () => ( +
+ +

Please select an option

+
+ ), +}; diff --git a/ui/src/stories/separator.stories.tsx b/ui/src/stories/separator.stories.tsx new file mode 100644 index 000000000..dda99b215 --- /dev/null +++ b/ui/src/stories/separator.stories.tsx @@ -0,0 +1,101 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Separator } from '../components/separator'; + +const meta = { + title: 'Components/Separator', + component: Separator, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Horizontal Separator +export const BasicHorizontal: Story = { + render: () => ( +
+
Above
+ +
Below
+
+ ), +}; + +// Vertical Separator +export const Vertical: Story = { + render: () => ( +
+
Left
+ +
Right
+
+ ), +}; + +// With Content +export const WithContent: Story = { + render: () => ( +
+

Section 1

+

This is the first section of the content.

+ +

Section 2

+

This is the second section of the content.

+
+ ), +}; + +// Custom Styled +export const CustomStyled: Story = { + render: () => ( +
+ + + +
+
Left
+ +
Right
+
+
+ ), +}; + +// In Card Layout +export const InCardLayout: Story = { + render: () => ( +
+
+

Account Settings

+ +
+ +
+
+

Email

+

john@example.com

+
+ +
+

Password

+

Last changed 3 months ago

+
+
+
+ ), +}; + +// List Separator +export const ListSeparator: Story = { + render: () => ( +
+
Item 1
+ +
Item 2
+ +
Item 3
+ +
Item 4
+
+ ), +}; diff --git a/ui/src/stories/switch.stories.tsx b/ui/src/stories/switch.stories.tsx new file mode 100644 index 000000000..4ac55591c --- /dev/null +++ b/ui/src/stories/switch.stories.tsx @@ -0,0 +1,114 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { Label } from '../components/label'; +import { Switch } from '../components/switch'; + +const meta = { + title: 'Components/Switch', + component: Switch, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Switch +export const Basic: Story = { + render: () => , +}; + +// With Label +export const WithLabel: Story = { + render: () => ( +
+ + +
+ ), +}; + +// Disabled State +export const DisabledState: Story = { + render: () => ( +
+
+ + +
+
+ + +
+
+ ), +}; + +// With Form +export const WithForm: Story = { + render: () => ( +
{ + e.preventDefault(); + // Handle form submission + }} + > +
+
+

Notification Settings

+
+ + +
+
+ + +
+
+ +
+
+ ), +}; + +// Custom Styled +export const CustomStyled: Story = { + render: () => ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ), +}; + +// Interactive Example +export const InteractiveExample: Story = { + render: () => { + const [isEnabled, setIsEnabled] = React.useState(false); + + return ( +
+
+ + +
+

Current state: {isEnabled ? 'On' : 'Off'}

+
+ ); + }, +}; diff --git a/ui/src/stories/table.stories.tsx b/ui/src/stories/table.stories.tsx new file mode 100644 index 000000000..29845cf58 --- /dev/null +++ b/ui/src/stories/table.stories.tsx @@ -0,0 +1,189 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableFooter, + TableHead, + TableHeader, + TableRow, +} from '../components/table'; + +const meta = { + title: 'Components/Table', + component: Table, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Table +export const Basic: Story = { + render: () => ( + + + + Name + Email + Role + + + + + John Doe + john@example.com + Admin + + + Jane Smith + jane@example.com + User + + +
+ ), +}; + +// With Caption +export const WithCaption: Story = { + render: () => ( + + A list of recent users and their roles. + + + Name + Email + Role + + + + + John Doe + john@example.com + Admin + + + Jane Smith + jane@example.com + User + + +
+ ), +}; + +// With Footer +export const WithFooter: Story = { + render: () => ( + + + + Item + Amount + Price + + + + + Product A + 2 + $20.00 + + + Product B + 1 + $15.00 + + + + + Total + 3 + $35.00 + + +
+ ), +}; + +// Custom Styled +export const CustomStyled: Story = { + render: () => ( + + + + Product + Stock + Price + + + + + Product A + 32 + $20.00 + + + Product B + 12 + $15.00 + + + Product C + 4 + $25.00 + + +
+ ), +}; + +// With Status Column +export const WithStatusColumn: Story = { + render: () => ( + + + + Order + Status + Last Updated + Amount + + + + + #123 + + + Completed + + + 2 hours ago + $25.00 + + + #124 + + + Pending + + + 3 hours ago + $42.00 + + + #125 + + + Failed + + + 5 hours ago + $15.00 + + +
+ ), +}; diff --git a/ui/src/stories/tabs.stories.tsx b/ui/src/stories/tabs.stories.tsx new file mode 100644 index 000000000..d08f004aa --- /dev/null +++ b/ui/src/stories/tabs.stories.tsx @@ -0,0 +1,157 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/tabs'; + +const meta = { + title: 'Components/Tabs', + component: Tabs, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic +export const Basic: Story = { + render: () => ( + + + Account + Password + + Make changes to your account here. + Change your password here. + + ), +}; + +// Multiple Tabs +export const MultipleTabs: Story = { + render: () => ( + + + Tab 1 + Tab 2 + Tab 3 + Tab 4 + + Content of tab 1 + Content of tab 2 + Content of tab 3 + Content of tab 4 + + ), +}; + +// With Disabled +export const WithDisabled: Story = { + render: () => ( + + + Active + + Disabled + + Active 2 + + This tab is active + This tab is disabled + This is another active tab + + ), +}; + +// Full Width +export const FullWidth: Story = { + render: () => ( + + + Messages + Notifications + Settings + + Messages content + Notifications content + Settings content + + ), +}; + +// With Icons +export const WithIcons: Story = { + render: () => ( + + + + + + + Messages + + + + + + + Settings + + + Messages content + Settings content + + ), +}; + +// With Rich Content +export const WithRichContent: Story = { + render: () => ( + + + Account + Preferences + + +
+

Account Settings

+

Manage your account settings and preferences.

+
+
+
+ +
+
+
+ +
+

Preferences

+

Customize your application preferences.

+
+
+ +
+
+
+ ), +}; diff --git a/ui/src/stories/toggle.stories.tsx b/ui/src/stories/toggle.stories.tsx new file mode 100644 index 000000000..7f3e52c42 --- /dev/null +++ b/ui/src/stories/toggle.stories.tsx @@ -0,0 +1,180 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Toggle } from '../components/toggle'; + +const meta = { + title: 'Components/Toggle', + component: Toggle, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Toggle +export const Basic: Story = { + render: () => ( + + + + + + + + ), +}; + +// With Text +export const WithText: Story = { + render: () => ( + + + + + + Bold + + ), +}; + +// Different Sizes +export const DifferentSizes: Story = { + render: () => ( +
+ + Small + + + Default + + + Large + +
+ ), +}; + +// Variants +export const Variants: Story = { + render: () => ( +
+ + Default + + + Outline + +
+ ), +}; + +// Disabled State +export const DisabledState: Story = { + render: () => ( +
+ + Disabled + + + Disabled Pressed + +
+ ), +}; + +// With Icons Group +export const WithIconsGroup: Story = { + render: () => ( +
+ + + + + + + + + + + + + + + + + + + +
+ ), +}; + +// Custom Styled +export const CustomStyled: Story = { + render: () => ( +
+ + Custom + + + Custom + +
+ ), +}; diff --git a/ui/src/stories/tooltip.stories.tsx b/ui/src/stories/tooltip.stories.tsx new file mode 100644 index 000000000..8f741c522 --- /dev/null +++ b/ui/src/stories/tooltip.stories.tsx @@ -0,0 +1,187 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { Button } from '../components/button'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../components/tooltip'; + +const meta = { + title: 'Components/Tooltip', + component: Tooltip, + tags: ['autodocs'], + decorators: [ + (Story) => ( + + + + ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic Tooltip +export const Basic: Story = { + render: () => ( + + + + + +

Add to library

+
+
+ ), +}; + +// With Icon +export const WithIcon: Story = { + render: () => ( + + + + + +

More information

+
+
+ ), +}; + +// Different Positions +export const DifferentPositions: Story = { + render: () => ( +
+ + + + + +

Tooltip on top

+
+
+ + + + + +

Tooltip on right

+
+
+ + + + + +

Tooltip on bottom

+
+
+ + + + + +

Tooltip on left

+
+
+
+ ), +}; + +// With Rich Content +export const WithRichContent: Story = { + render: () => ( + + + + + +
+

John Doe

+

Software Engineer at Example Corp

+
+ + + + + San Francisco, CA +
+
+
+
+ ), +}; + +// Custom Styled +export const CustomStyled: Story = { + render: () => ( + + + + + +

Custom styled tooltip

+
+
+ ), +}; + +// With Delay +export const WithDelay: Story = { + render: () => ( + + + + + +

This tooltip has a 700ms delay

+
+
+ ), +}; + +// Interactive Example +export const InteractiveExample: Story = { + render: () => { + const [isOpen, setIsOpen] = React.useState(false); + + return ( + + + + + +

This tooltip can be controlled programmatically

+
+
+ ); + }, +}; diff --git a/ui/src/stories/typography.stories.tsx b/ui/src/stories/typography.stories.tsx new file mode 100644 index 000000000..51925bc57 --- /dev/null +++ b/ui/src/stories/typography.stories.tsx @@ -0,0 +1,145 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Typography } from '../components/typography'; +import type { FontColor } from '../interfaces/color'; + +// Sample colors for the demo since we can't use the enum directly +const FONT_COLORS: FontColor[] = [ + 'background', + 'foreground', + 'primary', + 'primary-foreground', + 'secondary', + 'secondary-foreground', + 'accent', + 'accent-foreground', + 'destructive', + 'destructive-foreground', + 'muted', + 'muted-foreground', + 'card', + 'card-foreground', + 'popover', + 'popover-foreground', +]; + +const meta = { + title: 'Components/Typography', + component: Typography, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Font Sizes +export const Sizes: Story = { + render: () => ( +
+ 6XL Typography + 5XL Typography + 4XL Typography + 3XL Typography + 2XL Typography + XL Typography + Large Typography + Medium Typography + Small Typography + XS Typography +
+ ), +}; + +// Font Weights +export const Weights: Story = { + render: () => ( +
+ Bold Text + Medium Text + Normal Text +
+ ), +}; + +// Line Heights +export const LineHeights: Story = { + render: () => ( +
+ + Line Height None - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut + labore et dolore magna aliqua. + + + Line Height Tight - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut + labore et dolore magna aliqua. + + + Line Height Snug - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut + labore et dolore magna aliqua. + + + Line Height Normal - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. + + + Line Height Relaxed - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. + + + Line Height Loose - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut + labore et dolore magna aliqua. + +
+ ), +}; + +// Colors +export const Colors: Story = { + render: () => ( +
+ {FONT_COLORS.map((color) => ( + + {color} Text Color + + ))} +
+ ), +}; + +// Different HTML Elements +export const Elements: Story = { + render: () => ( +
+ + Heading 1 + + + Heading 2 + + + Heading 3 + + Regular paragraph text + + Small span text + +
+ ), +}; + +// Combined Properties +export const Combined: Story = { + render: () => ( +
+ + Primary Heading + + + Secondary Subheading + + + Muted body text with relaxed line height. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. + +
+ ), +}; diff --git a/website/src/app/[lang]/[region]/(website)/me/contributions/contributions-table.tsx b/website/src/app/[lang]/[region]/(website)/me/contributions/contributions-table.tsx index 1e2caf889..6117e32f6 100644 --- a/website/src/app/[lang]/[region]/(website)/me/contributions/contributions-table.tsx +++ b/website/src/app/[lang]/[region]/(website)/me/contributions/contributions-table.tsx @@ -2,10 +2,18 @@ import { DefaultParams } from '@/app/[lang]/[region]'; import { useContributions } from '@/app/[lang]/[region]/(website)/me/hooks'; -import { SpinnerIcon } from '@/components/logos/spinner-icon'; import { useTranslator } from '@/hooks/useTranslator'; import { toDateTime } from '@socialincome/shared/src/utils/date'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Typography } from '@socialincome/ui'; +import { + SpinnerIcon, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Typography, +} from '@socialincome/ui'; import _ from 'lodash'; type ContributionsTableProps = { diff --git a/website/src/app/[lang]/[region]/(website)/me/donation-certificates/donation-certificates-table.tsx b/website/src/app/[lang]/[region]/(website)/me/donation-certificates/donation-certificates-table.tsx index ec93dffb5..cae003c80 100644 --- a/website/src/app/[lang]/[region]/(website)/me/donation-certificates/donation-certificates-table.tsx +++ b/website/src/app/[lang]/[region]/(website)/me/donation-certificates/donation-certificates-table.tsx @@ -2,8 +2,17 @@ import { DefaultParams } from '@/app/[lang]/[region]'; import { useDonationCertificates } from '@/app/[lang]/[region]/(website)/me/hooks'; -import { SpinnerIcon } from '@/components/logos/spinner-icon'; -import { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Typography } from '@socialincome/ui'; +import { + Button, + SpinnerIcon, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Typography, +} from '@socialincome/ui'; import Link from 'next/link'; type ContributionsTableProps = { diff --git a/website/src/app/[lang]/[region]/(website)/me/subscriptions/subscriptions-client.tsx b/website/src/app/[lang]/[region]/(website)/me/subscriptions/subscriptions-client.tsx index a564fae6a..c4ca4e028 100644 --- a/website/src/app/[lang]/[region]/(website)/me/subscriptions/subscriptions-client.tsx +++ b/website/src/app/[lang]/[region]/(website)/me/subscriptions/subscriptions-client.tsx @@ -3,11 +3,20 @@ import { DefaultParams } from '@/app/[lang]/[region]'; import { useSubscriptions } from '@/app/[lang]/[region]/(website)/me/hooks'; import { BillingPortalButton } from '@/app/[lang]/[region]/(website)/me/subscriptions/billing-portal-button'; -import { SpinnerIcon } from '@/components/logos/spinner-icon'; import { useTranslator } from '@/hooks/useTranslator'; import { PlusCircleIcon } from '@heroicons/react/24/outline'; import { toDateTime } from '@socialincome/shared/src/utils/date'; -import { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Typography } from '@socialincome/ui'; +import { + Button, + SpinnerIcon, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Typography, +} from '@socialincome/ui'; import Link from 'next/link'; type SubscriptionsTableProps = { diff --git a/website/src/app/[lang]/[region]/(website)/me/work-info/employers-list.tsx b/website/src/app/[lang]/[region]/(website)/me/work-info/employers-list.tsx index 51396c329..692453e82 100644 --- a/website/src/app/[lang]/[region]/(website)/me/work-info/employers-list.tsx +++ b/website/src/app/[lang]/[region]/(website)/me/work-info/employers-list.tsx @@ -6,8 +6,7 @@ import { useDeleteEmployer, useEmployers, } from '@/app/[lang]/[region]/(website)/me/hooks'; -import { SpinnerIcon } from '@/components/logos/spinner-icon'; -import { Button, Table, TableBody, TableCell, TableRow, Typography } from '@socialincome/ui'; +import { Button, SpinnerIcon, Table, TableBody, TableCell, TableRow, Typography } from '@socialincome/ui'; import { DefaultParams } from '../../..'; import { AddEmployerForm, AddEmployerFormProps } from './add-employer-form'; diff --git a/website/src/app/[lang]/[region]/(website)/newsletter/subscription-info-form.tsx b/website/src/app/[lang]/[region]/(website)/newsletter/subscription-info-form.tsx index f611521da..66ff38b43 100644 --- a/website/src/app/[lang]/[region]/(website)/newsletter/subscription-info-form.tsx +++ b/website/src/app/[lang]/[region]/(website)/newsletter/subscription-info-form.tsx @@ -1,11 +1,20 @@ 'use client'; import { DefaultParams } from '@/app/[lang]/[region]'; -import { SpinnerIcon } from '@/components/logos/spinner-icon'; import { useApi } from '@/hooks/useApi'; import { zodResolver } from '@hookform/resolvers/zod'; import { NewsletterSubscriptionData } from '@socialincome/shared/src/sendgrid/SendgridSubscriptionClient'; -import { Button, Form, FormControl, FormField, FormItem, FormLabel, FormMessage, Input } from '@socialincome/ui'; +import { + Button, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + Input, + SpinnerIcon, +} from '@socialincome/ui'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; import toast from 'react-hot-toast'; diff --git a/website/src/components/navbar/navbar-client.tsx b/website/src/components/navbar/navbar-client.tsx index af90fdd57..0ccc6ea13 100644 --- a/website/src/components/navbar/navbar-client.tsx +++ b/website/src/components/navbar/navbar-client.tsx @@ -1,15 +1,13 @@ 'use client'; import { DefaultParams } from '@/app/[lang]/[region]'; -import { DonateIcon } from '@/components/logos/donate-icon'; import { SIAnimatedLogo } from '@/components/logos/si-animated-logo'; -import { SIIcon } from '@/components/logos/si-icon'; import { SILogo } from '@/components/logos/si-logo'; import { useI18n } from '@/components/providers/context-providers'; import { useGlobalStateProvider } from '@/components/providers/global-state-provider'; import { WebsiteCurrency, WebsiteLanguage, WebsiteRegion } from '@/i18n'; import { Bars3Icon, CheckIcon, ChevronLeftIcon, XMarkIcon } from '@heroicons/react/24/outline'; -import { Typography } from '@socialincome/ui'; +import { DonateIcon, SIIcon, Typography } from '@socialincome/ui'; import { getFlagImageURL } from '@socialincome/ui/src/lib/utils'; import classNames from 'classnames'; import _ from 'lodash'; From a9fe18cfda4e667a0f3627c1396281c67e8a9c48 Mon Sep 17 00:00:00 2001 From: mdumond <48355472+mdumond@users.noreply.github.com> Date: Thu, 23 Jan 2025 19:47:46 +0100 Subject: [PATCH 14/15] new footer design (#1017) --- .../components/containers/base-container.tsx | 10 +- .../[region]/(website)/arts/section-2.tsx | 2 +- website/src/components/footer/footer.tsx | 94 +++---------------- 3 files changed, 20 insertions(+), 86 deletions(-) diff --git a/ui/src/components/containers/base-container.tsx b/ui/src/components/containers/base-container.tsx index ba4f1b77d..bef37fae1 100644 --- a/ui/src/components/containers/base-container.tsx +++ b/ui/src/components/containers/base-container.tsx @@ -1,17 +1,15 @@ import React from 'react'; -import { twMerge } from 'tailwind-merge'; -import { BackgroundColor } from '../../interfaces/color'; type BaseContainerProps = { - backgroundColor?: BackgroundColor; + baseClassNames?: string; } & React.HTMLAttributes; export const BaseContainer = React.forwardRef( - ({ children, className, backgroundColor, ...props }, ref) => { + ({ children, className, baseClassNames, ...props }, ref) => { return ( -
+
-
+
{children}
diff --git a/website/src/app/[lang]/[region]/(website)/arts/section-2.tsx b/website/src/app/[lang]/[region]/(website)/arts/section-2.tsx index d2ff11153..d8a105708 100644 --- a/website/src/app/[lang]/[region]/(website)/arts/section-2.tsx +++ b/website/src/app/[lang]/[region]/(website)/arts/section-2.tsx @@ -12,7 +12,7 @@ export default async function Section2({ params }: DefaultPageProps) { const cards = translator.t(`section-2.cards`); return ( - + {translator.t(`section-2.topic`)} diff --git a/website/src/components/footer/footer.tsx b/website/src/components/footer/footer.tsx index b99077efa..3a7c57ba9 100644 --- a/website/src/components/footer/footer.tsx +++ b/website/src/components/footer/footer.tsx @@ -1,9 +1,4 @@ import { DefaultParams } from '@/app/[lang]/[region]'; -import { FooterClient } from '@/components/footer/footer-client'; -import { SILogo } from '@/components/logos/si-logo'; -import { WebsiteLanguage, websiteRegions } from '@/i18n'; -import { DocumentTextIcon, InformationCircleIcon, UserCircleIcon } from '@heroicons/react/24/solid'; -import { SiFacebook, SiGithub, SiInstagram, SiLinkedin, SiMaildotru, SiX } from '@icons-pack/react-simple-icons'; import { Translator } from '@socialincome/shared/src/utils/i18n'; import { BaseContainer, Typography } from '@socialincome/ui'; import Link from 'next/link'; @@ -20,7 +15,7 @@ function FooterLink({ label, url, Icon, target = '_self' }: FooterLinkProps) { return ( {Icon && } - + {label} @@ -33,82 +28,31 @@ export default async function Footer({ lang, region }: DefaultParams) { namespaces: ['common', 'website-common', 'website-me'], }); - const supportedTranslatedLanguages = (['de', 'en', 'it', 'fr'] as WebsiteLanguage[]).map((lang) => { - return { translation: translator.t(`languages.${lang}`), code: lang }; - }); - const supportedTranslatedCountries = websiteRegions.map((region) => { - return { translation: translator.t(`regions.${region}`), code: region }; - }); - return ( - +
-
- -
- -
-
-
- + {translator.t('footer.follow-us')} - - - - - - + + + + + +
- + {translator.t('footer.resources')} - - - + + +
- + {translator.t('navigation.our-work')}
- + {translator.t('navigation.about-us')}
-
- -
); From 79b50af9f24c02bd8b4fe4cc37a9187e5b729bc5 Mon Sep 17 00:00:00 2001 From: Alexey Shestakov Date: Fri, 24 Jan 2025 12:45:42 +0400 Subject: [PATCH 15/15] Update badge component (#1019) * Add interactive/non-interactive badge variants and sizes * Use the new badge on the site --- ui/src/components/badge.tsx | 42 ++- ui/src/stories/badge.stories.tsx | 245 +++++++++++++++--- .../about-us/(sections)/landing-page.tsx | 4 +- .../our-work/(sections)/our-work.tsx | 2 +- .../partners/(components)/PartnerBadges.tsx | 20 +- .../partners/(components)/PartnerHome.tsx | 4 +- .../(sections)/selection-process.tsx | 4 +- 7 files changed, 248 insertions(+), 73 deletions(-) diff --git a/ui/src/components/badge.tsx b/ui/src/components/badge.tsx index 6e00e44b1..e7f738cd0 100644 --- a/ui/src/components/badge.tsx +++ b/ui/src/components/badge.tsx @@ -4,29 +4,49 @@ import * as React from 'react'; import { cn } from '../lib/utils'; const badgeVariants = cva( - 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', + 'inline-flex items-center rounded-full border transition-colors focus:outline-none font-semibold', { variants: { variant: { - default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary-muted', - secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary-muted', - destructive: 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive-muted', - muted: 'border-muted-foreground text-muted-foreground hover:bg-muted-foreground hover:text-muted', - accent: 'border-transparent bg-accent text-accent-foreground hover:bg-muted-foreground hover:text-muted', - outline: - 'text-primary border-primary hover:bg-primary-muted hover:text-primary-foreground hover:border-primary-muted', + default: 'border-transparent bg-primary text-primary-foreground', + faded: 'border-transparent bg-primary bg-opacity-10 text-primary', + secondary: 'border-transparent bg-secondary text-secondary-foreground', + destructive: 'border-transparent bg-destructive text-destructive-foreground', + muted: 'border-muted-foreground text-muted-foreground', + accent: 'border-transparent bg-accent text-accent-foreground', + outline: 'text-primary border-primary', + interactive: + 'border-transparent bg-primary bg-opacity-10 text-primary hover:bg-opacity-100 hover:text-white focus:ring-2 focus:ring-ring focus:ring-offset-2', + 'interactive-accent': + 'border-transparent bg-accent bg-opacity-50 text-primary hover:bg-opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2', + 'interactive-secondary': + 'border-transparent bg-secondary bg-opacity-10 text-secondary hover:bg-opacity-100 hover:text-white focus:ring-2 focus:ring-ring focus:ring-offset-2', + 'interactive-destructive': + 'border-transparent bg-destructive bg-opacity-10 text-destructive hover:bg-opacity-100 hover:text-white focus:ring-2 focus:ring-ring focus:ring-offset-2', + 'interactive-muted': + 'border-muted-foreground text-muted-foreground hover:bg-muted-foreground hover:text-muted focus:ring-2 focus:ring-ring focus:ring-offset-2', + 'interactive-outline': + 'border-primary bg-transparent text-primary hover:bg-primary hover:text-white focus:ring-2 focus:ring-ring focus:ring-offset-2', + }, + size: { + sm: 'px-2 py-0.5 text-xs', + md: 'px-2.5 py-1 text-sm', + lg: 'px-3 py-1.5 text-base', }, }, defaultVariants: { variant: 'default', + size: 'sm', }, }, ); -export interface BadgeProps extends React.HTMLAttributes, VariantProps {} +export interface BadgeProps extends React.HTMLAttributes, VariantProps { + size?: 'sm' | 'md' | 'lg'; +} -function Badge({ className, variant, ...props }: BadgeProps) { - return
; +function Badge({ className, variant, size, ...props }: BadgeProps) { + return
; } export { Badge, badgeVariants }; diff --git a/ui/src/stories/badge.stories.tsx b/ui/src/stories/badge.stories.tsx index 55059394d..e05d4b439 100644 --- a/ui/src/stories/badge.stories.tsx +++ b/ui/src/stories/badge.stories.tsx @@ -1,22 +1,117 @@ +import { ArrowRightIcon, CheckCircleIcon, ExclamationTriangleIcon } from '@heroicons/react/24/solid'; import type { Meta, StoryObj } from '@storybook/react'; -import { Badge } from '../components/badge'; +import { Badge, BadgeProps } from '../components/badge'; const meta = { title: 'Components/Badge', component: Badge, tags: ['autodocs'], + argTypes: { + variant: { + description: 'Style variant of the badge', + options: [ + 'default', + 'secondary', + 'destructive', + 'muted', + 'accent', + 'outline', + 'interactive', + 'interactive-accent', + 'interactive-secondary', + 'interactive-destructive', + 'interactive-muted', + 'interactive-outline', + ], + control: { type: 'select' }, + }, + size: { + description: 'Size of the badge', + options: ['sm', 'md', 'lg'], + control: { type: 'select' }, + }, + }, } satisfies Meta; export default meta; type Story = StoryObj; -// Default Badge +/** + * The Badge component is used to highlight and display short pieces of information. + * It supports various variants and sizes to accommodate different use cases. + * + * Variants: + * - Basic variants: + * - default: Primary colored badge for general use + * - secondary: Secondary colored badge for less emphasis + * - destructive: For error or warning states + * - muted: For less prominent information + * - accent: For highlighted information + * - outline: Border-only style for subtle emphasis + * + * - Interactive variants (with hover effects): + * - interactive: Primary colored with opacity hover effect + * - interactive-accent: Accent colored for important interactive elements + * - interactive-secondary: Secondary colored for less prominent actions + * - interactive-destructive: For removable or dangerous actions + * - interactive-muted: For subtle interactive elements + * - interactive-outline: Outlined style that fills on hover + * + * Features: + * - Three size options (sm, md, lg) + * - Customizable through className prop + * - Support for icons and custom content + * - Accessible by default + * + * @see {@link https://www.figma.com/file/...} Figma Design + */ export const Default: Story = { - render: () => Default Badge, + args: { + children: 'Badge', + }, }; -// All Variants -export const Variants: Story = { +/** + * Interactive badges feature opacity and color effects on hover. + * This is the primary interactive variant, using the primary color scheme. + * For other interactive styles, see interactive-accent, interactive-secondary, + * interactive-destructive, interactive-muted, and interactive-outline variants. + */ +export const Interactive: Story = { + args: { + variant: 'interactive', + children: 'Interactive Badge', + }, +}; + +/** + * Interactive accent badge with opacity effects. + * Useful for highlighting important interactive elements. + */ +export const InteractiveAccent: Story = { + args: { + variant: 'interactive-accent', + children: 'Interactive Accent', + }, +}; + +/** + * Badges come in three sizes: small (default), medium, and large. + */ +export const Sizes: Story = { + render: () => ( +
+ Small Badge + Medium Badge + Large Badge +
+ ), +}; + +/** + * All available badge variants. + */ +export const AllVariants: Story = { render: () => (
Default @@ -25,62 +120,126 @@ export const Variants: Story = { Muted Accent Outline + Interactive + Interactive Accent + Interactive Secondary + Interactive Destructive + Interactive Muted + Interactive Outline
), }; -// Interactive States -export const Interactive: Story = { +/** + * Examples of common use cases for badges in the application: + * - Status indicators + * - Category labels + * - Counters + * - Interactive filters + */ +export const CommonUseCases: Story = { render: () => ( -
- Clickable Badge - - Clickable Secondary - - - Clickable Destructive - +
+ {/* Status indicators */} +
+ + Active + + + Pending + + + Blocked + + + Archived + +
+ {/* Content labels */} +
+ Featured + Trending + New + Premium +
+ {/* Categories and counts */} +
+ Documentation + Tutorial + 5 new +
), }; -// With Icons -export const WithIcons: Story = { - render: () => ( -
- - Online +interface WithIconsProps { + size: BadgeProps['size']; + onlineVariant: BadgeProps['variant']; + newVariant: BadgeProps['variant']; + warningVariant: BadgeProps['variant']; + onlineText: string; + newText: string; + warningText: string; +} + +/** + * Example of badges with icons. + * Icons can be added as children of the Badge component. + */ +export const WithIcons: StoryObj = { + args: { + size: 'md', + onlineVariant: 'interactive', + newVariant: 'interactive', + warningVariant: 'destructive', + onlineText: 'Online', + newText: 'New', + warningText: 'Warning', + }, + argTypes: { + onlineVariant: { + control: 'select', + options: ['default', 'secondary', 'destructive', 'outline', 'interactive', 'interactive-accent'], + }, + newVariant: { + control: 'select', + options: ['default', 'secondary', 'destructive', 'outline', 'interactive', 'interactive-accent'], + }, + warningVariant: { + control: 'select', + options: ['default', 'secondary', 'destructive', 'outline', 'interactive', 'interactive-accent'], + }, + size: { + control: 'select', + options: ['sm', 'md', 'lg'], + }, + }, + render: (args) => ( +
+ + {args.onlineText} - - New + + + {args.newText} - - Critical + + {args.warningText}
), }; -// Custom Styled -export const CustomStyled: Story = { +/** + * Example of customizing badges using className prop. + * While we recommend using the built-in variants and sizes, + * you can still customize badges using Tailwind classes when needed. + */ +export const CustomStyling: Story = { render: () => ( -
+
Custom Blue - Custom Green - Custom Border - Custom Rounded -
- ), -}; - -// Different Sizes -export const Sizes: Story = { - render: () => ( -
- Small - Default - Large - Extra Large + Custom Border + Gradient
), }; diff --git a/website/src/app/[lang]/[region]/(website)/about-us/(sections)/landing-page.tsx b/website/src/app/[lang]/[region]/(website)/about-us/(sections)/landing-page.tsx index d049cf8e8..aba31f916 100644 --- a/website/src/app/[lang]/[region]/(website)/about-us/(sections)/landing-page.tsx +++ b/website/src/app/[lang]/[region]/(website)/about-us/(sections)/landing-page.tsx @@ -27,7 +27,7 @@ export default async function LandingPage({ lang }: { lang: WebsiteLanguage }) {
- + {translator.t('landing-page.contact')} @@ -52,7 +52,7 @@ export default async function LandingPage({ lang }: { lang: WebsiteLanguage }) {
- + {translator.t('landing-page.registration')} diff --git a/website/src/app/[lang]/[region]/(website)/our-work/(sections)/our-work.tsx b/website/src/app/[lang]/[region]/(website)/our-work/(sections)/our-work.tsx index 832148102..851c979c1 100644 --- a/website/src/app/[lang]/[region]/(website)/our-work/(sections)/our-work.tsx +++ b/website/src/app/[lang]/[region]/(website)/our-work/(sections)/our-work.tsx @@ -27,7 +27,7 @@ export async function OurWork({ params }: DefaultPageProps) {
- + {translator.t('our-work.watch')} diff --git a/website/src/app/[lang]/[region]/(website)/partners/(components)/PartnerBadges.tsx b/website/src/app/[lang]/[region]/(website)/partners/(components)/PartnerBadges.tsx index 8d6c78446..86df6e12b 100644 --- a/website/src/app/[lang]/[region]/(website)/partners/(components)/PartnerBadges.tsx +++ b/website/src/app/[lang]/[region]/(website)/partners/(components)/PartnerBadges.tsx @@ -6,7 +6,6 @@ import { } from '@/app/[lang]/[region]/(website)/partners/(types)/PartnerBadges'; import { UsersIcon } from '@heroicons/react/24/solid'; import { Badge, HoverCard, HoverCardContent, HoverCardTrigger, Separator, Typography } from '@socialincome/ui'; -import { cn } from '@socialincome/ui/src/lib/utils'; import Image from 'next/image'; function RecipientsBadge({ @@ -22,18 +21,15 @@ function RecipientsBadge({ translatorBadgeFormer, translatorBadgeSuspended, }: RecipientsBadgeType) { - let badgeClassName = 'bg-primary hover:bg-primary text-primary bg-opacity-10 hover:bg-opacity-100 hover:text-white'; - if (isInsideHoverCard) { - badgeClassName = cn(badgeClassName, ' py-2'); - } + const size = isInsideHoverCard ? 'md' : 'sm'; const userIconClassName = isInsideHoverCard ? 'mr-2 h-5 w-5 rounded-full' : 'mr-1 h-4 w-4 rounded-full'; return ( - + - + {hoverCardTotalRecipients || 0} {isInsideHoverCard ? translatorBadgeRecipients : ''} @@ -46,17 +42,17 @@ function RecipientsBadge({
- + {hoverCardTotalActiveRecipients || 0} {translatorBadgeActive} - + {hoverCardTotalFormerRecipients || 0} {translatorBadgeFormer} - + {hoverCardTotalSuspendedRecipients || 0} {translatorBadgeSuspended} @@ -78,7 +74,7 @@ function SDGBadge({ return ( - + {translatorSdgTitle} @@ -106,7 +102,7 @@ function FundraiserBadge({ fundRaiserTranslation }: FundRaiserBadgeType) { return ( - + {fundRaiserTranslation} diff --git a/website/src/app/[lang]/[region]/(website)/partners/(components)/PartnerHome.tsx b/website/src/app/[lang]/[region]/(website)/partners/(components)/PartnerHome.tsx index 60eace2f8..60a975ee6 100644 --- a/website/src/app/[lang]/[region]/(website)/partners/(components)/PartnerHome.tsx +++ b/website/src/app/[lang]/[region]/(website)/partners/(components)/PartnerHome.tsx @@ -101,7 +101,7 @@ export function PartnerHome({ currentNgo, currentNgoCountry, translations, lang, translatorBadgeFormer={translations.badgeFormer} translatorBadgeSuspended={translations.badgeSuspended} /> - + {countryBadge?.countryFlagComponent || } {currentNgoCountry} @@ -129,7 +129,7 @@ export function PartnerHome({ currentNgo, currentNgoCountry, translations, lang,
)} - {ngoHoverCard.orgDescriptionParagraphs.map((paragraph, index) => { + {ngoHoverCard.orgDescriptionParagraphs?.map((paragraph, index) => { return (
{paragraph.map((fragment, index2) => { diff --git a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/selection-process.tsx b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/selection-process.tsx index a870a7ef6..c7f344cac 100644 --- a/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/selection-process.tsx +++ b/website/src/app/[lang]/[region]/(website)/transparency/recipient-selection/[currency]/(sections)/selection-process.tsx @@ -108,7 +108,7 @@ export function SelectionProcess({ lang }: DefaultParams) { {translator?.t('section-3.preselection-desc')}
- + {translator?.t('section-3.goal')} {translator?.t('section-3.preselection-goal')} @@ -142,7 +142,7 @@ export function SelectionProcess({ lang }: DefaultParams) { {translator?.t('section-3.selection-desc')}
- + {translator?.t('section-3.goal')} {translator?.t('section-3.selection-goal')}