diff --git a/apps/skilltree/apps/web/.env.production.example b/apps/skilltree/apps/web/.env.production.example
new file mode 100644
index 000000000..313246818
--- /dev/null
+++ b/apps/skilltree/apps/web/.env.production.example
@@ -0,0 +1,12 @@
+# SkillTree Web App - Production Environment Variables
+# Copy this file to .env.production and fill in the values
+
+# =============================================================================
+# REQUIRED
+# =============================================================================
+
+# Backend API URL
+PUBLIC_BACKEND_URL=https://skilltree-api.mana.how
+
+# Mana Core Auth URL for authentication
+PUBLIC_MANA_CORE_AUTH_URL=https://auth.mana.how
diff --git a/apps/skilltree/apps/web/package.json b/apps/skilltree/apps/web/package.json
index 66774ff58..669146ff4 100644
--- a/apps/skilltree/apps/web/package.json
+++ b/apps/skilltree/apps/web/package.json
@@ -25,6 +25,7 @@
"vite": "^6.0.0"
},
"dependencies": {
+ "@manacore/shared-auth": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",
"@manacore/shared-utils": "workspace:*",
diff --git a/apps/skilltree/apps/web/src/hooks.server.ts b/apps/skilltree/apps/web/src/hooks.server.ts
new file mode 100644
index 000000000..a450239b2
--- /dev/null
+++ b/apps/skilltree/apps/web/src/hooks.server.ts
@@ -0,0 +1,24 @@
+/**
+ * Server Hooks for SvelteKit
+ * - Injects runtime environment variables for client-side use
+ * - Auth is handled client-side via Mana Core Auth
+ */
+
+import type { Handle } from '@sveltejs/kit';
+
+const PUBLIC_MANA_CORE_AUTH_URL_CLIENT =
+ process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || '';
+const PUBLIC_BACKEND_URL_CLIENT =
+ process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || '';
+
+export const handle: Handle = async ({ event, resolve }) => {
+ return resolve(event, {
+ transformPageChunk: ({ html }) => {
+ const envScript = ``;
+ return html.replace('
', `${envScript}`);
+ },
+ });
+};
diff --git a/apps/skilltree/apps/web/src/lib/api/activities.ts b/apps/skilltree/apps/web/src/lib/api/activities.ts
new file mode 100644
index 000000000..d2cca8c5f
--- /dev/null
+++ b/apps/skilltree/apps/web/src/lib/api/activities.ts
@@ -0,0 +1,23 @@
+import { apiClient } from './client';
+import type { Activity } from '$lib/types';
+
+interface ActivitiesResponse {
+ activities: Activity[];
+}
+
+export async function getActivities(skillId?: string, limit?: number): Promise {
+ const params = new URLSearchParams();
+ if (skillId) params.append('skillId', skillId);
+ if (limit) params.append('limit', String(limit));
+ const queryString = params.toString() ? `?${params.toString()}` : '';
+ const response = await apiClient.get(`/api/v1/activities${queryString}`);
+ return response.activities;
+}
+
+export async function getRecentActivities(limit = 10): Promise {
+ return getActivities(undefined, limit);
+}
+
+export async function getSkillActivities(skillId: string): Promise {
+ return getActivities(skillId);
+}
diff --git a/apps/skilltree/apps/web/src/lib/api/client.ts b/apps/skilltree/apps/web/src/lib/api/client.ts
new file mode 100644
index 000000000..29bcbb7f4
--- /dev/null
+++ b/apps/skilltree/apps/web/src/lib/api/client.ts
@@ -0,0 +1,98 @@
+import { browser } from '$app/environment';
+import { PUBLIC_BACKEND_URL } from '$env/static/public';
+
+interface ApiOptions {
+ method?: string;
+ body?: unknown;
+ headers?: Record;
+}
+
+interface ApiError {
+ message: string;
+ statusCode: number;
+}
+
+/**
+ * Get the backend URL, preferring runtime-injected value in browser
+ * This allows Docker to inject PUBLIC_BACKEND_URL_CLIENT at runtime
+ */
+function getBackendUrl(): string {
+ if (browser && typeof window !== 'undefined') {
+ const runtimeUrl = (window as Window & { __PUBLIC_BACKEND_URL__?: string })
+ .__PUBLIC_BACKEND_URL__;
+ if (runtimeUrl) {
+ return runtimeUrl;
+ }
+ }
+ return PUBLIC_BACKEND_URL || 'http://localhost:3024';
+}
+
+class ApiClient {
+ private accessToken: string | null = null;
+
+ private get baseUrl(): string {
+ return getBackendUrl();
+ }
+
+ setAccessToken(token: string | null) {
+ this.accessToken = token;
+ }
+
+ getAccessToken(): string | null {
+ return this.accessToken;
+ }
+
+ async fetch(endpoint: string, options: ApiOptions = {}): Promise {
+ const { method = 'GET', body, headers = {} } = options;
+
+ const requestHeaders: Record = {
+ 'Content-Type': 'application/json',
+ ...headers,
+ };
+
+ if (this.accessToken) {
+ requestHeaders['Authorization'] = `Bearer ${this.accessToken}`;
+ }
+
+ const response = await fetch(`${this.baseUrl}${endpoint}`, {
+ method,
+ headers: requestHeaders,
+ body: body ? JSON.stringify(body) : undefined,
+ });
+
+ if (!response.ok) {
+ let errorMessage = 'An error occurred';
+ try {
+ const errorData = (await response.json()) as ApiError;
+ errorMessage = errorData.message || errorMessage;
+ } catch {
+ errorMessage = response.statusText || errorMessage;
+ }
+ throw new Error(errorMessage);
+ }
+
+ if (response.status === 204) {
+ return {} as T;
+ }
+
+ return response.json() as Promise;
+ }
+
+ get(endpoint: string, headers?: Record): Promise {
+ return this.fetch(endpoint, { method: 'GET', headers });
+ }
+
+ post(endpoint: string, body?: unknown, headers?: Record): Promise {
+ return this.fetch(endpoint, { method: 'POST', body, headers });
+ }
+
+ put(endpoint: string, body?: unknown, headers?: Record): Promise {
+ return this.fetch(endpoint, { method: 'PUT', body, headers });
+ }
+
+ delete(endpoint: string, headers?: Record): Promise {
+ return this.fetch(endpoint, { method: 'DELETE', headers });
+ }
+}
+
+export const apiClient = new ApiClient();
diff --git a/apps/skilltree/apps/web/src/lib/api/skills.ts b/apps/skilltree/apps/web/src/lib/api/skills.ts
new file mode 100644
index 000000000..6324a51cf
--- /dev/null
+++ b/apps/skilltree/apps/web/src/lib/api/skills.ts
@@ -0,0 +1,80 @@
+import { apiClient } from './client';
+import type { Skill, Activity, UserStats, SkillBranch } from '$lib/types';
+
+interface CreateSkillDto {
+ name: string;
+ description?: string;
+ branch: SkillBranch;
+ parentId?: string;
+ icon?: string;
+ color?: string;
+}
+
+interface UpdateSkillDto {
+ name?: string;
+ description?: string;
+ branch?: SkillBranch;
+ parentId?: string | null;
+ icon?: string;
+ color?: string | null;
+}
+
+interface AddXpDto {
+ xp: number;
+ description: string;
+ duration?: number;
+}
+
+interface AddXpResponse {
+ skill: Skill;
+ activity: Activity;
+ leveledUp: boolean;
+ previousLevel: number;
+ newLevel: number;
+}
+
+interface SkillsResponse {
+ skills: Skill[];
+}
+
+interface SkillResponse {
+ skill: Skill;
+}
+
+interface StatsResponse {
+ stats: UserStats;
+}
+
+export async function getSkills(branch?: SkillBranch): Promise {
+ const queryString = branch ? `?branch=${branch}` : '';
+ const response = await apiClient.get(`/api/v1/skills${queryString}`);
+ return response.skills;
+}
+
+export async function getSkill(id: string): Promise {
+ const response = await apiClient.get(`/api/v1/skills/${id}`);
+ return response.skill;
+}
+
+export async function createSkill(data: CreateSkillDto): Promise {
+ const response = await apiClient.post('/api/v1/skills', data);
+ return response.skill;
+}
+
+export async function updateSkill(id: string, data: UpdateSkillDto): Promise {
+ const response = await apiClient.put(`/api/v1/skills/${id}`, data);
+ return response.skill;
+}
+
+export async function deleteSkill(id: string): Promise {
+ await apiClient.delete(`/api/v1/skills/${id}`);
+}
+
+export async function addXp(skillId: string, data: AddXpDto): Promise {
+ return await apiClient.post(`/api/v1/skills/${skillId}/xp`, data);
+}
+
+export async function getStats(): Promise {
+ const response = await apiClient.get('/api/v1/skills/stats');
+ return response.stats;
+}
diff --git a/apps/skilltree/apps/web/src/lib/stores/auth.svelte.ts b/apps/skilltree/apps/web/src/lib/stores/auth.svelte.ts
new file mode 100644
index 000000000..c922cd75a
--- /dev/null
+++ b/apps/skilltree/apps/web/src/lib/stores/auth.svelte.ts
@@ -0,0 +1,212 @@
+/**
+ * Auth Store - Manages authentication state using Svelte 5 runes
+ * Uses Mana Core Auth
+ */
+
+import { browser } from '$app/environment';
+import { initializeWebAuth, type UserData } from '@manacore/shared-auth';
+import { apiClient } from '$lib/api/client';
+
+const DEV_AUTH_URL = 'http://localhost:3001';
+const DEV_BACKEND_URL = 'http://localhost:3024';
+
+function getAuthUrl(): string {
+ if (browser && typeof window !== 'undefined') {
+ const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
+ .__PUBLIC_MANA_CORE_AUTH_URL__;
+ if (injectedUrl) return injectedUrl;
+ return import.meta.env.DEV ? DEV_AUTH_URL : '';
+ }
+ return process.env.PUBLIC_MANA_CORE_AUTH_URL || DEV_AUTH_URL;
+}
+
+function getBackendUrl(): string {
+ if (browser && typeof window !== 'undefined') {
+ const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string })
+ .__PUBLIC_BACKEND_URL__;
+ if (injectedUrl) return injectedUrl;
+ return import.meta.env.DEV ? DEV_BACKEND_URL : '';
+ }
+ return process.env.PUBLIC_BACKEND_URL || DEV_BACKEND_URL;
+}
+
+let _authService: ReturnType['authService'] | null = null;
+let _tokenManager: ReturnType['tokenManager'] | null = null;
+
+function getAuthService() {
+ if (!browser) return null;
+ if (!_authService) {
+ const auth = initializeWebAuth({
+ baseUrl: getAuthUrl(),
+ backendUrl: getBackendUrl(),
+ });
+ _authService = auth.authService;
+ _tokenManager = auth.tokenManager;
+ }
+ return _authService;
+}
+
+function getTokenManager() {
+ if (!browser) return null;
+ getAuthService();
+ return _tokenManager;
+}
+
+let user = $state(null);
+let loading = $state(true);
+let initialized = $state(false);
+
+export const authStore = {
+ get user() {
+ return user;
+ },
+ get loading() {
+ return loading;
+ },
+ get isAuthenticated() {
+ return !!user;
+ },
+ get initialized() {
+ return initialized;
+ },
+
+ async initialize() {
+ if (initialized) return;
+
+ const authService = getAuthService();
+ if (!authService) {
+ initialized = true;
+ loading = false;
+ return;
+ }
+
+ loading = true;
+ try {
+ const authenticated = await authService.isAuthenticated();
+ if (authenticated) {
+ const userData = await authService.getUserFromToken();
+ user = userData;
+
+ const token = await authService.getAppToken();
+ if (token) {
+ apiClient.setAccessToken(token);
+ }
+ }
+ initialized = true;
+ } catch (error) {
+ console.error('Failed to initialize auth:', error);
+ user = null;
+ } finally {
+ loading = false;
+ }
+ },
+
+ async signIn(email: string, password: string) {
+ const authService = getAuthService();
+ if (!authService) {
+ return { success: false, error: 'Auth not available on server' };
+ }
+
+ try {
+ const result = await authService.signIn(email, password);
+
+ if (!result.success) {
+ return { success: false, error: result.error || 'Login failed' };
+ }
+
+ const userData = await authService.getUserFromToken();
+ user = userData;
+
+ const token = await authService.getAppToken();
+ if (token) {
+ apiClient.setAccessToken(token);
+ }
+
+ return { success: true };
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+ return { success: false, error: errorMessage };
+ }
+ },
+
+ async signUp(email: string, password: string) {
+ const authService = getAuthService();
+ if (!authService) {
+ return { success: false, error: 'Auth not available on server', needsVerification: false };
+ }
+
+ try {
+ const sourceAppUrl = browser ? window.location.origin : undefined;
+ const result = await authService.signUp(email, password, undefined, sourceAppUrl);
+
+ if (!result.success) {
+ return { success: false, error: result.error || 'Signup failed', needsVerification: false };
+ }
+
+ if (result.needsVerification) {
+ return { success: true, needsVerification: true };
+ }
+
+ const signInResult = await this.signIn(email, password);
+ return { ...signInResult, needsVerification: false };
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+ return { success: false, error: errorMessage, needsVerification: false };
+ }
+ },
+
+ async signOut() {
+ const authService = getAuthService();
+ if (!authService) {
+ user = null;
+ apiClient.setAccessToken(null);
+ return;
+ }
+
+ try {
+ await authService.signOut();
+ user = null;
+ apiClient.setAccessToken(null);
+ } catch (error) {
+ console.error('Sign out error:', error);
+ user = null;
+ apiClient.setAccessToken(null);
+ }
+ },
+
+ async resetPassword(email: string) {
+ const authService = getAuthService();
+ if (!authService) {
+ return { success: false, error: 'Auth not available on server' };
+ }
+
+ try {
+ const result = await authService.forgotPassword(email);
+
+ if (!result.success) {
+ return { success: false, error: result.error || 'Password reset failed' };
+ }
+
+ return { success: true };
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+ return { success: false, error: errorMessage };
+ }
+ },
+
+ async getAccessToken() {
+ const authService = getAuthService();
+ if (!authService) {
+ return null;
+ }
+ return await authService.getAppToken();
+ },
+
+ async getValidToken(): Promise {
+ const tokenManager = getTokenManager();
+ if (!tokenManager) {
+ return null;
+ }
+ return await tokenManager.getValidToken();
+ },
+};
diff --git a/apps/skilltree/apps/web/src/lib/stores/skills.svelte.ts b/apps/skilltree/apps/web/src/lib/stores/skills.svelte.ts
index c5a5c0e96..cc79b6acb 100644
--- a/apps/skilltree/apps/web/src/lib/stores/skills.svelte.ts
+++ b/apps/skilltree/apps/web/src/lib/stores/skills.svelte.ts
@@ -1,11 +1,9 @@
import type { Skill, Activity, UserStats, SkillBranch } from '$lib/types';
-import {
- calculateLevel,
- createDefaultSkill,
- createActivity,
- BRANCH_INFO,
-} from '$lib/types';
+import { calculateLevel, createDefaultSkill, createActivity, BRANCH_INFO } from '$lib/types';
import * as storage from '$lib/services/storage';
+import * as skillsApi from '$lib/api/skills';
+import * as activitiesApi from '$lib/api/activities';
+import { authStore } from './auth.svelte';
// Reactive state using Svelte 5 runes
let skills = $state([]);
@@ -19,6 +17,7 @@ let userStats = $state({
});
let isLoading = $state(true);
let initialized = $state(false);
+let useApi = $state(false);
// Derived values
const skillsByBranch = $derived(() => {
@@ -42,13 +41,14 @@ const topSkills = $derived(() => {
});
const recentActivities = $derived(() => {
- return [...activities].sort((a, b) =>
- new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
- ).slice(0, 10);
+ return [...activities]
+ .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
+ .slice(0, 10);
});
const branchStats = $derived(() => {
- const stats: Record = {} as any;
+ const stats: Record =
+ {} as Record;
for (const branch of Object.keys(BRANCH_INFO) as SkillBranch[]) {
const branchSkills = skills.filter((s) => s.branch === branch);
stats[branch] = {
@@ -69,81 +69,171 @@ async function initialize() {
isLoading = true;
try {
- const [loadedSkills, loadedActivities, loadedStats] = await Promise.all([
- storage.getAllSkills(),
- storage.getAllActivities(),
- storage.getUserStats(),
- ]);
- skills = loadedSkills;
- activities = loadedActivities;
- userStats = loadedStats;
+ // Check if user is authenticated
+ if (authStore.isAuthenticated) {
+ useApi = true;
+ const [loadedSkills, loadedActivities, loadedStats] = await Promise.all([
+ skillsApi.getSkills(),
+ activitiesApi.getRecentActivities(50),
+ skillsApi.getStats(),
+ ]);
+ skills = loadedSkills;
+ activities = loadedActivities;
+ userStats = loadedStats;
+ } else {
+ // Fallback to IndexedDB for offline/unauthenticated use
+ useApi = false;
+ const [loadedSkills, loadedActivities, loadedStats] = await Promise.all([
+ storage.getAllSkills(),
+ storage.getAllActivities(),
+ storage.getUserStats(),
+ ]);
+ skills = loadedSkills;
+ activities = loadedActivities;
+ userStats = loadedStats;
+ }
initialized = true;
} catch (error) {
console.error('Failed to initialize skills store:', error);
+ // On error, try IndexedDB as fallback
+ if (useApi) {
+ try {
+ useApi = false;
+ const [loadedSkills, loadedActivities, loadedStats] = await Promise.all([
+ storage.getAllSkills(),
+ storage.getAllActivities(),
+ storage.getUserStats(),
+ ]);
+ skills = loadedSkills;
+ activities = loadedActivities;
+ userStats = loadedStats;
+ } catch (fallbackError) {
+ console.error('Fallback to IndexedDB also failed:', fallbackError);
+ }
+ }
} finally {
isLoading = false;
}
}
async function addSkill(data: Partial): Promise {
- const skill = createDefaultSkill(data);
- await storage.saveSkill(skill);
- skills = [...skills, skill];
- await updateStats();
- return skill;
+ if (useApi && authStore.isAuthenticated) {
+ const skill = await skillsApi.createSkill({
+ name: data.name || '',
+ description: data.description,
+ branch: data.branch || 'custom',
+ parentId: data.parentId ?? undefined,
+ icon: data.icon,
+ color: data.color ?? undefined,
+ });
+ skills = [...skills, skill];
+ await updateStats();
+ return skill;
+ } else {
+ const skill = createDefaultSkill(data);
+ await storage.saveSkill(skill);
+ skills = [...skills, skill];
+ await updateStats();
+ return skill;
+ }
}
async function updateSkill(id: string, updates: Partial): Promise {
const index = skills.findIndex((s) => s.id === id);
if (index === -1) return;
- const updatedSkill = { ...skills[index], ...updates, updatedAt: new Date().toISOString() };
- await storage.saveSkill(updatedSkill);
- skills = [...skills.slice(0, index), updatedSkill, ...skills.slice(index + 1)];
+ if (useApi && authStore.isAuthenticated) {
+ const skill = await skillsApi.updateSkill(id, {
+ name: updates.name,
+ description: updates.description,
+ branch: updates.branch,
+ parentId: updates.parentId,
+ icon: updates.icon,
+ color: updates.color,
+ });
+ skills = [...skills.slice(0, index), skill, ...skills.slice(index + 1)];
+ } else {
+ const updatedSkill = { ...skills[index], ...updates, updatedAt: new Date().toISOString() };
+ await storage.saveSkill(updatedSkill);
+ skills = [...skills.slice(0, index), updatedSkill, ...skills.slice(index + 1)];
+ }
await updateStats();
}
async function deleteSkill(id: string): Promise {
- await storage.deleteSkill(id);
+ if (useApi && authStore.isAuthenticated) {
+ await skillsApi.deleteSkill(id);
+ } else {
+ await storage.deleteSkill(id);
+ }
skills = skills.filter((s) => s.id !== id);
activities = activities.filter((a) => a.skillId !== id);
await updateStats();
}
-async function addXp(skillId: string, xp: number, description: string, duration?: number): Promise<{ leveledUp: boolean; newLevel: number }> {
+async function addXp(
+ skillId: string,
+ xp: number,
+ description: string,
+ duration?: number
+): Promise<{ leveledUp: boolean; newLevel: number }> {
const index = skills.findIndex((s) => s.id === skillId);
if (index === -1) return { leveledUp: false, newLevel: 0 };
- const skill = skills[index];
- const newTotalXp = skill.totalXp + xp;
- const newCurrentXp = skill.currentXp + xp;
- const newLevel = calculateLevel(newTotalXp);
- const leveledUp = newLevel > skill.level;
+ if (useApi && authStore.isAuthenticated) {
+ const result = await skillsApi.addXp(skillId, { xp, description, duration });
+ skills = [...skills.slice(0, index), result.skill, ...skills.slice(index + 1)];
+ activities = [...activities, result.activity];
+ await updateStats();
+ return { leveledUp: result.leveledUp, newLevel: result.newLevel };
+ } else {
+ const skill = skills[index];
+ const newTotalXp = skill.totalXp + xp;
+ const newCurrentXp = skill.currentXp + xp;
+ const newLevel = calculateLevel(newTotalXp);
+ const leveledUp = newLevel > skill.level;
- const updatedSkill: Skill = {
- ...skill,
- totalXp: newTotalXp,
- currentXp: newCurrentXp,
- level: newLevel,
- updatedAt: new Date().toISOString(),
- };
+ const updatedSkill: Skill = {
+ ...skill,
+ totalXp: newTotalXp,
+ currentXp: newCurrentXp,
+ level: newLevel,
+ updatedAt: new Date().toISOString(),
+ };
- const activity = createActivity(skillId, xp, description, duration);
+ const activity = createActivity(skillId, xp, description, duration);
- await Promise.all([
- storage.saveSkill(updatedSkill),
- storage.saveActivity(activity),
- ]);
+ await Promise.all([storage.saveSkill(updatedSkill), storage.saveActivity(activity)]);
- skills = [...skills.slice(0, index), updatedSkill, ...skills.slice(index + 1)];
- activities = [...activities, activity];
- await updateStats();
+ skills = [...skills.slice(0, index), updatedSkill, ...skills.slice(index + 1)];
+ activities = [...activities, activity];
+ await updateStats();
- return { leveledUp, newLevel };
+ return { leveledUp, newLevel };
+ }
}
async function updateStats(): Promise {
- userStats = await storage.recalculateStats();
+ if (useApi && authStore.isAuthenticated) {
+ try {
+ userStats = await skillsApi.getStats();
+ } catch {
+ // Calculate locally as fallback
+ userStats = calculateLocalStats();
+ }
+ } else {
+ userStats = await storage.recalculateStats();
+ }
+}
+
+function calculateLocalStats(): UserStats {
+ return {
+ totalXp: skills.reduce((sum, s) => sum + s.totalXp, 0),
+ totalSkills: skills.length,
+ highestLevel: skills.reduce((max, s) => Math.max(max, s.level), 0),
+ streakDays: 0,
+ lastActivityDate: activities.length > 0 ? activities[activities.length - 1].timestamp : null,
+ };
}
function getSkill(id: string): Skill | undefined {
@@ -154,19 +244,56 @@ function getSkillActivities(skillId: string): Activity[] {
return activities.filter((a) => a.skillId === skillId);
}
+// Reinitialize when auth state changes
+async function reinitialize() {
+ initialized = false;
+ skills = [];
+ activities = [];
+ userStats = {
+ totalXp: 0,
+ totalSkills: 0,
+ highestLevel: 0,
+ streakDays: 0,
+ lastActivityDate: null,
+ };
+ await initialize();
+}
+
// Export store as object with getters for reactive access
export const skillStore = {
- get skills() { return skills; },
- get activities() { return activities; },
- get userStats() { return userStats; },
- get isLoading() { return isLoading; },
- get initialized() { return initialized; },
- get skillsByBranch() { return skillsByBranch; },
- get topSkills() { return topSkills; },
- get recentActivities() { return recentActivities; },
- get branchStats() { return branchStats; },
+ get skills() {
+ return skills;
+ },
+ get activities() {
+ return activities;
+ },
+ get userStats() {
+ return userStats;
+ },
+ get isLoading() {
+ return isLoading;
+ },
+ get initialized() {
+ return initialized;
+ },
+ get skillsByBranch() {
+ return skillsByBranch;
+ },
+ get topSkills() {
+ return topSkills;
+ },
+ get recentActivities() {
+ return recentActivities;
+ },
+ get branchStats() {
+ return branchStats;
+ },
+ get useApi() {
+ return useApi;
+ },
initialize,
+ reinitialize,
addSkill,
updateSkill,
deleteSkill,