feat(figgos): scaffold backend + mobile app

Add new Figgos project under apps/figgos/ with:
- NestJS backend (port 3025) with Drizzle ORM, health check, metrics
- Expo React Native mobile app with Mana Core Auth, tab navigation
- Shared types package (@figgos/shared)
- Root integration: env generation, dev:figgos:full script, MinIO bucket

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Chr1st1anG 2026-02-10 20:53:54 +01:00
parent 5b6f231e1a
commit 1cffc6de81
40 changed files with 1572 additions and 194 deletions

View file

@ -324,6 +324,13 @@ PLANTA_GEMINI_API_KEY=AIzaSyC_-hPWpVttTlqJdU4jbXR5H0OAnRi2LgI
SKILLTREE_BACKEND_PORT=3024
SKILLTREE_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/skilltree
# ============================================
# FIGGOS PROJECT
# ============================================
FIGGOS_BACKEND_PORT=3025
FIGGOS_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/figgos
# ============================================
# WORLDREAM GAME
# ============================================

68
apps/figgos/CLAUDE.md Normal file
View file

@ -0,0 +1,68 @@
# Figgos
A collectible figure game where users create and collect AI-generated fantasy figures.
## Project Structure
```
apps/figgos/
├── apps/
│ ├── backend/ # @figgos/backend - NestJS API (port 3025)
│ └── mobile/ # @figgos/mobile - Expo React Native app
├── packages/
│ └── shared/ # @figgos/shared - Shared types & constants
└── package.json
```
## Commands
### From monorepo root
```bash
pnpm dev:figgos:mobile # Start mobile app
pnpm dev:figgos:backend # Start backend
pnpm dev:figgos:app # Start web + backend together
pnpm dev:figgos:full # Start with auth + auto DB setup
pnpm figgos:db:push # Push schema to database
pnpm figgos:db:studio # Open Drizzle Studio
```
## Technology Stack
- **Mobile**: React Native 0.76 + Expo SDK 52, NativeWind, Expo Router
- **Backend**: NestJS 10, Drizzle ORM, PostgreSQL
- **Auth**: Mana Core Auth (JWT via @manacore/shared-nestjs-auth)
- **AI**: Google Gemini API (planned)
- **Storage**: MinIO (local) / Hetzner S3 (production)
## Ports
| App | Port |
|-----|------|
| Backend | 3025 |
| Web (planned) | 5181 |
## Environment Variables
### Backend (.env)
```env
PORT=3025
DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/figgos
MANA_CORE_AUTH_URL=http://localhost:3001
```
### Mobile (.env)
```env
EXPO_PUBLIC_BACKEND_URL=http://localhost:3025
EXPO_PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001
```
## Game Concept
- Users create fantasy figures by providing a name/subject
- AI generates character info (description, lore, items) + image
- Figures have rarities: common, rare, epic, legendary
- Users can browse public figures, like them, and collect their own

View file

@ -0,0 +1,6 @@
import { createDrizzleConfig } from '@manacore/shared-drizzle-config';
export default createDrizzleConfig({
dbName: 'figgos',
additionalEnvVars: ['FIGGOS_DATABASE_URL'],
});

View file

@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": false,
"assets": [],
"watchAssets": false
}
}

View file

@ -0,0 +1,52 @@
{
"name": "@figgos/backend",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "nest build",
"start": "nest start",
"dev": "nest start --watch",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"type-check": "tsc --noEmit",
"migration:generate": "drizzle-kit generate",
"migration:run": "tsx src/db/migrate.ts",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio"
},
"dependencies": {
"@figgos/shared": "workspace:*",
"@manacore/shared-drizzle-config": "workspace:*",
"@manacore/shared-nestjs-auth": "workspace:*",
"@manacore/shared-nestjs-health": "workspace:*",
"@manacore/shared-nestjs-metrics": "workspace:*",
"@manacore/shared-nestjs-setup": "workspace:*",
"@nestjs/common": "^10.4.15",
"@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.4.15",
"@nestjs/platform-express": "^10.4.15",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dotenv": "^16.4.7",
"drizzle-kit": "^0.30.2",
"drizzle-orm": "^0.38.3",
"postgres": "^3.4.5",
"prom-client": "^15.1.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@nestjs/cli": "^10.4.9",
"@nestjs/schematics": "^10.2.3",
"@types/express": "^5.0.0",
"@types/node": "^22.10.2",
"source-map-support": "^0.5.21",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.19.2",
"typescript": "^5.7.2"
}
}

View file

@ -0,0 +1,21 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { MetricsModule } from '@manacore/shared-nestjs-metrics';
import { HealthModule } from '@manacore/shared-nestjs-health';
import { DatabaseModule } from './db/database.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
MetricsModule.register({
prefix: 'figgos_',
excludePaths: ['/health'],
}),
DatabaseModule,
HealthModule.forRoot({ serviceName: 'figgos-backend' }),
],
})
export class AppModule {}

View file

@ -0,0 +1,38 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import * as schema from './schema';
// Use require for postgres to avoid ESM/CommonJS interop issues
// eslint-disable-next-line @typescript-eslint/no-var-requires
const postgres = require('postgres');
let connection: ReturnType<typeof postgres> | null = null;
let db: ReturnType<typeof drizzle> | null = null;
export function getConnection(databaseUrl: string) {
if (!connection) {
connection = postgres(databaseUrl, {
max: 10,
idle_timeout: 20,
connect_timeout: 10,
});
}
return connection;
}
export function getDb(databaseUrl: string) {
if (!db) {
const conn = getConnection(databaseUrl);
db = drizzle(conn, { schema });
}
return db;
}
export async function closeConnection() {
if (connection) {
await connection.end();
connection = null;
db = null;
}
}
export type Database = ReturnType<typeof getDb>;

View file

@ -0,0 +1,29 @@
import { Module, Global, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { getDb, closeConnection } from './connection';
import type { Database } from './connection';
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
@Global()
@Module({
providers: [
{
provide: DATABASE_CONNECTION,
useFactory: (configService: ConfigService): Database => {
const databaseUrl = configService.get<string>('DATABASE_URL');
if (!databaseUrl) {
throw new Error('DATABASE_URL environment variable is not set');
}
return getDb(databaseUrl);
},
inject: [ConfigService],
},
],
exports: [DATABASE_CONNECTION],
})
export class DatabaseModule implements OnModuleDestroy {
async onModuleDestroy() {
await closeConnection();
}
}

View file

@ -0,0 +1,3 @@
// Database schema exports
// Will be populated as features are added
export {};

View file

@ -0,0 +1,8 @@
import { bootstrapApp } from '@manacore/shared-nestjs-setup';
import { AppModule } from './app.module';
bootstrapApp(AppModule, {
defaultPort: 3025,
serviceName: 'Figgos',
additionalCorsOrigins: ['http://localhost:5181'],
});

View file

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2021",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"outDir": "./dist",
"baseUrl": "./",
"rootDir": "./src",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"noImplicitAny": true,
"strictBindCallApply": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

14
apps/figgos/apps/mobile/.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
node_modules/
.expo/
dist/
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
ios/
android/
.env

1
apps/figgos/apps/mobile/app-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="expo/types" />

View file

@ -0,0 +1,51 @@
{
"expo": {
"name": "Figgos",
"slug": "figgos",
"version": "1.0.0",
"scheme": "figgos",
"web": {
"bundler": "metro",
"output": "server",
"favicon": "./assets/favicon.png"
},
"plugins": [
"expo-router",
[
"expo-dev-launcher",
{
"launchMode": "most-recent"
}
]
],
"experiments": {
"typedRoutes": true,
"tsconfigPaths": true
},
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.tilljs.figgos"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.tilljs.figgos"
},
"extra": {
"router": {
"origin": false
}
}
}
}

View file

@ -0,0 +1,9 @@
import { Stack } from 'expo-router';
export default function AuthLayout() {
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="login" />
</Stack>
);
}

View file

@ -0,0 +1,79 @@
import { useState } from 'react';
import { View, Text, TextInput, Pressable, KeyboardAvoidingView, Platform } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useAuth } from '~/contexts/AuthContext';
export default function LoginScreen() {
const { signIn } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleLogin = async () => {
if (!email || !password) {
setError('Please enter email and password');
return;
}
setLoading(true);
setError(null);
const result = await signIn(email, password);
if (result.error) {
setError(result.error.message || 'Login failed');
}
setLoading(false);
};
return (
<SafeAreaView className="flex-1 bg-background dark:bg-dark-background">
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
className="flex-1 justify-center px-6"
>
<View className="items-center mb-12">
<Text className="text-4xl font-bold text-primary">Figgos</Text>
<Text className="text-base text-muted mt-2">Collect your fantasy figures</Text>
</View>
<View className="space-y-4">
<TextInput
className="bg-card dark:bg-dark-card border border-border dark:border-dark-border rounded-lg px-4 py-3 text-textColor dark:text-dark-textColor"
placeholder="Email"
placeholderTextColor="#B2BEC3"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
/>
<TextInput
className="bg-card dark:bg-dark-card border border-border dark:border-dark-border rounded-lg px-4 py-3 text-textColor dark:text-dark-textColor mt-3"
placeholder="Password"
placeholderTextColor="#B2BEC3"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
{error && <Text className="text-red-500 text-center mt-2">{error}</Text>}
<Pressable
onPress={handleLogin}
disabled={loading}
className={({ pressed }) =>
`bg-primary rounded-lg py-3 mt-4 ${pressed ? 'opacity-80' : ''} ${loading ? 'opacity-50' : ''}`
}
>
<Text className="text-white text-center font-semibold text-base">
{loading ? 'Signing in...' : 'Sign In'}
</Text>
</Pressable>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
}

View file

@ -0,0 +1,49 @@
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#6C5CE7',
tabBarInactiveTintColor: '#B2BEC3',
tabBarStyle: {
backgroundColor: '#FFFFFF',
borderTopColor: '#DFE6E9',
},
headerStyle: {
backgroundColor: '#FFFFFF',
},
headerTintColor: '#2D3436',
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Community',
tabBarIcon: ({ color, size }) => (
<Ionicons name="globe-outline" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="create"
options={{
title: 'Create',
tabBarIcon: ({ color, size }) => (
<Ionicons name="add-circle-outline" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="shelf"
options={{
title: 'Collection',
tabBarIcon: ({ color, size }) => (
<Ionicons name="grid-outline" size={size} color={color} />
),
}}
/>
</Tabs>
);
}

View file

@ -0,0 +1,15 @@
import { View, Text } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
export default function CreateScreen() {
return (
<SafeAreaView className="flex-1 bg-background dark:bg-dark-background" edges={['bottom']}>
<View className="flex-1 items-center justify-center px-6">
<Text className="text-2xl font-bold text-textColor dark:text-dark-textColor">Create</Text>
<Text className="text-muted mt-2 text-center">
Generate your own AI-powered fantasy figures.
</Text>
</View>
</SafeAreaView>
);
}

View file

@ -0,0 +1,17 @@
import { View, Text } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
export default function CommunityScreen() {
return (
<SafeAreaView className="flex-1 bg-background dark:bg-dark-background" edges={['bottom']}>
<View className="flex-1 items-center justify-center px-6">
<Text className="text-2xl font-bold text-textColor dark:text-dark-textColor">
Community
</Text>
<Text className="text-muted mt-2 text-center">
Public figures from the community will appear here.
</Text>
</View>
</SafeAreaView>
);
}

View file

@ -0,0 +1,17 @@
import { View, Text } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
export default function ShelfScreen() {
return (
<SafeAreaView className="flex-1 bg-background dark:bg-dark-background" edges={['bottom']}>
<View className="flex-1 items-center justify-center px-6">
<Text className="text-2xl font-bold text-textColor dark:text-dark-textColor">
My Collection
</Text>
<Text className="text-muted mt-2 text-center">
Your collected figures will appear here.
</Text>
</View>
</SafeAreaView>
);
}

View file

@ -0,0 +1,18 @@
import { View, Text } from 'react-native';
import { Link, Stack } from 'expo-router';
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: 'Not Found' }} />
<View className="flex-1 items-center justify-center bg-background dark:bg-dark-background">
<Text className="text-xl font-bold text-textColor dark:text-dark-textColor">
Page not found
</Text>
<Link href="/(tabs)" className="mt-4">
<Text className="text-primary text-base">Go to home</Text>
</Link>
</View>
</>
);
}

View file

@ -0,0 +1,47 @@
import '../global.css';
import { Stack, useRouter, useSegments } from 'expo-router';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { AuthProvider, useAuth } from '~/contexts/AuthContext';
import { useEffect } from 'react';
function AuthGuard({ children }: { children: React.ReactNode }) {
const { user, loading } = useAuth();
const segments = useSegments();
const router = useRouter();
useEffect(() => {
if (loading) return;
const inAuthGroup = segments[0] === '(auth)';
if (!user && !inAuthGroup) {
router.replace('/(auth)/login');
} else if (user && inAuthGroup) {
router.replace('/(tabs)');
}
}, [user, loading, segments]);
return <>{children}</>;
}
function Layout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
</Stack>
</GestureHandlerRootView>
);
}
export default function RootLayout() {
return (
<AuthProvider>
<AuthGuard>
<Layout />
</AuthGuard>
</AuthProvider>
);
}

View file

@ -0,0 +1,12 @@
module.exports = function (api) {
api.cache(true);
const plugins = [];
plugins.push('react-native-reanimated/plugin');
return {
presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'],
plugins,
};
};

View file

@ -0,0 +1,216 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import { ActivityIndicator, View, Text } from 'react-native';
import * as SecureStore from 'expo-secure-store';
import {
createAuthService,
createTokenManager,
setStorageAdapter,
setDeviceAdapter,
setNetworkAdapter,
type UserData,
} from '@manacore/shared-auth';
// Mana Core Auth URL from environment
const MANA_AUTH_URL = process.env.EXPO_PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
// Create SecureStore adapter for React Native
const createSecureStoreAdapter = () => ({
async getItem<T>(key: string): Promise<T | null> {
try {
const value = await SecureStore.getItemAsync(key);
return value ? JSON.parse(value) : null;
} catch {
return null;
}
},
async setItem(key: string, value: unknown): Promise<void> {
await SecureStore.setItemAsync(key, JSON.stringify(value));
},
async removeItem(key: string): Promise<void> {
await SecureStore.deleteItemAsync(key);
},
});
// Create device adapter for React Native
const createReactNativeDeviceAdapter = () => {
let deviceId: string | null = null;
return {
async getDeviceInfo() {
if (!deviceId) {
deviceId = await SecureStore.getItemAsync('@device/id');
if (!deviceId) {
deviceId = `rn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
await SecureStore.setItemAsync('@device/id', deviceId);
}
}
return {
deviceId,
deviceName: 'React Native Device',
deviceType: 'mobile',
platform: 'react-native',
};
},
async getStoredDeviceId() {
return deviceId || (await SecureStore.getItemAsync('@device/id'));
},
};
};
// Create network adapter
const createReactNativeNetworkAdapter = () => ({
async isDeviceConnected() {
return true;
},
async hasStableConnection() {
return true;
},
});
// Initialize adapters
setStorageAdapter(createSecureStoreAdapter());
setDeviceAdapter(createReactNativeDeviceAdapter());
setNetworkAdapter(createReactNativeNetworkAdapter());
// Create auth service
const authService = createAuthService({ baseUrl: MANA_AUTH_URL });
const tokenManager = createTokenManager(authService);
// Export for use in API client
export { authService, tokenManager };
// Auth context type
type AuthContextType = {
user: UserData | null;
loading: boolean;
signIn: (email: string, password: string) => Promise<{ error: any | null }>;
signUp: (
email: string,
password: string,
username?: string
) => Promise<{ error: any | null; data: any | null }>;
signOut: () => Promise<void>;
};
// Create auth context
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// Hook to access auth context
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
return {
user: null,
loading: true,
signIn: async () => ({ error: null }),
signUp: async () => ({ error: null, data: null }),
signOut: async () => {},
};
}
return context;
};
// AuthProvider component
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<UserData | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const initialize = async () => {
try {
setLoading(true);
const authenticated = await authService.isAuthenticated();
if (authenticated) {
const userData = await authService.getUserFromToken();
setUser(userData);
}
} catch (error) {
console.error('Error initializing auth session:', error);
setUser(null);
} finally {
setLoading(false);
}
};
initialize();
}, []);
const signIn = async (email: string, password: string) => {
try {
const result = await authService.signIn(email, password);
if (!result.success) {
return { error: { message: result.error } };
}
const userData = await authService.getUserFromToken();
setUser(userData);
return { error: null };
} catch (error: any) {
return { error };
}
};
const signUp = async (email: string, password: string, _username?: string) => {
try {
const result = await authService.signUp(email, password);
if (!result.success) {
return { data: null, error: { message: result.error } };
}
const signInResult = await signIn(email, password);
if (signInResult.error) {
return { data: null, error: signInResult.error };
}
return { data: user, error: null };
} catch (error) {
return { data: null, error };
}
};
const signOut = async () => {
try {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Sign out timeout after 5s')), 5000)
);
try {
await Promise.race([authService.signOut(), timeout]);
} catch {
// Force local logout on failure
}
setUser(null);
} catch {
setUser(null);
}
};
if (loading) {
return (
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#1A1A2E',
}}
>
<ActivityIndicator size="large" color="#6C5CE7" />
<Text style={{ marginTop: 16, color: '#fff' }}>Loading...</Text>
</View>
);
}
return (
<AuthContext.Provider value={{ user, loading, signIn, signUp, signOut }}>
{children}
</AuthContext.Provider>
);
}

View file

@ -0,0 +1,18 @@
{
"cli": {
"version": ">= 5.0.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {}
},
"submit": {
"production": {}
}
}

View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -0,0 +1,9 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');
const { withNativeWind } = require('nativewind/metro');
/** @type {import('expo/metro-config').MetroConfig} */
// eslint-disable-next-line no-undef
const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, { input: './global.css' });

View file

@ -0,0 +1 @@
/// <reference types="nativewind/types" />

View file

@ -0,0 +1,60 @@
{
"name": "@figgos/mobile",
"version": "1.0.0",
"main": "expo-router/entry",
"type": "commonjs",
"scripts": {
"dev": "expo start --dev-client",
"start": "expo start --dev-client",
"ios": "expo run:ios",
"android": "expo run:android",
"build:dev": "eas build --profile development",
"build:preview": "eas build --profile preview",
"build:prod": "eas build --profile production",
"prebuild": "expo prebuild",
"lint": "eslint .",
"format": "eslint . --fix"
},
"dependencies": {
"@expo/vector-icons": "^14.0.0",
"@manacore/shared-auth": "workspace:*",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-navigation/bottom-tabs": "^7.0.5",
"@react-navigation/native": "^7.0.3",
"expo": "^52.0.39",
"expo-constants": "~17.0.8",
"expo-dev-client": "~5.0.4",
"expo-dev-launcher": "^5.0.17",
"expo-linking": "~7.0.5",
"expo-router": "~4.0.6",
"expo-secure-store": "~14.0.1",
"expo-status-bar": "~2.0.1",
"expo-system-ui": "~4.0.8",
"expo-web-browser": "~14.0.2",
"nativewind": "latest",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.7",
"react-native-gesture-handler": "~2.20.2",
"react-native-reanimated": "3.16.2",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-web": "~0.19.10"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@types/react": "~18.3.12",
"dotenv": "^16.4.7",
"eslint": "^9.39.1",
"eslint-config-universe": "^12.0.1",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.0",
"typescript": "~5.3.3"
},
"eslintConfig": {
"extends": "universe/native",
"root": true
},
"private": true
}

View file

@ -0,0 +1,26 @@
import { authService } from '~/contexts/AuthContext';
const BACKEND_URL = process.env.EXPO_PUBLIC_BACKEND_URL || 'http://localhost:3025';
export async function fetchApi<T = any>(path: string, options?: RequestInit): Promise<T> {
const token = await authService.getAccessToken?.();
const response = await fetch(`${BACKEND_URL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options?.headers,
},
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
export const api = {
health: () => fetchApi('/health'),
};

View file

@ -0,0 +1,32 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./app/**/*.{js,ts,tsx}', './components/**/*.{js,ts,tsx}'],
presets: [require('nativewind/preset')],
darkMode: 'class',
theme: {
extend: {
colors: {
primary: '#6C5CE7',
secondary: '#A29BFE',
background: '#F8F9FA',
card: '#FFFFFF',
textColor: '#2D3436',
border: '#DFE6E9',
accent: '#00B894',
muted: '#B2BEC3',
dark: {
primary: '#A29BFE',
secondary: '#6C5CE7',
background: '#1A1A2E',
card: '#16213E',
textColor: '#FFFFFF',
border: '#2D3436',
accent: '#55EFC4',
muted: '#636E72',
},
},
},
},
plugins: [],
};

View file

@ -0,0 +1,12 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"~/*": ["*"]
}
},
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts", "nativewind-env.d.ts"]
}

8
apps/figgos/package.json Normal file
View file

@ -0,0 +1,8 @@
{
"name": "figgos",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "turbo run dev"
}
}

View file

@ -0,0 +1,16 @@
{
"name": "@figgos/shared",
"version": "1.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"type-check": "tsc --noEmit"
},
"devDependencies": {
"typescript": "^5.7.2"
}
}

View file

@ -0,0 +1,3 @@
// @figgos/shared - Shared types and constants
// Will be populated as features are added
export {};

View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2021",
"module": "commonjs",
"declaration": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View file

@ -87,7 +87,9 @@ services:
mc mb --ignore-existing myminio/inventory-storage;
mc mb --ignore-existing myminio/planta-storage;
mc mb --ignore-existing myminio/projectdoc-storage;
mc mb --ignore-existing myminio/figgos-storage;
mc anonymous set download myminio/picture-storage;
mc anonymous set download myminio/figgos-storage;
mc anonymous set download myminio/planta-storage;
mc anonymous set download myminio/inventory-storage;
echo 'Buckets created successfully';

View file

@ -145,6 +145,7 @@
"dev:figgos:android": "pnpm --filter @figgos/mobile android",
"figgos:db:push": "pnpm --filter @figgos/backend db:push",
"figgos:db:studio": "pnpm --filter @figgos/backend db:studio",
"dev:figgos:full": "./scripts/setup-databases.sh figgos && ./scripts/setup-databases.sh auth && concurrently -n auth,backend -c blue,green \"pnpm dev:auth\" \"pnpm dev:figgos:backend\"",
"worldream:dev": "turbo run dev --filter=worldream...",
"dev:worldream:web": "pnpm --filter @worldream/web dev",
"context:dev": "turbo run dev --filter=context...",

718
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -601,6 +601,34 @@ const APP_CONFIGS = [
},
},
// Figgos Backend (NestJS)
{
path: 'apps/figgos/apps/backend/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.FIGGOS_BACKEND_PORT || '3025',
DATABASE_URL: (env) => env.FIGGOS_DATABASE_URL,
MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL,
DEV_BYPASS_AUTH: () => 'true',
DEV_USER_ID: () => '00000000-0000-0000-0000-000000000000',
S3_ENDPOINT: (env) => env.S3_ENDPOINT || 'http://localhost:9000',
S3_REGION: (env) => env.S3_REGION || 'us-east-1',
S3_ACCESS_KEY: (env) => env.S3_ACCESS_KEY || 'minioadmin',
S3_SECRET_KEY: (env) => env.S3_SECRET_KEY || 'minioadmin',
S3_BUCKET: () => 'figgos-storage',
CORS_ORIGINS: () => 'http://localhost:5181,http://localhost:8081',
},
},
// Figgos Mobile (Expo)
{
path: 'apps/figgos/apps/mobile/.env',
vars: {
EXPO_PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.FIGGOS_BACKEND_PORT || '3025'}`,
EXPO_PUBLIC_MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL,
},
},
// Worldream Web (SvelteKit)
{
path: 'games/worldream/apps/web/.env',