Merge branch 'dev' into backend/5-event-schema

This commit is contained in:
José Salazar
2025-11-17 12:14:42 -05:00
80 changed files with 12356 additions and 134 deletions

View File

@@ -0,0 +1,80 @@
# Base44 Entity Schemas Reference
This document serves as a developer reference for the original data schemas from the Base44 backend. It is used to ensure feature parity and data integrity during the migration to the new GCP/Firebase backend.
---
## User Schema
```
role (text, required)
- The role of the user in the app
- Options: admin, user
email (text, required)
- The email of the user
full_name (text, required)
- Full name of the user
user_role (text)
- User's role in the system
- Options: admin, procurement, operator, sector, client, vendor, workforce
company_name (text)
- Company or organization name
profile_picture (text)
- URL to profile picture
phone (text)
- Phone number
address (text)
- Address
preferred_vendor_id (text)
- ID of the client's preferred/default vendor
preferred_vendor_name (text)
- Name of the client's preferred vendor
backup_vendor_ids (array)
- List of backup vendor IDs
dashboard_layout (object)
- User's customized dashboard layout preferences (legacy, kept for backward compatibility)
- widgets (array): Ordered list of visible widgets
- hidden_widgets (array): List of hidden widgets
- layout_version (text): Layout version for migration
dashboard_layout_client (object)
- Client dashboard layout
- widgets (array)
- hidden_widgets (array)
- layout_version (text)
dashboard_layout_vendor (object)
- Vendor dashboard layout
- widgets (array)
- hidden_widgets (array)
- layout_version (text)
dashboard_layout_operator (object)
- Operator dashboard layout
- widgets (array)
- hidden_widgets (array)
- layout_version (text)
dashboard_layout_workforce (object)
- Workforce dashboard layout
- widgets (array)
- hidden_widgets (array)
- layout_version (text)
preferences (object)
- User preferences and settings
- theme (text): Default: "light", Options: light, dark
- notifications_enabled (boolean): Default: true
- email_notifications (boolean): Default: true
```

295
Makefile
View File

@@ -4,7 +4,7 @@
# It is designed to be the main entry point for developers. # It is designed to be the main entry point for developers.
# Use .PHONY to declare targets that are not files, to avoid conflicts. # Use .PHONY to declare targets that are not files, to avoid conflicts.
.PHONY: install dev build prepare-export help deploy-launchpad deploy-app .PHONY: help install dev build integrate-export prepare-export deploy-launchpad deploy-launchpad-full deploy-app admin-install admin-dev admin-build deploy-admin deploy-admin-full configure-iap-launchpad configure-iap-admin list-iap-users remove-iap-user setup-labels export-issues create-issues-from-file install-git-hooks
# The default command to run if no target is specified (e.g., just 'make'). # The default command to run if no target is specified (e.g., just 'make').
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
@@ -12,9 +12,20 @@
# --- Firebase & GCP Configuration --- # --- Firebase & GCP Configuration ---
GCP_DEV_PROJECT_ID := krow-workforce-dev GCP_DEV_PROJECT_ID := krow-workforce-dev
GCP_STAGING_PROJECT_ID := krow-workforce-staging GCP_STAGING_PROJECT_ID := krow-workforce-staging
IAP_SERVICE_ACCOUNT := service-933560802882@gcp-sa-iap.iam.gserviceaccount.com
# --- Cloud Run Configuration ---
CR_LAUNCHPAD_SERVICE_NAME := internal-launchpad
CR_LAUNCHPAD_REGION := us-central1
CR_LAUNCHPAD_IMAGE_URI := us-docker.pkg.dev/$(GCP_DEV_PROJECT_ID)/gcr-io/$(CR_LAUNCHPAD_SERVICE_NAME)
CR_ADMIN_SERVICE_NAME := admin-console
CR_ADMIN_REGION := us-central1
CR_ADMIN_IMAGE_URI = us-docker.pkg.dev/$(GCP_PROJECT_ID)/gcr-io/$(CR_ADMIN_SERVICE_NAME)
# --- Environment Detection --- # --- Environment Detection ---
ENV ?= dev ENV ?= dev
SERVICE ?= launchpad # Default service for IAP commands: 'launchpad' or 'admin'
# --- Conditional Variables by Environment --- # --- Conditional Variables by Environment ---
ifeq ($(ENV),staging) ifeq ($(ENV),staging)
@@ -27,26 +38,230 @@ else
HOSTING_TARGET := app-dev HOSTING_TARGET := app-dev
endif endif
# Installs all project dependencies using npm. # --- Conditional Variables by Service for IAP commands ---
ifeq ($(SERVICE),admin)
IAP_SERVICE_NAME := $(CR_ADMIN_SERVICE_NAME)
IAP_SERVICE_REGION := $(CR_ADMIN_REGION)
IAP_PROJECT_ID := $(GCP_PROJECT_ID) # Admin console is env-specific
else
IAP_SERVICE_NAME := $(CR_LAUNCHPAD_SERVICE_NAME)
IAP_SERVICE_REGION := $(CR_LAUNCHPAD_REGION)
IAP_PROJECT_ID := $(GCP_DEV_PROJECT_ID) # Launchpad is dev-only
endif
# Shows this help message.
help:
@echo "--------------------------------------------------"
@echo " KROW Workforce - Available Makefile Commands"
@echo "--------------------------------------------------"
@echo ""
@echo " --- CORE DEVELOPMENT ---"
@echo " make install - Installs web frontend dependencies."
@echo " make dev - Starts the local web frontend server."
@echo " make build - Builds the web frontend for production."
@echo ""
@echo " --- DEPLOYMENT ---"
@echo " make deploy-launchpad-full - Deploys internal launchpad to Cloud Run (dev only) with IAP."
@echo " make deploy-admin-full [ENV=staging] - Deploys Admin Console to Cloud Run with IAP (default: dev)."
@echo " make deploy-app [ENV=staging] - Builds and deploys the main web app via Firebase Hosting (default: dev)."
@echo ""
@echo " --- CLOUD IAP MANAGEMENT ---"
@echo " make list-iap-users [SERVICE=admin] - Lists IAP users for a service (default: launchpad)."
@echo " make remove-iap-user USER=... [SERVICE=admin] - Removes an IAP user from a service."
@echo ""
@echo " --- PROJECT MANAGEMENT & TOOLS ---"
@echo " make setup-labels - Creates/updates GitHub labels from labels.yml."
@echo " make export-issues [ARGS="--state=all --label=bug"] - Exports GitHub issues to a markdown file. See scripts/export_issues.sh for options."
@echo " make create-issues-from-file - Bulk creates GitHub issues from a markdown file."
@echo " make install-git-hooks - Installs git pre-push hook to protect main/dev branches."
@echo ""
@echo " --- DATA CONNECT MANAGEMENT ---"
@echo " make dataconnect-enable-apis - Enables required GCP APIs for Data Connect."
@echo " make dataconnect-init - Initializes Firebase Data Connect (interactive wizard)."
@echo " make dataconnect-deploy - Deploys Data Connect schemas (GraphQL -> Cloud SQL)."
@echo ""
@echo " --- BASE44 EXPORT WORKFLOW ---"
@echo " make integrate-export - Integrates a new Base44 export from '../krow-workforce-export-latest'."
@echo " make prepare-export - Prepares a fresh Base44 export for local use."
@echo ""
@echo " make help - Shows this help message."
@echo "--------------------------------------------------"
# --- Core Development ---
install: install:
@echo "--> Installing web frontend dependencies..." @echo "--> Installing web frontend dependencies..."
@cd frontend-web && npm install @cd frontend-web && npm install
# Starts the local development server.
dev: dev:
@echo "--> Ensuring web frontend dependencies are installed..." @echo "--> Ensuring web frontend dependencies are installed..."
@cd frontend-web && npm install @cd frontend-web && npm install
@echo "--> Starting web frontend development server on http://localhost:5173 ..." @echo "--> Starting web frontend development server on http://localhost:5173 ..."
@cd frontend-web && npm run dev @cd frontend-web && npm run dev
# Builds the application for production.
build: build:
@echo "--> Building web frontend for production..." @echo "--> Building web frontend for production..."
@cd frontend-web && VITE_APP_ENV=$(ENV) npm run build @cd frontend-web && VITE_APP_ENV=$(ENV) npm run build
# Integrates a new Base44 export into the current project. # --- Deployment ---
# It replaces the src directory and the index.html file in the frontend-web directory. deploy-launchpad:
# Prerequisite: The new export must be in a folder named '../krow-workforce-export-latest'. @echo "--> Building and deploying Internal Launchpad to Cloud Run..."
@echo " - Step 1: Building container image..."
@cd firebase/internal-launchpad && gcloud builds submit \
--tag $(CR_LAUNCHPAD_IMAGE_URI) \
--project=$(GCP_DEV_PROJECT_ID)
@echo " - Step 2: Deploying to Cloud Run..."
@gcloud run deploy $(CR_LAUNCHPAD_SERVICE_NAME) \
--image $(CR_LAUNCHPAD_IMAGE_URI) \
--platform managed \
--region $(CR_LAUNCHPAD_REGION) \
--no-allow-unauthenticated \
--project=$(GCP_DEV_PROJECT_ID)
@echo " - Step 3: Enabling IAP on the service..."
@gcloud beta run services update $(CR_LAUNCHPAD_SERVICE_NAME) \
--region=$(CR_LAUNCHPAD_REGION) \
--project=$(GCP_DEV_PROJECT_ID) \
--iap
@echo "--> ✅ Deployment to Cloud Run successful."
deploy-launchpad-full: deploy-launchpad configure-iap-launchpad
@echo "✅ Launchpad deployed and IAP configured successfully!"
deploy-app: build
@echo "--> Deploying Frontend Web App to [$(ENV)] environment..."
@firebase deploy --only hosting:$(HOSTING_TARGET) --project=$(FIREBASE_ALIAS)
# --- Admin Console ---
admin-install:
@echo "--> Installing admin console dependencies..."
@cd admin-web && npm install
admin-dev:
@echo "--> Starting admin console development server on http://localhost:5174 ..."
@cd admin-web && npm run dev -- --port 5174
admin-build:
@echo "--> Building admin console for production..."
@node scripts/patch-admin-layout-for-env-label.js
@cd admin-web && VITE_APP_ENV=$(ENV) npm run build
deploy-admin: admin-build
@echo "--> Building and deploying Admin Console to Cloud Run [$(ENV)]..."
@echo " - Step 1: Building container image..."
@cd admin-web && gcloud builds submit \
--tag $(CR_ADMIN_IMAGE_URI) \
--project=$(GCP_PROJECT_ID)
@echo " - Step 2: Deploying to Cloud Run..."
@gcloud run deploy $(CR_ADMIN_SERVICE_NAME) \
--image $(CR_ADMIN_IMAGE_URI) \
--platform managed \
--region $(CR_ADMIN_REGION) \
--no-allow-unauthenticated \
--project=$(GCP_PROJECT_ID)
@echo " - Step 3: Enabling IAP on the service..."
@gcloud beta run services update $(CR_ADMIN_SERVICE_NAME) \
--region=$(CR_ADMIN_REGION) \
--project=$(GCP_PROJECT_ID) \
--iap
@echo "--> ✅ Admin Console deployment to Cloud Run successful."
deploy-admin-full: deploy-admin configure-iap-admin
@echo "✅ Admin Console deployed and IAP configured successfully!"
# --- Cloud IAP Configuration ---
configure-iap-launchpad:
@echo "--> Configuring IAP for Cloud Run service [$(CR_LAUNCHPAD_SERVICE_NAME)]..."
@echo " - Granting Cloud Run Invoker role to IAP Service Account..."
@gcloud run services add-iam-policy-binding $(CR_LAUNCHPAD_SERVICE_NAME) \
--region=$(CR_LAUNCHPAD_REGION) \
--project=$(GCP_DEV_PROJECT_ID) \
--member="serviceAccount:$(IAP_SERVICE_ACCOUNT)" \
--role='roles/run.invoker' \
--quiet
@echo " - Adding users from iap-users.txt..."
@cd firebase/internal-launchpad && \
grep -v '^#' iap-users.txt | grep -v '^$$' | while read -r member; do \
echo " Adding $$member as IAP-secured Web App User..."; \
gcloud beta iap web add-iam-policy-binding \
--project=$(GCP_DEV_PROJECT_ID) \
--resource-type=cloud-run \
--service=$(CR_LAUNCHPAD_SERVICE_NAME) \
--region=$(CR_LAUNCHPAD_REGION) \
--member="$$member" \
--role='roles/iap.httpsResourceAccessor' \
--quiet; \
done
@echo "✅ IAP configuration for Launchpad complete."
configure-iap-admin:
@echo "--> Configuring IAP for Cloud Run service [$(CR_ADMIN_SERVICE_NAME)] in [$(ENV)]..."
@echo " - Granting Cloud Run Invoker role to IAP Service Account..."
@gcloud run services add-iam-policy-binding $(CR_ADMIN_SERVICE_NAME) \
--region=$(CR_ADMIN_REGION) \
--project=$(GCP_PROJECT_ID) \
--member="serviceAccount:$(IAP_SERVICE_ACCOUNT)" \
--role='roles/run.invoker' \
--quiet
@echo " - Adding users from iap-users.txt..."
@cd admin-web && \
grep -v '^#' iap-users.txt | grep -v '^$$' | while read -r member; do \
echo " Adding $$member as IAP-secured Web App User..."; \
gcloud beta iap web add-iam-policy-binding \
--project=$(GCP_PROJECT_ID) \
--resource-type=cloud-run \
--service=$(CR_ADMIN_SERVICE_NAME) \
--region=$(CR_ADMIN_REGION) \
--member="$$member" \
--role='roles/iap.httpsResourceAccessor' \
--quiet; \
done
@echo "✅ IAP configuration for Admin Console complete."
list-iap-users:
@echo "--> Current IAP users for Cloud Run service [$(IAP_SERVICE_NAME)]:"
@gcloud beta iap web get-iam-policy \
--project=$(IAP_PROJECT_ID) \
--resource-type=cloud-run \
--service=$(IAP_SERVICE_NAME) \
--region=$(IAP_SERVICE_REGION)
remove-iap-user:
@if [ -z "$(USER)" ]; then \
echo "❌ Error: Please specify USER=user:email@example.com"; \
exit 1; \
fi
@echo "--> Removing IAP access for $(USER) from Cloud Run service [$(IAP_SERVICE_NAME)]..."
@gcloud beta iap web remove-iam-policy-binding \
--project=$(IAP_PROJECT_ID) \
--resource-type=cloud-run \
--service=$(IAP_SERVICE_NAME) \
--region=$(IAP_SERVICE_REGION) \
--member="$(USER)" \
--role='roles/iap.httpsResourceAccessor' \
--quiet
@echo "✅ User removed from IAP."
# --- Project Management ---
setup-labels:
@echo "--> Setting up GitHub labels..."
@./scripts/setup-github-labels.sh
export-issues:
@echo "--> Exporting GitHub issues to documentation..."
@./scripts/export_issues.sh $(ARGS)
create-issues-from-file:
@echo "--> Creating GitHub issues from file..."
@./scripts/create_issues.py
# --- Development Tools ---
install-git-hooks:
@echo "--> Installing Git hooks..."
@ln -sf ../../scripts/git-hooks/pre-push .git/hooks/pre-push
@echo "✅ pre-push hook installed successfully. Direct pushes to 'main' and 'dev' are now blocked."
# --- Base44 Export Workflow ---
integrate-export: integrate-export:
@echo "--> Integrating new Base44 export into frontend-web/..." @echo "--> Integrating new Base44 export into frontend-web/..."
@if [ ! -d "../krow-workforce-export-latest" ]; then \ @if [ ! -d "../krow-workforce-export-latest" ]; then \
@@ -69,56 +284,32 @@ integrate-export:
@node scripts/patch-index-html.js @node scripts/patch-index-html.js
@echo "--> Integration complete. Next step: 'make prepare-export'." @echo "--> Integration complete. Next step: 'make prepare-export'."
# Applies all necessary patches to a fresh Base44 export to run it locally.
# This is the main command for the hybrid workflow.
prepare-export: prepare-export:
@echo "--> Preparing fresh Base44 export for local development..." @echo "--> Preparing fresh Base44 export for local development..."
@node scripts/prepare-export.js @node scripts/prepare-export.js
@echo "--> Preparation complete. You can now run 'make dev'." @echo "--> Preparation complete. You can now run 'make dev'."
# --- Firebase Deployment --- # --- Data Connect / Backend ---
deploy-launchpad:
@echo "--> Deploying Internal Launchpad to DEV project..."
@firebase deploy --only hosting:launchpad --project=dev
deploy-app: build # Enable all required APIs for Firebase Data Connect + Cloud SQL
@echo "--> Deploying Frontend Web App to [$(ENV)] environment..." dataconnect-enable-apis:
@firebase deploy --only hosting:$(HOSTING_TARGET) --project=$(FIREBASE_ALIAS) @echo "--> Enabling Firebase & Data Connect APIs on project [$(GCP_PROJECT_ID)]..."
@gcloud services enable firebase.googleapis.com --project=$(GCP_PROJECT_ID)
@gcloud services enable firebasedataconnect.googleapis.com --project=$(GCP_PROJECT_ID)
@gcloud services enable sqladmin.googleapis.com --project=$(GCP_PROJECT_ID)
@gcloud services enable iam.googleapis.com --project=$(GCP_PROJECT_ID)
@gcloud services enable cloudresourcemanager.googleapis.com --project=$(GCP_PROJECT_ID)
@echo "✅ APIs enabled for project [$(GCP_PROJECT_ID)]."
# Shows this help message. # Initialize Firebase Data Connect (interactive wizard).
help: # This wraps the command so we remember how to run it for dev/staging/prod.
@echo "--------------------------------------------------" dataconnect-init:
@echo " KROW Workforce - Available Makefile Commands" @echo "--> Initializing Firebase Data Connect for alias [$(FIREBASE_ALIAS)] (project: $(GCP_PROJECT_ID))..."
@echo "--------------------------------------------------" @firebase init dataconnect --project $(FIREBASE_ALIAS)
@echo " make install - Installs web frontend dependencies." @echo "✅ Data Connect initialization command executed. Follow the interactive steps in the CLI."
@echo " make dev - Starts the local web frontend server."
@echo " make build - Builds the web frontend for production."
@echo " make integrate-export - Integrates a new Base44 export from '../krow-workforce-export-latest'."
@echo " make prepare-export - Prepares a fresh Base44 export for local use."
@echo ""
@echo " --- DEPLOYMENT ---"
@echo " make deploy-launchpad - Deploys the internal launchpad (always to dev)."
@echo " make deploy-app [ENV=staging] - Builds and deploys the main web app (default: dev)."
@echo ""
@echo " make help - Shows this help message."
@echo "--------------------------------------------------"
# --- Project Management ---
setup-labels:
@echo "--> Setting up GitHub labels..."
@./scripts/setup-github-labels.sh
export-issues:
@echo "--> Exporting GitHub issues to documentation..."
@./scripts/export_issues.sh
create-issues-from-file:
@echo "--> Creating GitHub issues from file..."
@./scripts/create_issues.py
# --- Development Tools ---
install-git-hooks:
@echo "--> Installing Git hooks..."
@ln -sf ../../scripts/git-hooks/pre-push .git/hooks/pre-push
@echo "✅ pre-push hook installed successfully. Direct pushes to 'main' and 'dev' are now blocked."
# Deploy Data Connect schemas (GraphQL → Cloud SQL)
dataconnect-deploy:
@echo "--> Deploying Firebase Data Connect schemas to [$(ENV)] (project: $(FIREBASE_ALIAS))..."
@firebase deploy --only dataconnect --project=$(FIREBASE_ALIAS)
@echo "✅ Data Connect deployment completed for [$(ENV)]."

View File

@@ -1,43 +1,38 @@
# KROW Workforce Platform # Krow Workforce Management Platform
This monorepo contains the complete source code for the KROW Workforce platform, including the web frontend, mobile applications, and backend services. Krow is a comprehensive monorepo platform designed to streamline workforce management for events, hospitality, and large-scale enterprise operations. It connects clients, operators, vendors, and staff in a unified ecosystem, leveraging modern technology to optimize every step from procurement to payroll.
## 🚀 What's in this Monorepo? ## 🚀 What's in this Monorepo?
- **/firebase/**: Contains the Firebase Data Connect configuration (GraphQL schema, queries, mutations) and Firebase Hosting configuration. - **/firebase/**: This directory is the backbone of our backend infrastructure. It contains:
- **/frontend-web/**: The React/Vite web application used by administrators and managers. - **Firestore Rules (`firestore.rules`):** Defines the security and access control logic for our NoSQL database, ensuring that users can only read and write data they are authorized to access.
- **/mobile-apps/**: Contains the two Flutter-based mobile applications: - **Cloud Storage Rules (`storage.rules`):** Secures file uploads and downloads (like compliance documents and user avatars).
- `client-app`: For clients managing events. - **Data Connect (`dataconnect/`):** Houses the configuration for Firebase Data Connect, including the GraphQL schema, connectors, queries, and mutations that define our API layer.
- `staff-app`: For staff members managing their shifts and profile.
- **/docs/**: All project documentation (vision, roadmaps, architecture, guides).
- **/scripts/**: Automation scripts used by the `Makefile`.
- **/secrets/**: Contains sensitive credentials (ignored by Git).
## ▶️ Getting Started - **/admin-web/**: The central "mission control" for platform administrators. This React/Vite web application provides a secure interface for:
- **User Management:** Inviting new users (the very first client or operator) and managing roles/permissions across the entire ecosystem.
- **Platform Analytics:** Monitoring application usage, top pages, and user activity.
- **System Logs:** A log explorer for diagnosing issues and monitoring the platform's health.
This project uses a central `Makefile` to orchestrate all common tasks. - **/frontend-web/**: The primary web application for core business operations, used by procurement managers, operators, and clients. This React/Vite application includes modules for:
- **Vendor Management:** Onboarding, compliance tracking, and performance scorecards.
- **Event & Order Management:** Creating and managing staffing orders.
- **Invoicing & Payroll:** Financial workflows for clients and staff.
- **Dashboards:** Role-specific dashboards for different user types.
1. **Install Dependencies:** - **/mobile-apps/**: This directory is planned for our future mobile applications. It is structured to contain two distinct apps:
```bash - `client-app`: A dedicated application for clients to create orders, track events, and manage billing on the go.
make install - `staff-app`: An essential tool for workforce members to view schedules, clock in/out, manage their profiles, and track earnings.
```
*(This will install dependencies for the web frontend. Mobile dependency installation is handled within their respective directories.)*
2. **Run a Service:** - **/docs/**: The single source of truth for all project documentation. This includes:
- To run the web frontend: `make dev` - **Vision & Roadmaps:** High-level strategy, product goals, and technical direction.
- *(Additional commands for mobile and backend will be added as development progresses.)* - **Architecture:** Detailed diagrams and explanations of the system architecture.
- **API Specifications:** Documentation for our GraphQL API.
- **Development Guides:** Conventions, setup instructions, and maintenance procedures.
3. **See All Commands:** - **/scripts/**: A collection of automation scripts to streamline development and operational tasks. These scripts are primarily executed via the `Makefile` and handle tasks like database patching, environment setup, and code generation.
For a full list of available commands, run:
```bash
make help
```
## 🤝 Contributing - **/secrets/**: A Git-ignored directory for storing sensitive credentials, API keys, and environment-specific configuration files. This ensures that no confidential information is ever committed to the repository.
New to the KROW team? Start here to set up your environment and understand our development practices: **[CONTRIBUTING.md](./CONTRIBUTING.md)**
---
## 📚 Documentation Overview ## 📚 Documentation Overview
@@ -52,3 +47,35 @@ This section provides a quick guide to the most important documentation files in
- **[06-maintenance-guide.md](./docs/06-maintenance-guide.md)**: The operational manual for integrating updates from the Base44 visual builder. - **[06-maintenance-guide.md](./docs/06-maintenance-guide.md)**: The operational manual for integrating updates from the Base44 visual builder.
- **[07-reference-base44-api-export.md](./docs/07-reference-base44-api-export.md)**: The raw API documentation exported from Base44, used as a reference. - **[07-reference-base44-api-export.md](./docs/07-reference-base44-api-export.md)**: The raw API documentation exported from Base44, used as a reference.
- **[08-reference-base44-prompts.md](./docs/08-reference-base44-prompts.md)**: A collection of standardized prompts for interacting with the Base44 AI. - **[08-reference-base44-prompts.md](./docs/08-reference-base44-prompts.md)**: A collection of standardized prompts for interacting with the Base44 AI.
## 🤝 Contributing
New to the KROW team? Start here to set up your environment and understand our development practices: **[CONTRIBUTING.md](./CONTRIBUTING.md)**
## 🛠️ Tech Stack
- **Frontend:** React with Vite (JavaScript)
- **Styling:** Tailwind CSS
- **Backend:** Firebase (Firestore, Cloud Storage, Authentication, Data Connect)
- **Mobile:** Flutter (planned)
## 📦 Getting Started
1. **Clone the repository:**
```bash
git clone https://github.com/Oloodi/krow.git
cd krow
```
2. **Install dependencies for the main web app:**
```bash
cd frontend-web
npm install
```
3. **Run the development server:**
```bash
npm run dev
```
The main application will be available at `http://localhost:5173`. For other packages, refer to their respective `README.md` files.

17
admin-web/.gcloudignore Normal file
View File

@@ -0,0 +1,17 @@
# This file specifies files that are *not* uploaded to Google Cloud
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore
# Node.js dependencies:
node_modules/

24
admin-web/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

29
admin-web/Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
# STAGE 1: Build the React application
FROM node:20-alpine AS build
WORKDIR /app
# Copy package files and install dependencies
COPY package.json package-lock.json ./
RUN npm install
# Copy the rest of the application source code
COPY . .
# Build the application for production
RUN npm run build
# STAGE 2: Serve the static files with Nginx
FROM nginx:alpine
# Copy the built files from the build stage
COPY --from=build /app/dist /usr/share/nginx/html
# Copy the custom Nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose the port Nginx is listening on
EXPOSE 8080
# Command to run Nginx in the foreground
CMD ["nginx", "-g", "daemon off;"]

35
admin-web/README.md Normal file
View File

@@ -0,0 +1,35 @@
# Krow Admin Console
This is the central administration web application for the Krow platform, built with React and Vite.
## Overview
The Krow Admin Console provides a secure and centralized interface for platform administrators to perform high-level management tasks. Its primary functions are:
- **User Ecosystem Management:** Invite, view, and manage access for all users across the platform (Clients, Operators, Vendors, etc.).
- **Platform Health Monitoring:** Get a high-level overview of system activity, usage, and performance.
- **Diagnostics and Troubleshooting:** Access system logs for maintenance and issue resolution.
## Features
- **Dashboard:** A central hub displaying key platform metrics like total users, active vendors, and pending invitations.
- **User Management:** A complete interface to invite new users and manage existing ones, including role assignments.
- **Analytics:** A view of application usage statistics, such as top users and most visited pages.
- **Logs Explorer:** A simple tool to monitor and filter system logs for diagnostics.
## Getting Started
1. Navigate to the `admin-web` directory:
```bash
cd admin-web
```
2. Install the dependencies:
```bash
npm install
```
3. Start the development server:
```bash
npm run dev
```
The application will be available at `http://localhost:5173/admin-web/`.

21
admin-web/components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": false,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

18
admin-web/favicon.svg Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="200 180 280 140" style="enable-background:new 0 0 500 500;" xml:space="preserve">
<style type="text/css">
.st0{fill:#24303B;}
.st1{fill:#002FE3;}
</style>
<g>
<path class="st1" d="M459.81,202.55c-5.03,0.59-9.08,4.49-10.36,9.38l-15.99,59.71l-16.24-56.3
c-1.68-5.92-6.22-10.86-12.19-12.34c-1.58-0.39-3.11-0.54-4.64-0.49h-0.15c-1.53-0.05-3.11,0.1-4.64,0.49
c-5.97,1.48-10.51,6.42-12.24,12.34l-3.6,12.53l-11.35,39.38l-7.9-27.54c-10.76-37.5-48.56-62.23-88.38-55.32
c-33.26,5.82-57.05,35.68-56.99,69.48v0.79c0,4.34,0.39,8.73,1.13,13.18c0.18,1.02,0.37,2.03,0.6,3.03
c1.84,8.31,10.93,12.73,18.49,8.8v0c5.36-2.79,7.84-8.89,6.42-14.77c-0.85-3.54-1.28-7.23-1.23-11.03
c0-25.02,20.48-45.5,45.55-45.2c7.6,0.1,15.59,2.07,23.59,6.37c13.52,7.3,23.15,20.18,27.34,34.94l13.32,46.34
c1.73,5.97,6.22,11,12.24,12.58c9.62,2.62,19-3.06,21.51-12.04l16.09-56.7l0.2-0.1l16.09,56.85c1.63,5.68,5.87,10.41,11.55,11.99
c9.13,2.57,18.11-2.66,20.67-11.2l24.13-79.6C475.35,209.85,468.64,201.56,459.81,202.55z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

8
admin-web/iap-users.txt Normal file
View File

@@ -0,0 +1,8 @@
# List of authorized users for the Admin Console
# Format: one email per line, lines starting with # are comments
#
# IMPORTANT: These users must belong to the 'krowwithus.com' organization.
# This is a known limitation of enabling IAP directly on Cloud Run.
# See: https://docs.cloud.google.com/run/docs/securing/identity-aware-proxy-cloud-run#known_limitations
user:admin@krowwithus.com

13
admin-web/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>KROW Admin Console</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

10
admin-web/jsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"jsx": "react-jsx"
},
"include": ["src/**/*.js", "src/**/*.jsx"]
}

18
admin-web/logo.svg Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="200 180 280 140" style="enable-background:new 0 0 500 500;" xml:space="preserve">
<style type="text/css">
.st0{fill:#24303B;}
.st1{fill:#002FE3;}
</style>
<g>
<path class="st1" d="M459.81,202.55c-5.03,0.59-9.08,4.49-10.36,9.38l-15.99,59.71l-16.24-56.3
c-1.68-5.92-6.22-10.86-12.19-12.34c-1.58-0.39-3.11-0.54-4.64-0.49h-0.15c-1.53-0.05-3.11,0.1-4.64,0.49
c-5.97,1.48-10.51,6.42-12.24,12.34l-3.6,12.53l-11.35,39.38l-7.9-27.54c-10.76-37.5-48.56-62.23-88.38-55.32
c-33.26,5.82-57.05,35.68-56.99,69.48v0.79c0,4.34,0.39,8.73,1.13,13.18c0.18,1.02,0.37,2.03,0.6,3.03
c1.84,8.31,10.93,12.73,18.49,8.8v0c5.36-2.79,7.84-8.89,6.42-14.77c-0.85-3.54-1.28-7.23-1.23-11.03
c0-25.02,20.48-45.5,45.55-45.2c7.6,0.1,15.59,2.07,23.59,6.37c13.52,7.3,23.15,20.18,27.34,34.94l13.32,46.34
c1.73,5.97,6.22,11,12.24,12.58c9.62,2.62,19-3.06,21.51-12.04l16.09-56.7l0.2-0.1l16.09,56.85c1.63,5.68,5.87,10.41,11.55,11.99
c9.13,2.57,18.11-2.66,20.67-11.2l24.13-79.6C475.35,209.85,468.64,201.56,459.81,202.55z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

13
admin-web/nginx.conf Normal file
View File

@@ -0,0 +1,13 @@
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ /index.html;
}
}

4967
admin-web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
admin-web/package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "admin-web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-slot": "^1.2.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.553.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.9.6",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0",
"autoprefixer": "^10.4.22",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.18",
"vite": "^7.2.2"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

42
admin-web/src/App.css Normal file
View File

@@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

24
admin-web/src/App.jsx Normal file
View File

@@ -0,0 +1,24 @@
import React from "react";
import Layout from "./pages/Layout";
import Dashboard from "./pages/Dashboard";
import UserManagement from "./pages/UserManagement";
import Analytics from "./pages/Analytics";
import Logs from "./pages/Logs";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
function App() {
return (
<Router>
<Layout>
<Routes>
<Route path="/admin-web/" element={<Dashboard />} />
<Route path="/admin-web/user-management" element={<UserManagement />} />
<Route path="/admin-web/analytics" element={<Analytics />} />
<Route path="/admin-web/logs" element={<Logs />} />
</Routes>
</Layout>
</Router>
);
}
export default App;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

58
admin-web/src/index.css Normal file
View File

@@ -0,0 +1,58 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,6 @@
import { clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs) {
return twMerge(clsx(inputs))
}

10
admin-web/src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@@ -0,0 +1,61 @@
import React from "react";
const topUsers = [
{ email: "ian.gomez@example.com", visits: 6950 },
{ email: "admin@krow.com", visits: 193 },
{ email: "paula.orte@example.com", visits: 81 },
{ email: "test.user@example.com", visits: 50 },
];
const topPages = [
{ name: "VendorManagement", visits: 718 },
{ name: "Dashboard", visits: 600 },
{ name: "unknown", visits: 587 },
{ name: "ClientDashboard", visits: 475 },
{ name: "Events", visits: 456 },
];
export default function Analytics() {
return (
<div>
<h1 className="text-3xl font-bold text-slate-800">Analytics</h1>
<p className="mt-2 text-base text-slate-600">
An overview of your application's data.
</p>
<div className="mt-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white shadow-lg rounded-2xl p-6">
<h3 className="text-lg font-medium text-gray-900">Total Unique Users</h3>
<p className="mt-2 text-4xl font-bold text-blue-600">3</p>
</div>
</div>
</div>
<div className="mt-8 grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<h3 className="text-xl font-semibold text-gray-900">Top Users</h3>
<ul className="mt-4 space-y-4">
{topUsers.map((user) => (
<li key={user.email} className="bg-white shadow-lg rounded-2xl p-4">
<p className="font-semibold text-slate-800">{user.email}</p>
<p className="text-slate-500">{user.visits.toLocaleString()} visits</p>
</li>
))}
</ul>
</div>
<div>
<h3 className="text-xl font-semibold text-gray-900">Top Pages</h3>
<ul className="mt-4 space-y-4">
{topPages.map((page) => (
<li key={page.name} className="bg-white shadow-lg rounded-2xl p-4 flex justify-between items-center">
<p className="font-semibold text-slate-800">{page.name}</p>
<p className="text-slate-500 font-medium">{page.visits.toLocaleString()}</p>
</li>
))}
</ul>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,38 @@
import React from "react";
import { Users, Building2, Mail, FileText } from "lucide-react";
const stats = [
{ name: "Total Users", stat: "15", icon: Users, color: "bg-blue-500" },
{ name: "Active Vendors", stat: "8", icon: Building2, color: "bg-green-500" },
{ name: "Pending Invitations", stat: "3", icon: Mail, color: "bg-yellow-500" },
{ name: "Open Orders", stat: "12", icon: FileText, color: "bg-purple-500" },
];
export default function Dashboard() {
return (
<div>
<h1 className="text-3xl font-bold text-slate-800">Admin Dashboard</h1>
<p className="mt-2 text-base text-slate-600">
Welcome to the Krow Admin Console. Here is a quick overview of the platform.
</p>
<div className="mt-8">
<dl className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((item) => (
<div key={item.name} className="relative overflow-hidden rounded-2xl bg-white px-4 py-5 shadow-lg sm:px-6 sm:py-6">
<dt>
<div className={`absolute rounded-xl p-3 ${item.color}`}>
<item.icon className="h-8 w-8 text-white" aria-hidden="true" />
</div>
<p className="ml-20 truncate text-sm font-medium text-gray-500">{item.name}</p>
</dt>
<dd className="ml-20 flex items-baseline">
<p className="text-3xl font-semibold text-gray-900">{item.stat}</p>
</dd>
</div>
))}
</dl>
</div>
</div>
);
}

View File

@@ -0,0 +1,88 @@
import React from "react";
import { Link, useLocation } from "react-router-dom";
import { Building2, Users, BarChart3, Terminal, LayoutDashboard, LogOut } from "lucide-react";
const navigation = [
{ name: "Dashboard", href: "/admin-web/", icon: LayoutDashboard },
{ name: "User Management", href: "/admin-web/user-management", icon: Users },
{ name: "Analytics", href: "/admin-web/analytics", icon: BarChart3 },
{ name: "Logs Explorer", href: "/admin-web/logs", icon: Terminal },
];
export default function Layout({ children }) {
const location = useLocation();
return (
<div className="flex h-screen bg-slate-50">
{/* Sidebar */}
<div className="hidden md:flex md:flex-shrink-0">
<div className="flex flex-col w-64">
{/* Sidebar header */}
<div className="flex items-center h-16 flex-shrink-0 px-4 bg-white border-b border-slate-200">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-lg flex items-center justify-center">
<img src="/logo.svg" alt="Krow Logo" className="h-full w-full" />
</div>
<div>
<h1 className="text-lg font-bold text-slate-800">KROW</h1>
<p className="text-xs text-slate-500">Admin Console</p>
</div>
</div>
{import.meta.env.VITE_APP_ENV && (
<span className={`ml-auto inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
import.meta.env.VITE_APP_ENV === 'staging' ? 'bg-amber-100 text-amber-800' : 'bg-blue-100 text-blue-800'
}`}>
{import.meta.env.VITE_APP_ENV === 'staging' ? 'Staging' : 'Dev'}
</span>
)}
</div>
{/* Sidebar content */}
<div className="flex-1 flex flex-col overflow-y-auto bg-white">
<nav className="flex-1 px-4 py-4">
{navigation.map((item) => (
<Link
key={item.name}
to={item.href}
className={`
${
location.pathname === item.href
? "bg-blue-50 text-blue-600"
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900"
}
group flex items-center px-3 py-3 text-sm font-medium rounded-lg
`}
>
<item.icon
className="mr-3 h-6 w-6"
aria-hidden="true"
/>
{item.name}
</Link>
))}
</nav>
<div className="px-4 py-4 border-t border-slate-200">
<Link
to="#"
className="group flex items-center px-3 py-3 text-sm font-medium rounded-lg text-slate-600 hover:bg-slate-100 hover:text-slate-900"
>
<LogOut className="mr-3 h-6 w-6" />
Logout
</Link>
</div>
</div>
</div>
</div>
{/* Main content */}
<div className="flex flex-col w-0 flex-1 overflow-hidden">
<main className="flex-1 relative z-0 overflow-y-auto focus:outline-none">
<div className="py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
{children}
</div>
</div>
</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,54 @@
import React from "react";
const logs = [
{ level: "INFO", message: "User admin@krow.com logged in.", timestamp: "2024-05-21T10:00:00Z" },
{ level: "WARN", message: "API response time is slow: 2500ms for /api/vendors", timestamp: "2024-05-21T10:01:00Z" },
{ level: "ERROR", message: "Failed to process payment for invoice INV-12345.", timestamp: "2024-05-21T10:02:00Z" },
{ level: "INFO", message: "New vendor 'New Staffing Co' invited by admin@krow.com.", timestamp: "2024-05-21T10:05:00Z" },
];
export default function Logs() {
return (
<div>
<h1 className="text-3xl font-bold text-slate-800">Logs Explorer</h1>
<p className="mt-2 text-base text-slate-600">
Monitor system logs across your application.
</p>
<div className="mt-8 flow-root">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-2xl">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-slate-50">
<tr>
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Level</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Message</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Timestamp</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
{logs.map((log, index) => (
<tr key={index}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${
log.level === 'ERROR' ? 'bg-red-100 text-red-800' :
log.level === 'WARN' ? 'bg-yellow-100 text-yellow-800' :
'bg-green-100 text-green-800'
}`}>
{log.level}
</span>
</td>
<td className="px-3 py-4 text-sm text-gray-500 font-mono">{log.message}</td>
<td className="px-3 py-4 text-sm text-gray-500">{new Date(log.timestamp).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,146 @@
import React, { useState } from "react";
import { Plus, Send } from "lucide-react";
const users = [
{ name: "Alex Johnson", email: "alex.j@krow.com", role: "Admin", status: "Active", avatar: "https://randomuser.me/api/portraits/men/1.jpg" },
{ name: "Samantha Lee", email: "samantha.lee@vendor.com", role: "Vendor", status: "Active", avatar: "https://randomuser.me/api/portraits/women/2.jpg" },
{ name: "Michael Chen", email: "michael.chen@client.com", role: "Client", status: "Pending", avatar: "https://randomuser.me/api/portraits/men/3.jpg" },
];
function InviteUserModal({ isOpen, onClose }) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity">
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<div className="relative transform overflow-hidden rounded-2xl bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<h3 className="text-xl font-semibold leading-6 text-slate-900">Send Invitation</h3>
<p className="mt-2 text-sm text-slate-500">The user will receive an email with a link to set up their account.</p>
<div className="mt-6 space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email address
</label>
<input
type="email"
name="email"
id="email"
className="mt-2 block w-full rounded-lg border-0 py-2.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
placeholder="colleague@company.com"
/>
</div>
<div>
<label htmlFor="role" className="block text-sm font-medium text-gray-700">
Access level
</label>
<select
id="role"
name="role"
className="mt-2 block w-full rounded-lg border-0 py-2.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"
defaultValue="User"
>
<option>Admin</option>
<option>Vendor</option>
<option>Client</option>
<option>User</option>
</select>
</div>
</div>
</div>
<div className="bg-slate-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
<button
type="button"
className="inline-flex w-full items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 sm:ml-3 sm:w-auto"
onClick={onClose}
>
<Send className="h-5 w-5 mr-2" />
Send Invitation
</button>
<button
type="button"
className="mt-3 inline-flex w-full justify-center rounded-lg bg-white px-4 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
onClick={onClose}
>
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
);
}
export default function UserManagement() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<div className="sm:flex sm:items-center">
<div className="sm:flex-auto">
<h1 className="text-3xl font-bold text-slate-800">User Management</h1>
<p className="mt-2 text-base text-slate-600">
A list of all the users in your account including their name, email and role.
</p>
</div>
<div className="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<button
type="button"
className="inline-flex items-center justify-center rounded-lg border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 sm:w-auto"
onClick={() => setIsModalOpen(true)}
>
<Plus className="h-5 w-5 mr-2" />
Invite User
</button>
</div>
</div>
<div className="mt-8 flow-root">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-2xl">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-slate-50">
<tr>
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Name</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Email</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Role</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
{users.map((user) => (
<tr key={user.email}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
<div className="flex items-center">
<div className="h-11 w-11 flex-shrink-0">
<img className="h-11 w-11 rounded-full" src={user.avatar} alt="" />
</div>
<div className="ml-4">
<div className="font-medium text-gray-900">{user.name}</div>
</div>
</div>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{user.email}</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{user.role}</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
user.status === 'Active' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
}`}>
{user.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
<InviteUserModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
</div>
);
}

View File

@@ -0,0 +1,89 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"],
theme: {
extend: {
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
},
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))'
}
},
keyframes: {
'accordion-down': {
from: {
height: '0'
},
to: {
height: 'var(--radix-accordion-content-height)'
}
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
height: '0'
}
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out'
}
}
},
plugins: [require("tailwindcss-animate")],
}

13
admin-web/vite.config.js Normal file
View File

@@ -0,0 +1,13 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})

View File

@@ -0,0 +1,13 @@
specVersion: "v1"
serviceId: "krow-workforce"
location: "us-central1"
schema:
source: "./schema"
datasource:
postgresql:
database: "fdcdb"
cloudSql:
instanceId: "krow-workforce-fdc"
# schemaValidation: "STRICT" # STRICT mode makes Postgres schema match Data Connect exactly.
# schemaValidation: "COMPATIBLE" # COMPATIBLE mode makes Postgres schema compatible with Data Connect.
connectorDirs: ["./example"]

View File

@@ -0,0 +1,8 @@
connectorId: example
generate:
javascriptSdk:
- outputDir: ../../frontend-web/src/dataconnect-generated
package: "@dataconnect/generated"
packageJsonDir: ../../frontend-web
react: true
angular: false

View File

@@ -0,0 +1,33 @@
# Example mutations for a simple movie app
# Create a movie based on user input
mutation CreateMovie($title: String!, $genre: String!, $imageUrl: String!)
@auth(level: USER_EMAIL_VERIFIED, insecureReason: "Any email verified users can create a new movie.") {
movie_insert(data: { title: $title, genre: $genre, imageUrl: $imageUrl })
}
# Upsert (update or insert) a user's username based on their auth.uid
mutation UpsertUser($username: String!) @auth(level: USER) {
# The "auth.uid" server value ensures that users can only register their own user.
user_upsert(data: { id_expr: "auth.uid", username: $username })
}
# Add a review for a movie
mutation AddReview($movieId: UUID!, $rating: Int!, $reviewText: String!)
@auth(level: USER) {
review_upsert(
data: {
userId_expr: "auth.uid"
movieId: $movieId
rating: $rating
reviewText: $reviewText
# reviewDate defaults to today in the schema. No need to set it manually.
}
)
}
# Logged in user can delete their review for a movie
mutation DeleteReview($movieId: UUID!) @auth(level: USER) {
# The "auth.uid" server value ensures that users can only delete their own reviews.
review_delete(key: { userId_expr: "auth.uid", movieId: $movieId })
}

View File

@@ -0,0 +1,78 @@
# Example queries for a simple movie app.
# @auth() directives control who can call each operation.
# Anyone should be able to list all movies, so the auth level is set to PUBLIC
query ListMovies @auth(level: PUBLIC, insecureReason: "Anyone can list all movies.") {
movies {
id
title
imageUrl
genre
}
}
# List all users, only admins should be able to list all users, so we use NO_ACCESS
query ListUsers @auth(level: NO_ACCESS) {
users {
id
username
}
}
# Logged in users can list all their reviews and movie titles associated with the review
# Since the query uses the uid of the current authenticated user, we set auth level to USER
query ListUserReviews @auth(level: USER) {
user(key: { id_expr: "auth.uid" }) {
id
username
# <field>_on_<foreign_key_field> makes it easy to grab info from another table
# Here, we use it to grab all the reviews written by the user.
reviews: reviews_on_user {
rating
reviewDate
reviewText
movie {
id
title
}
}
}
}
# Get movie by id
query GetMovieById($id: UUID!) @auth(level: PUBLIC, insecureReason: "Anyone can get a movie by id.") {
movie(id: $id) {
id
title
imageUrl
genre
metadata: movieMetadata_on_movie {
rating
releaseYear
description
}
reviews: reviews_on_movie {
reviewText
reviewDate
rating
user {
id
username
}
}
}
}
# Search for movies, actors, and reviews
query SearchMovie($titleInput: String, $genre: String) @auth(level: PUBLIC, insecureReason: "Anyone can search for movies.") {
movies(
where: {
_and: [{ genre: { eq: $genre } }, { title: { contains: $titleInput } }]
}
) {
id
title
genre
imageUrl
}
}

View File

@@ -0,0 +1,52 @@
# Example schema for simple movie review app
# User table is keyed by Firebase Auth UID.
type User @table {
# `@default(expr: "auth.uid")` sets it to Firebase Auth UID during insert and upsert.
id: String! @default(expr: "auth.uid")
username: String! @col(dataType: "varchar(50)")
# The `user: User!` field in the Review table generates the following one-to-many query field.
# reviews_on_user: [Review!]!
# The `Review` join table the following many-to-many query field.
# movies_via_Review: [Movie!]!
}
# Movie is keyed by a randomly generated UUID.
type Movie @table {
# If you do not pass a 'key' to `@table`, Data Connect automatically adds the following 'id' column.
# Feel free to uncomment and customize it.
# id: UUID! @default(expr: "uuidV4()")
title: String!
imageUrl: String!
genre: String
}
# MovieMetadata is a metadata attached to a Movie.
# Movie <-> MovieMetadata is a one-to-one relationship
type MovieMetadata @table {
# @unique ensures each Movie can only one MovieMetadata.
movie: Movie! @unique
# The movie field adds the following foreign key field. Feel free to uncomment and customize it.
# movieId: UUID!
rating: Float
releaseYear: Int
description: String
}
# Reviews is a join table between User and Movie.
# It has a composite primary keys `userUid` and `movieId`.
# A user can leave reviews for many movies. A movie can have reviews from many users.
# User <-> Review is a one-to-many relationship
# Movie <-> Review is a one-to-many relationship
# Movie <-> User is a many-to-many relationship
type Review @table(name: "Reviews", key: ["movie", "user"]) {
user: User!
# The user field adds the following foreign key field. Feel free to uncomment and customize it.
# userUid: String!
movie: Movie!
# The movie field adds the following foreign key field. Feel free to uncomment and customize it.
# movieId: UUID!
rating: Int
reviewText: String
reviewDate: Date! @default(expr: "request.time")
}

279
dataconnect/seed_data.gql Normal file
View File

@@ -0,0 +1,279 @@
mutation @transaction {
movie_insertMany(
data: [
{
id: "550e8400-e29b-41d4-a716-446655440000",
title: "Quantum Paradox",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fquantum_paradox.jpeg?alt=media&token=4142e2a1-bf43-43b5-b7cf-6616be3fd4e3",
genre: "sci-fi"
},
{
id: "550e8400-e29b-41d4-a716-446655440001",
title: "The Lone Outlaw",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Flone_outlaw.jpeg?alt=media&token=15525ffc-208f-4b59-b506-ae8348e06e85",
genre: "western"
},
{
id: "550e8400-e29b-41d4-a716-446655440002",
title: "Celestial Harmony",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fcelestial_harmony.jpeg?alt=media&token=3edf1cf9-c2f5-4c75-9819-36ff6a734c9a",
genre: "romance"
},
{
id: "550e8400-e29b-41d4-a716-446655440003",
title: "Noir Mystique",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fnoir_mystique.jpeg?alt=media&token=3299adba-cb98-4302-8b23-aeb679a4f913",
genre: "mystery"
},
{
id: "550e8400-e29b-41d4-a716-446655440004",
title: "The Forgotten Island",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fforgotten_island.jpeg?alt=media&token=bc2b16e1-caed-4649-952c-73b6113f205c",
genre: "adventure"
},
{
id: "550e8400-e29b-41d4-a716-446655440005",
title: "Digital Nightmare",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fdigital_nightmare.jpeg?alt=media&token=335ec842-1ca4-4b09-abd1-e96d9f5c0c2f",
genre: "horror"
},
{
id: "550e8400-e29b-41d4-a716-446655440006",
title: "Eclipse of Destiny",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Feclipse_destiny.jpeg?alt=media&token=346649b3-cb5c-4d7e-b0d4-6f02e3df5959",
genre: "fantasy"
},
{
id: "550e8400-e29b-41d4-a716-446655440007",
title: "Heart of Steel",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fheart_steel.jpeg?alt=media&token=17883d71-329b-415a-86f8-dd4d9e941d7f",
genre: "sci-fi"
},
{
id: "550e8400-e29b-41d4-a716-446655440008",
title: "Rise of the Crimson Empire",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Frise_crimson_empire.jpeg?alt=media&token=6faa73ad-7504-4146-8f3a-50b90f607f33",
genre: "action"
},
{
id: "550e8400-e29b-41d4-a716-446655440009",
title: "Silent Waves",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fsilent_waves.jpeg?alt=media&token=bd626bf1-ec60-4e57-aa07-87ba14e35bb7",
genre: "drama"
},
{
id: "550e8400-e29b-41d4-a716-446655440010",
title: "Echoes of the Past",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fecho_of_past.jpeg?alt=media&token=d866aa27-8534-4d72-8988-9da4a1b9e452",
genre: "historical"
},
{
id: "550e8400-e29b-41d4-a716-446655440011",
title: "Beyond the Horizon",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fbeyond_horizon.jpeg?alt=media&token=31493973-0692-4e6e-8b88-afb1aaea17ee",
genre: "sci-fi"
},
{
id: "550e8400-e29b-41d4-a716-446655440012",
title: "Shadows and Lies",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fshadows_lies.jpeg?alt=media&token=01afb80d-caee-47f8-a00e-aea8b9e459a2",
genre: "crime"
},
{
id: "550e8400-e29b-41d4-a716-446655440013",
title: "The Last Symphony",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Flast_symphony.jpeg?alt=media&token=f9bf80cd-3d8e-4e24-8503-7feb11f4e397",
genre: "drama"
},
{
id: "550e8400-e29b-41d4-a716-446655440014",
title: "Moonlit Crusade",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fmoonlit_crusade.jpeg?alt=media&token=b13241f5-d7d0-4370-b651-07847ad99dc2",
genre: "fantasy"
},
{
id: "550e8400-e29b-41d4-a716-446655440015",
title: "Abyss of the Deep",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fabyss_deep.jpeg?alt=media&token=2417321d-2451-4ec0-9ed6-6297042170e6",
genre: "horror"
},
{
id: "550e8400-e29b-41d4-a716-446655440017",
title: "The Infinite Knot",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Finfinite_knot.jpeg?alt=media&token=93d54d93-d933-4663-a6fe-26b707ef823e",
genre: "romance"
},
{
id: "550e8400-e29b-41d4-a716-446655440019",
title: "Veil of Illusion",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fveil_illusion.jpeg?alt=media&token=7bf09a3c-c531-478a-9d02-5d99fca9393b",
genre: "mystery"
}
]
)
movieMetadata_insertMany(
data: [
{
movieId: "550e8400-e29b-41d4-a716-446655440000",
rating: 7.9,
releaseYear: 2025,
description: "A group of scientists accidentally open a portal to a parallel universe, causing a rift in time. As the team races to close the portal, they encounter alternate versions of themselves, leading to shocking revelations."
},
{
movieId: "550e8400-e29b-41d4-a716-446655440001",
rating: 8.2,
releaseYear: 2023,
description: "In the lawless Wild West, a mysterious gunslinger with a hidden past takes on a corrupt sheriff and his band of outlaws to bring justice to a small town."
},
{
movieId: "550e8400-e29b-41d4-a716-446655440002",
rating: 7.5,
releaseYear: 2024,
description: "Two astronauts, stationed on a remote space station, fall in love amidst the isolation of deep space. But when a mysterious signal disrupts their communication, they must find a way to reconnect and survive."
},
{
movieId: "550e8400-e29b-41d4-a716-446655440003",
rating: 8.0,
releaseYear: 2022,
description: "A private detective gets caught up in a web of lies, deception, and betrayal while investigating the disappearance of a famous actress in 1940s Hollywood."
},
{
movieId: "550e8400-e29b-41d4-a716-446655440004",
rating: 7.6,
releaseYear: 2025,
description: "An explorer leads an expedition to a remote island rumored to be home to mythical creatures. As the team ventures deeper into the island, they uncover secrets that change the course of history."
},
{
movieId: "550e8400-e29b-41d4-a716-446655440005",
rating: 6.9,
releaseYear: 2024,
description: "A tech-savvy teenager discovers a cursed app that brings nightmares to life. As the horrors of the digital world cross into reality, she must find a way to break the curse before it's too late."
},
{
movieId: "550e8400-e29b-41d4-a716-446655440006",
rating: 8.1,
releaseYear: 2026,
description: "In a kingdom on the brink of war, a prophecy speaks of an eclipse that will grant power to the rightful ruler. As factions vie for control, a young warrior must decide where his true loyalty lies."
},
{
movieId: "550e8400-e29b-41d4-a716-446655440007",
rating: 7.7,
releaseYear: 2023,
description: "A brilliant scientist creates a robot with a human heart. As the robot struggles to understand emotions, it becomes entangled in a plot that could change the fate of humanity."
},
{
movieId: "550e8400-e29b-41d4-a716-446655440008",
rating: 8.4,
releaseYear: 2025,
description: "A legendary warrior rises to challenge the tyrannical rule of a powerful empire. As rebellion brews, the warrior must unite different factions to lead an uprising."
},
{
movieId: "550e8400-e29b-41d4-a716-446655440009",
rating: 8.2,
releaseYear: 2024,
description: "A talented pianist, who loses his hearing in a tragic accident, must rediscover his passion for music with the help of a young music teacher who believes in him."
},
{
movieId: "550e8400-e29b-41d4-a716-446655440010",
rating: 7.8,
releaseYear: 2023,
description: "A historian stumbles upon an ancient artifact that reveals hidden truths about an empire long forgotten. As she deciphers the clues, a shadowy organization tries to stop her from unearthing the past."
},
{
movieId: "550e8400-e29b-41d4-a716-446655440011",
rating: 8.5,
releaseYear: 2026,
description: "In the future, Earth's best pilots are sent on a mission to explore a mysterious planet beyond the solar system. What they find changes humanity's understanding of the universe forever."
},
{
movieId: "550e8400-e29b-41d4-a716-446655440012",
rating: 7.9,
releaseYear: 2022,
description: "A young detective with a dark past investigates a series of mysterious murders in a city plagued by corruption. As she digs deeper, she realizes nothing is as it seems."
},
{
movieId: "550e8400-e29b-41d4-a716-446655440013",
rating: 8.0,
releaseYear: 2024,
description: "An aging composer struggling with memory loss attempts to complete his final symphony. With the help of a young prodigy, he embarks on an emotional journey through his memories and legacy."
},
{
movieId: "550e8400-e29b-41d4-a716-446655440014",
rating: 8.3,
releaseYear: 2025,
description: "A knight is chosen by an ancient order to embark on a quest under the light of the full moon. Facing mythical beasts and treacherous landscapes, he seeks a relic that could save his kingdom."
},
{
movieId: "550e8400-e29b-41d4-a716-446655440015",
rating: 7.2,
releaseYear: 2023,
description: "When a group of marine biologists descends into the unexplored depths of the ocean, they encounter a terrifying and ancient force. Now, they must survive as the abyss comes alive."
},
{
movieId: "550e8400-e29b-41d4-a716-446655440017",
rating: 7.4,
releaseYear: 2026,
description: "Two souls destined to meet across multiple lifetimes struggle to find each other in a chaotic world. With each incarnation, they get closer, but time itself becomes their greatest obstacle."
},
{
movieId: "550e8400-e29b-41d4-a716-446655440019",
rating: 7.8,
releaseYear: 2022,
description: "A magician-turned-detective uses his skills in illusion to solve crimes. When a series of murders leaves the city in fear, he must reveal the truth hidden behind a veil of deceit."
}
]
)
user_insertMany(
data: [
{ id: "SnLgOC3lN4hcIl69s53cW0Q8R1T2", username: "sherlock_h" },
{ id: "fep4fXpGWsaRpuphq9CIrBIXQ0S2", username: "hercule_p" },
{ id: "TBedjwCX0Jf955Uuoxk6k74sY0l1", username: "jane_d" }
]
)
review_insertMany(
data: [
{
userId: "SnLgOC3lN4hcIl69s53cW0Q8R1T2",
movieId: "550e8400-e29b-41d4-a716-446655440000",
rating: 5,
reviewText: "An incredible movie with a mind-blowing plot!",
reviewDate: "2025-10-01"
},
{
userId: "fep4fXpGWsaRpuphq9CIrBIXQ0S2",
movieId: "550e8400-e29b-41d4-a716-446655440001",
rating: 5,
reviewText: "A revolutionary film that changed cinema forever.",
reviewDate: "2025-10-01"
},
{
userId: "TBedjwCX0Jf955Uuoxk6k74sY0l1",
movieId: "550e8400-e29b-41d4-a716-446655440002",
rating: 5,
reviewText: "A visually stunning and emotionally impactful movie.",
reviewDate: "2025-10-01"
},
{
userId: "SnLgOC3lN4hcIl69s53cW0Q8R1T2",
movieId: "550e8400-e29b-41d4-a716-446655440003",
rating: 4,
reviewText: "A fantastic superhero film with great performances.",
reviewDate: "2025-10-01"
},
{
userId: "fep4fXpGWsaRpuphq9CIrBIXQ0S2",
movieId: "550e8400-e29b-41d4-a716-446655440004",
rating: 5,
reviewText: "An amazing film that keeps you on the edge of your seat.",
reviewDate: "2025-10-01"
},
{
userId: "TBedjwCX0Jf955Uuoxk6k74sY0l1",
movieId: "550e8400-e29b-41d4-a716-446655440005",
rating: 5,
reviewText: "An absolute classic with unforgettable dialogue.",
reviewDate: "2025-10-01"
}
]
)
}

200
docs/issues/template.md Normal file
View File

@@ -0,0 +1,200 @@
# [Auth] Implement Firebase Authentication via krowSDK Facade
Labels: feature, infra, platform:web, platform:backend, priority:high, sred-eligible
Milestone: Foundation & Dev Environment Setup
### 🎯 Objective
Replace the Base44 authentication client with a new internal SDK module, `krowSDK.auth`, backed by Firebase Authentication. This foundational task will unblock all future backend development and align the web application with the new GCP-based architecture.
### 🔬 SR&ED Justification
- **Technological Uncertainty:** What is the optimal way to create a seamless abstraction layer (`krowSDK`) that perfectly mimics the existing `base44` SDK's interface to minimize frontend refactoring, while integrating a completely different authentication provider (Firebase Auth)? A key uncertainty is how this facade will handle the significant differences in user session management and data retrieval (e.g., custom user fields like `user_role` which are not native to the Firebase Auth user object) between the two systems.
- **Systematic Investigation:** We will conduct experimental development to build a `krowSDK.auth` module. This involves systematically mapping each `base44.auth` method (`me`, `logout`, `isAuthenticated`) to its Firebase Auth equivalent. We will investigate and prototype a solution for fetching supplementary user data (like `user_role`) from our Firestore database and merging it with the core Firebase Auth user object. This will establish a clean, reusable, and scalable architectural pattern for all future SDK modules.
### Details
This task is the most critical prerequisite for migrating our backend. As defined in the `03-backend-api-specification.md`, every request to our new Data Connect and Cloud Functions API will require a `Bearer <Firebase-Auth-Token>`. Without this, no backend work can proceed.
#### The Strategy: A Facade SDK (`krowSDK`)
Instead of replacing `base44` calls with Firebase calls directly throughout the codebase, we will create an abstraction layer, or "Facade". This approach has three major benefits:
1. **Simplified Migration:** The new `krowSDK.auth` will expose the *exact same methods* as the old `base44.auth`. This means we can swap out the authentication logic with minimal changes to the UI components, drastically reducing the scope of refactoring.
2. **High Maintainability:** All authentication logic will be centralized in one place. If we ever need to change providers again, we only modify the SDK, not the entire application.
3. **Clear Separation of Concerns:** The UI components remain agnostic about the authentication provider. They just need to call `krowSDK.auth.me()`, not worry about the underlying implementation details.
#### Implementation Plan
The developer should create two new files:
**1. Firebase Configuration (`frontend-web/src/firebase/config.js`)**
This file will initialize the Firebase app and export the necessary services. It's crucial to use environment variables for the configuration keys to keep them secure and environment-specific.
```javascript
// frontend-web/src/firebase/config.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
// Your web app's Firebase configuration
// IMPORTANT: Use environment variables for these values
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
// Export Firebase services
export const auth = getAuth(app);
export const db = getFirestore(app);
export default app;
```
**2. Krow SDK (`frontend-web/src/lib/krowSDK.js`)**
This is the core of the task. This file will implement the facade, importing the Firebase `auth` service and recreating the `base44.auth` interface.
```javascript
// frontend-web/src/lib/krowSDK.js
import { auth, db } from '../firebase/config';
import {
onAuthStateChanged,
signOut,
updateProfile,
// Import other necessary auth functions like signInWithEmailAndPassword, createUserWithEmailAndPassword, etc.
} from "firebase/auth";
import { doc, getDoc } from "firebase/firestore";
/**
* A promise-based wrapper for onAuthStateChanged to check the current auth state.
* @returns {Promise<boolean>} - A promise that resolves to true if authenticated, false otherwise.
*/
const isAuthenticated = () => {
return new Promise((resolve) => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
unsubscribe();
resolve(!!user);
});
});
};
/**
* Fetches the current authenticated user's profile.
* This function mimics `base44.auth.me()` by combining the Firebase Auth user
* with custom data from our Firestore database (e.g., user_role).
* @returns {Promise<object|null>} - A promise that resolves with the user object or null.
*/
const me = () => {
return new Promise((resolve, reject) => {
const unsubscribe = onAuthStateChanged(auth, async (user) => {
unsubscribe();
if (user) {
try {
// 1. Get the core user from Firebase Auth
const { uid, email, displayName } = user;
const baseProfile = {
id: uid,
email: email,
full_name: displayName,
};
// 2. Get custom fields from Firestore
// We assume a 'users' collection where the document ID is the user's UID.
const userDocRef = doc(db, "users", uid);
const userDoc = await getDoc(userDocRef);
if (userDoc.exists()) {
const customData = userDoc.data();
// 3. Merge the two data sources
resolve({
...baseProfile,
user_role: customData.user_role, // Example custom field
// ... other custom fields from Firestore
});
} else {
// User exists in Auth but not in Firestore. This can happen during sign-up.
// Resolve with base profile; the app should handle creating the Firestore doc.
resolve(baseProfile);
}
} catch (error) {
console.error("Error fetching user profile from Firestore:", error);
reject(error);
}
} else {
// No user is signed in.
resolve(null);
}
});
});
};
/**
* Updates the current user's profile.
* This mimics `base44.auth.updateMe()`.
* @param {object} profileData - Data to update, e.g., { full_name: "New Name" }.
*/
const updateMe = async (profileData) => {
if (auth.currentUser) {
// Firebase Auth's updateProfile only supports displayName and photoURL.
if (profileData.full_name) {
await updateProfile(auth.currentUser, {
displayName: profileData.full_name,
});
}
// For other custom fields, you would need to update the user's document in Firestore.
// const userDocRef = doc(db, "users", auth.currentUser.uid);
// await updateDoc(userDocRef, { custom_field: profileData.custom_field });
} else {
throw new Error("No authenticated user to update.");
}
};
/**
* Logs the user out and redirects them.
* @param {string} [redirectUrl='/'] - The URL to redirect to after logout.
*/
const logout = (redirectUrl = '/') => {
signOut(auth).then(() => {
// Redirect after sign-out.
window.location.href = redirectUrl;
}).catch((error) => {
console.error("Logout failed:", error);
});
};
// The krowSDK object that mimics the Base44 SDK structure
export const krowSDK = {
auth: {
isAuthenticated,
me,
updateMe,
logout,
// Note: redirectToLogin is not implemented as it's a concern of the routing library (e.g., React Router),
// which should protect routes and redirect based on the authentication state.
},
// Future modules will be added here, e.g., krowSDK.entities.Event
};
```
### ✅ Acceptance Criteria
- [ ] A `frontend-web/src/firebase/config.js` file is created, correctly initializing the Firebase app using environment variables.
- [ ] A `frontend-web/src/lib/krowSDK.js` file is created and implements the `krowSDK.auth` facade.
- [ ] The `krowSDK.auth` module exports `isAuthenticated`, `me`, `updateMe`, and `logout` functions with interfaces identical to their `base44.auth` counterparts.
- [ ] Key parts of the application (e.g., login pages, user profile components, auth checks) are refactored to import and use `krowSDK.auth` instead of `base44.auth`.
- [ ] A new user can sign up using a Firebase-powered form.
- [ ] An existing user can log in using a Firebase-powered form.
- [ ] The application UI correctly updates to reflect the user's authenticated state (e.g., showing user name, hiding login button).
- [ ] After logging in, the user's Firebase Auth ID Token can be retrieved and is ready to be sent in an `Authorization: Bearer` header for API calls.

View File

@@ -0,0 +1,80 @@
## What the prompt does
- Generate a full architecture diagram for a Flutter project based on the given overview and use case diagram
- The architecture diagram should include the frontend, backend, and database layers
- The architecture diagram should be generated using mermaid syntax
## Assumption
- Flutter project is given
- Overview mermaid diagram is given
- Use case diagram is given
- Backend architecture mermaid diagram is given
- The backend architecture diagram should be generated based on the given overview and use case diagrams
## How to use the prompt
For the given Flutter project, I want to generate a complete architecture document. Use the codebase together with the following Mermaid files:
* `overview.mermaid`
* `api_map.mermaid`
* `backend_architecture.mermaid`
* `use_case_flows.mermaid`
* `use-case-flowchart.mermaid` (duplicate file if needed for additional reference)
Your tasks:
1. Analyze the **Flutter project structure**, the relevant backend integrations, and all provided Mermaid diagrams.
2. Create a **comprehensive Markdown architecture document** (`architecture.md`) that includes:
### A. Introduction
* High-level summary of the project.
* Brief description of the core purpose of the app.
### B. Full Architecture Overview
* Explanation of app architecture used (e.g., layered architecture, MVVM, Clean Architecture, etc.).
* Description of key modules, layers, and responsibilities.
* Integration points between UI, domain, and data layers.
### C. Backend Architecture
* Based on the backend diagrams, describe:
* How GraphQL is used.
* How Firebase services (Auth, Firestore, Storage, Functions, etc.) are integrated.
* How the app communicates with the backend end-to-end.
* API flow between Flutter → GraphQL → Firebase.
### D. API Layer
* Summaries of GraphQL queries, mutations, and subscriptions.
* Explanation of how the app handles API errors, retries, caching, and parsing.
* Any backend-dependent logic highlighted in diagrams.
### E. State Management
* Identify the state management approach used (Bloc, Riverpod, Provider, Cubit, ValueNotifier, etc.).
* Explain:
* Why this method was chosen.
* How state flows between UI, logic, and backend.
* How the state management integrates with the API layer.
### F. Use-Case Flows
* Explain each major use case using the `use_case_flows.mermaid` and `use-case-flowchart.mermaid` diagrams.
* Describe the UI → Logic → Backend → Response cycle for each use case.
### G. Backend Replacement Section
Add a dedicated section titled **“Replacing or Plugging in a New Backend: Considerations & Recommendations”**, including:
* What parts of the codebase are tightly coupled to the current backend.
* What should be abstracted (e.g., repositories, services, DTOs, error handling).
* How to structure interfaces to allow backend swapping.
* Suggested design improvements to make the architecture more backend-agnostic.
* Migration strategies for replacing GraphQL + Firebase with another backend (REST, Supabase, Hasura, etc.).
3. Make the Markdown document clear, well-structured, and easy for developers to use as a long-term reference.
4. Output the final result as a **single `architecture.md` file**.

View File

@@ -0,0 +1,53 @@
## What the prompt does
This prompt generates a Mermaid diagram that visualizes the backend architecture of a Flutter project. It uses the given overview and use case diagrams to create a detailed diagram that shows the relationships between different components and services.
## Assumption
- Flutter project is given
- Overview mermaid diagram is given
- Use case diagram is given
## How to use the prompt
For the given Flutter project, the backend uses **GraphQL** and **Firebase**. I want multiple detailed Mermaid diagrams to understand how everything is connected.
Please do the following:
1. **Read and analyze** the entire project, along with these two files:
* `overview.mermaid`
* `use-case-flowchart.mermaid`
2. Based on all available information, generate **three separate Mermaid diagrams**:
### A. Backend Architecture Diagram
* Show the high-level structure of the backend.
* Include GraphQL server components, Firebase services (Auth, Firestore, Storage, Functions, etc.), and how the Flutter app connects to them.
* Show data flow between Flutter → GraphQL → Firebase → back to the client.
### B. API Map (GraphQL Operations + Firebase Interactions)
* List and group all GraphQL queries, mutations, and subscriptions.
* Show which ones interact with Firebase and how.
* If Firestore collections or documents are involved, show them as nodes.
* Clearly illustrate the relationship between API operations and backend resources.
### C. Use-Case Flow Diagrams
* For each major use case in the project:
* Show how the request moves from the Flutter UI to the backend.
* Show the sequence of steps involving GraphQL operations and Firebase services.
* Show how responses return back to the UI.
* Organize all use cases into **one combined Mermaid diagram** or **multiple subgraph clusters**.
3. Ensure all diagrams are:
* Clean, readable, and logically grouped
* Consistent with the structure of the existing project and the two Mermaid reference files
* Detailed enough for developers to understand backend behavior at a glance
4. Output the three diagrams clearly labeled as:
* **Backend Architecture**
* **API Map**
* **Use-Case Flows**

View File

@@ -20,10 +20,3 @@ Format the output as a **clear Mermaid diagram** using `flowchart TD` or `flowch
Ensure the flow represents the **real behavior of the code**, not assumptions. Ensure the flow represents the **real behavior of the code**, not assumptions.
Ask for any missing files if needed to build a complete and accurate use-case hierarchy. Ask for any missing files if needed to build a complete and accurate use-case hierarchy.
---
If you want, I can also generate a version tailored for:
✔ UML use-case diagrams
✔ Sequence diagrams
✔ System interaction diagrams

View File

@@ -45,5 +45,13 @@
} }
] ]
} }
] ],
"emulators": {
"dataconnect": {
"dataDir": "dataconnect/.dataconnect/pgliteData"
}
},
"dataconnect": {
"source": "dataconnect"
}
} }

View File

@@ -0,0 +1,18 @@
# This file specifies files that are *not* uploaded to Google Cloud
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore
# Node.js dependencies:
node_modules/
*.log

View File

@@ -0,0 +1,28 @@
# Utiliser nginx pour servir les fichiers statiques
FROM nginx:alpine
# Copier les fichiers statiques
COPY index.html /usr/share/nginx/html/
COPY assets /usr/share/nginx/html/assets/
COPY favicon.svg /usr/share/nginx/html/
COPY logo.svg /usr/share/nginx/html/
# Configuration nginx pour le routing SPA
RUN echo 'server { \
listen 8080; \
server_name _; \
root /usr/share/nginx/html; \
index index.html; \
location / { \
try_files $uri $uri/ /index.html; \
} \
# Headers de sécurité \
add_header X-Frame-Options "SAMEORIGIN" always; \
add_header X-Content-Type-Options "nosniff" always; \
add_header X-XSS-Protection "1; mode=block" always; \
}' > /etc/nginx/conf.d/default.conf
# Nginx écoute sur le port 8080 (requis par Cloud Run)
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -58,5 +58,53 @@
"title": "Legacy App Use Cases", "title": "Legacy App Use Cases",
"type": "mermaid", "type": "mermaid",
"icon": "bi-diagram-2" "icon": "bi-diagram-2"
},
{
"path": "assets/diagrams/legacy/staff-mobile-application/api-map.mermaid",
"title": "Legacy App API Map",
"type": "mermaid",
"icon": "bi-phone"
},
{
"path": "assets/diagrams/legacy/staff-mobile-application/use-case-flows.mermaid",
"title": "Legacy App Use Cases BE Chart",
"type": "mermaid",
"icon": "bi-diagram-2"
},
{
"path": "assets/diagrams/legacy/staff-mobile-application/backend-architecture.mermaid",
"title": "Legacy App Backend Architecture",
"type": "mermaid",
"icon": "bi-diagram-2"
},
{
"path": "assets/diagrams/legacy/client-mobile-application/overview.mermaid",
"title": "Legacy App Overview",
"type": "mermaid",
"icon": "bi-phone"
},
{
"path": "assets/diagrams/legacy/client-mobile-application/use-case-flowchart.mermaid",
"title": "Legacy App Use Cases",
"type": "mermaid",
"icon": "bi-diagram-2"
},
{
"path": "assets/diagrams/legacy/client-mobile-application/api-map.mermaid",
"title": "Legacy App API Map",
"type": "mermaid",
"icon": "bi-phone"
},
{
"path": "assets/diagrams/legacy/client-mobile-application/use-case-flows.mermaid",
"title": "Legacy App Use Cases BE Chart",
"type": "mermaid",
"icon": "bi-diagram-2"
},
{
"path": "assets/diagrams/legacy/client-mobile-application/backend-architecture.mermaid",
"title": "Legacy App Backend Architecture",
"type": "mermaid",
"icon": "bi-diagram-2"
} }
] ]

View File

@@ -0,0 +1,55 @@
flowchart TD
subgraph "GraphQL API"
direction LR
subgraph "Queries"
Q1[getEvents]
Q2[getEventDetails]
Q3[getInvoices]
Q4[getInvoiceDetails]
Q5[getNotifications]
Q6[getNotificationDetails]
Q7[getProfile]
Q8[getAssignedStaff]
end
subgraph "Mutations"
M1[createEvent]
M2[updateProfile]
M3[rateStaff]
M4[clockIn]
M5[clockOut]
M6[uploadProfilePicture]
end
end
subgraph "Firebase"
direction LR
subgraph "Firestore Collections"
FS1[events]
FS2[invoices]
FS3[notifications]
FS4[users]
end
subgraph "Firebase Storage"
FB1[Profile Pictures]
end
end
Q1 --> FS1
Q2 --> FS1
Q3 --> FS2
Q4 --> FS2
Q5 --> FS3
Q6 --> FS3
Q7 --> FS4
Q8 --> FS1
Q8 --> FS4
M1 --> FS1
M2 --> FS4
M3 --> FS1
M3 --> FS4
M4 --> FS1
M5 --> FS1
M6 --> FB1

View File

@@ -0,0 +1,28 @@
flowchart TD
subgraph "Client"
A[Flutter App]
end
subgraph "Backend"
B[GraphQL Server - Node.js]
C[Firebase]
end
subgraph "Firebase Services"
C1[Firebase Auth]
C2[Firebase Firestore]
C3[Firebase Storage]
end
A -- "GraphQL Queries/Mutations" --> B
A -- "Authentication" --> C1
B -- "Data Operations" --> C2
B -- "File Operations" --> C3
C1 -- "User Tokens" --> A
C2 -- "Data" --> B
C3 -- "Files" --> B
B -- "Data/Files" --> A

View File

@@ -0,0 +1,65 @@
flowchart TD
subgraph "App Initialization"
A[main.dart] --> B(KrowApp);
B --> C{MaterialApp.router};
C --> D[Initial Route: /];
end
subgraph "Authentication Flow"
D --> E(SplashRoute);
E --> F{SplashRedirectGuard};
F -- Authenticated --> G(HomeRoute);
F -- Unauthenticated/Error --> H(SignInFlowRoute);
end
subgraph "Sign-In/Welcome Flow"
H --> I[WelcomeRoute];
I --> J[SignInRoute];
J --> K[ResetPassRoute];
L[Deeplink with oobCode] --> M[EnterNewPassRoute];
J -- Forgot Password --> K;
I -- Sign In --> J;
end
subgraph "Main Application (Home)"
G --> G_Tabs((Bottom Navigation));
G_Tabs -- Events --> Events;
G_Tabs -- Invoices --> Invoices;
G_Tabs -- Notifications --> Notifications;
G_Tabs -- Profile --> Profile;
G_Tabs -- Create Event --> CreateEvent;
end
subgraph "Events Flow"
Events[EventsFlow] --> Events_List(EventsListMainRoute);
Events_List --> Events_Event_Details(EventDetailsRoute);
Events_Event_Details --> Events_Assigned_Staff(AssignedStaffRoute);
Events_Assigned_Staff --> Events_Clock_Manual(ClockManualRoute);
Events_Event_Details --> Events_Rate_Staff(RateStaffRoute);
end
subgraph "Create Event Flow"
CreateEvent[CreateEventFlow] --> Create_Event_Edit(CreateEventRoute);
Create_Event_Edit --> Create_Event_Preview(EventDetailsRoute);
end
subgraph "Invoice Flow"
Invoices[InvoiceFlow] --> Invoice_List(InvoicesListMainRoute);
Invoice_List --> Invoice_Details(InvoiceDetailsRoute);
Invoice_Details --> Invoice_Event_Details(EventDetailsRoute);
end
subgraph "Notifications Flow"
Notifications[NotificationFlow] --> Notification_List(NotificationsListRoute);
Notification_List --> Notification_Details(NotificationDetailsRoute);
end
subgraph "Profile Flow"
Profile[ProfileFlow] --> Profile_Preview(ProfilePreviewRoute);
Profile_Preview --> Profile_Edit(PersonalInfoRoute);
end
style F fill:#f9f,stroke:#333,stroke-width:2px
style G fill:#bbf,stroke:#333,stroke-width:2px
style H fill:#bbf,stroke:#333,stroke-width:2px
style L fill:#f9f,stroke:#333,stroke-width:2px

View File

@@ -0,0 +1,80 @@
flowchart TD
subgraph "User"
U((User))
end
subgraph "Authentication Use Cases"
UC1(Sign In)
UC2(Sign Out)
UC3(Password Reset)
end
subgraph "Event Management Use Cases"
UC4(Create Event)
UC5(View Event Details)
UC6(List Events)
end
subgraph "Invoice Management Use Cases"
UC7(List Invoices)
UC8(View Invoice Details)
end
subgraph "Staff Management Use Cases"
UC9(View Assigned Staff)
UC10(Rate Staff)
UC11(Manual Clock In/Out)
end
subgraph "Profile Management Use Cases"
UC12(View Profile)
UC13(Edit Profile)
end
subgraph "Notification Use Cases"
UC14(List Notifications)
UC15(View Notification Details)
end
U --> UC1
UC1 -- Success --> UC6
UC1 -- Forgot Password --> UC3
UC6 --> UC5
UC5 --> UC9
UC9 --> UC11
UC5 --> UC10
U --> UC4
UC4 -- Success --> UC5
UC6 -- Triggers --> UC7
UC7 --> UC8
UC8 -- View Event --> UC5
U --> UC12
UC12 --> UC13
UC13 -- Success --> UC12
U --> UC14
UC14 --> UC15
UC12 -- Sign Out --> UC2
UC2 -- Success --> UC1
%% Styling
style UC1 fill:#f9f,stroke:#333,stroke-width:2px
style UC2 fill:#f9f,stroke:#333,stroke-width:2px
style UC3 fill:#f9f,stroke:#333,stroke-width:2px
style UC4 fill:#bbf,stroke:#333,stroke-width:2px
style UC5 fill:#bbf,stroke:#333,stroke-width:2px
style UC6 fill:#bbf,stroke:#333,stroke-width:2px
style UC7 fill:#bbf,stroke:#333,stroke-width:2px
style UC8 fill:#bbf,stroke:#333,stroke-width:2px
style UC9 fill:#bbf,stroke:#333,stroke-width:2px
style UC10 fill:#bbf,stroke:#333,stroke-width:2px
style UC11 fill:#bbf,stroke:#333,stroke-width:2px
style UC12 fill:#bbf,stroke:#333,stroke-width:2px
style UC13 fill:#bbf,stroke:#333,stroke-width:2px
style UC14 fill:#bbf,stroke:#333,stroke-width:2px
style UC15 fill:#bbf,stroke:#333,stroke-width:2px

View File

@@ -0,0 +1,45 @@
flowchart TD
subgraph "Sign-In Flow"
A1[User enters credentials] --> B1{SignInBloc};
B1 --> C1[Firebase Auth: signInWithEmailAndPassword];
C1 -- Success --> D1[Navigate to Home];
C1 -- Failure --> E1[Show error message];
end
subgraph "Password Reset Flow"
A2[User requests password reset] --> B2{SignInBloc};
B2 --> C2[Firebase Auth: sendPasswordResetEmail];
C2 -- Email Sent --> D2[User clicks deep link];
D2 --> E2[UI with new password fields];
E2 --> F2{SignInBloc};
F2 --> G2[Firebase Auth: confirmPasswordReset];
G2 -- Success --> H2[Show success message];
G2 -- Failure --> I2[Show error message];
end
subgraph "Event Listing Flow"
A3[User navigates to Events screen] --> B3{EventsBloc};
B3 --> C3[GraphQL Query: getEvents];
C3 --> D3[Firestore: events collection];
D3 -- Returns event data --> C3;
C3 -- Returns data --> B3;
B3 --> E3[Display list of events];
end
subgraph "Create Event Flow"
A4[User submits new event form] --> B4{CreateEventBloc};
B4 --> C4[GraphQL Mutation: createEvent];
C4 --> D4[Firestore: events collection];
D4 -- Success --> C4;
C4 -- Returns success --> B4;
B4 --> E4[Navigate to event details];
end
subgraph "Profile Viewing Flow"
A5[User navigates to Profile screen] --> B5{ProfileBloc};
B5 --> C5[GraphQL Query: getProfile];
C5 --> D5[Firestore: users collection];
D5 -- Returns profile data --> C5;
C5 -- Returns data --> B5;
B5 --> E5[Display profile information];
end

View File

@@ -0,0 +1,57 @@
graph TD
subgraph GraphQL API
subgraph Queries
Q1[getStaffStatus]
Q2[getMe]
Q3[getStaffPersonalInfo]
Q4[getStaffProfileRoles]
Q5[getShifts]
Q6[staffNoBreakShifts]
Q7[getShiftPosition]
end
subgraph Mutations
M1[updateStaffPersonalInfo]
M2[updateStaffPersonalInfoWithAvatar]
M3[uploadStaffAvatar]
M4[acceptShift]
M5[trackStaffClockin]
M6[trackStaffClockout]
M7[trackStaffBreak]
M8[submitNoBreakStaffShift]
M9[cancelStaffShift]
M10[declineShift]
end
end
subgraph Firebase Services
FS[Firebase Storage]
FF[Firebase Firestore]
FA[Firebase Auth]
end
M2 --> FS;
M3 --> FS;
Q1 --> FF;
Q2 --> FF;
Q3 --> FF;
Q4 --> FF;
Q5 --> FF;
Q6 --> FF;
Q7 --> FF;
M1 --> FF;
M2 --> FF;
M4 --> FF;
M5 --> FF;
M6 --> FF;
M7 --> FF;
M8 --> FF;
M9 --> FF;
M10 --> FF;
Q1 --> FA;
Q2 --> FA;
Q3 --> FA;

View File

@@ -0,0 +1,37 @@
graph TD
subgraph Flutter App
A[Flutter UI]
B[GraphQL Client]
C[Firebase SDK]
end
subgraph Backend
D[GraphQL Server]
E[Firebase]
end
subgraph Firebase
F[Firebase Auth]
G[Firebase Firestore]
H[Firebase Storage]
I[Firebase Cloud Functions]
J[Firebase Cloud Messaging]
K[Firebase Remote Config]
end
A --> B;
A --> C;
B --> D;
C --> F;
C --> J;
C --> K;
D --> G;
D --> H;
D --> I;
D --> F;
I --> G;
I --> H;
I --> J;

View File

@@ -0,0 +1,39 @@
graph TD
subgraph User Authentication
direction LR
UA1[Flutter App] -->|Phone Number| UA2[Firebase Auth];
UA2 -->|Verification Code| UA1;
UA1 -->|Verification Code| UA2;
UA2 -->|Auth Token| UA1;
UA1 -->|Auth Token| UA3[GraphQL Server];
UA3 -->|User Data| UA1;
end
subgraph User Onboarding
direction LR
UO1[Flutter App] -->|Personal Info| UO2[GraphQL Server];
UO2 -->|update_staff_personal_info| UO3[Firebase Firestore];
UO2 -->|User Data| UO1;
end
subgraph Shift Management
direction LR
SM1[Flutter App] -->|Get Shifts| SM2[GraphQL Server];
SM2 -->|getShifts| SM3[Firebase Firestore];
SM3 -->|Shift Data| SM2;
SM2 -->|Shift Data| SM1;
SM1 -->|Accept Shift| SM2;
SM2 -->|accept_shift| SM3;
SM3 -->|Updated Shift| SM2;
SM2 -->|Updated Shift| SM1;
end
subgraph Profile Update with Avatar
direction LR
PU1[Flutter App] -->|Image| PU2[Firebase Storage];
PU2 -->|Image URL| PU1;
PU1 -->|Image URL & Personal Info| PU3[GraphQL Server];
PU3 -->|update_staff_personal_info & upload_staff_avatar| PU4[Firebase Firestore];
PU3 -->|User Data| PU1;
end

View File

@@ -0,0 +1,10 @@
[
{
"title": "Architecture Document & Migration Plan",
"path": "./assets/documents/legacy/staff-mobile-application/architecture.md"
},
{
"title": "Architecture Document & Migration Plan",
"path": "./assets/documents/legacy/client-mobile-application/architecture.md"
}
]

View File

@@ -0,0 +1,252 @@
# Krow Mobile Client App Architecture Document
## A. Introduction
This document provides a comprehensive overview of the Krow mobile client application's architecture. The Krow app is a Flutter-based mobile application designed to connect staff with work opportunities. It includes features for event management, invoicing, staff rating, and profile management.
The core purpose of the app is to provide a seamless experience for staff to find, manage, and get paid for work, while allowing clients to manage their events and staff effectively.
## B. Full Architecture Overview
The Krow app is built using a layered architecture that separates concerns and promotes modularity. The main layers are the **Presentation Layer**, the **Domain Layer**, and the **Data Layer**, organized into feature-based modules.
### Key Modules and Layers
* **Features:** The `lib/features` directory contains the main features of the app, such as `sign_in`, `events`, `profile`, etc. Each feature directory is further divided into `presentation` and `domain` layers.
* **Presentation Layer:** This layer is responsible for the UI and user interaction. It contains the screens (widgets) and the BLoCs (Business Logic Components) that manage the state of the UI.
* **Domain Layer:** This layer contains the core business logic of the application. It includes the BLoCs, which are responsible for orchestrating the flow of data between the UI and the data layer, and the business objects (entities).
* **Data Layer:** This layer is responsible for all data-related operations. It includes the repositories that fetch data from the backend and the data sources themselves (e.g., GraphQL API, local cache).
* **Core:** The `lib/core` directory contains shared code that is used across multiple features, such as the API client, dependency injection setup, routing, and common widgets.
### Integration Points
* **UI to Domain:** The UI (widgets) dispatches events to the BLoCs in the domain layer based on user interactions.
* **Domain to Data:** The BLoCs in the domain layer call methods on the repositories in the data layer to fetch or update data.
* **Data to Backend:** The repositories in the data layer use the `ApiClient` to make GraphQL calls to the backend.
## C. Backend Architecture
The backend of the Krow app is a hybrid system that leverages both a **GraphQL server** and **Firebase services**.
```mermaid
flowchart TD
subgraph "Client"
A[Flutter App]
end
subgraph "Backend"
B[GraphQL Server (e.g., Node.js)]
C[Firebase]
end
subgraph "Firebase Services"
C1[Firebase Auth]
C2[Firebase Firestore]
C3[Firebase Storage]
end
A -- "GraphQL Queries/Mutations" --> B
A -- "Authentication" --> C1
B -- "Data Operations" --> C2
B -- "File Operations" --> C3
C1 -- "User Tokens" --> A
C2 -- "Data" --> B
C3 -- "Files" --> B
B -- "Data/Files" --> A
```
### GraphQL
The GraphQL server acts as an intermediary between the Flutter app and the Firebase services. It exposes a set of queries and mutations that the app can use to interact with the backend. This provides a single, unified API for the app to consume, simplifying data fetching and manipulation.
### Firebase Integration
* **Firebase Auth:** Firebase Auth is used for user authentication. The Flutter app interacts directly with Firebase Auth to handle user sign-in, sign-up, and password reset flows. Once authenticated, the app retrieves a Firebase ID token, which is then used to authenticate with the GraphQL server.
* **Firebase Firestore:** Firestore is the primary database for the application. The GraphQL server is responsible for all interactions with Firestore, including fetching, creating, updating, and deleting data.
* **Firebase Storage:** Firebase Storage is used for storing user-generated content, such as profile pictures. The GraphQL server handles file uploads and retrieves file URLs that are then sent to the app.
### End-to-End Communication Flow
1. The Flutter app authenticates the user with Firebase Auth.
2. The app receives a Firebase ID token.
3. For all subsequent API requests, the app sends the Firebase ID token in the authorization header of the GraphQL request.
4. The GraphQL server verifies the token and then executes the requested query or mutation.
5. The GraphQL server interacts with Firestore or Firebase Storage to fulfill the request.
6. The GraphQL server returns the requested data to the app.
## D. API Layer
The API layer is responsible for all communication with the backend. It is built around the `graphql_flutter` package and a custom `ApiClient`.
```mermaid
flowchart TD
subgraph "GraphQL API"
direction LR
subgraph "Queries"
Q1[getEvents]
Q2[getEventDetails]
Q3[getInvoices]
Q4[getInvoiceDetails]
Q5[getNotifications]
Q6[getNotificationDetails]
Q7[getProfile]
Q8[getAssignedStaff]
end
subgraph "Mutations"
M1[createEvent]
M2[updateProfile]
M3[rateStaff]
M4[clockIn]
M5[clockOut]
M6[uploadProfilePicture]
end
end
subgraph "Firebase"
direction LR
subgraph "Firestore Collections"
FS1[events]
FS2[invoices]
FS3[notifications]
FS4[users]
end
subgraph "Firebase Storage"
FB1[Profile Pictures]
end
end
Q1 --> FS1
Q2 --> FS1
Q3 --> FS2
Q4 --> FS2
Q5 --> FS3
Q6 --> FS3
Q7 --> FS4
Q8 --> FS1
Q8 --> FS4
M1 --> FS1
M2 --> FS4
M3 --> FS1
M3 --> FS4
M4 --> FS1
M5 --> FS1
M6 --> FB1
```
### API Handling
* **Error Handling:** The `ApiClient` uses the `ErrorPolicy.all` policy to catch all GraphQL errors. The BLoCs are responsible for catching these errors and updating the UI state accordingly.
* **Caching:** The `GraphQLCache` with `HiveStore` is used to cache GraphQL query results. The `fetchPolicy` is set to `cacheAndNetwork` to provide a fast user experience while keeping the data up-to-date.
* **Parsing:** The app uses the `json_serializable` package to parse the JSON responses from the GraphQL server into Dart objects.
## E. State Management
The Krow app uses the **BLoC (Business Logic Component)** pattern for state management, powered by the `flutter_bloc` package.
### Why BLoC?
* **Separation of Concerns:** BLoC separates the business logic from the UI, making the code more organized, testable, and maintainable.
* **Testability:** BLoCs are easy to test in isolation from the UI.
* **Reactivity:** BLoC uses streams to manage state, which makes it easy to update the UI in response to state changes.
### State Flow
1. The UI dispatches an event to the BLoC.
2. The BLoC receives the event and interacts with the data layer (repositories) to fetch or update data.
3. The data layer returns data or a success/failure status to the BLoC.
4. The BLoC updates its state based on the result from the data layer.
5. The UI rebuilds itself in response to the new state.
### Integration with the API Layer
The BLoCs do not interact directly with the `ApiClient`. Instead, they go through a repository layer, which abstracts the data source. This makes it possible to switch out the backend without having to change the BLoCs.
## F. Use-Case Flows
The following diagrams illustrate the flow for some of the major use cases in the app.
```mermaid
flowchart TD
subgraph "Sign-In Flow"
A1[User enters credentials] --> B1{SignInBloc};
B1 --> C1[Firebase Auth: signInWithEmailAndPassword];
C1 -- Success --> D1[Navigate to Home];
C1 -- Failure --> E1[Show error message];
end
subgraph "Password Reset Flow"
A2[User requests password reset] --> B2{SignInBloc};
B2 --> C2[Firebase Auth: sendPasswordResetEmail];
C2 -- Email Sent --> D2[User clicks deep link];
D2 --> E2[UI with new password fields];
E2 --> F2{SignInBloc};
F2 --> G2[Firebase Auth: confirmPasswordReset];
G2 -- Success --> H2[Show success message];
G2 -- Failure --> I2[Show error message];
end
subgraph "Event Listing Flow"
A3[User navigates to Events screen] --> B3{EventsBloc};
B3 --> C3[GraphQL Query: getEvents];
C3 --> D3[Firestore: events collection];
D3 -- Returns event data --> C3;
C3 -- Returns data --> B3;
B3 --> E3[Display list of events];
end
subgraph "Create Event Flow"
A4[User submits new event form] --> B4{CreateEventBloc};
B4 --> C4[GraphQL Mutation: createEvent];
C4 --> D4[Firestore: events collection];
D4 -- Success --> C4;
C4 -- Returns success --> B4;
B4 --> E4[Navigate to event details];
end
subgraph "Profile Viewing Flow"
A5[User navigates to Profile screen] --> B5{ProfileBloc};
B5 --> C5[GraphQL Query: getProfile];
C5 --> D5[Firestore: users collection];
D5 -- Returns profile data --> C5;
C5 -- Returns data --> B5;
B5 --> E5[Display profile information];
end
```
## G. Replacing or Plugging in a New Backend: Considerations & Recommendations
This section provides guidance on how to replace the current GraphQL + Firebase backend with a different backend solution.
### Tightly Coupled Components
* **`ApiClient`:** This class is tightly coupled to `graphql_flutter`.
* **Firebase Auth:** The authentication logic is directly tied to the `firebase_auth` package.
* **BLoCs:** Some BLoCs might have direct dependencies on Firebase or GraphQL-specific models.
### Abstraction Recommendations
To make the architecture more backend-agnostic, the following abstractions should be implemented:
* **Repositories:** Create an abstract `Repository` class for each feature in the `domain` layer. The implementation of this repository will be in the `data` layer. The BLoCs should only depend on the abstract repository.
* **Authentication Service:** Create an abstract `AuthService` class that defines the methods for authentication (e.g., `signIn`, `signOut`, `getToken`). The implementation of this service will be in the `data` layer and will use the specific authentication provider (e.g., Firebase Auth, OAuth).
* **Data Transfer Objects (DTOs):** Use DTOs to transfer data between the data layer and the domain layer. This will prevent the domain layer from having dependencies on backend-specific models.
### Suggested Design Improvements
* **Formalize Clean Architecture:** While the current architecture has elements of Clean Architecture, it could be more formally implemented by creating a clear separation between the `domain`, `data`, and `presentation` layers for all features.
* **Introduce Use Cases:** Introduce `UseCase` classes in the `domain` layer to encapsulate specific business operations. This will make the BLoCs simpler and more focused on state management.
### Migration Strategies
To replace the current backend with a new one (e.g., REST API, Supabase), follow these steps:
1. **Implement New Repositories:** Create new implementations of the repository interfaces for the new backend.
2. **Implement New Auth Service:** Create a new implementation of the `AuthService` interface for the new authentication provider.
3. **Update Dependency Injection:** Use dependency injection (e.g., `get_it` and `injectable`) to provide the new repository and auth service implementations to the BLoCs.
4. **Gradual Migration:** If possible, migrate one feature at a time to the new backend. This will reduce the risk of breaking the entire application at once.

View File

@@ -0,0 +1,120 @@
# Krow Mobile Staff App - Architecture Document
## A. Introduction
This document outlines the architecture of the Krow Mobile Staff App, a Flutter application designed to connect staff with job opportunities. The app provides features for staff to manage their profiles, view and apply for shifts, track earnings, and complete necessary paperwork.
The core purpose of the app is to streamline the process of finding and managing temporary work, providing a seamless experience for staff from onboarding to payment.
## B. Full Architecture Overview
The application follows a **Clean Architecture** pattern, separating concerns into three main layers: **Presentation**, **Domain**, and **Data**. This layered approach promotes a separation of concerns, making the codebase more maintainable, scalable, and testable.
- **Presentation Layer:** This layer is responsible for the UI and user interaction. It consists of widgets, screens, and Blocs that manage the UI state. The Presentation Layer depends on the Domain Layer to execute business logic.
- **Domain Layer:** This layer contains the core business logic of the application. It consists of use cases (interactors), entities (business objects), and repository interfaces. The Domain Layer is independent of the other layers.
- **Data Layer:** This layer is responsible for data retrieval and storage. It consists of repository implementations, data sources (API clients, local database), and data transfer objects (DTOs). The Data Layer depends on the Domain Layer and implements the repository interfaces defined in it.
### Integration Points
- **UI → Domain:** The UI (e.g., a button press) triggers a method in a Bloc. The Bloc then calls a use case in the Domain Layer to execute the business logic.
- **Domain → Data:** The use case calls a method on a repository interface.
- **Data → External:** The repository implementation, located in the Data Layer, communicates with external data sources (GraphQL API, Firebase, local storage) to retrieve or store data.
## C. Backend Architecture
The backend is built on a combination of a **GraphQL server** and **Firebase services**.
- **GraphQL Server:** The primary endpoint for the Flutter app. It handles most of the business logic and data aggregation. The server is responsible for communicating with Firebase services to fulfill requests.
- **Firebase Services:**
- **Firebase Auth:** Used for user authentication, primarily with phone number verification.
- **Firebase Firestore:** The main database for storing application data, such as user profiles, shifts, and earnings.
- **Firebase Storage:** Used for storing user-generated content, such as profile avatars.
- **Firebase Cloud Messaging:** Used for sending push notifications to users.
- **Firebase Remote Config:** Used for remotely configuring app parameters.
### API Flow
1. **Flutter App to GraphQL:** The Flutter app sends GraphQL queries and mutations to the GraphQL server.
2. **GraphQL to Firebase:** The GraphQL server resolves these operations by interacting with Firebase services. For example, a `getShifts` query will fetch data from Firestore, and an `updateStaffPersonalInfoWithAvatar` mutation will update a document in Firestore and upload a file to Firebase Storage.
3. **Response Flow:** The data flows back from Firebase to the GraphQL server, which then sends it back to the Flutter app.
## D. API Layer
The API layer is responsible for all communication with the backend.
- **GraphQL Operations:** The app uses the `graphql_flutter` package to interact with the GraphQL server. Queries, mutations, and subscriptions are defined in `.dart` files within each feature's `data` directory.
- **API Error Handling:** The `ApiClient` class is responsible for handling API errors. It catches exceptions and returns a `Failure` object, which is then handled by the Bloc in the Presentation Layer to show an appropriate error message to the user.
- **Caching:** The `graphql_flutter` client provides caching capabilities. The app uses a `HiveStore` to cache GraphQL responses, reducing the number of network requests and improving performance.
- **Parsing:** JSON responses from the API are parsed into Dart objects using the `json_serializable` package.
## E. State Management
The application uses the **Bloc** library for state management.
- **Why Bloc?** Bloc is a predictable state management library that helps to separate business logic from the UI. It enforces a unidirectional data flow, making the app's state changes predictable and easier to debug.
- **State Flow:**
1. **UI Event:** The UI dispatches an event to the Bloc.
2. **Bloc Logic:** The Bloc receives the event, executes the necessary business logic (often by calling a use case), and emits a new state.
3. **UI Update:** The UI listens to the Bloc's state changes and rebuilds itself to reflect the new state.
- **Integration with API Layer:** Blocs interact with the API layer through use cases. When a Bloc needs to fetch data from the backend, it calls a use case, which in turn calls a repository that communicates with the API.
## F. Use-Case Flows
### User Authentication
1. **UI:** The user enters their phone number.
2. **Logic:** The `AuthBloc` sends the phone number to Firebase Auth for verification.
3. **Backend:** Firebase Auth sends a verification code to the user's phone.
4. **UI:** The user enters the verification code.
5. **Logic:** The `AuthBloc` verifies the code with Firebase Auth.
6. **Backend:** Firebase Auth returns an auth token.
7. **Logic:** The app sends the auth token to the GraphQL server to get the user's profile.
8. **Response:** The GraphQL server returns the user's data, and the app navigates to the home screen.
### Shift Management
1. **UI:** The user navigates to the shifts screen.
2. **Logic:** The `ShiftsBloc` requests a list of shifts.
3. **Backend:** The use case calls the `ShiftsRepository`, which sends a `getShifts` query to the GraphQL server. The server fetches the shifts from Firestore.
4. **Response:** The GraphQL server returns the list of shifts, which is then displayed on the UI.
## G. Replacing or Plugging in a New Backend: Considerations & Recommendations
This section provides guidance on how to replace the current GraphQL + Firebase backend with a different solution (e.g., REST, Supabase, Hasura).
### Tightly Coupled Components
- **Data Layer:** The current `ApiProvider` implementations are tightly coupled to the GraphQL API.
- **Authentication:** The authentication flow is tightly coupled to Firebase Auth.
- **DTOs:** The data transfer objects are generated based on the GraphQL schema.
### Abstraction Recommendations
To make the architecture more backend-agnostic, the following components should be abstracted:
- **Repositories:** The repository interfaces in the Domain Layer should remain unchanged. The implementations in the Data Layer will need to be rewritten for the new backend.
- **Services:** Services like authentication should be abstracted behind an interface. For example, an `AuthService` interface can be defined in the Domain Layer, with a `FirebaseAuthService` implementation in the Data Layer.
- **DTOs:** The DTOs should be mapped to domain entities in the Data Layer. This ensures that the Domain Layer is not affected by changes in the backend's data model.
- **Error Handling:** A generic error handling mechanism should be implemented to handle different types of backend errors.
### Suggested Design Improvements
- **Introduce a Service Locator:** Use a service locator like `get_it` to decouple the layers and make it easier to swap out implementations.
- **Define Abstract Data Sources:** Instead of directly calling the API client in the repository implementations, introduce abstract data source interfaces (e.g., `UserRemoteDataSource`). This adds another layer of abstraction and makes the repositories more testable.
### Migration Strategies
1. **Define Interfaces:** Start by defining abstract interfaces for all backend interactions (repositories, services).
2. **Implement New Data Layer:** Create a new implementation of the Data Layer for the new backend. This will involve writing new repository implementations, API clients, and DTOs.
3. **Swap Implementations:** Use the service locator to swap the old Data Layer implementation with the new one.
4. **Test:** Thoroughly test the application to ensure that everything works as expected with the new backend.
By following these recommendations, the Krow Mobile Staff App can be migrated to a new backend with minimal impact on the overall architecture and business logic.

View File

@@ -0,0 +1,9 @@
# List of authorized users for the Internal Launchpad
# Format: one email per line, lines starting with # are comments
#
# IMPORTANT: These users must belong to the 'krowwithus.com' organization.
# This is a known limitation of enabling IAP directly on Cloud Run.
# See: https://docs.cloud.google.com/run/docs/securing/identity-aware-proxy-cloud-run#known_limitations
user:admin@krowwithus.com
# user:boris@oloodi.com # External users are not supported with this IAP method

View File

@@ -13,6 +13,9 @@
<!-- Mermaid --> <!-- Mermaid -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.9.1/mermaid.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.9.1/mermaid.min.js"></script>
<!-- Marked.js for Markdown parsing -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Custom Tailwind Config --> <!-- Custom Tailwind Config -->
<script> <script>
tailwind.config = { tailwind.config = {
@@ -90,6 +93,170 @@
#diagram-container:active { #diagram-container:active {
cursor: grabbing; cursor: grabbing;
} }
/* Modal styles */
.modal-overlay {
backdrop-filter: blur(4px);
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal-content {
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Markdown styling */
.markdown-content {
line-height: 1.7;
color: #374151;
}
.markdown-content h1 {
font-size: 2em;
font-weight: 700;
margin-top: 1.5em;
margin-bottom: 0.5em;
padding-bottom: 0.3em;
border-bottom: 2px solid #e5e7eb;
color: #111827;
}
.markdown-content h1:first-child {
margin-top: 0;
}
.markdown-content h2 {
font-size: 1.5em;
font-weight: 600;
margin-top: 1.5em;
margin-bottom: 0.5em;
padding-bottom: 0.2em;
border-bottom: 1px solid #e5e7eb;
color: #111827;
}
.markdown-content h3 {
font-size: 1.25em;
font-weight: 600;
margin-top: 1.2em;
margin-bottom: 0.5em;
color: #111827;
}
.markdown-content h4 {
font-size: 1.1em;
font-weight: 600;
margin-top: 1em;
margin-bottom: 0.5em;
color: #111827;
}
.markdown-content p {
margin-bottom: 1em;
}
.markdown-content ul, .markdown-content ol {
margin-bottom: 1em;
padding-left: 2em;
}
.markdown-content ul {
list-style-type: disc;
}
.markdown-content ol {
list-style-type: decimal;
}
.markdown-content li {
margin-bottom: 0.5em;
}
.markdown-content code {
background-color: #f3f4f6;
padding: 0.2em 0.4em;
border-radius: 0.25em;
font-size: 0.9em;
font-family: 'Courier New', monospace;
color: #dc2626;
}
.markdown-content pre {
background-color: #1f2937;
color: #f9fafb;
padding: 1em;
border-radius: 0.5em;
overflow-x: auto;
margin-bottom: 1em;
}
.markdown-content pre code {
background-color: transparent;
padding: 0;
color: inherit;
}
.markdown-content blockquote {
border-left: 4px solid #3b82f6;
padding-left: 1em;
margin: 1em 0;
color: #6b7280;
font-style: italic;
}
.markdown-content a {
color: #3b82f6;
text-decoration: underline;
}
.markdown-content a:hover {
color: #2563eb;
}
.markdown-content table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1em;
}
.markdown-content th, .markdown-content td {
border: 1px solid #e5e7eb;
padding: 0.5em;
text-align: left;
}
.markdown-content th {
background-color: #f9fafb;
font-weight: 600;
}
.markdown-content img {
max-width: 100%;
height: auto;
border-radius: 0.5em;
margin: 1em 0;
}
.markdown-content hr {
border: none;
border-top: 2px solid #e5e7eb;
margin: 2em 0;
}
</style> </style>
</head> </head>
@@ -106,7 +273,7 @@
</div> </div>
<div> <div>
<h1 class="text-xl font-bold text-gray-900">KROW</h1> <h1 class="text-xl font-bold text-gray-900">KROW</h1>
<p class="text-xs text-gray-500">Workforce Platform</p> <p class="text-xs text-gray-500">Launchpad</p>
</div> </div>
</div> </div>
</div> </div>
@@ -123,6 +290,9 @@
<!-- Dynamic diagrams section - ALL diagrams loaded here --> <!-- Dynamic diagrams section - ALL diagrams loaded here -->
<div id="dynamic-diagrams-section"></div> <div id="dynamic-diagrams-section"></div>
<!-- Documentation section -->
<div id="documentation-section"></div>
</nav> </nav>
<!-- Footer --> <!-- Footer -->
@@ -350,6 +520,17 @@
</div> </div>
</div> </div>
<!-- Document Viewer -->
<div id="document-viewer" class="hidden h-full flex flex-col p-8">
<div class="mb-6">
<h3 id="document-title" class="text-2xl font-bold text-gray-900"></h3>
</div>
<div id="document-container"
class="flex-1 bg-white rounded-2xl shadow-xl border border-gray-200 overflow-y-auto p-8 markdown-content">
<!-- Document content will be loaded here -->
</div>
</div>
</main> </main>
</div> </div>
@@ -358,10 +539,14 @@
<script> <script>
let allDiagrams = []; let allDiagrams = [];
let allDocuments = [];
const homeView = document.getElementById('home-view'); const homeView = document.getElementById('home-view');
const diagramViewer = document.getElementById('diagram-viewer'); const diagramViewer = document.getElementById('diagram-viewer');
const diagramContainer = document.getElementById('diagram-container'); const diagramContainer = document.getElementById('diagram-container');
const diagramTitle = document.getElementById('diagram-title'); const diagramTitle = document.getElementById('diagram-title');
const documentViewer = document.getElementById('document-viewer');
const documentContainer = document.getElementById('document-container');
const documentTitle = document.getElementById('document-title');
const zoomInBtn = document.getElementById('zoomInBtn'); const zoomInBtn = document.getElementById('zoomInBtn');
const zoomOutBtn = document.getElementById('zoomOutBtn'); const zoomOutBtn = document.getElementById('zoomOutBtn');
const resetBtn = document.getElementById('resetBtn'); const resetBtn = document.getElementById('resetBtn');
@@ -415,6 +600,41 @@
return hierarchy; return hierarchy;
} }
// Build hierarchical structure from paths (for documents)
function buildDocumentHierarchy(documents) {
const hierarchy = {};
documents.forEach(doc => {
const parts = doc.path.split('/');
const relevantParts = parts.slice(2, -1); // Remove 'assets/documents/' and filename
let current = hierarchy;
relevantParts.forEach(part => {
if (!current[part]) {
current[part] = { _items: [], _children: {} };
}
current = current[part]._children;
});
// Add the item to appropriate level
if (relevantParts.length > 0) {
let parent = hierarchy[relevantParts[0]];
for (let i = 1; i < relevantParts.length; i++) {
parent = parent._children[relevantParts[i]];
}
parent._items.push(doc);
} else {
// Root level documents
if (!hierarchy._root) {
hierarchy._root = { _items: [], _children: {} };
}
hierarchy._root._items.push(doc);
}
});
return hierarchy;
}
// Create navigation from hierarchy // Create navigation from hierarchy
function createNavigation(hierarchy, parentElement, level = 0) { function createNavigation(hierarchy, parentElement, level = 0) {
// First, show root level items if any // First, show root level items if any
@@ -454,6 +674,64 @@
}); });
} }
// Create document navigation from hierarchy
function createDocumentNavigation(hierarchy, parentElement, level = 0) {
// First, show root level items if any
if (hierarchy._root && hierarchy._root._items.length > 0) {
const mainHeading = document.createElement('div');
mainHeading.className = 'px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider mt-6 mb-3';
mainHeading.textContent = 'Documentation';
parentElement.appendChild(mainHeading);
hierarchy._root._items.forEach(doc => {
createDocumentLink(doc, parentElement, 0);
});
}
// Then process nested categories
Object.keys(hierarchy).forEach(key => {
if (key === '_items' || key === '_children' || key === '_root') return;
const section = hierarchy[key];
const heading = document.createElement('div');
heading.className = 'px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider ' +
(level === 0 ? 'mt-6 mb-3' : 'mt-4 mb-2 pl-8');
heading.textContent = key.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
parentElement.appendChild(heading);
// Add items in this section
if (section._items && section._items.length > 0) {
section._items.forEach(doc => {
createDocumentLink(doc, parentElement, level);
});
}
// Recursively add children
if (section._children && Object.keys(section._children).length > 0) {
createDocumentNavigation(section._children, parentElement, level + 1);
}
});
}
// Helper function to create a document link
function createDocumentLink(doc, parentElement, level) {
const link = document.createElement('a');
link.href = '#';
link.className = 'nav-item flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:bg-gray-50 mb-1' +
(level > 0 ? ' pl-8' : '');
link.onclick = (e) => {
e.preventDefault();
showView('document', link, doc.path, doc.title);
};
const iconSvg = `<svg class="w-5 h-5 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>`;
link.innerHTML = `${iconSvg}<span class="text-sm">${doc.title}</span>`;
parentElement.appendChild(link);
}
// Helper function to create a diagram link // Helper function to create a diagram link
function createDiagramLink(diagram, parentElement, level) { function createDiagramLink(diagram, parentElement, level) {
const link = document.createElement('a'); const link = document.createElement('a');
@@ -477,7 +755,7 @@
</svg>`; </svg>`;
} }
link.innerHTML = `${iconSvg}<span class="text-sm">${diagram.title}</span>`; link.innerHTML = iconSvg + '<span class="text-sm">' + diagram.title + '</span>';
parentElement.appendChild(link); parentElement.appendChild(link);
} }
@@ -501,7 +779,6 @@
} }
} catch (error) { } catch (error) {
console.error('Error loading diagrams configuration:', error); console.error('Error loading diagrams configuration:', error);
// Show a helpful message in the UI
const errorDiv = document.createElement('div'); const errorDiv = document.createElement('div');
errorDiv.className = 'px-4 py-3 mx-2 mt-4 text-xs text-amber-600 bg-amber-50 rounded-lg border border-amber-200'; errorDiv.className = 'px-4 py-3 mx-2 mt-4 text-xs text-amber-600 bg-amber-50 rounded-lg border border-amber-200';
errorDiv.innerHTML = ` errorDiv.innerHTML = `
@@ -513,8 +790,39 @@
} }
} }
// Load all documentation from config
async function loadAllDocuments() {
const documentationSection = document.getElementById('documentation-section');
try {
const response = await fetch('./assets/documents/documents-config.json');
if (!response.ok) {
throw new Error(`Failed to load documents config: ${response.status}`);
}
const text = await response.text();
console.log('Loaded documents config:', text);
allDocuments = JSON.parse(text);
if (allDocuments && allDocuments.length > 0) {
const hierarchy = buildDocumentHierarchy(allDocuments);
createDocumentNavigation(hierarchy, documentationSection);
}
} catch (error) {
console.error('Error loading documents configuration:', error);
const errorDiv = document.createElement('div');
errorDiv.className = 'px-4 py-3 mx-2 mt-4 text-xs text-amber-600 bg-amber-50 rounded-lg border border-amber-200';
errorDiv.innerHTML = `
<div class="font-semibold mb-1">⚠️ Documentation</div>
<div>Unable to load documents-config.json</div>
<div class="mt-1 text-amber-500">${error.message}</div>
`;
documentationSection.appendChild(errorDiv);
}
}
function setActiveNav(activeLink) { function setActiveNav(activeLink) {
document.querySelectorAll('.sidebar a').forEach(link => { document.querySelectorAll('#sidebar-nav a').forEach(link => {
link.classList.remove('bg-primary-50', 'border', 'border-primary-200', 'text-primary-700'); link.classList.remove('bg-primary-50', 'border', 'border-primary-200', 'text-primary-700');
link.classList.add('text-gray-700'); link.classList.add('text-gray-700');
}); });
@@ -529,14 +837,17 @@
panzoomInstance = null; panzoomInstance = null;
} }
diagramContainer.innerHTML = ''; diagramContainer.innerHTML = '';
documentContainer.innerHTML = '';
currentScale = 1; currentScale = 1;
if (viewName === 'home') { if (viewName === 'home') {
homeView.classList.remove('hidden'); homeView.classList.remove('hidden');
diagramViewer.classList.add('hidden'); diagramViewer.classList.add('hidden');
documentViewer.classList.add('hidden');
} else if (viewName === 'diagram') { } else if (viewName === 'diagram') {
homeView.classList.add('hidden'); homeView.classList.add('hidden');
diagramViewer.classList.remove('hidden'); diagramViewer.classList.remove('hidden');
documentViewer.classList.add('hidden');
diagramTitle.textContent = title; diagramTitle.textContent = title;
diagramContainer.innerHTML = ` diagramContainer.innerHTML = `
<div class="flex flex-col items-center space-y-3"> <div class="flex flex-col items-center space-y-3">
@@ -608,6 +919,41 @@
</div> </div>
`; `;
} }
} else if (viewName === 'document') {
homeView.classList.add('hidden');
diagramViewer.classList.add('hidden');
documentViewer.classList.remove('hidden');
documentTitle.textContent = title;
documentContainer.innerHTML = `
<div class="flex flex-col items-center justify-center space-y-3 py-12">
<div class="w-12 h-12 border-4 border-primary-200 border-t-primary-600 rounded-full animate-spin"></div>
<p class="text-gray-600 font-medium">Loading document...</p>
</div>
`;
try {
const response = await fetch(filePath);
if (!response.ok) {
throw new Error(`Failed to load document: ${response.status}`);
}
const markdownText = await response.text();
const htmlContent = marked.parse(markdownText);
documentContainer.innerHTML = htmlContent;
} catch (error) {
console.error('Error loading document:', error);
documentContainer.innerHTML = `
<div class="bg-red-50 border border-red-200 rounded-xl p-6">
<div class="flex items-center space-x-3 mb-2">
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h4 class="font-bold text-red-900">Failed to load document</h4>
</div>
<p class="text-red-700 text-sm">${error.message}</p>
</div>
`;
}
} }
} }
@@ -634,6 +980,7 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
loadAllDiagrams(); loadAllDiagrams();
loadAllDocuments();
showView('home', document.getElementById('nav-home')); showView('home', document.getElementById('nav-home'));
}); });
</script> </script>

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@base44/sdk": "^0.1.2", "@base44/sdk": "^0.1.2",
"@dataconnect/generated": "file:src/dataconnect-generated",
"@hookform/resolvers": "^4.1.2", "@hookform/resolvers": "^4.1.2",
"@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-alert-dialog": "^1.1.6",

View File

@@ -0,0 +1,9 @@
{
"description": "A set of guides for interacting with the generated firebase dataconnect sdk",
"mcpServers": {
"firebase": {
"command": "npx",
"args": ["-y", "firebase-tools@latest", "experimental:mcp"]
}
}
}

View File

@@ -0,0 +1,62 @@
# Setup
If the user hasn't already installed the SDK, always run the user's node package manager of choice, and install the package in the directory ../package.json.
For more information on where the library is located, look at the connector.yaml file.
```ts
import { initializeApp } from 'firebase/app';
initializeApp({
// fill in your project config here using the values from your Firebase project or from the `firebase_get_sdk_config` tool from the Firebase MCP server.
});
```
Then, you can run the SDK as needed.
```ts
import { ... } from '@dataconnect/generated';
```
## React
### Setup
The user should make sure to install the `@tanstack/react-query` package, along with `@tanstack-query-firebase/react` and `firebase`.
Then, they should initialize Firebase:
```ts
import { initializeApp } from 'firebase/app';
initializeApp(firebaseConfig); /* your config here. To generate this, you can use the `firebase_sdk_config` MCP tool */
```
Then, they should add a `QueryClientProvider` to their root of their application.
Here's an example:
```ts
import { initializeApp } from 'firebase/app';
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const firebaseConfig = {
/* your config here. To generate this, you can use the `firebase_sdk_config` MCP tool */
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
// Create a TanStack Query client instance
const queryClient = new QueryClient();
function App() {
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<MyApplication />
</QueryClientProvider>
)
}
render(<App />, document.getElementById('root'));
```

View File

@@ -0,0 +1,104 @@
# Basic Usage
Always prioritize using a supported framework over using the generated SDK
directly. Supported frameworks simplify the developer experience and help ensure
best practices are followed.
### React
For each operation, there is a wrapper hook that can be used to call the operation.
Here are all of the hooks that get generated:
```ts
import { useCreateMovie, useUpsertUser, useAddReview, useDeleteReview, useListMovies, useListUsers, useListUserReviews, useGetMovieById, useSearchMovie } from '@dataconnect/generated/react';
// The types of these hooks are available in react/index.d.ts
const { data, isPending, isSuccess, isError, error } = useCreateMovie(createMovieVars);
const { data, isPending, isSuccess, isError, error } = useUpsertUser(upsertUserVars);
const { data, isPending, isSuccess, isError, error } = useAddReview(addReviewVars);
const { data, isPending, isSuccess, isError, error } = useDeleteReview(deleteReviewVars);
const { data, isPending, isSuccess, isError, error } = useListMovies();
const { data, isPending, isSuccess, isError, error } = useListUsers();
const { data, isPending, isSuccess, isError, error } = useListUserReviews();
const { data, isPending, isSuccess, isError, error } = useGetMovieById(getMovieByIdVars);
const { data, isPending, isSuccess, isError, error } = useSearchMovie(searchMovieVars);
```
Here's an example from a different generated SDK:
```ts
import { useListAllMovies } from '@dataconnect/generated/react';
function MyComponent() {
const { isLoading, data, error } = useListAllMovies();
if(isLoading) {
return <div>Loading...</div>
}
if(error) {
return <div> An Error Occurred: {error} </div>
}
}
// App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import MyComponent from './my-component';
function App() {
const queryClient = new QueryClient();
return <QueryClientProvider client={queryClient}>
<MyComponent />
</QueryClientProvider>
}
```
## Advanced Usage
If a user is not using a supported framework, they can use the generated SDK directly.
Here's an example of how to use it with the first 5 operations:
```js
import { createMovie, upsertUser, addReview, deleteReview, listMovies, listUsers, listUserReviews, getMovieById, searchMovie } from '@dataconnect/generated';
// Operation CreateMovie: For variables, look at type CreateMovieVars in ../index.d.ts
const { data } = await CreateMovie(dataConnect, createMovieVars);
// Operation UpsertUser: For variables, look at type UpsertUserVars in ../index.d.ts
const { data } = await UpsertUser(dataConnect, upsertUserVars);
// Operation AddReview: For variables, look at type AddReviewVars in ../index.d.ts
const { data } = await AddReview(dataConnect, addReviewVars);
// Operation DeleteReview: For variables, look at type DeleteReviewVars in ../index.d.ts
const { data } = await DeleteReview(dataConnect, deleteReviewVars);
// Operation ListMovies:
const { data } = await ListMovies(dataConnect);
// Operation ListUsers:
const { data } = await ListUsers(dataConnect);
// Operation ListUserReviews:
const { data } = await ListUserReviews(dataConnect);
// Operation GetMovieById: For variables, look at type GetMovieByIdVars in ../index.d.ts
const { data } = await GetMovieById(dataConnect, getMovieByIdVars);
// Operation SearchMovie: For variables, look at type SearchMovieVars in ../index.d.ts
const { data } = await SearchMovie(dataConnect, searchMovieVars);
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,107 @@
import { queryRef, executeQuery, mutationRef, executeMutation, validateArgs } from 'firebase/data-connect';
export const connectorConfig = {
connector: 'example',
service: 'krow-workforce',
location: 'us-central1'
};
export const createMovieRef = (dcOrVars, vars) => {
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
dcInstance._useGeneratedSdk();
return mutationRef(dcInstance, 'CreateMovie', inputVars);
}
createMovieRef.operationName = 'CreateMovie';
export function createMovie(dcOrVars, vars) {
return executeMutation(createMovieRef(dcOrVars, vars));
}
export const upsertUserRef = (dcOrVars, vars) => {
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
dcInstance._useGeneratedSdk();
return mutationRef(dcInstance, 'UpsertUser', inputVars);
}
upsertUserRef.operationName = 'UpsertUser';
export function upsertUser(dcOrVars, vars) {
return executeMutation(upsertUserRef(dcOrVars, vars));
}
export const addReviewRef = (dcOrVars, vars) => {
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
dcInstance._useGeneratedSdk();
return mutationRef(dcInstance, 'AddReview', inputVars);
}
addReviewRef.operationName = 'AddReview';
export function addReview(dcOrVars, vars) {
return executeMutation(addReviewRef(dcOrVars, vars));
}
export const deleteReviewRef = (dcOrVars, vars) => {
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
dcInstance._useGeneratedSdk();
return mutationRef(dcInstance, 'DeleteReview', inputVars);
}
deleteReviewRef.operationName = 'DeleteReview';
export function deleteReview(dcOrVars, vars) {
return executeMutation(deleteReviewRef(dcOrVars, vars));
}
export const listMoviesRef = (dc) => {
const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined);
dcInstance._useGeneratedSdk();
return queryRef(dcInstance, 'ListMovies');
}
listMoviesRef.operationName = 'ListMovies';
export function listMovies(dc) {
return executeQuery(listMoviesRef(dc));
}
export const listUsersRef = (dc) => {
const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined);
dcInstance._useGeneratedSdk();
return queryRef(dcInstance, 'ListUsers');
}
listUsersRef.operationName = 'ListUsers';
export function listUsers(dc) {
return executeQuery(listUsersRef(dc));
}
export const listUserReviewsRef = (dc) => {
const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined);
dcInstance._useGeneratedSdk();
return queryRef(dcInstance, 'ListUserReviews');
}
listUserReviewsRef.operationName = 'ListUserReviews';
export function listUserReviews(dc) {
return executeQuery(listUserReviewsRef(dc));
}
export const getMovieByIdRef = (dcOrVars, vars) => {
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
dcInstance._useGeneratedSdk();
return queryRef(dcInstance, 'GetMovieById', inputVars);
}
getMovieByIdRef.operationName = 'GetMovieById';
export function getMovieById(dcOrVars, vars) {
return executeQuery(getMovieByIdRef(dcOrVars, vars));
}
export const searchMovieRef = (dcOrVars, vars) => {
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars);
dcInstance._useGeneratedSdk();
return queryRef(dcInstance, 'SearchMovie', inputVars);
}
searchMovieRef.operationName = 'SearchMovie';
export function searchMovie(dcOrVars, vars) {
return executeQuery(searchMovieRef(dcOrVars, vars));
}

View File

@@ -0,0 +1 @@
{"type":"module"}

View File

@@ -0,0 +1,116 @@
const { queryRef, executeQuery, mutationRef, executeMutation, validateArgs } = require('firebase/data-connect');
const connectorConfig = {
connector: 'example',
service: 'krow-workforce',
location: 'us-central1'
};
exports.connectorConfig = connectorConfig;
const createMovieRef = (dcOrVars, vars) => {
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
dcInstance._useGeneratedSdk();
return mutationRef(dcInstance, 'CreateMovie', inputVars);
}
createMovieRef.operationName = 'CreateMovie';
exports.createMovieRef = createMovieRef;
exports.createMovie = function createMovie(dcOrVars, vars) {
return executeMutation(createMovieRef(dcOrVars, vars));
};
const upsertUserRef = (dcOrVars, vars) => {
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
dcInstance._useGeneratedSdk();
return mutationRef(dcInstance, 'UpsertUser', inputVars);
}
upsertUserRef.operationName = 'UpsertUser';
exports.upsertUserRef = upsertUserRef;
exports.upsertUser = function upsertUser(dcOrVars, vars) {
return executeMutation(upsertUserRef(dcOrVars, vars));
};
const addReviewRef = (dcOrVars, vars) => {
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
dcInstance._useGeneratedSdk();
return mutationRef(dcInstance, 'AddReview', inputVars);
}
addReviewRef.operationName = 'AddReview';
exports.addReviewRef = addReviewRef;
exports.addReview = function addReview(dcOrVars, vars) {
return executeMutation(addReviewRef(dcOrVars, vars));
};
const deleteReviewRef = (dcOrVars, vars) => {
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
dcInstance._useGeneratedSdk();
return mutationRef(dcInstance, 'DeleteReview', inputVars);
}
deleteReviewRef.operationName = 'DeleteReview';
exports.deleteReviewRef = deleteReviewRef;
exports.deleteReview = function deleteReview(dcOrVars, vars) {
return executeMutation(deleteReviewRef(dcOrVars, vars));
};
const listMoviesRef = (dc) => {
const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined);
dcInstance._useGeneratedSdk();
return queryRef(dcInstance, 'ListMovies');
}
listMoviesRef.operationName = 'ListMovies';
exports.listMoviesRef = listMoviesRef;
exports.listMovies = function listMovies(dc) {
return executeQuery(listMoviesRef(dc));
};
const listUsersRef = (dc) => {
const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined);
dcInstance._useGeneratedSdk();
return queryRef(dcInstance, 'ListUsers');
}
listUsersRef.operationName = 'ListUsers';
exports.listUsersRef = listUsersRef;
exports.listUsers = function listUsers(dc) {
return executeQuery(listUsersRef(dc));
};
const listUserReviewsRef = (dc) => {
const { dc: dcInstance} = validateArgs(connectorConfig, dc, undefined);
dcInstance._useGeneratedSdk();
return queryRef(dcInstance, 'ListUserReviews');
}
listUserReviewsRef.operationName = 'ListUserReviews';
exports.listUserReviewsRef = listUserReviewsRef;
exports.listUserReviews = function listUserReviews(dc) {
return executeQuery(listUserReviewsRef(dc));
};
const getMovieByIdRef = (dcOrVars, vars) => {
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars, true);
dcInstance._useGeneratedSdk();
return queryRef(dcInstance, 'GetMovieById', inputVars);
}
getMovieByIdRef.operationName = 'GetMovieById';
exports.getMovieByIdRef = getMovieByIdRef;
exports.getMovieById = function getMovieById(dcOrVars, vars) {
return executeQuery(getMovieByIdRef(dcOrVars, vars));
};
const searchMovieRef = (dcOrVars, vars) => {
const { dc: dcInstance, vars: inputVars} = validateArgs(connectorConfig, dcOrVars, vars);
dcInstance._useGeneratedSdk();
return queryRef(dcInstance, 'SearchMovie', inputVars);
}
searchMovieRef.operationName = 'SearchMovie';
exports.searchMovieRef = searchMovieRef;
exports.searchMovie = function searchMovie(dcOrVars, vars) {
return executeQuery(searchMovieRef(dcOrVars, vars));
};

View File

@@ -0,0 +1,250 @@
import { ConnectorConfig, DataConnect, QueryRef, QueryPromise, MutationRef, MutationPromise } from 'firebase/data-connect';
export const connectorConfig: ConnectorConfig;
export type TimestampString = string;
export type UUIDString = string;
export type Int64String = string;
export type DateString = string;
export interface AddReviewData {
review_upsert: Review_Key;
}
export interface AddReviewVariables {
movieId: UUIDString;
rating: number;
reviewText: string;
}
export interface CreateMovieData {
movie_insert: Movie_Key;
}
export interface CreateMovieVariables {
title: string;
genre: string;
imageUrl: string;
}
export interface DeleteReviewData {
review_delete?: Review_Key | null;
}
export interface DeleteReviewVariables {
movieId: UUIDString;
}
export interface GetMovieByIdData {
movie?: {
id: UUIDString;
title: string;
imageUrl: string;
genre?: string | null;
metadata?: {
rating?: number | null;
releaseYear?: number | null;
description?: string | null;
};
reviews: ({
reviewText?: string | null;
reviewDate: DateString;
rating?: number | null;
user: {
id: string;
username: string;
} & User_Key;
})[];
} & Movie_Key;
}
export interface GetMovieByIdVariables {
id: UUIDString;
}
export interface ListMoviesData {
movies: ({
id: UUIDString;
title: string;
imageUrl: string;
genre?: string | null;
} & Movie_Key)[];
}
export interface ListUserReviewsData {
user?: {
id: string;
username: string;
reviews: ({
rating?: number | null;
reviewDate: DateString;
reviewText?: string | null;
movie: {
id: UUIDString;
title: string;
} & Movie_Key;
})[];
} & User_Key;
}
export interface ListUsersData {
users: ({
id: string;
username: string;
} & User_Key)[];
}
export interface MovieMetadata_Key {
id: UUIDString;
__typename?: 'MovieMetadata_Key';
}
export interface Movie_Key {
id: UUIDString;
__typename?: 'Movie_Key';
}
export interface Review_Key {
userId: string;
movieId: UUIDString;
__typename?: 'Review_Key';
}
export interface SearchMovieData {
movies: ({
id: UUIDString;
title: string;
genre?: string | null;
imageUrl: string;
} & Movie_Key)[];
}
export interface SearchMovieVariables {
titleInput?: string | null;
genre?: string | null;
}
export interface UpsertUserData {
user_upsert: User_Key;
}
export interface UpsertUserVariables {
username: string;
}
export interface User_Key {
id: string;
__typename?: 'User_Key';
}
interface CreateMovieRef {
/* Allow users to create refs without passing in DataConnect */
(vars: CreateMovieVariables): MutationRef<CreateMovieData, CreateMovieVariables>;
/* Allow users to pass in custom DataConnect instances */
(dc: DataConnect, vars: CreateMovieVariables): MutationRef<CreateMovieData, CreateMovieVariables>;
operationName: string;
}
export const createMovieRef: CreateMovieRef;
export function createMovie(vars: CreateMovieVariables): MutationPromise<CreateMovieData, CreateMovieVariables>;
export function createMovie(dc: DataConnect, vars: CreateMovieVariables): MutationPromise<CreateMovieData, CreateMovieVariables>;
interface UpsertUserRef {
/* Allow users to create refs without passing in DataConnect */
(vars: UpsertUserVariables): MutationRef<UpsertUserData, UpsertUserVariables>;
/* Allow users to pass in custom DataConnect instances */
(dc: DataConnect, vars: UpsertUserVariables): MutationRef<UpsertUserData, UpsertUserVariables>;
operationName: string;
}
export const upsertUserRef: UpsertUserRef;
export function upsertUser(vars: UpsertUserVariables): MutationPromise<UpsertUserData, UpsertUserVariables>;
export function upsertUser(dc: DataConnect, vars: UpsertUserVariables): MutationPromise<UpsertUserData, UpsertUserVariables>;
interface AddReviewRef {
/* Allow users to create refs without passing in DataConnect */
(vars: AddReviewVariables): MutationRef<AddReviewData, AddReviewVariables>;
/* Allow users to pass in custom DataConnect instances */
(dc: DataConnect, vars: AddReviewVariables): MutationRef<AddReviewData, AddReviewVariables>;
operationName: string;
}
export const addReviewRef: AddReviewRef;
export function addReview(vars: AddReviewVariables): MutationPromise<AddReviewData, AddReviewVariables>;
export function addReview(dc: DataConnect, vars: AddReviewVariables): MutationPromise<AddReviewData, AddReviewVariables>;
interface DeleteReviewRef {
/* Allow users to create refs without passing in DataConnect */
(vars: DeleteReviewVariables): MutationRef<DeleteReviewData, DeleteReviewVariables>;
/* Allow users to pass in custom DataConnect instances */
(dc: DataConnect, vars: DeleteReviewVariables): MutationRef<DeleteReviewData, DeleteReviewVariables>;
operationName: string;
}
export const deleteReviewRef: DeleteReviewRef;
export function deleteReview(vars: DeleteReviewVariables): MutationPromise<DeleteReviewData, DeleteReviewVariables>;
export function deleteReview(dc: DataConnect, vars: DeleteReviewVariables): MutationPromise<DeleteReviewData, DeleteReviewVariables>;
interface ListMoviesRef {
/* Allow users to create refs without passing in DataConnect */
(): QueryRef<ListMoviesData, undefined>;
/* Allow users to pass in custom DataConnect instances */
(dc: DataConnect): QueryRef<ListMoviesData, undefined>;
operationName: string;
}
export const listMoviesRef: ListMoviesRef;
export function listMovies(): QueryPromise<ListMoviesData, undefined>;
export function listMovies(dc: DataConnect): QueryPromise<ListMoviesData, undefined>;
interface ListUsersRef {
/* Allow users to create refs without passing in DataConnect */
(): QueryRef<ListUsersData, undefined>;
/* Allow users to pass in custom DataConnect instances */
(dc: DataConnect): QueryRef<ListUsersData, undefined>;
operationName: string;
}
export const listUsersRef: ListUsersRef;
export function listUsers(): QueryPromise<ListUsersData, undefined>;
export function listUsers(dc: DataConnect): QueryPromise<ListUsersData, undefined>;
interface ListUserReviewsRef {
/* Allow users to create refs without passing in DataConnect */
(): QueryRef<ListUserReviewsData, undefined>;
/* Allow users to pass in custom DataConnect instances */
(dc: DataConnect): QueryRef<ListUserReviewsData, undefined>;
operationName: string;
}
export const listUserReviewsRef: ListUserReviewsRef;
export function listUserReviews(): QueryPromise<ListUserReviewsData, undefined>;
export function listUserReviews(dc: DataConnect): QueryPromise<ListUserReviewsData, undefined>;
interface GetMovieByIdRef {
/* Allow users to create refs without passing in DataConnect */
(vars: GetMovieByIdVariables): QueryRef<GetMovieByIdData, GetMovieByIdVariables>;
/* Allow users to pass in custom DataConnect instances */
(dc: DataConnect, vars: GetMovieByIdVariables): QueryRef<GetMovieByIdData, GetMovieByIdVariables>;
operationName: string;
}
export const getMovieByIdRef: GetMovieByIdRef;
export function getMovieById(vars: GetMovieByIdVariables): QueryPromise<GetMovieByIdData, GetMovieByIdVariables>;
export function getMovieById(dc: DataConnect, vars: GetMovieByIdVariables): QueryPromise<GetMovieByIdData, GetMovieByIdVariables>;
interface SearchMovieRef {
/* Allow users to create refs without passing in DataConnect */
(vars?: SearchMovieVariables): QueryRef<SearchMovieData, SearchMovieVariables>;
/* Allow users to pass in custom DataConnect instances */
(dc: DataConnect, vars?: SearchMovieVariables): QueryRef<SearchMovieData, SearchMovieVariables>;
operationName: string;
}
export const searchMovieRef: SearchMovieRef;
export function searchMovie(vars?: SearchMovieVariables): QueryPromise<SearchMovieData, SearchMovieVariables>;
export function searchMovie(dc: DataConnect, vars?: SearchMovieVariables): QueryPromise<SearchMovieData, SearchMovieVariables>;

View File

@@ -0,0 +1,32 @@
{
"name": "@dataconnect/generated",
"version": "1.0.0",
"author": "Firebase <firebase-support@google.com> (https://firebase.google.com/)",
"description": "Generated SDK For example",
"license": "Apache-2.0",
"engines": {
"node": " >=18.0"
},
"typings": "index.d.ts",
"module": "esm/index.esm.js",
"main": "index.cjs.js",
"browser": "esm/index.esm.js",
"exports": {
".": {
"types": "./index.d.ts",
"require": "./index.cjs.js",
"default": "./esm/index.esm.js"
},
"./react": {
"types": "./react/index.d.ts",
"require": "./react/index.cjs.js",
"import": "./react/esm/index.esm.js",
"default": "./react/esm/index.esm.js"
},
"./package.json": "./package.json"
},
"peerDependencies": {
"firebase": "^11.3.0 || ^12.0.0",
"@tanstack-query-firebase/react": "^2.0.0"
}
}

View File

@@ -0,0 +1,952 @@
# Generated React README
This README will guide you through the process of using the generated React SDK package for the connector `example`. It will also provide examples on how to use your generated SDK to call your Data Connect queries and mutations.
**If you're looking for the `JavaScript README`, you can find it at [`dataconnect-generated/README.md`](../README.md)**
***NOTE:** This README is generated alongside the generated SDK. If you make changes to this file, they will be overwritten when the SDK is regenerated.*
You can use this generated SDK by importing from the package `@dataconnect/generated/react` as shown below. Both CommonJS and ESM imports are supported.
You can also follow the instructions from the [Data Connect documentation](https://firebase.google.com/docs/data-connect/web-sdk#react).
# Table of Contents
- [**Overview**](#generated-react-readme)
- [**TanStack Query Firebase & TanStack React Query**](#tanstack-query-firebase-tanstack-react-query)
- [*Package Installation*](#installing-tanstack-query-firebase-and-tanstack-react-query-packages)
- [*Configuring TanStack Query*](#configuring-tanstack-query)
- [**Accessing the connector**](#accessing-the-connector)
- [*Connecting to the local Emulator*](#connecting-to-the-local-emulator)
- [**Queries**](#queries)
- [*ListMovies*](#listmovies)
- [*ListUsers*](#listusers)
- [*ListUserReviews*](#listuserreviews)
- [*GetMovieById*](#getmoviebyid)
- [*SearchMovie*](#searchmovie)
- [**Mutations**](#mutations)
- [*CreateMovie*](#createmovie)
- [*UpsertUser*](#upsertuser)
- [*AddReview*](#addreview)
- [*DeleteReview*](#deletereview)
# TanStack Query Firebase & TanStack React Query
This SDK provides [React](https://react.dev/) hooks generated specific to your application, for the operations found in the connector `example`. These hooks are generated using [TanStack Query Firebase](https://react-query-firebase.invertase.dev/) by our partners at Invertase, a library built on top of [TanStack React Query v5](https://tanstack.com/query/v5/docs/framework/react/overview).
***You do not need to be familiar with Tanstack Query or Tanstack Query Firebase to use this SDK.*** However, you may find it useful to learn more about them, as they will empower you as a user of this Generated React SDK.
## Installing TanStack Query Firebase and TanStack React Query Packages
In order to use the React generated SDK, you must install the `TanStack React Query` and `TanStack Query Firebase` packages.
```bash
npm i --save @tanstack/react-query @tanstack-query-firebase/react
```
```bash
npm i --save firebase@latest # Note: React has a peer dependency on ^11.3.0
```
You can also follow the installation instructions from the [Data Connect documentation](https://firebase.google.com/docs/data-connect/web-sdk#tanstack-install), or the [TanStack Query Firebase documentation](https://react-query-firebase.invertase.dev/react) and [TanStack React Query documentation](https://tanstack.com/query/v5/docs/framework/react/installation).
## Configuring TanStack Query
In order to use the React generated SDK in your application, you must wrap your application's component tree in a `QueryClientProvider` component from TanStack React Query. None of your generated React SDK hooks will work without this provider.
```javascript
import { QueryClientProvider } from '@tanstack/react-query';
// Create a TanStack Query client instance
const queryClient = new QueryClient()
function App() {
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<MyApplication />
</QueryClientProvider>
)
}
```
To learn more about `QueryClientProvider`, see the [TanStack React Query documentation](https://tanstack.com/query/latest/docs/framework/react/quick-start) and the [TanStack Query Firebase documentation](https://invertase.docs.page/tanstack-query-firebase/react#usage).
# Accessing the connector
A connector is a collection of Queries and Mutations. One SDK is generated for each connector - this SDK is generated for the connector `example`.
You can find more information about connectors in the [Data Connect documentation](https://firebase.google.com/docs/data-connect#how-does).
```javascript
import { getDataConnect } from 'firebase/data-connect';
import { connectorConfig } from '@dataconnect/generated';
const dataConnect = getDataConnect(connectorConfig);
```
## Connecting to the local Emulator
By default, the connector will connect to the production service.
To connect to the emulator, you can use the following code.
You can also follow the emulator instructions from the [Data Connect documentation](https://firebase.google.com/docs/data-connect/web-sdk#emulator-react-angular).
```javascript
import { connectDataConnectEmulator, getDataConnect } from 'firebase/data-connect';
import { connectorConfig } from '@dataconnect/generated';
const dataConnect = getDataConnect(connectorConfig);
connectDataConnectEmulator(dataConnect, 'localhost', 9399);
```
After it's initialized, you can call your Data Connect [queries](#queries) and [mutations](#mutations) using the hooks provided from your generated React SDK.
# Queries
The React generated SDK provides Query hook functions that call and return [`useDataConnectQuery`](https://react-query-firebase.invertase.dev/react/data-connect/querying) hooks from TanStack Query Firebase.
Calling these hook functions will return a `UseQueryResult` object. This object holds the state of your Query, including whether the Query is loading, has completed, or has succeeded/failed, and the most recent data returned by the Query, among other things. To learn more about these hooks and how to use them, see the [TanStack Query Firebase documentation](https://react-query-firebase.invertase.dev/react/data-connect/querying).
TanStack React Query caches the results of your Queries, so using the same Query hook function in multiple places in your application allows the entire application to automatically see updates to that Query's data.
Query hooks execute their Queries automatically when called, and periodically refresh, unless you change the `queryOptions` for the Query. To learn how to stop a Query from automatically executing, including how to make a query "lazy", see the [TanStack React Query documentation](https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries).
To learn more about TanStack React Query's Queries, see the [TanStack React Query documentation](https://tanstack.com/query/v5/docs/framework/react/guides/queries).
## Using Query Hooks
Here's a general overview of how to use the generated Query hooks in your code:
- If the Query has no variables, the Query hook function does not require arguments.
- If the Query has any required variables, the Query hook function will require at least one argument: an object that contains all the required variables for the Query.
- If the Query has some required and some optional variables, only required variables are necessary in the variables argument object, and optional variables may be provided as well.
- If all of the Query's variables are optional, the Query hook function does not require any arguments.
- Query hook functions can be called with or without passing in a `DataConnect` instance as an argument. If no `DataConnect` argument is passed in, then the generated SDK will call `getDataConnect(connectorConfig)` behind the scenes for you.
- Query hooks functions can be called with or without passing in an `options` argument of type `useDataConnectQueryOptions`. To learn more about the `options` argument, see the [TanStack React Query documentation](https://tanstack.com/query/v5/docs/framework/react/guides/query-options).
- ***Special case:*** If the Query has all optional variables and you would like to provide an `options` argument to the Query hook function without providing any variables, you must pass `undefined` where you would normally pass the Query's variables, and then may provide the `options` argument.
Below are examples of how to use the `example` connector's generated Query hook functions to execute each Query. You can also follow the examples from the [Data Connect documentation](https://firebase.google.com/docs/data-connect/web-sdk#operations-react-angular).
## ListMovies
You can execute the `ListMovies` Query using the following Query hook function, which is defined in [dataconnect-generated/react/index.d.ts](./index.d.ts):
```javascript
useListMovies(dc: DataConnect, options?: useDataConnectQueryOptions<ListMoviesData>): UseDataConnectQueryResult<ListMoviesData, undefined>;
```
You can also pass in a `DataConnect` instance to the Query hook function.
```javascript
useListMovies(options?: useDataConnectQueryOptions<ListMoviesData>): UseDataConnectQueryResult<ListMoviesData, undefined>;
```
### Variables
The `ListMovies` Query has no variables.
### Return Type
Recall that calling the `ListMovies` Query hook function returns a `UseQueryResult` object. This object holds the state of your Query, including whether the Query is loading, has completed, or has succeeded/failed, and any data returned by the Query, among other things.
To check the status of a Query, use the `UseQueryResult.status` field. You can also check for pending / success / error status using the `UseQueryResult.isPending`, `UseQueryResult.isSuccess`, and `UseQueryResult.isError` fields.
To access the data returned by a Query, use the `UseQueryResult.data` field. The data for the `ListMovies` Query is of type `ListMoviesData`, which is defined in [dataconnect-generated/index.d.ts](../index.d.ts). It has the following fields:
```javascript
export interface ListMoviesData {
movies: ({
id: UUIDString;
title: string;
imageUrl: string;
genre?: string | null;
} & Movie_Key)[];
}
```
To learn more about the `UseQueryResult` object, see the [TanStack React Query documentation](https://tanstack.com/query/v5/docs/framework/react/reference/useQuery).
### Using `ListMovies`'s Query hook function
```javascript
import { getDataConnect } from 'firebase/data-connect';
import { connectorConfig } from '@dataconnect/generated';
import { useListMovies } from '@dataconnect/generated/react'
export default function ListMoviesComponent() {
// You don't have to do anything to "execute" the Query.
// Call the Query hook function to get a `UseQueryResult` object which holds the state of your Query.
const query = useListMovies();
// You can also pass in a `DataConnect` instance to the Query hook function.
const dataConnect = getDataConnect(connectorConfig);
const query = useListMovies(dataConnect);
// You can also pass in a `useDataConnectQueryOptions` object to the Query hook function.
const options = { staleTime: 5 * 1000 };
const query = useListMovies(options);
// You can also pass both a `DataConnect` instance and a `useDataConnectQueryOptions` object.
const dataConnect = getDataConnect(connectorConfig);
const options = { staleTime: 5 * 1000 };
const query = useListMovies(dataConnect, options);
// Then, you can render your component dynamically based on the status of the Query.
if (query.isPending) {
return <div>Loading...</div>;
}
if (query.isError) {
return <div>Error: {query.error.message}</div>;
}
// If the Query is successful, you can access the data returned using the `UseQueryResult.data` field.
if (query.isSuccess) {
console.log(query.data.movies);
}
return <div>Query execution {query.isSuccess ? 'successful' : 'failed'}!</div>;
}
```
## ListUsers
You can execute the `ListUsers` Query using the following Query hook function, which is defined in [dataconnect-generated/react/index.d.ts](./index.d.ts):
```javascript
useListUsers(dc: DataConnect, options?: useDataConnectQueryOptions<ListUsersData>): UseDataConnectQueryResult<ListUsersData, undefined>;
```
You can also pass in a `DataConnect` instance to the Query hook function.
```javascript
useListUsers(options?: useDataConnectQueryOptions<ListUsersData>): UseDataConnectQueryResult<ListUsersData, undefined>;
```
### Variables
The `ListUsers` Query has no variables.
### Return Type
Recall that calling the `ListUsers` Query hook function returns a `UseQueryResult` object. This object holds the state of your Query, including whether the Query is loading, has completed, or has succeeded/failed, and any data returned by the Query, among other things.
To check the status of a Query, use the `UseQueryResult.status` field. You can also check for pending / success / error status using the `UseQueryResult.isPending`, `UseQueryResult.isSuccess`, and `UseQueryResult.isError` fields.
To access the data returned by a Query, use the `UseQueryResult.data` field. The data for the `ListUsers` Query is of type `ListUsersData`, which is defined in [dataconnect-generated/index.d.ts](../index.d.ts). It has the following fields:
```javascript
export interface ListUsersData {
users: ({
id: string;
username: string;
} & User_Key)[];
}
```
To learn more about the `UseQueryResult` object, see the [TanStack React Query documentation](https://tanstack.com/query/v5/docs/framework/react/reference/useQuery).
### Using `ListUsers`'s Query hook function
```javascript
import { getDataConnect } from 'firebase/data-connect';
import { connectorConfig } from '@dataconnect/generated';
import { useListUsers } from '@dataconnect/generated/react'
export default function ListUsersComponent() {
// You don't have to do anything to "execute" the Query.
// Call the Query hook function to get a `UseQueryResult` object which holds the state of your Query.
const query = useListUsers();
// You can also pass in a `DataConnect` instance to the Query hook function.
const dataConnect = getDataConnect(connectorConfig);
const query = useListUsers(dataConnect);
// You can also pass in a `useDataConnectQueryOptions` object to the Query hook function.
const options = { staleTime: 5 * 1000 };
const query = useListUsers(options);
// You can also pass both a `DataConnect` instance and a `useDataConnectQueryOptions` object.
const dataConnect = getDataConnect(connectorConfig);
const options = { staleTime: 5 * 1000 };
const query = useListUsers(dataConnect, options);
// Then, you can render your component dynamically based on the status of the Query.
if (query.isPending) {
return <div>Loading...</div>;
}
if (query.isError) {
return <div>Error: {query.error.message}</div>;
}
// If the Query is successful, you can access the data returned using the `UseQueryResult.data` field.
if (query.isSuccess) {
console.log(query.data.users);
}
return <div>Query execution {query.isSuccess ? 'successful' : 'failed'}!</div>;
}
```
## ListUserReviews
You can execute the `ListUserReviews` Query using the following Query hook function, which is defined in [dataconnect-generated/react/index.d.ts](./index.d.ts):
```javascript
useListUserReviews(dc: DataConnect, options?: useDataConnectQueryOptions<ListUserReviewsData>): UseDataConnectQueryResult<ListUserReviewsData, undefined>;
```
You can also pass in a `DataConnect` instance to the Query hook function.
```javascript
useListUserReviews(options?: useDataConnectQueryOptions<ListUserReviewsData>): UseDataConnectQueryResult<ListUserReviewsData, undefined>;
```
### Variables
The `ListUserReviews` Query has no variables.
### Return Type
Recall that calling the `ListUserReviews` Query hook function returns a `UseQueryResult` object. This object holds the state of your Query, including whether the Query is loading, has completed, or has succeeded/failed, and any data returned by the Query, among other things.
To check the status of a Query, use the `UseQueryResult.status` field. You can also check for pending / success / error status using the `UseQueryResult.isPending`, `UseQueryResult.isSuccess`, and `UseQueryResult.isError` fields.
To access the data returned by a Query, use the `UseQueryResult.data` field. The data for the `ListUserReviews` Query is of type `ListUserReviewsData`, which is defined in [dataconnect-generated/index.d.ts](../index.d.ts). It has the following fields:
```javascript
export interface ListUserReviewsData {
user?: {
id: string;
username: string;
reviews: ({
rating?: number | null;
reviewDate: DateString;
reviewText?: string | null;
movie: {
id: UUIDString;
title: string;
} & Movie_Key;
})[];
} & User_Key;
}
```
To learn more about the `UseQueryResult` object, see the [TanStack React Query documentation](https://tanstack.com/query/v5/docs/framework/react/reference/useQuery).
### Using `ListUserReviews`'s Query hook function
```javascript
import { getDataConnect } from 'firebase/data-connect';
import { connectorConfig } from '@dataconnect/generated';
import { useListUserReviews } from '@dataconnect/generated/react'
export default function ListUserReviewsComponent() {
// You don't have to do anything to "execute" the Query.
// Call the Query hook function to get a `UseQueryResult` object which holds the state of your Query.
const query = useListUserReviews();
// You can also pass in a `DataConnect` instance to the Query hook function.
const dataConnect = getDataConnect(connectorConfig);
const query = useListUserReviews(dataConnect);
// You can also pass in a `useDataConnectQueryOptions` object to the Query hook function.
const options = { staleTime: 5 * 1000 };
const query = useListUserReviews(options);
// You can also pass both a `DataConnect` instance and a `useDataConnectQueryOptions` object.
const dataConnect = getDataConnect(connectorConfig);
const options = { staleTime: 5 * 1000 };
const query = useListUserReviews(dataConnect, options);
// Then, you can render your component dynamically based on the status of the Query.
if (query.isPending) {
return <div>Loading...</div>;
}
if (query.isError) {
return <div>Error: {query.error.message}</div>;
}
// If the Query is successful, you can access the data returned using the `UseQueryResult.data` field.
if (query.isSuccess) {
console.log(query.data.user);
}
return <div>Query execution {query.isSuccess ? 'successful' : 'failed'}!</div>;
}
```
## GetMovieById
You can execute the `GetMovieById` Query using the following Query hook function, which is defined in [dataconnect-generated/react/index.d.ts](./index.d.ts):
```javascript
useGetMovieById(dc: DataConnect, vars: GetMovieByIdVariables, options?: useDataConnectQueryOptions<GetMovieByIdData>): UseDataConnectQueryResult<GetMovieByIdData, GetMovieByIdVariables>;
```
You can also pass in a `DataConnect` instance to the Query hook function.
```javascript
useGetMovieById(vars: GetMovieByIdVariables, options?: useDataConnectQueryOptions<GetMovieByIdData>): UseDataConnectQueryResult<GetMovieByIdData, GetMovieByIdVariables>;
```
### Variables
The `GetMovieById` Query requires an argument of type `GetMovieByIdVariables`, which is defined in [dataconnect-generated/index.d.ts](../index.d.ts). It has the following fields:
```javascript
export interface GetMovieByIdVariables {
id: UUIDString;
}
```
### Return Type
Recall that calling the `GetMovieById` Query hook function returns a `UseQueryResult` object. This object holds the state of your Query, including whether the Query is loading, has completed, or has succeeded/failed, and any data returned by the Query, among other things.
To check the status of a Query, use the `UseQueryResult.status` field. You can also check for pending / success / error status using the `UseQueryResult.isPending`, `UseQueryResult.isSuccess`, and `UseQueryResult.isError` fields.
To access the data returned by a Query, use the `UseQueryResult.data` field. The data for the `GetMovieById` Query is of type `GetMovieByIdData`, which is defined in [dataconnect-generated/index.d.ts](../index.d.ts). It has the following fields:
```javascript
export interface GetMovieByIdData {
movie?: {
id: UUIDString;
title: string;
imageUrl: string;
genre?: string | null;
metadata?: {
rating?: number | null;
releaseYear?: number | null;
description?: string | null;
};
reviews: ({
reviewText?: string | null;
reviewDate: DateString;
rating?: number | null;
user: {
id: string;
username: string;
} & User_Key;
})[];
} & Movie_Key;
}
```
To learn more about the `UseQueryResult` object, see the [TanStack React Query documentation](https://tanstack.com/query/v5/docs/framework/react/reference/useQuery).
### Using `GetMovieById`'s Query hook function
```javascript
import { getDataConnect } from 'firebase/data-connect';
import { connectorConfig, GetMovieByIdVariables } from '@dataconnect/generated';
import { useGetMovieById } from '@dataconnect/generated/react'
export default function GetMovieByIdComponent() {
// The `useGetMovieById` Query hook requires an argument of type `GetMovieByIdVariables`:
const getMovieByIdVars: GetMovieByIdVariables = {
id: ...,
};
// You don't have to do anything to "execute" the Query.
// Call the Query hook function to get a `UseQueryResult` object which holds the state of your Query.
const query = useGetMovieById(getMovieByIdVars);
// Variables can be defined inline as well.
const query = useGetMovieById({ id: ..., });
// You can also pass in a `DataConnect` instance to the Query hook function.
const dataConnect = getDataConnect(connectorConfig);
const query = useGetMovieById(dataConnect, getMovieByIdVars);
// You can also pass in a `useDataConnectQueryOptions` object to the Query hook function.
const options = { staleTime: 5 * 1000 };
const query = useGetMovieById(getMovieByIdVars, options);
// You can also pass both a `DataConnect` instance and a `useDataConnectQueryOptions` object.
const dataConnect = getDataConnect(connectorConfig);
const options = { staleTime: 5 * 1000 };
const query = useGetMovieById(dataConnect, getMovieByIdVars, options);
// Then, you can render your component dynamically based on the status of the Query.
if (query.isPending) {
return <div>Loading...</div>;
}
if (query.isError) {
return <div>Error: {query.error.message}</div>;
}
// If the Query is successful, you can access the data returned using the `UseQueryResult.data` field.
if (query.isSuccess) {
console.log(query.data.movie);
}
return <div>Query execution {query.isSuccess ? 'successful' : 'failed'}!</div>;
}
```
## SearchMovie
You can execute the `SearchMovie` Query using the following Query hook function, which is defined in [dataconnect-generated/react/index.d.ts](./index.d.ts):
```javascript
useSearchMovie(dc: DataConnect, vars?: SearchMovieVariables, options?: useDataConnectQueryOptions<SearchMovieData>): UseDataConnectQueryResult<SearchMovieData, SearchMovieVariables>;
```
You can also pass in a `DataConnect` instance to the Query hook function.
```javascript
useSearchMovie(vars?: SearchMovieVariables, options?: useDataConnectQueryOptions<SearchMovieData>): UseDataConnectQueryResult<SearchMovieData, SearchMovieVariables>;
```
### Variables
The `SearchMovie` Query has an optional argument of type `SearchMovieVariables`, which is defined in [dataconnect-generated/index.d.ts](../index.d.ts). It has the following fields:
```javascript
export interface SearchMovieVariables {
titleInput?: string | null;
genre?: string | null;
}
```
### Return Type
Recall that calling the `SearchMovie` Query hook function returns a `UseQueryResult` object. This object holds the state of your Query, including whether the Query is loading, has completed, or has succeeded/failed, and any data returned by the Query, among other things.
To check the status of a Query, use the `UseQueryResult.status` field. You can also check for pending / success / error status using the `UseQueryResult.isPending`, `UseQueryResult.isSuccess`, and `UseQueryResult.isError` fields.
To access the data returned by a Query, use the `UseQueryResult.data` field. The data for the `SearchMovie` Query is of type `SearchMovieData`, which is defined in [dataconnect-generated/index.d.ts](../index.d.ts). It has the following fields:
```javascript
export interface SearchMovieData {
movies: ({
id: UUIDString;
title: string;
genre?: string | null;
imageUrl: string;
} & Movie_Key)[];
}
```
To learn more about the `UseQueryResult` object, see the [TanStack React Query documentation](https://tanstack.com/query/v5/docs/framework/react/reference/useQuery).
### Using `SearchMovie`'s Query hook function
```javascript
import { getDataConnect } from 'firebase/data-connect';
import { connectorConfig, SearchMovieVariables } from '@dataconnect/generated';
import { useSearchMovie } from '@dataconnect/generated/react'
export default function SearchMovieComponent() {
// The `useSearchMovie` Query hook has an optional argument of type `SearchMovieVariables`:
const searchMovieVars: SearchMovieVariables = {
titleInput: ..., // optional
genre: ..., // optional
};
// You don't have to do anything to "execute" the Query.
// Call the Query hook function to get a `UseQueryResult` object which holds the state of your Query.
const query = useSearchMovie(searchMovieVars);
// Variables can be defined inline as well.
const query = useSearchMovie({ titleInput: ..., genre: ..., });
// Since all variables are optional for this Query, you can omit the `SearchMovieVariables` argument.
// (as long as you don't want to provide any `options`!)
const query = useSearchMovie();
// You can also pass in a `DataConnect` instance to the Query hook function.
const dataConnect = getDataConnect(connectorConfig);
const query = useSearchMovie(dataConnect, searchMovieVars);
// You can also pass in a `useDataConnectQueryOptions` object to the Query hook function.
const options = { staleTime: 5 * 1000 };
const query = useSearchMovie(searchMovieVars, options);
// If you'd like to provide options without providing any variables, you must
// pass `undefined` where you would normally pass the variables.
const query = useSearchMovie(undefined, options);
// You can also pass both a `DataConnect` instance and a `useDataConnectQueryOptions` object.
const dataConnect = getDataConnect(connectorConfig);
const options = { staleTime: 5 * 1000 };
const query = useSearchMovie(dataConnect, searchMovieVars /** or undefined */, options);
// Then, you can render your component dynamically based on the status of the Query.
if (query.isPending) {
return <div>Loading...</div>;
}
if (query.isError) {
return <div>Error: {query.error.message}</div>;
}
// If the Query is successful, you can access the data returned using the `UseQueryResult.data` field.
if (query.isSuccess) {
console.log(query.data.movies);
}
return <div>Query execution {query.isSuccess ? 'successful' : 'failed'}!</div>;
}
```
# Mutations
The React generated SDK provides Mutations hook functions that call and return [`useDataConnectMutation`](https://react-query-firebase.invertase.dev/react/data-connect/mutations) hooks from TanStack Query Firebase.
Calling these hook functions will return a `UseMutationResult` object. This object holds the state of your Mutation, including whether the Mutation is loading, has completed, or has succeeded/failed, and the most recent data returned by the Mutation, among other things. To learn more about these hooks and how to use them, see the [TanStack Query Firebase documentation](https://react-query-firebase.invertase.dev/react/data-connect/mutations).
Mutation hooks do not execute their Mutations automatically when called. Rather, after calling the Mutation hook function and getting a `UseMutationResult` object, you must call the `UseMutationResult.mutate()` function to execute the Mutation.
To learn more about TanStack React Query's Mutations, see the [TanStack React Query documentation](https://tanstack.com/query/v5/docs/framework/react/guides/mutations).
## Using Mutation Hooks
Here's a general overview of how to use the generated Mutation hooks in your code:
- Mutation hook functions are not called with the arguments to the Mutation. Instead, arguments are passed to `UseMutationResult.mutate()`.
- If the Mutation has no variables, the `mutate()` function does not require arguments.
- If the Mutation has any required variables, the `mutate()` function will require at least one argument: an object that contains all the required variables for the Mutation.
- If the Mutation has some required and some optional variables, only required variables are necessary in the variables argument object, and optional variables may be provided as well.
- If all of the Mutation's variables are optional, the Mutation hook function does not require any arguments.
- Mutation hook functions can be called with or without passing in a `DataConnect` instance as an argument. If no `DataConnect` argument is passed in, then the generated SDK will call `getDataConnect(connectorConfig)` behind the scenes for you.
- Mutation hooks also accept an `options` argument of type `useDataConnectMutationOptions`. To learn more about the `options` argument, see the [TanStack React Query documentation](https://tanstack.com/query/v5/docs/framework/react/guides/mutations#mutation-side-effects).
- `UseMutationResult.mutate()` also accepts an `options` argument of type `useDataConnectMutationOptions`.
- ***Special case:*** If the Mutation has no arguments (or all optional arguments and you wish to provide none), and you want to pass `options` to `UseMutationResult.mutate()`, you must pass `undefined` where you would normally pass the Mutation's arguments, and then may provide the options argument.
Below are examples of how to use the `example` connector's generated Mutation hook functions to execute each Mutation. You can also follow the examples from the [Data Connect documentation](https://firebase.google.com/docs/data-connect/web-sdk#operations-react-angular).
## CreateMovie
You can execute the `CreateMovie` Mutation using the `UseMutationResult` object returned by the following Mutation hook function (which is defined in [dataconnect-generated/react/index.d.ts](./index.d.ts)):
```javascript
useCreateMovie(options?: useDataConnectMutationOptions<CreateMovieData, FirebaseError, CreateMovieVariables>): UseDataConnectMutationResult<CreateMovieData, CreateMovieVariables>;
```
You can also pass in a `DataConnect` instance to the Mutation hook function.
```javascript
useCreateMovie(dc: DataConnect, options?: useDataConnectMutationOptions<CreateMovieData, FirebaseError, CreateMovieVariables>): UseDataConnectMutationResult<CreateMovieData, CreateMovieVariables>;
```
### Variables
The `CreateMovie` Mutation requires an argument of type `CreateMovieVariables`, which is defined in [dataconnect-generated/index.d.ts](../index.d.ts). It has the following fields:
```javascript
export interface CreateMovieVariables {
title: string;
genre: string;
imageUrl: string;
}
```
### Return Type
Recall that calling the `CreateMovie` Mutation hook function returns a `UseMutationResult` object. This object holds the state of your Mutation, including whether the Mutation is loading, has completed, or has succeeded/failed, among other things.
To check the status of a Mutation, use the `UseMutationResult.status` field. You can also check for pending / success / error status using the `UseMutationResult.isPending`, `UseMutationResult.isSuccess`, and `UseMutationResult.isError` fields.
To execute the Mutation, call `UseMutationResult.mutate()`. This function executes the Mutation, but does not return the data from the Mutation.
To access the data returned by a Mutation, use the `UseMutationResult.data` field. The data for the `CreateMovie` Mutation is of type `CreateMovieData`, which is defined in [dataconnect-generated/index.d.ts](../index.d.ts). It has the following fields:
```javascript
export interface CreateMovieData {
movie_insert: Movie_Key;
}
```
To learn more about the `UseMutationResult` object, see the [TanStack React Query documentation](https://tanstack.com/query/v5/docs/framework/react/reference/useMutation).
### Using `CreateMovie`'s Mutation hook function
```javascript
import { getDataConnect } from 'firebase/data-connect';
import { connectorConfig, CreateMovieVariables } from '@dataconnect/generated';
import { useCreateMovie } from '@dataconnect/generated/react'
export default function CreateMovieComponent() {
// Call the Mutation hook function to get a `UseMutationResult` object which holds the state of your Mutation.
const mutation = useCreateMovie();
// You can also pass in a `DataConnect` instance to the Mutation hook function.
const dataConnect = getDataConnect(connectorConfig);
const mutation = useCreateMovie(dataConnect);
// You can also pass in a `useDataConnectMutationOptions` object to the Mutation hook function.
const options = {
onSuccess: () => { console.log('Mutation succeeded!'); }
};
const mutation = useCreateMovie(options);
// You can also pass both a `DataConnect` instance and a `useDataConnectMutationOptions` object.
const dataConnect = getDataConnect(connectorConfig);
const options = {
onSuccess: () => { console.log('Mutation succeeded!'); }
};
const mutation = useCreateMovie(dataConnect, options);
// After calling the Mutation hook function, you must call `UseMutationResult.mutate()` to execute the Mutation.
// The `useCreateMovie` Mutation requires an argument of type `CreateMovieVariables`:
const createMovieVars: CreateMovieVariables = {
title: ...,
genre: ...,
imageUrl: ...,
};
mutation.mutate(createMovieVars);
// Variables can be defined inline as well.
mutation.mutate({ title: ..., genre: ..., imageUrl: ..., });
// You can also pass in a `useDataConnectMutationOptions` object to `UseMutationResult.mutate()`.
const options = {
onSuccess: () => { console.log('Mutation succeeded!'); }
};
mutation.mutate(createMovieVars, options);
// Then, you can render your component dynamically based on the status of the Mutation.
if (mutation.isPending) {
return <div>Loading...</div>;
}
if (mutation.isError) {
return <div>Error: {mutation.error.message}</div>;
}
// If the Mutation is successful, you can access the data returned using the `UseMutationResult.data` field.
if (mutation.isSuccess) {
console.log(mutation.data.movie_insert);
}
return <div>Mutation execution {mutation.isSuccess ? 'successful' : 'failed'}!</div>;
}
```
## UpsertUser
You can execute the `UpsertUser` Mutation using the `UseMutationResult` object returned by the following Mutation hook function (which is defined in [dataconnect-generated/react/index.d.ts](./index.d.ts)):
```javascript
useUpsertUser(options?: useDataConnectMutationOptions<UpsertUserData, FirebaseError, UpsertUserVariables>): UseDataConnectMutationResult<UpsertUserData, UpsertUserVariables>;
```
You can also pass in a `DataConnect` instance to the Mutation hook function.
```javascript
useUpsertUser(dc: DataConnect, options?: useDataConnectMutationOptions<UpsertUserData, FirebaseError, UpsertUserVariables>): UseDataConnectMutationResult<UpsertUserData, UpsertUserVariables>;
```
### Variables
The `UpsertUser` Mutation requires an argument of type `UpsertUserVariables`, which is defined in [dataconnect-generated/index.d.ts](../index.d.ts). It has the following fields:
```javascript
export interface UpsertUserVariables {
username: string;
}
```
### Return Type
Recall that calling the `UpsertUser` Mutation hook function returns a `UseMutationResult` object. This object holds the state of your Mutation, including whether the Mutation is loading, has completed, or has succeeded/failed, among other things.
To check the status of a Mutation, use the `UseMutationResult.status` field. You can also check for pending / success / error status using the `UseMutationResult.isPending`, `UseMutationResult.isSuccess`, and `UseMutationResult.isError` fields.
To execute the Mutation, call `UseMutationResult.mutate()`. This function executes the Mutation, but does not return the data from the Mutation.
To access the data returned by a Mutation, use the `UseMutationResult.data` field. The data for the `UpsertUser` Mutation is of type `UpsertUserData`, which is defined in [dataconnect-generated/index.d.ts](../index.d.ts). It has the following fields:
```javascript
export interface UpsertUserData {
user_upsert: User_Key;
}
```
To learn more about the `UseMutationResult` object, see the [TanStack React Query documentation](https://tanstack.com/query/v5/docs/framework/react/reference/useMutation).
### Using `UpsertUser`'s Mutation hook function
```javascript
import { getDataConnect } from 'firebase/data-connect';
import { connectorConfig, UpsertUserVariables } from '@dataconnect/generated';
import { useUpsertUser } from '@dataconnect/generated/react'
export default function UpsertUserComponent() {
// Call the Mutation hook function to get a `UseMutationResult` object which holds the state of your Mutation.
const mutation = useUpsertUser();
// You can also pass in a `DataConnect` instance to the Mutation hook function.
const dataConnect = getDataConnect(connectorConfig);
const mutation = useUpsertUser(dataConnect);
// You can also pass in a `useDataConnectMutationOptions` object to the Mutation hook function.
const options = {
onSuccess: () => { console.log('Mutation succeeded!'); }
};
const mutation = useUpsertUser(options);
// You can also pass both a `DataConnect` instance and a `useDataConnectMutationOptions` object.
const dataConnect = getDataConnect(connectorConfig);
const options = {
onSuccess: () => { console.log('Mutation succeeded!'); }
};
const mutation = useUpsertUser(dataConnect, options);
// After calling the Mutation hook function, you must call `UseMutationResult.mutate()` to execute the Mutation.
// The `useUpsertUser` Mutation requires an argument of type `UpsertUserVariables`:
const upsertUserVars: UpsertUserVariables = {
username: ...,
};
mutation.mutate(upsertUserVars);
// Variables can be defined inline as well.
mutation.mutate({ username: ..., });
// You can also pass in a `useDataConnectMutationOptions` object to `UseMutationResult.mutate()`.
const options = {
onSuccess: () => { console.log('Mutation succeeded!'); }
};
mutation.mutate(upsertUserVars, options);
// Then, you can render your component dynamically based on the status of the Mutation.
if (mutation.isPending) {
return <div>Loading...</div>;
}
if (mutation.isError) {
return <div>Error: {mutation.error.message}</div>;
}
// If the Mutation is successful, you can access the data returned using the `UseMutationResult.data` field.
if (mutation.isSuccess) {
console.log(mutation.data.user_upsert);
}
return <div>Mutation execution {mutation.isSuccess ? 'successful' : 'failed'}!</div>;
}
```
## AddReview
You can execute the `AddReview` Mutation using the `UseMutationResult` object returned by the following Mutation hook function (which is defined in [dataconnect-generated/react/index.d.ts](./index.d.ts)):
```javascript
useAddReview(options?: useDataConnectMutationOptions<AddReviewData, FirebaseError, AddReviewVariables>): UseDataConnectMutationResult<AddReviewData, AddReviewVariables>;
```
You can also pass in a `DataConnect` instance to the Mutation hook function.
```javascript
useAddReview(dc: DataConnect, options?: useDataConnectMutationOptions<AddReviewData, FirebaseError, AddReviewVariables>): UseDataConnectMutationResult<AddReviewData, AddReviewVariables>;
```
### Variables
The `AddReview` Mutation requires an argument of type `AddReviewVariables`, which is defined in [dataconnect-generated/index.d.ts](../index.d.ts). It has the following fields:
```javascript
export interface AddReviewVariables {
movieId: UUIDString;
rating: number;
reviewText: string;
}
```
### Return Type
Recall that calling the `AddReview` Mutation hook function returns a `UseMutationResult` object. This object holds the state of your Mutation, including whether the Mutation is loading, has completed, or has succeeded/failed, among other things.
To check the status of a Mutation, use the `UseMutationResult.status` field. You can also check for pending / success / error status using the `UseMutationResult.isPending`, `UseMutationResult.isSuccess`, and `UseMutationResult.isError` fields.
To execute the Mutation, call `UseMutationResult.mutate()`. This function executes the Mutation, but does not return the data from the Mutation.
To access the data returned by a Mutation, use the `UseMutationResult.data` field. The data for the `AddReview` Mutation is of type `AddReviewData`, which is defined in [dataconnect-generated/index.d.ts](../index.d.ts). It has the following fields:
```javascript
export interface AddReviewData {
review_upsert: Review_Key;
}
```
To learn more about the `UseMutationResult` object, see the [TanStack React Query documentation](https://tanstack.com/query/v5/docs/framework/react/reference/useMutation).
### Using `AddReview`'s Mutation hook function
```javascript
import { getDataConnect } from 'firebase/data-connect';
import { connectorConfig, AddReviewVariables } from '@dataconnect/generated';
import { useAddReview } from '@dataconnect/generated/react'
export default function AddReviewComponent() {
// Call the Mutation hook function to get a `UseMutationResult` object which holds the state of your Mutation.
const mutation = useAddReview();
// You can also pass in a `DataConnect` instance to the Mutation hook function.
const dataConnect = getDataConnect(connectorConfig);
const mutation = useAddReview(dataConnect);
// You can also pass in a `useDataConnectMutationOptions` object to the Mutation hook function.
const options = {
onSuccess: () => { console.log('Mutation succeeded!'); }
};
const mutation = useAddReview(options);
// You can also pass both a `DataConnect` instance and a `useDataConnectMutationOptions` object.
const dataConnect = getDataConnect(connectorConfig);
const options = {
onSuccess: () => { console.log('Mutation succeeded!'); }
};
const mutation = useAddReview(dataConnect, options);
// After calling the Mutation hook function, you must call `UseMutationResult.mutate()` to execute the Mutation.
// The `useAddReview` Mutation requires an argument of type `AddReviewVariables`:
const addReviewVars: AddReviewVariables = {
movieId: ...,
rating: ...,
reviewText: ...,
};
mutation.mutate(addReviewVars);
// Variables can be defined inline as well.
mutation.mutate({ movieId: ..., rating: ..., reviewText: ..., });
// You can also pass in a `useDataConnectMutationOptions` object to `UseMutationResult.mutate()`.
const options = {
onSuccess: () => { console.log('Mutation succeeded!'); }
};
mutation.mutate(addReviewVars, options);
// Then, you can render your component dynamically based on the status of the Mutation.
if (mutation.isPending) {
return <div>Loading...</div>;
}
if (mutation.isError) {
return <div>Error: {mutation.error.message}</div>;
}
// If the Mutation is successful, you can access the data returned using the `UseMutationResult.data` field.
if (mutation.isSuccess) {
console.log(mutation.data.review_upsert);
}
return <div>Mutation execution {mutation.isSuccess ? 'successful' : 'failed'}!</div>;
}
```
## DeleteReview
You can execute the `DeleteReview` Mutation using the `UseMutationResult` object returned by the following Mutation hook function (which is defined in [dataconnect-generated/react/index.d.ts](./index.d.ts)):
```javascript
useDeleteReview(options?: useDataConnectMutationOptions<DeleteReviewData, FirebaseError, DeleteReviewVariables>): UseDataConnectMutationResult<DeleteReviewData, DeleteReviewVariables>;
```
You can also pass in a `DataConnect` instance to the Mutation hook function.
```javascript
useDeleteReview(dc: DataConnect, options?: useDataConnectMutationOptions<DeleteReviewData, FirebaseError, DeleteReviewVariables>): UseDataConnectMutationResult<DeleteReviewData, DeleteReviewVariables>;
```
### Variables
The `DeleteReview` Mutation requires an argument of type `DeleteReviewVariables`, which is defined in [dataconnect-generated/index.d.ts](../index.d.ts). It has the following fields:
```javascript
export interface DeleteReviewVariables {
movieId: UUIDString;
}
```
### Return Type
Recall that calling the `DeleteReview` Mutation hook function returns a `UseMutationResult` object. This object holds the state of your Mutation, including whether the Mutation is loading, has completed, or has succeeded/failed, among other things.
To check the status of a Mutation, use the `UseMutationResult.status` field. You can also check for pending / success / error status using the `UseMutationResult.isPending`, `UseMutationResult.isSuccess`, and `UseMutationResult.isError` fields.
To execute the Mutation, call `UseMutationResult.mutate()`. This function executes the Mutation, but does not return the data from the Mutation.
To access the data returned by a Mutation, use the `UseMutationResult.data` field. The data for the `DeleteReview` Mutation is of type `DeleteReviewData`, which is defined in [dataconnect-generated/index.d.ts](../index.d.ts). It has the following fields:
```javascript
export interface DeleteReviewData {
review_delete?: Review_Key | null;
}
```
To learn more about the `UseMutationResult` object, see the [TanStack React Query documentation](https://tanstack.com/query/v5/docs/framework/react/reference/useMutation).
### Using `DeleteReview`'s Mutation hook function
```javascript
import { getDataConnect } from 'firebase/data-connect';
import { connectorConfig, DeleteReviewVariables } from '@dataconnect/generated';
import { useDeleteReview } from '@dataconnect/generated/react'
export default function DeleteReviewComponent() {
// Call the Mutation hook function to get a `UseMutationResult` object which holds the state of your Mutation.
const mutation = useDeleteReview();
// You can also pass in a `DataConnect` instance to the Mutation hook function.
const dataConnect = getDataConnect(connectorConfig);
const mutation = useDeleteReview(dataConnect);
// You can also pass in a `useDataConnectMutationOptions` object to the Mutation hook function.
const options = {
onSuccess: () => { console.log('Mutation succeeded!'); }
};
const mutation = useDeleteReview(options);
// You can also pass both a `DataConnect` instance and a `useDataConnectMutationOptions` object.
const dataConnect = getDataConnect(connectorConfig);
const options = {
onSuccess: () => { console.log('Mutation succeeded!'); }
};
const mutation = useDeleteReview(dataConnect, options);
// After calling the Mutation hook function, you must call `UseMutationResult.mutate()` to execute the Mutation.
// The `useDeleteReview` Mutation requires an argument of type `DeleteReviewVariables`:
const deleteReviewVars: DeleteReviewVariables = {
movieId: ...,
};
mutation.mutate(deleteReviewVars);
// Variables can be defined inline as well.
mutation.mutate({ movieId: ..., });
// You can also pass in a `useDataConnectMutationOptions` object to `UseMutationResult.mutate()`.
const options = {
onSuccess: () => { console.log('Mutation succeeded!'); }
};
mutation.mutate(deleteReviewVars, options);
// Then, you can render your component dynamically based on the status of the Mutation.
if (mutation.isPending) {
return <div>Loading...</div>;
}
if (mutation.isError) {
return <div>Error: {mutation.error.message}</div>;
}
// If the Mutation is successful, you can access the data returned using the `UseMutationResult.data` field.
if (mutation.isSuccess) {
console.log(mutation.data.review_delete);
}
return <div>Mutation execution {mutation.isSuccess ? 'successful' : 'failed'}!</div>;
}
```

View File

@@ -0,0 +1,66 @@
import { createMovieRef, upsertUserRef, addReviewRef, deleteReviewRef, listMoviesRef, listUsersRef, listUserReviewsRef, getMovieByIdRef, searchMovieRef, connectorConfig } from '../../esm/index.esm.js';
import { validateArgs, CallerSdkTypeEnum } from 'firebase/data-connect';
import { useDataConnectQuery, useDataConnectMutation, validateReactArgs } from '@tanstack-query-firebase/react/data-connect';
export function useCreateMovie(dcOrOptions, options) {
const { dc: dcInstance, vars: inputOpts } = validateArgs(connectorConfig, dcOrOptions, options);
function refFactory(vars) {
return createMovieRef(dcInstance, vars);
}
return useDataConnectMutation(refFactory, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}
export function useUpsertUser(dcOrOptions, options) {
const { dc: dcInstance, vars: inputOpts } = validateArgs(connectorConfig, dcOrOptions, options);
function refFactory(vars) {
return upsertUserRef(dcInstance, vars);
}
return useDataConnectMutation(refFactory, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}
export function useAddReview(dcOrOptions, options) {
const { dc: dcInstance, vars: inputOpts } = validateArgs(connectorConfig, dcOrOptions, options);
function refFactory(vars) {
return addReviewRef(dcInstance, vars);
}
return useDataConnectMutation(refFactory, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}
export function useDeleteReview(dcOrOptions, options) {
const { dc: dcInstance, vars: inputOpts } = validateArgs(connectorConfig, dcOrOptions, options);
function refFactory(vars) {
return deleteReviewRef(dcInstance, vars);
}
return useDataConnectMutation(refFactory, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}
export function useListMovies(dcOrOptions, options) {
const { dc: dcInstance, options: inputOpts } = validateReactArgs(connectorConfig, dcOrOptions, options);
const ref = listMoviesRef(dcInstance);
return useDataConnectQuery(ref, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}
export function useListUsers(dcOrOptions, options) {
const { dc: dcInstance, options: inputOpts } = validateReactArgs(connectorConfig, dcOrOptions, options);
const ref = listUsersRef(dcInstance);
return useDataConnectQuery(ref, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}
export function useListUserReviews(dcOrOptions, options) {
const { dc: dcInstance, options: inputOpts } = validateReactArgs(connectorConfig, dcOrOptions, options);
const ref = listUserReviewsRef(dcInstance);
return useDataConnectQuery(ref, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}
export function useGetMovieById(dcOrVars, varsOrOptions, options) {
const { dc: dcInstance, vars: inputVars, options: inputOpts } = validateReactArgs(connectorConfig, dcOrVars, varsOrOptions, options, true, true);
const ref = getMovieByIdRef(dcInstance, inputVars);
return useDataConnectQuery(ref, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}
export function useSearchMovie(dcOrVars, varsOrOptions, options) {
const { dc: dcInstance, vars: inputVars, options: inputOpts } = validateReactArgs(connectorConfig, dcOrVars, varsOrOptions, options, true, false);
const ref = searchMovieRef(dcInstance, inputVars);
return useDataConnectQuery(ref, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}

View File

@@ -0,0 +1 @@
{"type":"module"}

View File

@@ -0,0 +1,66 @@
const { createMovieRef, upsertUserRef, addReviewRef, deleteReviewRef, listMoviesRef, listUsersRef, listUserReviewsRef, getMovieByIdRef, searchMovieRef, connectorConfig } = require('../index.cjs.js');
const { validateArgs, CallerSdkTypeEnum } = require('firebase/data-connect');
const { useDataConnectQuery, useDataConnectMutation, validateReactArgs } = require('@tanstack-query-firebase/react/data-connect');
exports.useCreateMovie = function useCreateMovie(dcOrOptions, options) {
const { dc: dcInstance, vars: inputOpts } = validateArgs(connectorConfig, dcOrOptions, options);
function refFactory(vars) {
return createMovieRef(dcInstance, vars);
}
return useDataConnectMutation(refFactory, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}
exports.useUpsertUser = function useUpsertUser(dcOrOptions, options) {
const { dc: dcInstance, vars: inputOpts } = validateArgs(connectorConfig, dcOrOptions, options);
function refFactory(vars) {
return upsertUserRef(dcInstance, vars);
}
return useDataConnectMutation(refFactory, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}
exports.useAddReview = function useAddReview(dcOrOptions, options) {
const { dc: dcInstance, vars: inputOpts } = validateArgs(connectorConfig, dcOrOptions, options);
function refFactory(vars) {
return addReviewRef(dcInstance, vars);
}
return useDataConnectMutation(refFactory, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}
exports.useDeleteReview = function useDeleteReview(dcOrOptions, options) {
const { dc: dcInstance, vars: inputOpts } = validateArgs(connectorConfig, dcOrOptions, options);
function refFactory(vars) {
return deleteReviewRef(dcInstance, vars);
}
return useDataConnectMutation(refFactory, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}
exports.useListMovies = function useListMovies(dcOrOptions, options) {
const { dc: dcInstance, options: inputOpts } = validateReactArgs(connectorConfig, dcOrOptions, options);
const ref = listMoviesRef(dcInstance);
return useDataConnectQuery(ref, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}
exports.useListUsers = function useListUsers(dcOrOptions, options) {
const { dc: dcInstance, options: inputOpts } = validateReactArgs(connectorConfig, dcOrOptions, options);
const ref = listUsersRef(dcInstance);
return useDataConnectQuery(ref, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}
exports.useListUserReviews = function useListUserReviews(dcOrOptions, options) {
const { dc: dcInstance, options: inputOpts } = validateReactArgs(connectorConfig, dcOrOptions, options);
const ref = listUserReviewsRef(dcInstance);
return useDataConnectQuery(ref, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}
exports.useGetMovieById = function useGetMovieById(dcOrVars, varsOrOptions, options) {
const { dc: dcInstance, vars: inputVars, options: inputOpts } = validateReactArgs(connectorConfig, dcOrVars, varsOrOptions, options, true, true);
const ref = getMovieByIdRef(dcInstance, inputVars);
return useDataConnectQuery(ref, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}
exports.useSearchMovie = function useSearchMovie(dcOrVars, varsOrOptions, options) {
const { dc: dcInstance, vars: inputVars, options: inputOpts } = validateReactArgs(connectorConfig, dcOrVars, varsOrOptions, options, true, false);
const ref = searchMovieRef(dcInstance, inputVars);
return useDataConnectQuery(ref, inputOpts, CallerSdkTypeEnum.GeneratedReact);
}

View File

@@ -0,0 +1,33 @@
import { CreateMovieData, CreateMovieVariables, UpsertUserData, UpsertUserVariables, AddReviewData, AddReviewVariables, DeleteReviewData, DeleteReviewVariables, ListMoviesData, ListUsersData, ListUserReviewsData, GetMovieByIdData, GetMovieByIdVariables, SearchMovieData, SearchMovieVariables } from '../';
import { UseDataConnectQueryResult, useDataConnectQueryOptions, UseDataConnectMutationResult, useDataConnectMutationOptions} from '@tanstack-query-firebase/react/data-connect';
import { UseQueryResult, UseMutationResult} from '@tanstack/react-query';
import { DataConnect } from 'firebase/data-connect';
import { FirebaseError } from 'firebase/app';
export function useCreateMovie(options?: useDataConnectMutationOptions<CreateMovieData, FirebaseError, CreateMovieVariables>): UseDataConnectMutationResult<CreateMovieData, CreateMovieVariables>;
export function useCreateMovie(dc: DataConnect, options?: useDataConnectMutationOptions<CreateMovieData, FirebaseError, CreateMovieVariables>): UseDataConnectMutationResult<CreateMovieData, CreateMovieVariables>;
export function useUpsertUser(options?: useDataConnectMutationOptions<UpsertUserData, FirebaseError, UpsertUserVariables>): UseDataConnectMutationResult<UpsertUserData, UpsertUserVariables>;
export function useUpsertUser(dc: DataConnect, options?: useDataConnectMutationOptions<UpsertUserData, FirebaseError, UpsertUserVariables>): UseDataConnectMutationResult<UpsertUserData, UpsertUserVariables>;
export function useAddReview(options?: useDataConnectMutationOptions<AddReviewData, FirebaseError, AddReviewVariables>): UseDataConnectMutationResult<AddReviewData, AddReviewVariables>;
export function useAddReview(dc: DataConnect, options?: useDataConnectMutationOptions<AddReviewData, FirebaseError, AddReviewVariables>): UseDataConnectMutationResult<AddReviewData, AddReviewVariables>;
export function useDeleteReview(options?: useDataConnectMutationOptions<DeleteReviewData, FirebaseError, DeleteReviewVariables>): UseDataConnectMutationResult<DeleteReviewData, DeleteReviewVariables>;
export function useDeleteReview(dc: DataConnect, options?: useDataConnectMutationOptions<DeleteReviewData, FirebaseError, DeleteReviewVariables>): UseDataConnectMutationResult<DeleteReviewData, DeleteReviewVariables>;
export function useListMovies(options?: useDataConnectQueryOptions<ListMoviesData>): UseDataConnectQueryResult<ListMoviesData, undefined>;
export function useListMovies(dc: DataConnect, options?: useDataConnectQueryOptions<ListMoviesData>): UseDataConnectQueryResult<ListMoviesData, undefined>;
export function useListUsers(options?: useDataConnectQueryOptions<ListUsersData>): UseDataConnectQueryResult<ListUsersData, undefined>;
export function useListUsers(dc: DataConnect, options?: useDataConnectQueryOptions<ListUsersData>): UseDataConnectQueryResult<ListUsersData, undefined>;
export function useListUserReviews(options?: useDataConnectQueryOptions<ListUserReviewsData>): UseDataConnectQueryResult<ListUserReviewsData, undefined>;
export function useListUserReviews(dc: DataConnect, options?: useDataConnectQueryOptions<ListUserReviewsData>): UseDataConnectQueryResult<ListUserReviewsData, undefined>;
export function useGetMovieById(vars: GetMovieByIdVariables, options?: useDataConnectQueryOptions<GetMovieByIdData>): UseDataConnectQueryResult<GetMovieByIdData, GetMovieByIdVariables>;
export function useGetMovieById(dc: DataConnect, vars: GetMovieByIdVariables, options?: useDataConnectQueryOptions<GetMovieByIdData>): UseDataConnectQueryResult<GetMovieByIdData, GetMovieByIdVariables>;
export function useSearchMovie(vars?: SearchMovieVariables, options?: useDataConnectQueryOptions<SearchMovieData>): UseDataConnectQueryResult<SearchMovieData, SearchMovieVariables>;
export function useSearchMovie(dc: DataConnect, vars?: SearchMovieVariables, options?: useDataConnectQueryOptions<SearchMovieData>): UseDataConnectQueryResult<SearchMovieData, SearchMovieVariables>;

View File

@@ -0,0 +1,17 @@
{
"name": "@dataconnect/generated-react",
"version": "1.0.0",
"author": "Firebase <firebase-support@google.com> (https://firebase.google.com/)",
"description": "Generated SDK For example",
"license": "Apache-2.0",
"engines": {
"node": " >=18.0"
},
"typings": "index.d.ts",
"main": "index.cjs.js",
"module": "esm/index.esm.js",
"browser": "esm/index.esm.js",
"peerDependencies": {
"@tanstack-query-firebase/react": "^2.0.0"
}
}

View File

@@ -20,6 +20,9 @@
- name: "refactor" - name: "refactor"
description: "Code changes that neither fix a bug nor add a feature" description: "Code changes that neither fix a bug nor add a feature"
color: "f29513" color: "f29513"
- name: "security"
description: "Tasks related to security enhancements, audits, or fixes"
color: "000000" # Black for security, to make it stand out
# By Platform # By Platform
- name: "platform:web" - name: "platform:web"
@@ -31,6 +34,9 @@
- name: "platform:backend" - name: "platform:backend"
description: "Tasks for Data Connect or Cloud Functions" description: "Tasks for Data Connect or Cloud Functions"
color: "5319e7" color: "5319e7"
- name: "platform:admin"
description: "Tasks specific to the Admin Console web app"
color: "5319e7"
# For Project Management # For Project Management
- name: "sred-eligible" - name: "sred-eligible"

132
memo-iap.txt Normal file
View File

@@ -0,0 +1,132 @@
# ===================================================================
# MEMO : Sécurisation d'un service Cloud Run avec IAP (Identity-Aware Proxy)
# ===================================================================
#
# Objectif : Ce document résume les étapes et les permissions nécessaires
# pour sécuriser un service Cloud Run avec IAP, en se basant sur
# l'expérience acquise avec le service 'internal-launchpad'.
#
# Date : 2025-11-16
# ---
# 1. PRINCIPE FONDAMENTAL ET CONTRAINTE MAJEURE
# ---
#
# L'enseignement le plus important est le suivant :
#
# > L'activation d'IAP directement sur un service Cloud Run (sans Load Balancer)
# > restreint l'accès aux utilisateurs qui font partie de la MÊME organisation
# > Google Workspace que le projet Google Cloud.
#
# Dans notre cas, le projet 'krow-workforce-dev' appartient à l'organisation
# 'krowwithus.com'. C'est pourquoi :
# - 'admin@krowwithus.com' A PU ACCÉDER au service.
# - 'boris@oloodi.com' N'A PAS PU ACCÉDER au service, même avec les bonnes permissions.
#
# Solution adoptée : Créer des comptes utilisateurs via Google Workspace ou
# Cloud Identity (qui est gratuit) dans le domaine 'krowwithus.com' pour
# tous les développeurs et administrateurs qui ont besoin d'accéder aux
# services internes.
# ---
# 2. PRÉREQUIS INDISPENSABLES
# ---
#
# Avant de commencer, les éléments suivants doivent être configurés pour le projet :
#
# a) APIs Google Cloud activées :
# - Cloud Run API (`run.googleapis.com`)
# - Identity-Aware Proxy API (`iap.googleapis.com`)
# - Cloud Build API (`cloudbuild.googleapis.com`)
# - Artifact Registry API (`artifactregistry.googleapis.com`)
#
# Commande pour les activer :
# gcloud services enable run.googleapis.com iap.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com --project=krow-workforce-dev
#
# b) Écran de consentement OAuth configuré :
# - IAP utilise OAuth2 pour identifier les utilisateurs. L'écran de consentement est obligatoire.
# - URL : https://console.cloud.google.com/apis/credentials/consent?project=krow-workforce-dev
# - Type d'utilisateur : "Interne" est préférable si disponible.
# - Nom de l'application : Utiliser un nom général (ex: "Krow Workforce Platform").
#
# c) Compte de service IAP existant :
# - Parfois, ce compte n'est pas créé automatiquement. Il est plus sûr de forcer sa création.
#
# Commande pour le créer :
# gcloud beta services identity create --service=iap.googleapis.com --project=krow-workforce-dev
# ---
# 3. LOGIQUE DE CONFIGURATION EN 3 ÉTAPES (AUTOMATISÉE DANS LE MAKEFILE)
# ---
#
# Le processus complet pour sécuriser un service se déroule en 3 étapes séquentielles.
#
# ÉTAPE A : DÉPLOIEMENT SÉCURISÉ DU SERVICE
# -----------------------------------------
# Le service doit être déployé en mode privé, puis IAP doit être activé dessus.
#
# 1. Déployer le service avec l'accès public désactivé :
# gcloud run deploy <SERVICE_NAME> --image <IMAGE_URI> --no-allow-unauthenticated ...
#
# 2. Activer IAP sur le service :
# gcloud beta run services update <SERVICE_NAME> --iap ...
#
#
# ÉTAPE B : AUTORISER IAP À APPELER LE SERVICE
# -------------------------------------------
# Une fois IAP activé, le proxy IAP a besoin de la permission d'invoquer (d'appeler)
# votre service Cloud Run. Cette permission est accordée au compte de service IAP.
#
# Commande :
# gcloud run services add-iam-policy-binding <SERVICE_NAME> \
# --member="serviceAccount:service-<PROJECT_NUMBER>@gcp-sa-iap.iam.gserviceaccount.com" \
# --role="roles/run.invoker"
#
#
# ÉTAPE C : AUTORISER LES UTILISATEURS À PASSER LE PROXY IAP
# -------------------------------------------------------
# C'est ici qu'on ajoute les utilisateurs finaux (qui doivent être dans l'organisation).
# Cette commande est spécifique à IAP pour Cloud Run.
#
# Commande :
# gcloud beta iap web add-iam-policy-binding \
# --resource-type=cloud-run \
# --service=<SERVICE_NAME> \
# --member="user:email@krowwithus.com" \
# --role="roles/iap.httpsResourceAccessor"
# ---
# 4. AUTOMATISATION VIA LE MAKEFILE
# ---
#
# Tout ce processus est géré par la commande `make deploy-launchpad-full`.
#
# a) `deploy-launchpad` s'occupe de l'ÉTAPE A.
#
# b) `configure-iap-launchpad` s'occupe des ÉTAPES B et C.
#
# - Il lit les utilisateurs depuis `firebase/internal-launchpad/iap-users.txt`.
# - Il exécute la commande de l'ÉTAPE B pour le compte de service IAP.
# - Il exécute la commande de l'ÉTAPE C pour chaque utilisateur du fichier.
#
# Pour adapter cela à 'admin-web', il faudra :
# 1. Créer des variables similaires à `CR_LAUNCHPAD_...` pour `admin-web`.
# 2. Créer de nouvelles cibles `deploy-admin-web-full`, `configure-iap-admin-web`, etc.,
# en copiant la logique de celles du launchpad et en adaptant les noms de service.
# ---
# 5. VÉRIFICATION ET DÉPANNAGE
# ---
#
# a) Lister les utilisateurs autorisés :
# La commande `make list-iap-users` est le meilleur moyen de vérifier qui a accès.
#
# b) Erreur "You don't have access" :
# - Cause 1 : L'utilisateur n'est pas dans la bonne organisation (le plus probable).
# - Cause 2 : L'utilisateur n'est pas dans le fichier `iap-users.txt` et `make configure-iap-launchpad` n'a pas été lancé.
# - Cause 3 : Problème de cache de navigateur. Toujours tester dans une nouvelle fenêtre de navigation privée.
# - Cause 4 : Délai de propagation des permissions IAM (attendre 2-3 minutes).
#
# c) Erreur "Forbidden" :
# - Cela signifie que l'authentification a réussi mais que la permission d'invoquer le service est manquante.
# - La cause la plus probable est que l'ÉTAPE B (donner la permission au compte de service IAP) a échoué ou a été oubliée.

View File

@@ -1,17 +1,49 @@
#!/bin/bash #!/bin/bash
# ==================================================================================== # ====================================================================================
# SCRIPT TO EXPORT SR&ED-ELIGIBLE GITHUB ISSUES TO A MARKDOWN FILE # SCRIPT TO EXPORT GITHUB ISSUES TO A MARKDOWN FILE
# ==================================================================================== # ====================================================================================
set -e # Exit script if a command fails set -e # Exit script if a command fails
# --- Configuration --- # --- Default Configuration ---
OUTPUT_FILE="sred-issues-export.md"
# This is the label we will use to identify SR&ED-eligible tasks
SRED_LABEL="sred-eligible"
ISSUE_LIMIT=1000 ISSUE_LIMIT=1000
DEFAULT_LABEL="sred-eligible"
DEFAULT_STATE="open"
echo "🚀 Starting export of SR&ED-eligible issues to '${OUTPUT_FILE}'..." # --- Parse Command Line Arguments ---
STATE=""
LABEL=""
# If no arguments are provided, run in legacy SR&ED mode
if [ $# -eq 0 ]; then
STATE=$DEFAULT_STATE
LABEL=$DEFAULT_LABEL
else
while [[ "$#" -gt 0 ]]; do
case $1 in
--state=*) STATE="${1#*=}" ;;
--state) STATE="$2"; shift ;;
--label=*) LABEL="${1#*=}" ;;
--label) LABEL="$2"; shift ;;
*) echo "Unknown parameter passed: $1"; exit 1 ;;
esac
shift
done
fi
# --- Dynamic Configuration ---
STATE_FOR_CMD=${STATE:-"open"} # Default to open if state is empty
LABEL_FOR_FILENAME=${LABEL:-"all"}
STATE_FOR_FILENAME=${STATE:-"open"}
OUTPUT_FILE="export-issues-${STATE_FOR_FILENAME}-${LABEL_FOR_FILENAME}.md"
TITLE="Export of GitHub Issues (State: ${STATE_FOR_FILENAME}, Label: ${LABEL_FOR_FILENAME})"
FETCH_MESSAGE="Fetching issues with state '${STATE_FOR_CMD}'"
if [ -n "$LABEL" ]; then
FETCH_MESSAGE="${FETCH_MESSAGE} and label '${LABEL}'"
fi
echo "🚀 Starting export of GitHub issues to '${OUTPUT_FILE}'..."
# --- Step 1: Dependency Check --- # --- Step 1: Dependency Check ---
echo "1. Checking for 'gh' CLI dependency..." echo "1. Checking for 'gh' CLI dependency..."
@@ -22,27 +54,30 @@ fi
echo "✅ 'gh' CLI found." echo "✅ 'gh' CLI found."
# --- Step 2: Initialize Output File --- # --- Step 2: Initialize Output File ---
echo "# Export of SR&ED-Eligible Issues" > "$OUTPUT_FILE" echo "# ${TITLE}" > "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE" echo "" >> "$OUTPUT_FILE"
echo "*This document lists the systematic investigations and experimental development tasks undertaken during this period. Export generated on $(date)*." >> "$OUTPUT_FILE" echo "*Export generated on $(date)*." >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE" echo "" >> "$OUTPUT_FILE"
# --- Step 3: Fetch SR&ED-Eligible Issues --- # --- Step 3: Build 'gh' command and Fetch Issues ---
echo "2. Fetching open issues with the '${SRED_LABEL}' label..." echo "2. ${FETCH_MESSAGE}..."
# We use 'gh issue list' with a JSON output and parse it with 'jq' for robustness. GH_COMMAND=("gh" "issue" "list" "--state" "${STATE_FOR_CMD}" "--limit" "$ISSUE_LIMIT" "--json" "number")
# This is more reliable than parsing text output. if [ -n "$LABEL" ]; then
issue_numbers=$(gh issue list --state open --label "${SRED_LABEL}" --limit $ISSUE_LIMIT --json number | jq -r '.[].number') GH_COMMAND+=("--label" "${LABEL}")
fi
issue_numbers=$("${GH_COMMAND[@]}" | jq -r '.[].number')
if [ -z "$issue_numbers" ]; then if [ -z "$issue_numbers" ]; then
echo "⚠️ No open issues found with the label '${SRED_LABEL}'. The export file will be minimal." echo "⚠️ No issues found matching the criteria."
echo "" >> "$OUTPUT_FILE" echo "" >> "$OUTPUT_FILE"
echo "**No SR&ED-eligible issues found for this period.**" >> "$OUTPUT_FILE" echo "**No issues found.**" >> "$OUTPUT_FILE"
exit 0 exit 0
fi fi
total_issues=$(echo "$issue_numbers" | wc -l | xargs) total_issues=$(echo "$issue_numbers" | wc -l | xargs)
echo "✅ Found ${total_issues} SR&ED-eligible issue(s)." echo "✅ Found ${total_issues} issue(s)."
# --- Step 4: Loop Through Each Issue and Format the Output --- # --- Step 4: Loop Through Each Issue and Format the Output ---
echo "3. Formatting details for each issue..." echo "3. Formatting details for each issue..."
@@ -53,11 +88,10 @@ for number in $issue_numbers; do
echo " -> Processing issue #${number} (${current_issue}/${total_issues})" echo " -> Processing issue #${number} (${current_issue}/${total_issues})"
# Use 'gh issue view' with a template to format the output for each issue # Use 'gh issue view' with a template to format the output for each issue
# and append it to the output file. gh issue view "$number" --json number,title,body,author,createdAt,state --template \
gh issue view "$number" --json number,title,body,author,createdAt --template \ '\n### [#{{.number}}] {{.title}} ({{.state}})\n\n{{if .body}}{{.body}}{{else}}*No description provided.*{{end}}\n\n**Meta:** {{.author.login}} | **Created:** {{timefmt "2006-01-02" .createdAt}}\n***\n' >> "$OUTPUT_FILE"
'\n### Task: [#{{.number}}] {{.title}}\n\n**Hypothesis/Goal:** \n> *(Briefly describe the technological uncertainty this task addresses. What was the technical challenge or question?)*\n\n**Systematic Investigation:**\n{{if .body}}\n{{.body}}\n{{else}}\n*No detailed description provided in the issue.*\n{{end}}\n\n**Team:** {{.author.login}} | **Date Initiated:** {{timefmt "2006-01-02" .createdAt}}\n***\n' >> "$OUTPUT_FILE"
done done
echo "" echo ""
echo "🎉 Export complete!" echo "🎉 Export complete!"
echo "Your SR&ED-ready markdown file is ready: ${OUTPUT_FILE}" echo "Your markdown file is ready: ${OUTPUT_FILE}"