mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-17 22:07:20 +00:00
new file: client/src/lib/embed-utils.ts
This commit is contained in:
parent
ad5f15271e
commit
293d3c0d02
24 changed files with 782 additions and 421 deletions
|
|
@ -1,14 +1,5 @@
|
|||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||
|
||||
// Copy web assets to the native project
|
||||
task copyWebApp(type: Copy) {
|
||||
from '../../dist'
|
||||
into 'src/main/assets/public'
|
||||
}
|
||||
|
||||
// Before building the app, run the copyWebApp task
|
||||
preBuild.dependsOn copyWebApp
|
||||
|
||||
android {
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_21
|
||||
|
|
@ -36,6 +27,7 @@ dependencies {
|
|||
implementation project(':capacitor-splash-screen')
|
||||
implementation project(':capacitor-status-bar')
|
||||
implementation project(':capacitor-toast')
|
||||
implementation project(':capacitor-native-biometric')
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,22 @@
|
|||
</application>
|
||||
|
||||
<!-- Permissions -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
|
||||
<!-- Hardware features -->
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.fingerprint" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
|
||||
</manifest>
|
||||
|
|
|
|||
26
android/app/src/main/res/values/colors.xml
Normal file
26
android/app/src/main/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- AeThex Brand Colors -->
|
||||
<color name="colorPrimary">#DC2626</color>
|
||||
<color name="colorPrimaryDark">#0a0a0a</color>
|
||||
<color name="colorAccent">#D4AF37</color>
|
||||
|
||||
<!-- Splash Screen -->
|
||||
<color name="splash_background">#0a0a0a</color>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<color name="status_bar">#0a0a0a</color>
|
||||
|
||||
<!-- Navigation Bar -->
|
||||
<color name="navigation_bar">#0a0a0a</color>
|
||||
|
||||
<!-- Foundation Theme -->
|
||||
<color name="foundation_primary">#DC2626</color>
|
||||
<color name="foundation_gold">#D4AF37</color>
|
||||
<color name="foundation_dark">#1a0505</color>
|
||||
|
||||
<!-- Corp Theme -->
|
||||
<color name="corp_primary">#3B82F6</color>
|
||||
<color name="corp_silver">#C0C0C0</color>
|
||||
<color name="corp_dark">#0f172a</color>
|
||||
</resources>
|
||||
|
|
@ -2,21 +2,27 @@
|
|||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="android:windowBackground">@color/splash_background</item>
|
||||
<item name="android:statusBarColor">@color/status_bar</item>
|
||||
<item name="android:navigationBarColor">@color/navigation_bar</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:background">@null</item>
|
||||
<item name="android:statusBarColor">@color/status_bar</item>
|
||||
<item name="android:navigationBarColor">@color/navigation_bar</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||
<item name="android:background">@drawable/splash</item>
|
||||
<item name="android:statusBarColor">@color/splash_background</item>
|
||||
<item name="android:navigationBarColor">@color/splash_background</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -55,3 +55,6 @@ project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacit
|
|||
|
||||
include ':capacitor-toast'
|
||||
project(':capacitor-toast').projectDir = new File('../node_modules/@capacitor/toast/android')
|
||||
|
||||
include ':capacitor-native-biometric'
|
||||
project(':capacitor-native-biometric').projectDir = new File('../node_modules/capacitor-native-biometric/android')
|
||||
|
|
|
|||
|
|
@ -20,22 +20,23 @@ const config: CapacitorConfig = {
|
|||
},
|
||||
plugins: {
|
||||
SplashScreen: {
|
||||
launchShowDuration: 0,
|
||||
launchShowDuration: 2000,
|
||||
launchAutoHide: true,
|
||||
backgroundColor: '#000000',
|
||||
launchFadeOutDuration: 500,
|
||||
backgroundColor: '#0a0a0a',
|
||||
androidSplashResourceName: 'splash',
|
||||
androidScaleType: 'CENTER_CROP',
|
||||
showSpinner: false,
|
||||
androidSpinnerStyle: 'large',
|
||||
showSpinner: true,
|
||||
androidSpinnerStyle: 'small',
|
||||
iosSpinnerStyle: 'small',
|
||||
spinnerColor: '#999999',
|
||||
spinnerColor: '#DC2626',
|
||||
splashFullScreen: true,
|
||||
splashImmersive: true
|
||||
},
|
||||
StatusBar: {
|
||||
style: 'DARK',
|
||||
backgroundColor: '#000000',
|
||||
overlaysWebView: true
|
||||
backgroundColor: '#0a0a0a',
|
||||
overlaysWebView: false
|
||||
},
|
||||
App: {
|
||||
backButtonEnabled: true
|
||||
|
|
@ -45,8 +46,18 @@ const config: CapacitorConfig = {
|
|||
},
|
||||
LocalNotifications: {
|
||||
smallIcon: 'ic_stat_icon_config_sample',
|
||||
iconColor: '#488AFF',
|
||||
iconColor: '#DC2626',
|
||||
sound: 'beep.wav'
|
||||
},
|
||||
Keyboard: {
|
||||
resize: 'body',
|
||||
resizeOnFullScreen: true,
|
||||
style: 'dark'
|
||||
},
|
||||
Haptics: {
|
||||
selectionStart: true,
|
||||
selectionChanged: true,
|
||||
selectionEnd: true
|
||||
}
|
||||
},
|
||||
android: {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Toaster } from "@/components/ui/toaster";
|
|||
import { AuthProvider } from "@/lib/auth";
|
||||
import { TutorialProvider } from "@/components/Tutorial";
|
||||
import { ProtectedRoute } from "@/components/ProtectedRoute";
|
||||
import { HapticProvider } from "@/components/mobile/HapticFeedback";
|
||||
import NotFound from "@/pages/not-found";
|
||||
import Home from "@/pages/home";
|
||||
import Passport from "@/pages/passport";
|
||||
|
|
@ -114,12 +115,14 @@ function App() {
|
|||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<LabTerminalProvider>
|
||||
<TutorialProvider>
|
||||
<Toaster />
|
||||
<Router />
|
||||
</TutorialProvider>
|
||||
</LabTerminalProvider>
|
||||
<HapticProvider enableGlobalTouchFeedback={true}>
|
||||
<LabTerminalProvider>
|
||||
<TutorialProvider>
|
||||
<Toaster />
|
||||
<Router />
|
||||
</TutorialProvider>
|
||||
</LabTerminalProvider>
|
||||
</HapticProvider>
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
|
|
|||
37
client/src/components/mobile/HapticButton.tsx
Normal file
37
client/src/components/mobile/HapticButton.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import * as React from "react";
|
||||
import { Button, ButtonProps } from "@/components/ui/button";
|
||||
import { useHaptics } from "@/hooks/use-haptics";
|
||||
|
||||
export interface HapticButtonProps extends ButtonProps {
|
||||
hapticStyle?: 'light' | 'medium' | 'heavy';
|
||||
hapticOnPress?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Button wrapper that adds haptic feedback on mobile devices.
|
||||
* Falls back gracefully to normal button on web.
|
||||
*/
|
||||
export const HapticButton = React.forwardRef<HTMLButtonElement, HapticButtonProps>(
|
||||
({ hapticStyle = 'light', hapticOnPress = true, onClick, ...props }, ref) => {
|
||||
const haptics = useHaptics();
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (hapticOnPress) {
|
||||
haptics.impact(hapticStyle);
|
||||
}
|
||||
onClick?.(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
onClick={handleClick}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
HapticButton.displayName = "HapticButton";
|
||||
|
||||
export default HapticButton;
|
||||
90
client/src/components/mobile/HapticFeedback.tsx
Normal file
90
client/src/components/mobile/HapticFeedback.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import React, { createContext, useContext, useEffect } from 'react';
|
||||
import { useHaptics } from '@/hooks/use-haptics';
|
||||
import { isMobile } from '@/lib/platform';
|
||||
|
||||
interface HapticContextValue {
|
||||
triggerImpact: (style?: 'light' | 'medium' | 'heavy') => void;
|
||||
triggerNotification: (type?: 'success' | 'warning' | 'error') => void;
|
||||
triggerSelection: () => void;
|
||||
}
|
||||
|
||||
const HapticContext = createContext<HapticContextValue>({
|
||||
triggerImpact: () => {},
|
||||
triggerNotification: () => {},
|
||||
triggerSelection: () => {},
|
||||
});
|
||||
|
||||
export function useHapticFeedback() {
|
||||
return useContext(HapticContext);
|
||||
}
|
||||
|
||||
interface HapticProviderProps {
|
||||
children: React.ReactNode;
|
||||
enableGlobalTouchFeedback?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider that enables haptic feedback throughout the app.
|
||||
* Wrap your app with this to enable automatic haptics on touch events.
|
||||
*/
|
||||
export function HapticProvider({ children, enableGlobalTouchFeedback = true }: HapticProviderProps) {
|
||||
const haptics = useHaptics();
|
||||
|
||||
// Add global touch feedback for interactive elements
|
||||
useEffect(() => {
|
||||
if (!enableGlobalTouchFeedback || !isMobile()) return;
|
||||
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Check if the touched element is interactive
|
||||
const isInteractive =
|
||||
target.tagName === 'BUTTON' ||
|
||||
target.tagName === 'A' ||
|
||||
target.closest('button') ||
|
||||
target.closest('a') ||
|
||||
target.closest('[role="button"]') ||
|
||||
target.closest('[data-haptic]');
|
||||
|
||||
if (isInteractive) {
|
||||
haptics.impact('light');
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('touchstart', handleTouchStart, { passive: true });
|
||||
return () => document.removeEventListener('touchstart', handleTouchStart);
|
||||
}, [enableGlobalTouchFeedback, haptics]);
|
||||
|
||||
const value: HapticContextValue = {
|
||||
triggerImpact: (style = 'medium') => haptics.impact(style),
|
||||
triggerNotification: (type = 'success') => haptics.notification(type),
|
||||
triggerSelection: () => haptics.selectionChanged(),
|
||||
};
|
||||
|
||||
return (
|
||||
<HapticContext.Provider value={value}>
|
||||
{children}
|
||||
</HapticContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* HOC to add haptic feedback to any component
|
||||
*/
|
||||
export function withHapticFeedback<P extends object>(
|
||||
WrappedComponent: React.ComponentType<P>,
|
||||
hapticStyle: 'light' | 'medium' | 'heavy' = 'light'
|
||||
) {
|
||||
return function HapticWrapper(props: P & { onClick?: (e: React.MouseEvent) => void }) {
|
||||
const haptics = useHaptics();
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
haptics.impact(hapticStyle);
|
||||
props.onClick?.(e);
|
||||
};
|
||||
|
||||
return <WrappedComponent {...props} onClick={handleClick} />;
|
||||
};
|
||||
}
|
||||
|
||||
export default HapticProvider;
|
||||
|
|
@ -8,6 +8,15 @@ interface MobileHeaderProps {
|
|||
backPath?: string;
|
||||
}
|
||||
|
||||
// Check if we're inside an iframe (embedded in the OS)
|
||||
const isEmbedded = () => {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
} catch (e) {
|
||||
return true; // If we can't access parent, assume embedded
|
||||
}
|
||||
};
|
||||
|
||||
export function MobileHeader({
|
||||
title = 'AeThex OS',
|
||||
onMenuClick,
|
||||
|
|
@ -16,6 +25,11 @@ export function MobileHeader({
|
|||
}: MobileHeaderProps) {
|
||||
const [, navigate] = useLocation();
|
||||
|
||||
// Don't render the header if we're embedded inside the OS iframe
|
||||
if (isEmbedded()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 left-0 right-0 z-50 bg-black/95 backdrop-blur-xl border-b border-emerald-500/30">
|
||||
<div className="flex items-center justify-between px-4 py-3 safe-area-inset-top">
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
|
||||
// Note: Biometric auth requires native-auth plugin or similar
|
||||
// For now we'll create the interface and you can install the plugin later
|
||||
import { NativeBiometric, BiometryType } from 'capacitor-native-biometric';
|
||||
|
||||
interface BiometricAuthResult {
|
||||
isAvailable: boolean;
|
||||
biometricType: 'fingerprint' | 'face' | 'iris' | 'none';
|
||||
authenticate: () => Promise<boolean>;
|
||||
authenticate: (reason?: string) => Promise<boolean>;
|
||||
isAuthenticated: boolean;
|
||||
checkCredentials: () => Promise<boolean>;
|
||||
setCredentials: (username: string, password: string) => Promise<void>;
|
||||
deleteCredentials: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useBiometricAuth(): BiometricAuthResult {
|
||||
|
|
@ -23,31 +24,46 @@ export function useBiometricAuth(): BiometricAuthResult {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if biometrics are available
|
||||
// This would use @capacitor-community/native-biometric or similar
|
||||
// For now, we'll assume it's available on mobile
|
||||
setIsAvailable(true);
|
||||
setBiometricType('fingerprint'); // Default assumption
|
||||
try {
|
||||
const result = await NativeBiometric.isAvailable();
|
||||
setIsAvailable(result.isAvailable);
|
||||
|
||||
// Map biometry type
|
||||
switch (result.biometryType) {
|
||||
case BiometryType.FINGERPRINT:
|
||||
case BiometryType.TOUCH_ID:
|
||||
setBiometricType('fingerprint');
|
||||
break;
|
||||
case BiometryType.FACE_ID:
|
||||
case BiometryType.FACE_AUTHENTICATION:
|
||||
setBiometricType('face');
|
||||
break;
|
||||
case BiometryType.IRIS_AUTHENTICATION:
|
||||
setBiometricType('iris');
|
||||
break;
|
||||
default:
|
||||
setBiometricType('none');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Biometric availability check failed:', error);
|
||||
setIsAvailable(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkAvailability();
|
||||
}, []);
|
||||
|
||||
const authenticate = async (): Promise<boolean> => {
|
||||
const authenticate = useCallback(async (reason?: string): Promise<boolean> => {
|
||||
if (!isAvailable) return false;
|
||||
|
||||
try {
|
||||
// This is where you'd call the actual biometric auth
|
||||
// For example with @capacitor-community/native-biometric:
|
||||
// const result = await NativeBiometric.verifyIdentity({
|
||||
// reason: "Authenticate to access AeThex OS",
|
||||
// title: "Biometric Authentication",
|
||||
// subtitle: "Use your fingerprint or face",
|
||||
// description: "Please authenticate"
|
||||
// });
|
||||
await NativeBiometric.verifyIdentity({
|
||||
reason: reason || 'Authenticate to access AeThex OS',
|
||||
title: 'Biometric Authentication',
|
||||
subtitle: 'Use your fingerprint or face',
|
||||
description: 'Verify your identity to continue',
|
||||
});
|
||||
|
||||
// For now, simulate success
|
||||
console.log('Biometric auth would trigger here');
|
||||
setIsAuthenticated(true);
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
|
@ -55,12 +71,43 @@ export function useBiometricAuth(): BiometricAuthResult {
|
|||
setIsAuthenticated(false);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}, [isAvailable]);
|
||||
|
||||
const checkCredentials = useCallback(async (): Promise<boolean> => {
|
||||
if (!Capacitor.isNativePlatform()) return false;
|
||||
try {
|
||||
await NativeBiometric.getCredentials({ server: 'com.aethex.os' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setCredentials = useCallback(async (username: string, password: string): Promise<void> => {
|
||||
if (!Capacitor.isNativePlatform()) return;
|
||||
await NativeBiometric.setCredentials({
|
||||
server: 'com.aethex.os',
|
||||
username,
|
||||
password,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const deleteCredentials = useCallback(async (): Promise<void> => {
|
||||
if (!Capacitor.isNativePlatform()) return;
|
||||
try {
|
||||
await NativeBiometric.deleteCredentials({ server: 'com.aethex.os' });
|
||||
} catch {
|
||||
// Credentials may not exist
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isAvailable,
|
||||
biometricType,
|
||||
authenticate,
|
||||
isAuthenticated
|
||||
isAuthenticated,
|
||||
checkCredentials,
|
||||
setCredentials,
|
||||
deleteCredentials,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
12
client/src/lib/embed-utils.ts
Normal file
12
client/src/lib/embed-utils.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* Utility to detect if the current page is embedded in an iframe
|
||||
* Used by hub pages to hide their own navigation when loaded inside the OS window system
|
||||
*/
|
||||
export const isEmbedded = (): boolean => {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
} catch (e) {
|
||||
// If cross-origin, we can't access parent - assume embedded
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
|
@ -3,6 +3,7 @@ import { Link } from "wouter";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { ArrowLeft, TrendingUp, Code, Star, Eye, Heart, Share2, Loader2 } from "lucide-react";
|
||||
import { isEmbedded } from "@/lib/embed-utils";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
|
||||
|
|
@ -53,21 +54,25 @@ export default function CodeGallery() {
|
|||
}
|
||||
};
|
||||
|
||||
const embedded = isEmbedded();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
||||
{/* Header */}
|
||||
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/">
|
||||
<button className="text-slate-400 hover:text-white">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<Code className="w-6 h-6 text-cyan-400" />
|
||||
<h1 className="text-2xl font-bold text-white">Code Gallery</h1>
|
||||
{/* Header - hidden when embedded in OS iframe */}
|
||||
{!embedded && (
|
||||
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/">
|
||||
<button className="text-slate-400 hover:text-white">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<Code className="w-6 h-6 text-cyan-400" />
|
||||
<h1 className="text-2xl font-bold text-white">Code Gallery</h1>
|
||||
</div>
|
||||
<Button className="bg-cyan-600 hover:bg-cyan-700">Share Snippet</Button>
|
||||
</div>
|
||||
<Button className="bg-cyan-600 hover:bg-cyan-700">Share Snippet</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Link } from "wouter";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { ArrowLeft, FileText, Folder, Plus, Trash2, Download, Copy, Loader2 } from "lucide-react";
|
||||
import { isEmbedded } from "@/lib/embed-utils";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { nanoid } from "nanoid";
|
||||
|
|
@ -70,23 +71,27 @@ export default function FileManager() {
|
|||
}
|
||||
};
|
||||
|
||||
const embedded = isEmbedded();
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col bg-slate-900">
|
||||
{/* Header */}
|
||||
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/">
|
||||
<button className="text-slate-400 hover:text-white">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold text-white">File Manager</h1>
|
||||
{/* Header - hidden when embedded in OS iframe */}
|
||||
{!embedded && (
|
||||
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/">
|
||||
<button className="text-slate-400 hover:text-white">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold text-white">File Manager</h1>
|
||||
</div>
|
||||
<Button className="bg-cyan-600 hover:bg-cyan-700 gap-2">
|
||||
<Plus className="w-4 h-4" />
|
||||
New File
|
||||
</Button>
|
||||
</div>
|
||||
<Button className="bg-cyan-600 hover:bg-cyan-700 gap-2">
|
||||
<Plus className="w-4 h-4" />
|
||||
New File
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* File List */}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
ArrowLeft, ShoppingCart, Star, Plus, Loader2, Gamepad2,
|
||||
Zap, Trophy, Users, DollarSign, TrendingUp, Filter, Search
|
||||
} from "lucide-react";
|
||||
import { isEmbedded } from "@/lib/embed-utils";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
|
||||
|
|
@ -196,55 +197,59 @@ export default function GameMarketplace() {
|
|||
item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const embedded = isEmbedded();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-950 text-white">
|
||||
{/* Header */}
|
||||
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-4 gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/hub">
|
||||
<button className="text-slate-400 hover:text-white transition-colors">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<Gamepad2 className="w-6 h-6 text-cyan-400" />
|
||||
<h1 className="text-2xl font-bold">Game Marketplace</h1>
|
||||
{/* Header - hidden when embedded in OS iframe */}
|
||||
{!embedded && (
|
||||
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-4 gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/hub">
|
||||
<button className="text-slate-400 hover:text-white transition-colors">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<Gamepad2 className="w-6 h-6 text-cyan-400" />
|
||||
<h1 className="text-2xl font-bold">Game Marketplace</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Wallet Balance */}
|
||||
<div className="bg-slate-800 px-4 py-2 rounded-lg border border-slate-700 flex items-center gap-2">
|
||||
<DollarSign className="w-4 h-4 text-yellow-400" />
|
||||
<span className="font-mono font-bold text-lg text-cyan-400">{wallet.balance} {wallet.currency}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Wallet Balance */}
|
||||
<div className="bg-slate-800 px-4 py-2 rounded-lg border border-slate-700 flex items-center gap-2">
|
||||
<DollarSign className="w-4 h-4 text-yellow-400" />
|
||||
<span className="font-mono font-bold text-lg text-cyan-400">{wallet.balance} {wallet.currency}</span>
|
||||
{/* Search & Filter */}
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
|
||||
<Input
|
||||
placeholder="Search games, assets, creators..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 bg-slate-800 border-slate-700 text-white"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as any)}
|
||||
className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"
|
||||
>
|
||||
<option value="newest">Newest</option>
|
||||
<option value="popular">Popular</option>
|
||||
<option value="price-low">Price: Low→High</option>
|
||||
<option value="price-high">Price: High→Low</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search & Filter */}
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
|
||||
<Input
|
||||
placeholder="Search games, assets, creators..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 bg-slate-800 border-slate-700 text-white"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as any)}
|
||||
className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"
|
||||
>
|
||||
<option value="newest">Newest</option>
|
||||
<option value="popular">Popular</option>
|
||||
<option value="price-low">Price: Low→High</option>
|
||||
<option value="price-high">Price: High→Low</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="max-w-7xl mx-auto p-4 md:p-6">
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
ArrowLeft, Radio, Eye, Heart, MessageCircle, Share2,
|
||||
Twitch, Youtube, Play, Clock, Users, TrendingUp, Filter, Search
|
||||
} from "lucide-react";
|
||||
import { isEmbedded } from "@/lib/embed-utils";
|
||||
|
||||
interface Stream {
|
||||
id: string;
|
||||
|
|
@ -180,43 +181,47 @@ export default function GameStreaming() {
|
|||
const liveStreams = filteredStreams.filter(s => s.isLive);
|
||||
const recordedStreams = filteredStreams.filter(s => !s.isLive);
|
||||
|
||||
const embedded = isEmbedded();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-950 text-white">
|
||||
{/* Header */}
|
||||
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Link href="/hub">
|
||||
<button className="text-slate-400 hover:text-white transition-colors">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold">Game Streaming Hub</h1>
|
||||
</div>
|
||||
|
||||
{/* Search & Filter */}
|
||||
<div className="flex gap-2 flex-col sm:flex-row">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
placeholder="Search streams, channels..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm placeholder-slate-400"
|
||||
/>
|
||||
{/* Header - hidden when embedded in OS iframe */}
|
||||
{!embedded && (
|
||||
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Link href="/hub">
|
||||
<button className="text-slate-400 hover:text-white transition-colors">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold">Game Streaming Hub</h1>
|
||||
</div>
|
||||
|
||||
{/* Search & Filter */}
|
||||
<div className="flex gap-2 flex-col sm:flex-row">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
placeholder="Search streams, channels..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm placeholder-slate-400"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={selectedPlatform}
|
||||
onChange={(e) => setSelectedPlatform(e.target.value as any)}
|
||||
className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"
|
||||
>
|
||||
<option value="all">All Platforms</option>
|
||||
<option value="twitch">Twitch</option>
|
||||
<option value="youtube">YouTube</option>
|
||||
</select>
|
||||
</div>
|
||||
<select
|
||||
value={selectedPlatform}
|
||||
onChange={(e) => setSelectedPlatform(e.target.value as any)}
|
||||
className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"
|
||||
>
|
||||
<option value="all">All Platforms</option>
|
||||
<option value="twitch">Twitch</option>
|
||||
<option value="youtube">YouTube</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
<div className="max-w-7xl mx-auto p-4 md:p-6">
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
Trash2, Award, User, Calendar, Search, Filter, Plus, Loader2,
|
||||
Package, AlertCircle, CheckCircle
|
||||
} from "lucide-react";
|
||||
import { isEmbedded } from "@/lib/embed-utils";
|
||||
|
||||
interface Mod {
|
||||
id: string;
|
||||
|
|
@ -199,69 +200,72 @@ export default function ModWorkshop() {
|
|||
|
||||
const games = ["all", "Minecraft", "Roblox", "Steam Games", "All Games"];
|
||||
|
||||
const embedded = isEmbedded();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-950 text-white">
|
||||
{/* Header */}
|
||||
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-4 gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/hub">
|
||||
<button className="text-slate-400 hover:text-white transition-colors">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="w-6 h-6 text-cyan-400" />
|
||||
<h1 className="text-2xl font-bold">Mod Workshop</h1>
|
||||
{/* Header - hidden when embedded in OS iframe */}
|
||||
{!embedded && (
|
||||
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-4 gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/hub">
|
||||
<button className="text-slate-400 hover:text-white transition-colors">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="w-6 h-6 text-cyan-400" />
|
||||
<h1 className="text-2xl font-bold">Mod Workshop</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => setShowUploadModal(true)}
|
||||
className="bg-cyan-600 hover:bg-cyan-700 gap-2"
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Upload Mod</span>
|
||||
<span className="sm:hidden">Upload</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Search & Filters */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
placeholder="Search mods, authors..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm placeholder-slate-400"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as any)}
|
||||
className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"
|
||||
<Button
|
||||
onClick={() => setShowUploadModal(true)}
|
||||
className="bg-cyan-600 hover:bg-cyan-700 gap-2"
|
||||
>
|
||||
<option value="trending">Trending</option>
|
||||
<option value="newest">Newest</option>
|
||||
<option value="popular">Most Downloaded</option>
|
||||
<option value="rating">Highest Rated</option>
|
||||
</select>
|
||||
<Upload className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Upload Mod</span>
|
||||
<span className="sm:hidden">Upload</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Category & Game Filters */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{/* Search & Filters */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
{(["all", "gameplay", "cosmetic", "utility", "enhancement"] as const).map(cat => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setSelectedCategory(cat)}
|
||||
className={`px-3 py-1 rounded-lg text-xs font-medium capitalize transition-colors ${
|
||||
selectedCategory === cat
|
||||
? "bg-cyan-600 text-white"
|
||||
: "bg-slate-800 text-slate-300 hover:bg-slate-700"
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
placeholder="Search mods, authors..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm placeholder-slate-400"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as any)}
|
||||
className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"
|
||||
>
|
||||
<option value="trending">Trending</option>
|
||||
<option value="newest">Newest</option>
|
||||
<option value="popular">Most Downloaded</option>
|
||||
<option value="rating">Highest Rated</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Category & Game Filters */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<div className="flex gap-2">
|
||||
{(["all", "gameplay", "cosmetic", "utility", "enhancement"] as const).map(cat => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setSelectedCategory(cat)}
|
||||
className={`px-3 py-1 rounded-lg text-xs font-medium capitalize transition-colors ${
|
||||
selectedCategory === cat
|
||||
? "bg-cyan-600 text-white"
|
||||
: "bg-slate-800 text-slate-300 hover:bg-slate-700"
|
||||
}`}
|
||||
>
|
||||
{cat === "all" ? "All Categories" : cat}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input";
|
|||
import { Card } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ArrowLeft, ShoppingCart, Star, Plus, Loader2 } from "lucide-react";
|
||||
import { isEmbedded } from "@/lib/embed-utils";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
|
||||
|
|
@ -75,32 +76,36 @@ export default function Marketplace() {
|
|||
}
|
||||
};
|
||||
|
||||
const embedded = isEmbedded();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
||||
{/* Header */}
|
||||
<div className="bg-slate-950 border-b border-slate-700 px-3 md:px-6 py-3 md:py-4 sticky top-0 z-10">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 md:gap-4 min-w-0 flex-1">
|
||||
<Link href="/">
|
||||
<button className="text-slate-400 hover:text-white shrink-0">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<h1 className="text-lg md:text-2xl font-bold text-white truncate">Marketplace</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 md:gap-4 shrink-0">
|
||||
<div className="bg-slate-800 px-2 md:px-4 py-1.5 md:py-2 rounded-lg border border-slate-700">
|
||||
<p className="text-xs text-slate-400 hidden sm:block">Balance</p>
|
||||
<p className="text-sm md:text-xl font-bold text-cyan-400">{balance} LP</p>
|
||||
{/* Header - hidden when embedded in OS iframe */}
|
||||
{!embedded && (
|
||||
<div className="bg-slate-950 border-b border-slate-700 px-3 md:px-6 py-3 md:py-4 sticky top-0 z-10">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 md:gap-4 min-w-0 flex-1">
|
||||
<Link href="/">
|
||||
<button className="text-slate-400 hover:text-white shrink-0">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<h1 className="text-lg md:text-2xl font-bold text-white truncate">Marketplace</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 md:gap-4 shrink-0">
|
||||
<div className="bg-slate-800 px-2 md:px-4 py-1.5 md:py-2 rounded-lg border border-slate-700">
|
||||
<p className="text-xs text-slate-400 hidden sm:block">Balance</p>
|
||||
<p className="text-sm md:text-xl font-bold text-cyan-400">{balance} LP</p>
|
||||
</div>
|
||||
<Button className="bg-cyan-600 hover:bg-cyan-700 gap-1 md:gap-2 text-xs md:text-sm px-2 md:px-4 h-8 md:h-10">
|
||||
<Plus className="w-3 h-3 md:w-4 md:h-4" />
|
||||
<span className="hidden sm:inline">Sell Item</span>
|
||||
<span className="sm:hidden">Sell</span>
|
||||
</Button>
|
||||
</div>
|
||||
<Button className="bg-cyan-600 hover:bg-cyan-700 gap-1 md:gap-2 text-xs md:text-sm px-2 md:px-4 h-8 md:h-10">
|
||||
<Plus className="w-3 h-3 md:w-4 md:h-4" />
|
||||
<span className="hidden sm:inline">Sell Item</span>
|
||||
<span className="sm:hidden">Sell</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-3 md:p-6 max-w-7xl mx-auto">
|
||||
{/* Category Tabs */}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input";
|
|||
import { Card } from "@/components/ui/card";
|
||||
import { ArrowLeft, Send, Search, Loader2 } from "lucide-react";
|
||||
import { MobileHeader } from "@/components/mobile/MobileHeader";
|
||||
import { isEmbedded } from "@/lib/embed-utils";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { nanoid } from "nanoid";
|
||||
|
|
@ -96,22 +97,29 @@ export default function Messaging() {
|
|||
c.username.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const embedded = isEmbedded();
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col bg-slate-900">
|
||||
{/* Mobile Header */}
|
||||
<div className="md:hidden">
|
||||
<MobileHeader title="Messages" />
|
||||
</div>
|
||||
|
||||
{/* Desktop Header */}
|
||||
<div className="hidden md:flex bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center gap-4">
|
||||
<Link href="/">
|
||||
<button className="text-slate-400 hover:text-white">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold text-white">Messages</h1>
|
||||
</div>
|
||||
{/* Headers - hidden when embedded in OS iframe */}
|
||||
{!embedded && (
|
||||
<>
|
||||
{/* Mobile Header */}
|
||||
<div className="md:hidden">
|
||||
<MobileHeader title="Messages" />
|
||||
</div>
|
||||
|
||||
{/* Desktop Header */}
|
||||
<div className="hidden md:flex bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center gap-4">
|
||||
<Link href="/">
|
||||
<button className="text-slate-400 hover:text-white">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold text-white">Messages</h1>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Chat List */}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Card } from "@/components/ui/card";
|
|||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ArrowLeft, Plus, Trash2, ExternalLink, Github, Globe, Loader2 } from "lucide-react";
|
||||
import { MobileHeader } from "@/components/mobile/MobileHeader";
|
||||
import { isEmbedded } from "@/lib/embed-utils";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { nanoid } from "nanoid";
|
||||
|
|
@ -102,31 +103,38 @@ export default function Projects() {
|
|||
}
|
||||
};
|
||||
|
||||
const embedded = isEmbedded();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
||||
{/* Mobile Header */}
|
||||
<div className="md:hidden">
|
||||
<MobileHeader title="Projects" />
|
||||
</div>
|
||||
|
||||
{/* Desktop Header */}
|
||||
<div className="hidden md:block bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/">
|
||||
<button className="text-slate-400 hover:text-white transition-colors">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold text-white">Projects & Portfolio</h1>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setShowForm(!showForm)}
|
||||
className="bg-cyan-600 hover:bg-cyan-700 gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
New Project
|
||||
</Button>
|
||||
</div>
|
||||
{/* Headers - hidden when embedded in OS iframe */}
|
||||
{!embedded && (
|
||||
<>
|
||||
{/* Mobile Header */}
|
||||
<div className="md:hidden">
|
||||
<MobileHeader title="Projects" />
|
||||
</div>
|
||||
|
||||
{/* Desktop Header */}
|
||||
<div className="hidden md:block bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/">
|
||||
<button className="text-slate-400 hover:text-white transition-colors">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold text-white">Projects & Portfolio</h1>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setShowForm(!showForm)}
|
||||
className="bg-cyan-600 hover:bg-cyan-700 gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
New Project
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
{/* Add Project Form */}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Link } from "wouter";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { ArrowLeft, Settings, Bell, Lock, Palette, HardDrive, User, Loader2 } from "lucide-react";
|
||||
import { isEmbedded } from "@/lib/embed-utils";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { nanoid } from "nanoid";
|
||||
|
|
@ -88,18 +89,22 @@ export default function SettingsWorkspace() {
|
|||
saveSettings(newSettings);
|
||||
};
|
||||
|
||||
const embedded = isEmbedded();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
||||
{/* Header */}
|
||||
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center gap-4 sticky top-0 z-10">
|
||||
<Link href="/">
|
||||
<button className="text-slate-400 hover:text-white">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<Settings className="w-6 h-6 text-cyan-400" />
|
||||
<h1 className="text-2xl font-bold text-white">Workspace Settings</h1>
|
||||
</div>
|
||||
{/* Header - hidden when embedded in OS iframe */}
|
||||
{!embedded && (
|
||||
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center gap-4 sticky top-0 z-10">
|
||||
<Link href="/">
|
||||
<button className="text-slate-400 hover:text-white">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<Settings className="w-6 h-6 text-cyan-400" />
|
||||
<h1 className="text-2xl font-bold text-white">Workspace Settings</h1>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-6 max-w-4xl mx-auto">
|
||||
{/* Appearance Settings */}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import { useMobileNative } from "@/hooks/use-mobile-native";
|
|||
import { useNativeFeatures } from "@/hooks/use-native-features";
|
||||
import { useBiometricAuth } from "@/hooks/use-biometric-auth";
|
||||
import { StatusBar, Style } from '@capacitor/status-bar';
|
||||
import { App as CapacitorApp } from '@capacitor/app';
|
||||
import { isMobile } from '@/lib/platform';
|
||||
import { MobileQuickActions } from "@/components/MobileQuickActions";
|
||||
import { Minesweeper } from "@/components/games/Minesweeper";
|
||||
import { CookieClicker } from "@/components/games/CookieClicker";
|
||||
|
|
@ -1215,131 +1217,162 @@ export default function AeThexOS() {
|
|||
};
|
||||
}, [layout.isMobile]);
|
||||
|
||||
// Handle Android hardware back button
|
||||
useEffect(() => {
|
||||
if (!layout.isMobile || !isMobile()) return;
|
||||
|
||||
const backHandler = CapacitorApp.addListener('backButton', () => {
|
||||
// Get current active windows (non-minimized)
|
||||
const activeWindows = windows.filter(w => !w.minimized);
|
||||
|
||||
if (activeWindows.length > 0) {
|
||||
// Close the topmost window
|
||||
const topWindow = activeWindows[activeWindows.length - 1];
|
||||
closeWindow(topWindow.id);
|
||||
impact('light');
|
||||
} else {
|
||||
// No windows open - minimize app (don't exit)
|
||||
CapacitorApp.minimizeApp();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
backHandler.remove();
|
||||
};
|
||||
}, [layout.isMobile, windows, closeWindow, impact]);
|
||||
|
||||
// Native Android App Layout
|
||||
if (layout.isMobile) {
|
||||
const activeWindows = windows.filter(w => !w.minimized);
|
||||
const currentWindow = activeWindows[activeWindows.length - 1];
|
||||
|
||||
// Dynamic theme colors based on clearance mode
|
||||
const isFoundation = clearanceMode === 'foundation';
|
||||
const mobileTheme = {
|
||||
primary: isFoundation ? 'rgb(220, 38, 38)' : 'rgb(59, 130, 246)', // red-600 or blue-500
|
||||
secondary: isFoundation ? 'rgb(212, 175, 55)' : 'rgb(148, 163, 184)', // gold or slate-400
|
||||
primaryClass: isFoundation ? 'text-red-500' : 'text-blue-500',
|
||||
secondaryClass: isFoundation ? 'text-amber-400' : 'text-slate-300',
|
||||
borderClass: isFoundation ? 'border-red-900/50' : 'border-blue-900/50',
|
||||
bgAccent: isFoundation ? 'bg-red-900/20' : 'bg-blue-900/20',
|
||||
iconClass: isFoundation ? 'text-red-400' : 'text-blue-400',
|
||||
gradientBg: isFoundation
|
||||
? 'linear-gradient(135deg, #0a0a0a 0%, #1a0505 50%, #0a0a0a 100%)'
|
||||
: 'linear-gradient(135deg, #0a0a0a 0%, #050a14 50%, #0a0a0a 100%)',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen bg-black overflow-hidden flex flex-col">
|
||||
<style>{`
|
||||
@keyframes scan {
|
||||
0% { transform: translateY(-100%); }
|
||||
100% { transform: translateY(100%); }
|
||||
}
|
||||
@keyframes pulse-border {
|
||||
0%, 100% { opacity: 0.3; }
|
||||
50% { opacity: 0.8; }
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* Ingress Status Bar - Minimal */}
|
||||
<div className="relative h-8 bg-black/90 border-b border-emerald-500/50 shrink-0">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-emerald-500/5 to-transparent"></div>
|
||||
<div className="h-screen w-screen overflow-hidden flex flex-col" style={{ background: mobileTheme.gradientBg }}>
|
||||
{/* AeThex Mobile Status Bar */}
|
||||
<div className={`relative h-10 bg-black/90 ${mobileTheme.borderClass} border-b shrink-0`} style={{ paddingTop: 'env(safe-area-inset-top)' }}>
|
||||
<div className="relative flex items-center justify-between px-4 h-full">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<Wifi className="w-3.5 h-3.5 text-cyan-400" />
|
||||
<div className="flex items-center gap-0.5">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<div key={i} className="w-0.5 h-1.5 bg-emerald-400 rounded-full" style={{ height: `${(i + 1) * 2}px` }} />
|
||||
))}
|
||||
</div>
|
||||
<span className={`${mobileTheme.primaryClass} font-bold text-sm font-mono`}>AeThex</span>
|
||||
<span className={`${mobileTheme.secondaryClass} text-xs font-mono opacity-60`}>
|
||||
{isFoundation ? 'FOUNDATION' : 'CORP'}
|
||||
</span>
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse"></div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-cyan-400 text-xs font-mono font-bold tracking-wider">
|
||||
<div className={`flex items-center gap-3 ${mobileTheme.secondaryClass} opacity-80 text-xs font-mono`}>
|
||||
<Wifi className="w-3.5 h-3.5" />
|
||||
<span>{batteryInfo?.level || 100}%</span>
|
||||
<Battery className="w-4 h-4 text-green-400" />
|
||||
<span className="font-mono text-cyan-400">{time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
<Battery className="w-4 h-4" />
|
||||
<span>{time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 overflow-hidden relative bg-black">
|
||||
<AnimatePresence mode="wait">
|
||||
<div className="flex-1 overflow-hidden relative">
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
{currentWindow ? (
|
||||
// Fullscreen App View with 3D Card Flip
|
||||
<motion.div
|
||||
key={currentWindow.id}
|
||||
initial={{ rotateY: 90, opacity: 0 }}
|
||||
animate={{ rotateY: 0, opacity: 1 }}
|
||||
exit={{ rotateY: -90, opacity: 0 }}
|
||||
transition={{ duration: 0.4, type: "spring" }}
|
||||
style={{ transformStyle: "preserve-3d" }}
|
||||
className="h-full w-full flex flex-col relative"
|
||||
key={`window-${currentWindow.id}`}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||
className="absolute inset-0 flex flex-col"
|
||||
>
|
||||
{/* Ingress Style - Minimal App Bar */}
|
||||
<div className="relative h-12 bg-black/95 border-b-2 border-emerald-500/50 shrink-0">
|
||||
<div className="absolute inset-0" style={{ animation: 'pulse-border 2s ease-in-out infinite' }}>
|
||||
<div className="absolute inset-x-0 bottom-0 h-[2px] bg-gradient-to-r from-transparent via-cyan-500 to-transparent"></div>
|
||||
</div>
|
||||
<div className="relative flex items-center px-3 h-full">
|
||||
<button
|
||||
onClick={() => {
|
||||
impact('light');
|
||||
closeWindow(currentWindow.id);
|
||||
}}
|
||||
className="w-10 h-10 flex items-center justify-center border border-emerald-500/50 active:bg-emerald-500/20"
|
||||
style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5 text-emerald-400" />
|
||||
</button>
|
||||
<div className="flex-1 px-4">
|
||||
<h1 className="text-cyan-400 font-mono font-bold text-lg uppercase tracking-widest">
|
||||
{currentWindow.title}
|
||||
</h1>
|
||||
</div>
|
||||
<button
|
||||
className="w-10 h-10 flex items-center justify-center border border-cyan-500/50 active:bg-cyan-500/20"
|
||||
style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}
|
||||
>
|
||||
<MoreVertical className="w-5 h-5 text-cyan-400" />
|
||||
</button>
|
||||
{/* App Header */}
|
||||
<div className={`h-14 bg-black/95 ${mobileTheme.borderClass} border-b shrink-0 flex items-center px-4 gap-3`}>
|
||||
<button
|
||||
onClick={() => {
|
||||
impact('light');
|
||||
closeWindow(currentWindow.id);
|
||||
}}
|
||||
className={`w-10 h-10 rounded-lg ${mobileTheme.bgAccent} border ${mobileTheme.borderClass} flex items-center justify-center active:opacity-70`}
|
||||
>
|
||||
<ChevronLeft className={`w-5 h-5 ${mobileTheme.iconClass}`} />
|
||||
</button>
|
||||
<div className="flex-1">
|
||||
<h1 className={`${mobileTheme.secondaryClass} font-bold text-lg`}>{currentWindow.title}</h1>
|
||||
</div>
|
||||
<button className="w-10 h-10 rounded-lg bg-zinc-900/50 border border-zinc-800/40 flex items-center justify-center">
|
||||
<MoreVertical className="w-5 h-5 text-zinc-400" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* App Content */}
|
||||
<div className="flex-1 overflow-auto relative bg-black">
|
||||
<div className="flex-1 overflow-auto bg-black/80">
|
||||
{renderAppContent(currentWindow.component)}
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
// ULTRA FUTURISTIC LAUNCHER
|
||||
// Home Launcher
|
||||
<motion.div
|
||||
key="launcher"
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
className="h-full flex flex-col relative"
|
||||
key="launcher-home"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="absolute inset-0 flex flex-col"
|
||||
>
|
||||
{/* Ingress Style Search Bar */}
|
||||
<div className="px-4 pt-6 pb-4">
|
||||
<div className="relative bg-black/80 border border-emerald-500/50 p-3">
|
||||
<div className="absolute inset-0 border border-cyan-500/30" style={{ clipPath: 'polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px))' }}></div>
|
||||
<div className="relative flex items-center gap-3">
|
||||
<Search className="w-5 h-5 text-emerald-400" />
|
||||
{/* Header with Theme Toggle */}
|
||||
<div className="px-4 pt-4 pb-2 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className={`${mobileTheme.primaryClass} font-bold text-xl`}>
|
||||
{isFoundation ? 'The Foundation' : 'The Corporation'}
|
||||
</h1>
|
||||
<p className="text-zinc-500 text-xs">Welcome back, Architect</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
impact('medium');
|
||||
setClearanceMode(isFoundation ? 'corp' : 'foundation');
|
||||
}}
|
||||
className={`px-3 py-1.5 rounded-lg ${mobileTheme.bgAccent} border ${mobileTheme.borderClass}`}
|
||||
>
|
||||
<span className={`${mobileTheme.secondaryClass} text-xs font-mono`}>
|
||||
{isFoundation ? '→ CORP' : '→ FND'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="px-4 pt-2 pb-3">
|
||||
<div className={`relative bg-zinc-900/80 border ${mobileTheme.borderClass} rounded-xl p-3`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<Search className={`w-5 h-5 ${mobileTheme.iconClass}`} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="SCANNER SEARCH..."
|
||||
className="flex-1 bg-transparent text-emerald-400 placeholder:text-emerald-400/40 outline-none text-sm font-mono uppercase tracking-wide"
|
||||
placeholder="Search apps..."
|
||||
className="flex-1 bg-transparent text-white placeholder:text-zinc-500 outline-none text-sm"
|
||||
onFocus={() => impact('light')}
|
||||
/>
|
||||
<Mic className="w-5 h-5 text-cyan-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* App Grid - Hexagonal */}
|
||||
<div className="flex-1 overflow-auto px-4 pb-24">
|
||||
{/* App Grid */}
|
||||
<div className="flex-1 overflow-auto px-4 pb-32">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-3 px-2">
|
||||
<div className="w-2 h-2 bg-emerald-400"></div>
|
||||
<h2 className="text-emerald-400 text-xs uppercase tracking-widest font-mono font-bold">
|
||||
Quick Access
|
||||
</h2>
|
||||
<div className="flex-1 h-[1px] bg-emerald-500/30"></div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: mobileTheme.primary }}></div>
|
||||
<h2 className={`${mobileTheme.secondaryClass} opacity-90 text-xs uppercase tracking-widest font-bold`}>Quick Access</h2>
|
||||
<div className={`flex-1 h-px ${mobileTheme.borderClass} bg-current opacity-30`}></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{apps.slice(0, 8).map((app) => (
|
||||
<button
|
||||
key={app.id}
|
||||
|
|
@ -1347,16 +1380,12 @@ export default function AeThexOS() {
|
|||
impact('medium');
|
||||
openApp(app);
|
||||
}}
|
||||
className="flex flex-col items-center gap-2 p-2 active:bg-emerald-500/10"
|
||||
className={`flex flex-col items-center gap-2 p-2 rounded-xl active:${mobileTheme.bgAccent}`}
|
||||
>
|
||||
<div
|
||||
className="relative w-16 h-16 bg-black border-2 border-emerald-500/50 flex items-center justify-center active:border-cyan-400"
|
||||
style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}
|
||||
>
|
||||
<div className="text-emerald-400 scale-75">{app.icon}</div>
|
||||
<div className="absolute inset-0 border border-cyan-500/20" style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}></div>
|
||||
<div className={`w-14 h-14 rounded-2xl bg-gradient-to-br from-zinc-900 to-zinc-950 border ${mobileTheme.borderClass} flex items-center justify-center shadow-lg`}>
|
||||
<div className={mobileTheme.iconClass}>{app.icon}</div>
|
||||
</div>
|
||||
<span className="text-cyan-400 text-[9px] font-mono text-center line-clamp-2 leading-tight uppercase">
|
||||
<span className="text-zinc-300 text-[10px] font-medium text-center line-clamp-2 leading-tight">
|
||||
{app.title}
|
||||
</span>
|
||||
</button>
|
||||
|
|
@ -1364,14 +1393,12 @@ export default function AeThexOS() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* All Apps - Minimal List */}
|
||||
{/* All Apps List */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3 px-2">
|
||||
<div className="w-2 h-2 bg-cyan-400"></div>
|
||||
<h2 className="text-cyan-400 text-xs uppercase tracking-widest font-mono font-bold">
|
||||
All Systems
|
||||
</h2>
|
||||
<div className="flex-1 h-[1px] bg-cyan-500/30"></div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: mobileTheme.secondary }}></div>
|
||||
<h2 className={`${mobileTheme.secondaryClass} opacity-90 text-xs uppercase tracking-widest font-bold`}>All Apps</h2>
|
||||
<div className={`flex-1 h-px opacity-30`} style={{ backgroundColor: mobileTheme.secondary }}></div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{apps.slice(8).map((app) => (
|
||||
|
|
@ -1381,13 +1408,13 @@ export default function AeThexOS() {
|
|||
impact('medium');
|
||||
openApp(app);
|
||||
}}
|
||||
className="relative w-full flex items-center gap-3 p-3 border border-emerald-500/30 active:bg-emerald-500/10 active:border-cyan-500"
|
||||
className={`w-full flex items-center gap-4 p-3 rounded-xl bg-zinc-900/40 border border-zinc-800/40 active:${mobileTheme.bgAccent} active:${mobileTheme.borderClass}`}
|
||||
>
|
||||
<div className="w-10 h-10 bg-black border border-emerald-500/50 flex items-center justify-center shrink-0">
|
||||
<div className="text-emerald-400 scale-75">{app.icon}</div>
|
||||
<div className={`w-11 h-11 rounded-xl bg-gradient-to-br from-zinc-800 to-zinc-900 border ${mobileTheme.borderClass} flex items-center justify-center shrink-0`}>
|
||||
<div className={`${mobileTheme.iconClass} scale-90`}>{app.icon}</div>
|
||||
</div>
|
||||
<span className="text-cyan-400 font-mono text-sm text-left flex-1 uppercase tracking-wide">{app.title}</span>
|
||||
<ChevronRight className="w-4 h-4 text-emerald-400" />
|
||||
<span className="text-zinc-200 font-medium text-sm text-left flex-1">{app.title}</span>
|
||||
<ChevronRight className="w-4 h-4 text-zinc-600" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -1398,30 +1425,27 @@ export default function AeThexOS() {
|
|||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* INGRESS STYLE NAVIGATION BAR - Lightweight */}
|
||||
{/* Bottom Navigation */}
|
||||
<div
|
||||
className="relative bg-black/95 border-t-2 border-emerald-500/50 shrink-0 z-50"
|
||||
style={{
|
||||
paddingTop: '0.75rem',
|
||||
paddingBottom: 'calc(0.75rem + env(safe-area-inset-bottom))',
|
||||
}}
|
||||
className={`bg-black/95 border-t ${mobileTheme.borderClass} shrink-0`}
|
||||
style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}
|
||||
>
|
||||
<div className="absolute inset-x-0 top-0 h-[2px] bg-gradient-to-r from-transparent via-cyan-500 to-transparent" style={{ animation: 'pulse-border 2s ease-in-out infinite' }}></div>
|
||||
<div className="relative flex items-center justify-around px-6">
|
||||
<div className="flex items-center justify-around py-2 px-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
impact('medium');
|
||||
windows.forEach(w => closeWindow(w.id));
|
||||
}}
|
||||
className="relative w-14 h-14 bg-black border-2 border-emerald-500/70 flex items-center justify-center active:bg-emerald-500/20 active:border-cyan-400"
|
||||
style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}
|
||||
className={`flex flex-col items-center gap-1 p-3 rounded-2xl ${!currentWindow ? mobileTheme.bgAccent : ''}`}
|
||||
>
|
||||
<Home className="w-6 h-6 text-emerald-400" />
|
||||
<Home className={`w-6 h-6 ${!currentWindow ? mobileTheme.iconClass : 'text-zinc-500'}`} />
|
||||
<span className={`text-[10px] font-medium ${!currentWindow ? mobileTheme.secondaryClass : 'text-zinc-600'}`}>Home</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
impact('medium');
|
||||
// Open apps drawer / show minimized apps
|
||||
const minimized = windows.filter(w => w.minimized);
|
||||
if (minimized.length > 0) {
|
||||
setWindows(prev => prev.map(w =>
|
||||
|
|
@ -1429,12 +1453,12 @@ export default function AeThexOS() {
|
|||
));
|
||||
}
|
||||
}}
|
||||
className="relative w-14 h-14 bg-black border-2 border-cyan-500/70 flex items-center justify-center active:bg-cyan-500/20 active:border-emerald-400"
|
||||
style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}
|
||||
className="flex flex-col items-center gap-1 p-3 rounded-2xl relative"
|
||||
>
|
||||
<Square className="w-6 h-6 text-cyan-400" />
|
||||
<Layers className={`w-6 h-6 ${windows.length > 0 ? mobileTheme.iconClass : 'text-zinc-500'}`} />
|
||||
<span className="text-[10px] text-zinc-600 font-medium">Recents</span>
|
||||
{windows.filter(w => w.minimized).length > 0 && (
|
||||
<span className="absolute -top-1 -right-1 w-5 h-5 bg-emerald-500 text-black text-xs flex items-center justify-center font-bold">
|
||||
<span className="absolute top-1 right-1 w-5 h-5 text-white text-[10px] flex items-center justify-center rounded-full font-bold" style={{ backgroundColor: mobileTheme.primary }}>
|
||||
{windows.filter(w => w.minimized).length}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -1443,19 +1467,19 @@ export default function AeThexOS() {
|
|||
<button
|
||||
onClick={() => {
|
||||
impact('medium');
|
||||
if (currentWindow) {
|
||||
closeWindow(currentWindow.id);
|
||||
}
|
||||
// Find and open settings app
|
||||
const settingsApp = apps.find(a => a.id === 'settings');
|
||||
if (settingsApp) openApp(settingsApp);
|
||||
}}
|
||||
className="relative w-14 h-14 bg-black border-2 border-emerald-500/70 flex items-center justify-center active:bg-emerald-500/20 active:border-cyan-400"
|
||||
style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}
|
||||
className={`flex flex-col items-center gap-1 p-3 rounded-2xl ${currentWindow?.component === 'settings' ? mobileTheme.bgAccent : ''}`}
|
||||
>
|
||||
<ArrowLeft className="w-6 h-6 text-emerald-400" />
|
||||
<Settings className={`w-6 h-6 ${currentWindow?.component === 'settings' ? mobileTheme.iconClass : 'text-zinc-500'}`} />
|
||||
<span className={`text-[10px] font-medium ${currentWindow?.component === 'settings' ? mobileTheme.secondaryClass : 'text-zinc-600'}`}>Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Action Button with Orbital Menu */}
|
||||
{/* Floating Quick Actions */}
|
||||
<MobileQuickActions />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
61
package-lock.json
generated
61
package-lock.json
generated
|
|
@ -65,6 +65,7 @@
|
|||
"@tanstack/react-query": "^5.60.5",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"capacitor-native-biometric": "^4.2.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
|
|
@ -186,6 +187,7 @@
|
|||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
|
|
@ -566,6 +568,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.0.2.tgz",
|
||||
"integrity": "sha512-EXZfxkL6GFJS2cb7TIBR7RiHA5iz6ufDcl1VmUpI2pga3lJ5Ck2+iqbx7N+osL3XYem9ad4XCidJEMm64DX6UQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
|
|
@ -4832,6 +4835,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
|
||||
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
|
|
@ -4875,6 +4879,7 @@
|
|||
"integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"pg-protocol": "*",
|
||||
|
|
@ -4907,6 +4912,7 @@
|
|||
"integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
|
|
@ -4917,6 +4923,7 @@
|
|||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
|
|
@ -5088,6 +5095,7 @@
|
|||
"integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.18",
|
||||
"fflate": "^0.8.2",
|
||||
|
|
@ -5419,6 +5427,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
|
|
@ -5449,20 +5458,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bufferutil": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz",
|
||||
"integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.14.2"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
|
|
@ -5522,6 +5517,24 @@
|
|||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/capacitor-native-biometric": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/capacitor-native-biometric/-/capacitor-native-biometric-4.2.2.tgz",
|
||||
"integrity": "sha512-stg0h48UxgkNuNcCAgCXLp2DUspRQs79bCBPntpCBhsDxk2bhDRUu+J/QpFtDQHG4M4DioSUcYaAsVw2N6N7wA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@capacitor/core": "^3.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/capacitor-native-biometric/node_modules/@capacitor/core": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-3.9.0.tgz",
|
||||
"integrity": "sha512-j1lL0+/7stY8YhIq1Lm6xixvUqIn89vtyH5ZpJNNmcZ0kwz6K9eLkcG6fvq1UWMDgSVZg9JrRGSFhb4LLoYOsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||
|
|
@ -6047,6 +6060,7 @@
|
|||
"resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.39.3.tgz",
|
||||
"integrity": "sha512-EZ8ZpYvDIvKU9C56JYLOmUskazhad+uXZCTCRN4OnRMsL+xAJ05dv1eCpAG5xzhsm1hqiuC5kAZUCS924u2DTw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/client-rds-data": ">=3",
|
||||
"@cloudflare/workers-types": ">=4",
|
||||
|
|
@ -6212,7 +6226,8 @@
|
|||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
||||
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/embla-carousel-react": {
|
||||
"version": "8.6.0",
|
||||
|
|
@ -6402,6 +6417,7 @@
|
|||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
|
|
@ -7608,6 +7624,7 @@
|
|||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
||||
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"dompurify": "3.2.7",
|
||||
"marked": "14.0.0"
|
||||
|
|
@ -7940,6 +7957,7 @@
|
|||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz",
|
||||
"integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.11.0",
|
||||
"pg-pool": "^3.11.0",
|
||||
|
|
@ -8037,6 +8055,7 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -8108,6 +8127,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
|
|
@ -8274,6 +8294,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -8314,6 +8335,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
|
|
@ -8326,6 +8348,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
|
||||
"integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
|
|
@ -9100,7 +9123,8 @@
|
|||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tailwindcss-animate": {
|
||||
"version": "1.0.7",
|
||||
|
|
@ -9826,6 +9850,7 @@
|
|||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -9968,6 +9993,7 @@
|
|||
"integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
},
|
||||
|
|
@ -10055,6 +10081,7 @@
|
|||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
@ -10614,6 +10641,7 @@
|
|||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.18",
|
||||
"@vitest/mocker": "4.0.18",
|
||||
|
|
@ -10888,6 +10916,7 @@
|
|||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
|
|
|||
11
package.json
11
package.json
|
|
@ -20,11 +20,11 @@
|
|||
"start": "NODE_ENV=production node dist/index.js",
|
||||
"check": "tsc",
|
||||
"db:push": "drizzle-kit push",
|
||||
"tauri": "cd shell/aethex-shell && npm run tauri",
|
||||
"tauri:dev": "cd shell/aethex-shell && npm run tauri dev",
|
||||
"tauri:build": "cd shell/aethex-shell && npm run tauri build",
|
||||
"audit:org-scope": "tsx script/org-scope-audit.ts",
|
||||
"test:org-scope": "tsx --test server/org-scoping.test.ts"
|
||||
"tauri": "cd shell/aethex-shell && npm run tauri",
|
||||
"tauri:dev": "cd shell/aethex-shell && npm run tauri dev",
|
||||
"tauri:build": "cd shell/aethex-shell && npm run tauri build",
|
||||
"audit:org-scope": "tsx script/org-scope-audit.ts",
|
||||
"test:org-scope": "tsx --test server/org-scoping.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor-community/privacy-screen": "^6.0.0",
|
||||
|
|
@ -83,6 +83,7 @@
|
|||
"@tanstack/react-query": "^5.60.5",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"capacitor-native-biometric": "^4.2.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
|
|
|
|||
Loading…
Reference in a new issue