{projectId ? (
-
+
Back to Projects
@@ -37,41 +42,41 @@ function Sidebar({ logo, isOpen, onClose }) {
{projectId ? (
<>
Project
-
+
Overview
-
+
Database
-
+
Authentication
-
+
Storage
-
+
Webhooks
-
+
Analytics
-
+
Team
-
+
Settings
>
) : (
<>
General
-
+
Dashboard
-
+
What's New
-
+
Settings
>
@@ -80,7 +85,7 @@ function Sidebar({ logo, isOpen, onClose }) {
-
@@ -88,4 +93,4 @@ function Sidebar({ logo, isOpen, onClose }) {
);
}
-export default Sidebar;
\ No newline at end of file
+export default Sidebar;
diff --git a/apps/web-dashboard/src/components/Onboarding/OnboardingChecklist.jsx b/apps/web-dashboard/src/components/Onboarding/OnboardingChecklist.jsx
index d12bd6c60..23e0c1e8b 100644
--- a/apps/web-dashboard/src/components/Onboarding/OnboardingChecklist.jsx
+++ b/apps/web-dashboard/src/components/Onboarding/OnboardingChecklist.jsx
@@ -4,11 +4,11 @@ import { useOnboarding } from '../../context/OnboardingContext';
import { Check, ChevronRight, ChevronDown, ChevronUp, X, Rocket, ExternalLink } from 'lucide-react';
const OnboardingChecklist = () => {
- const { steps, progress, isVisible, dismissOnboarding } = useOnboarding();
+ const { steps, progress, isVisible, dismissOnboarding, allCompleted } = useOnboarding();
const [isExpanded, setIsExpanded] = useState(true);
const navigate = useNavigate();
- if (!isVisible) return null;
+ if (!isVisible || allCompleted) return null;
const totalSteps = steps?.length || 0;
const completedCount = steps?.filter((step) => progress[step.key]).length || 0;
diff --git a/apps/web-dashboard/src/components/ProtectedRoute.jsx b/apps/web-dashboard/src/components/ProtectedRoute.jsx
index cb83e7f3d..a7d410795 100644
--- a/apps/web-dashboard/src/components/ProtectedRoute.jsx
+++ b/apps/web-dashboard/src/components/ProtectedRoute.jsx
@@ -1,9 +1,15 @@
-import { Navigate } from 'react-router-dom';
+import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
// This component takes other components as children
-const ProtectedRoute = ({ children }) => {
- const { isAuthenticated, isLoading } = useAuth();
+const ProtectedRoute = ({
+ children,
+ allowUnverified = false,
+ allowIncompleteOnboarding = false,
+ onboardingOnly = false,
+}) => {
+ const { user, isAuthenticated, isLoading } = useAuth();
+ const location = useLocation();
if (isLoading) {
return (
@@ -25,8 +31,47 @@ const ProtectedRoute = ({ children }) => {
return
;
}
- // If authenticated, render the child component (e.g., the Dashboard)
+ if (!allowUnverified && !user?.isVerified) {
+ return
;
+ }
+
+ const onboardingCompleted = !!user?.onboarding?.completed;
+
+ if (onboardingCompleted) {
+ if (onboardingOnly || location.pathname.startsWith('/onboarding')) {
+ return
;
+ }
+ } else if (!allowIncompleteOnboarding) {
+ const steps = user?.onboarding?.steps || {};
+
+ if (!onboardingOnly || location.pathname === '/onboarding' || location.pathname === '/onboarding/') {
+ if (!steps.projectCreated) {
+ return
;
+ } else if (!steps.collectionCreated) {
+ return
;
+ } else {
+ return
;
+ }
+ }
+
+ // If on /onboarding/*, enforce step prerequisites strictly but allow revisiting earlier steps
+ if (!steps.projectCreated) {
+ if (location.pathname !== '/onboarding/project') {
+ return
;
+ }
+ } else if (!steps.collectionCreated) {
+ if (location.pathname !== '/onboarding/collection' && location.pathname !== '/onboarding/project') {
+ return
;
+ }
+ } else {
+ // All steps completed, allow any onboarding route, but default to /api if root
+ if (location.pathname === '/onboarding' || location.pathname === '/onboarding/') {
+ return
;
+ }
+ }
+ }
+
return children;
};
-export default ProtectedRoute;
\ No newline at end of file
+export default ProtectedRoute;
diff --git a/apps/web-dashboard/src/components/ThemeToggle.jsx b/apps/web-dashboard/src/components/ThemeToggle.jsx
index 844c35e28..1d4b718c2 100644
--- a/apps/web-dashboard/src/components/ThemeToggle.jsx
+++ b/apps/web-dashboard/src/components/ThemeToggle.jsx
@@ -84,6 +84,7 @@ const ThemeToggle = () => {
style={{ width: '100%', justifyContent: 'flex-start', marginBottom: '8px' }}
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
aria-pressed={isDark}
+ title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
>
{isDark ?
:
}
{isDark ? 'Light Mode' : 'Dark Mode'}
@@ -91,4 +92,4 @@ const ThemeToggle = () => {
);
};
-export default ThemeToggle;
\ No newline at end of file
+export default ThemeToggle;
diff --git a/apps/web-dashboard/src/constants/onboarding.js b/apps/web-dashboard/src/constants/onboarding.js
index e5710d0b8..de7d4255d 100644
--- a/apps/web-dashboard/src/constants/onboarding.js
+++ b/apps/web-dashboard/src/constants/onboarding.js
@@ -1,20 +1,20 @@
export const ONBOARDING_STEPS = [
{
key: 'create_project',
- title: 'Create your first project',
- description: 'Start by creating a workspace for your data.',
+ title: 'Create Project',
+ description: 'Create the backend container for your data, auth, storage, and API settings.',
getPath: () => '/create-project'
},
{
key: 'create_collection',
- title: 'Create a collection',
- description: 'Define your data schema and collections.',
+ title: 'Create Collection',
+ description: 'Add the first collection that defines how your backend stores data.',
getPath: ({ projectId }) => projectId ? `/project/${projectId}/create-collection` : '/dashboard'
},
{
key: 'get_api_key',
- title: 'Get your API Key',
- description: 'Access your project via the Public API.',
+ title: 'Reveal API Keys',
+ description: 'Verify your email to reveal live API keys and unlock production requests.',
getPath: ({ projectId }) => projectId ? `/project/${projectId}` : '/dashboard'
},
{
diff --git a/apps/web-dashboard/src/context/AuthContext.jsx b/apps/web-dashboard/src/context/AuthContext.jsx
index 7fa840c83..109018cb5 100644
--- a/apps/web-dashboard/src/context/AuthContext.jsx
+++ b/apps/web-dashboard/src/context/AuthContext.jsx
@@ -38,11 +38,18 @@ export const AuthProvider = ({ children }) => {
setUser(userData);
};
- const value = { user, login, logout, isAuthenticated: !!user, isLoading };
+ const updateUser = (updater) => {
+ setUser((currentUser) => {
+ if (!currentUser) return currentUser;
+ return typeof updater === 'function' ? updater(currentUser) : updater;
+ });
+ };
+
+ const value = { user, login, logout, updateUser, isAuthenticated: !!user, isLoading };
return
{children};
};
// eslint-disable-next-line react-refresh/only-export-components
-export const useAuth = () => useContext(AuthContext);
\ No newline at end of file
+export const useAuth = () => useContext(AuthContext);
diff --git a/apps/web-dashboard/src/context/OnboardingContext.jsx b/apps/web-dashboard/src/context/OnboardingContext.jsx
index a1fb5c5bb..200bd143a 100644
--- a/apps/web-dashboard/src/context/OnboardingContext.jsx
+++ b/apps/web-dashboard/src/context/OnboardingContext.jsx
@@ -2,24 +2,35 @@
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
import { ONBOARDING_STEPS } from '../constants/onboarding';
import { useAuth } from './AuthContext';
+import api from '../utils/api';
const OnboardingContext = createContext(null);
-const safeJsonParse = (value, fallback) => {
- try {
- return value ? JSON.parse(value) : fallback;
- } catch {
- return fallback;
- }
+const SERVER_STEP_BY_UI_STEP = {
+ create_project: 'projectCreated',
+ create_collection: 'collectionCreated',
+ make_api_call: 'firstApiCall'
+};
+
+const getServerProgress = (user) => {
+ const steps = user?.onboarding?.steps || {};
+ return {
+ create_project: Boolean(steps.projectCreated),
+ create_collection: Boolean(steps.collectionCreated),
+ get_api_key: Boolean(user?.isVerified),
+ make_api_call: Boolean(steps.firstApiCall),
+ currentStep: user?.onboarding?.currentStep || 'project',
+ projectId: user?.onboarding?.projectId || null,
+ collectionId: user?.onboarding?.collectionId || null
+ };
};
export const OnboardingProvider = ({ children }) => {
- const { user } = useAuth();
+ const { user, updateUser } = useAuth();
const userId = user?._id || 'anonymous';
const storageKeys = useMemo(() => {
return {
- progress: `onboarding_progress:${userId}`,
dismissed: `onboarding_dismissed:${userId}`,
activeProjectId: `onboarding_active_project_id:${userId}`
};
@@ -30,19 +41,14 @@ export const OnboardingProvider = ({ children }) => {
const [activeProjectId, setActiveProjectIdState] = useState(null);
useEffect(() => {
- const savedProgress = localStorage.getItem(storageKeys.progress);
const savedDismissed = localStorage.getItem(storageKeys.dismissed);
const savedActiveProjectId = localStorage.getItem(storageKeys.activeProjectId);
// eslint-disable-next-line react-hooks/set-state-in-effect
- setProgress(safeJsonParse(savedProgress, {}));
+ setProgress(getServerProgress(user));
setIsDismissed(savedDismissed === 'true');
setActiveProjectIdState(savedActiveProjectId || null);
- }, [storageKeys]);
-
- useEffect(() => {
- localStorage.setItem(storageKeys.progress, JSON.stringify(progress));
- }, [progress, storageKeys]);
+ }, [storageKeys, user]);
useEffect(() => {
localStorage.setItem(storageKeys.dismissed, isDismissed.toString());
@@ -53,15 +59,38 @@ export const OnboardingProvider = ({ children }) => {
else localStorage.removeItem(storageKeys.activeProjectId);
}, [activeProjectId, storageKeys]);
- const completeStep = useCallback((stepKey) => {
- setProgress(prev => {
- if (prev[stepKey]) return prev;
- return {
- ...prev,
- [stepKey]: true
- };
- });
- }, []);
+ const completeStep = useCallback((stepKey, meta = {}) => {
+ const serverStep = SERVER_STEP_BY_UI_STEP[stepKey];
+ if (!serverStep) return;
+
+ api.patch('/api/user/onboarding', {
+ steps: { [serverStep]: true },
+ ...meta
+ })
+ .then((response) => {
+ const onboarding = response.data?.data?.onboarding;
+ if (!onboarding) return;
+ setProgress(getServerProgress({ onboarding }));
+ updateUser((currentUser) => ({
+ ...currentUser,
+ onboarding
+ }));
+ })
+ .catch((err) => {
+ console.error('[onboarding] Failed to persist progress:', err.message);
+ });
+ }, [updateUser]);
+
+ const refreshUser = useCallback(async () => {
+ try {
+ const response = await api.get('/api/auth/me');
+ if (response.data.success) {
+ updateUser(response.data.data.user);
+ }
+ } catch (err) {
+ console.error("Failed to refresh user:", err.message);
+ }
+ }, [updateUser]);
const dismissOnboarding = useCallback(() => {
setIsDismissed(true);
@@ -84,7 +113,7 @@ export const OnboardingProvider = ({ children }) => {
});
}, [activeProjectId]);
- const allCompleted = ONBOARDING_STEPS.every(step => progress[step.key]);
+ const allCompleted = !!user?.onboarding?.completed;
const isVisible = !isDismissed;
const value = {
@@ -97,7 +126,11 @@ export const OnboardingProvider = ({ children }) => {
isDismissed,
allCompleted,
activeProjectId,
- setActiveProjectId
+ setActiveProjectId,
+ refreshUser,
+ currentStep: progress.currentStep,
+ projectId: progress.projectId,
+ collectionId: progress.collectionId
};
return (
diff --git a/apps/web-dashboard/src/index.css b/apps/web-dashboard/src/index.css
index 18f846901..0e07a6779 100644
--- a/apps/web-dashboard/src/index.css
+++ b/apps/web-dashboard/src/index.css
@@ -347,13 +347,14 @@ a {
.btn-primary {
background-color: var(--color-primary);
- color: #000;
+ color: #000 !important;
border-color: var(--color-primary);
box-shadow: 0 2px 10px rgba(62, 207, 142, 0.2);
}
.btn-primary:hover {
background-color: var(--color-primary-hover);
+ color: #000 !important;
box-shadow: 0 4px 15px rgba(62, 207, 142, 0.3);
}
diff --git a/apps/web-dashboard/src/pages/AdminMetrics.jsx b/apps/web-dashboard/src/pages/AdminMetrics.jsx
index 443718fef..3ccba0005 100644
--- a/apps/web-dashboard/src/pages/AdminMetrics.jsx
+++ b/apps/web-dashboard/src/pages/AdminMetrics.jsx
@@ -35,6 +35,7 @@ function FunnelBar({ step, count, max }) {
project_created: 'Project Created',
collection_created: 'Collection Created',
first_api_success: 'First API Success',
+ first_api_call: 'First API Call',
};
return (
diff --git a/apps/web-dashboard/src/pages/Analytics.jsx b/apps/web-dashboard/src/pages/Analytics.jsx
index 48a363976..d1bfae475 100644
--- a/apps/web-dashboard/src/pages/Analytics.jsx
+++ b/apps/web-dashboard/src/pages/Analytics.jsx
@@ -12,6 +12,7 @@ import {
ChevronRight, ArrowUpRight, TrendingUp, Filter
} from 'lucide-react';
import SectionHeader from '../components/Dashboard/SectionHeader';
+import { getProgressWidth, getUsagePercentage } from '../utils/quota';
const STATUS_COLORS = {
'2xx': '#10b981',
@@ -51,6 +52,7 @@ const RANGE_OPTIONS = [
export default function Analytics() {
const { projectId } = useParams();
+
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
@@ -145,226 +147,246 @@ export default function Analytics() {
- {/* KPI Row */}
-