Prettier format pending files
This commit is contained in:
parent
ad88034b7a
commit
7097f8408b
10 changed files with 877 additions and 718 deletions
|
|
@ -5,11 +5,13 @@ This guide will help you configure GitHub and Google OAuth login for your AeThex
|
|||
## 🔧 Supabase OAuth Configuration
|
||||
|
||||
### 1. Access Your Supabase Dashboard
|
||||
|
||||
1. Go to [app.supabase.com](https://app.supabase.com)
|
||||
2. Select your project: `kmdeisowhtsalsekkzqd`
|
||||
3. Navigate to **Authentication** > **Providers**
|
||||
|
||||
### 2. Configure Site URL
|
||||
|
||||
1. Go to **Authentication** > **Settings**
|
||||
2. Set your Site URL to: `https://e7c3806a9bfe4bdf9bb8a72a7f0d31cd-324f24a826ec4eb198c1a0eef.fly.dev`
|
||||
3. Add Redirect URLs:
|
||||
|
|
@ -19,6 +21,7 @@ This guide will help you configure GitHub and Google OAuth login for your AeThex
|
|||
## 🐙 GitHub OAuth Setup
|
||||
|
||||
### 1. Create GitHub OAuth App
|
||||
|
||||
1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
|
||||
2. Click **New OAuth App**
|
||||
3. Fill in the details:
|
||||
|
|
@ -29,6 +32,7 @@ This guide will help you configure GitHub and Google OAuth login for your AeThex
|
|||
5. Copy the **Client ID** and **Client Secret**
|
||||
|
||||
### 2. Configure in Supabase
|
||||
|
||||
1. In Supabase dashboard, go to **Authentication** > **Providers**
|
||||
2. Find **GitHub** and click to configure
|
||||
3. Enable GitHub provider
|
||||
|
|
@ -38,6 +42,7 @@ This guide will help you configure GitHub and Google OAuth login for your AeThex
|
|||
## 🌐 Google OAuth Setup
|
||||
|
||||
### 1. Create Google OAuth Credentials
|
||||
|
||||
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Create a new project or select existing one
|
||||
3. Enable **Google+ API** and **Google Identity API**
|
||||
|
|
@ -49,6 +54,7 @@ This guide will help you configure GitHub and Google OAuth login for your AeThex
|
|||
8. Copy the **Client ID** and **Client Secret**
|
||||
|
||||
### 2. Configure in Supabase
|
||||
|
||||
1. In Supabase dashboard, go to **Authentication** > **Providers**
|
||||
2. Find **Google** and click to configure
|
||||
3. Enable Google provider
|
||||
|
|
@ -74,6 +80,7 @@ Once configured:
|
|||
## 🚀 Features Enabled
|
||||
|
||||
With OAuth configured, users can:
|
||||
|
||||
- **One-click login** with GitHub or Google
|
||||
- **Automatic profile setup** with avatar and name from OAuth provider
|
||||
- **Seamless integration** with existing AeThex community platform
|
||||
|
|
|
|||
|
|
@ -218,11 +218,13 @@ CREATE TRIGGER update_community_posts_updated_at BEFORE UPDATE ON community_post
|
|||
Once set up, your AeThex app will have:
|
||||
|
||||
### 🔐 Authentication
|
||||
|
||||
- Email/password sign up and sign in
|
||||
- User session management
|
||||
- Profile creation and updates
|
||||
|
||||
### 🗄️ Database Features
|
||||
|
||||
- User profiles with extended information
|
||||
- Project portfolio management
|
||||
- Achievement system with XP and levels
|
||||
|
|
@ -230,16 +232,19 @@ Once set up, your AeThex app will have:
|
|||
- Real-time notifications
|
||||
|
||||
### 🎮 Gamification
|
||||
|
||||
- User levels and XP tracking
|
||||
- Achievement system
|
||||
- Progress tracking
|
||||
|
||||
### 💬 Community
|
||||
|
||||
- User-generated content
|
||||
- Real-time updates
|
||||
- Comment system
|
||||
|
||||
### 📊 Dashboard
|
||||
|
||||
- Personalized user dashboard
|
||||
- Project tracking
|
||||
- Achievement display
|
||||
|
|
@ -255,6 +260,7 @@ Once set up, your AeThex app will have:
|
|||
## 7. Production Deployment
|
||||
|
||||
For production:
|
||||
|
||||
1. Update your site URL in Supabase Authentication settings
|
||||
2. Add your production domain to redirect URLs
|
||||
3. Update your environment variables in your hosting platform
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import { isSupabaseConfigured } from '@/lib/supabase';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Database, ExternalLink, Info } from 'lucide-react';
|
||||
import React from "react";
|
||||
import { isSupabaseConfigured } from "@/lib/supabase";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Database, ExternalLink, Info } from "lucide-react";
|
||||
|
||||
export default function SupabaseStatus() {
|
||||
// Only show if Supabase is not configured (demo mode)
|
||||
|
|
@ -26,9 +26,11 @@ export default function SupabaseStatus() {
|
|||
className="border-blue-500/50 text-blue-300 hover:bg-blue-500/20"
|
||||
onClick={() => {
|
||||
// Hide the status notification
|
||||
const notification = document.querySelector('[data-supabase-status]');
|
||||
const notification = document.querySelector(
|
||||
"[data-supabase-status]",
|
||||
);
|
||||
if (notification) {
|
||||
notification.style.display = 'none';
|
||||
notification.style.display = "none";
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -39,7 +41,7 @@ export default function SupabaseStatus() {
|
|||
size="sm"
|
||||
variant="outline"
|
||||
className="border-blue-500/50 text-blue-300 hover:bg-blue-500/20"
|
||||
onClick={() => window.open('/login', '_self')}
|
||||
onClick={() => window.open("/login", "_self")}
|
||||
>
|
||||
<ExternalLink className="h-3 w-3 mr-1" />
|
||||
Try Login
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { User, Session } from '@supabase/supabase-js';
|
||||
import { supabase, isSupabaseConfigured } from '@/lib/supabase';
|
||||
import { UserProfile } from '@/lib/database.types';
|
||||
import { aethexToast } from '@/lib/aethex-toast';
|
||||
import { DemoStorageService } from '@/lib/demo-storage';
|
||||
import { aethexUserService, aethexAchievementService, type AethexUserProfile } from '@/lib/aethex-database-adapter';
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import { User, Session } from "@supabase/supabase-js";
|
||||
import { supabase, isSupabaseConfigured } from "@/lib/supabase";
|
||||
import { UserProfile } from "@/lib/database.types";
|
||||
import { aethexToast } from "@/lib/aethex-toast";
|
||||
import { DemoStorageService } from "@/lib/demo-storage";
|
||||
import {
|
||||
aethexUserService,
|
||||
aethexAchievementService,
|
||||
type AethexUserProfile,
|
||||
} from "@/lib/aethex-database-adapter";
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
|
|
@ -12,8 +16,12 @@ interface AuthContextType {
|
|||
session: Session | null;
|
||||
loading: boolean;
|
||||
signIn: (email: string, password: string) => Promise<void>;
|
||||
signUp: (email: string, password: string, userData?: Partial<AethexUserProfile>) => Promise<void>;
|
||||
signInWithOAuth: (provider: 'github' | 'google') => Promise<void>;
|
||||
signUp: (
|
||||
email: string,
|
||||
password: string,
|
||||
userData?: Partial<AethexUserProfile>,
|
||||
) => Promise<void>;
|
||||
signInWithOAuth: (provider: "github" | "google") => Promise<void>;
|
||||
signOut: () => Promise<void>;
|
||||
updateProfile: (updates: Partial<AethexUserProfile>) => Promise<void>;
|
||||
}
|
||||
|
|
@ -23,12 +31,14 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [profile, setProfile] = useState<AethexUserProfile | null>(null);
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
|
|
@ -37,21 +47,26 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||
useEffect(() => {
|
||||
// Check if Supabase is configured
|
||||
if (!isSupabaseConfigured) {
|
||||
console.warn('Supabase is not configured. Please set up your environment variables.');
|
||||
console.warn(
|
||||
"Supabase is not configured. Please set up your environment variables.",
|
||||
);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get initial session
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
supabase.auth
|
||||
.getSession()
|
||||
.then(({ data: { session } }) => {
|
||||
setSession(session);
|
||||
setUser(session?.user ?? null);
|
||||
if (session?.user) {
|
||||
fetchUserProfile(session.user.id);
|
||||
}
|
||||
setLoading(false);
|
||||
}).catch((error) => {
|
||||
console.error('Error getting session:', error);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error getting session:", error);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
|
|
@ -69,26 +84,30 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||
if (!profile && session.user.app_metadata?.provider) {
|
||||
try {
|
||||
await aethexUserService.createInitialProfile(session.user.id, {
|
||||
username: session.user.user_metadata?.user_name ||
|
||||
username:
|
||||
session.user.user_metadata?.user_name ||
|
||||
session.user.user_metadata?.preferred_username ||
|
||||
session.user.email?.split('@')[0] ||
|
||||
session.user.email?.split("@")[0] ||
|
||||
`user_${Date.now()}`,
|
||||
full_name: session.user.user_metadata?.full_name ||
|
||||
full_name:
|
||||
session.user.user_metadata?.full_name ||
|
||||
session.user.user_metadata?.name ||
|
||||
session.user.email?.split('@')[0],
|
||||
session.user.email?.split("@")[0],
|
||||
email: session.user.email,
|
||||
avatar_url: session.user.user_metadata?.avatar_url,
|
||||
user_type: 'community_member', // Default for OAuth users
|
||||
experience_level: 'beginner',
|
||||
user_type: "community_member", // Default for OAuth users
|
||||
experience_level: "beginner",
|
||||
});
|
||||
|
||||
// Fetch the newly created profile
|
||||
await fetchUserProfile(session.user.id);
|
||||
|
||||
// Award onboarding achievement for OAuth users
|
||||
await aethexAchievementService.checkAndAwardOnboardingAchievement(session.user.id);
|
||||
await aethexAchievementService.checkAndAwardOnboardingAchievement(
|
||||
session.user.id,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error creating OAuth user profile:', error);
|
||||
console.error("Error creating OAuth user profile:", error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -97,15 +116,15 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||
setLoading(false);
|
||||
|
||||
// Show toast notifications for auth events
|
||||
if (event === 'SIGNED_IN') {
|
||||
if (event === "SIGNED_IN") {
|
||||
aethexToast.success({
|
||||
title: 'Welcome back!',
|
||||
description: 'Successfully signed in to AeThex OS'
|
||||
title: "Welcome back!",
|
||||
description: "Successfully signed in to AeThex OS",
|
||||
});
|
||||
} else if (event === 'SIGNED_OUT') {
|
||||
} else if (event === "SIGNED_OUT") {
|
||||
aethexToast.info({
|
||||
title: 'Signed out',
|
||||
description: 'Come back soon!'
|
||||
title: "Signed out",
|
||||
description: "Come back soon!",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -113,7 +132,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
const fetchUserProfile = async (userId: string): Promise<AethexUserProfile | null> => {
|
||||
const fetchUserProfile = async (
|
||||
userId: string,
|
||||
): Promise<AethexUserProfile | null> => {
|
||||
if (!isSupabaseConfigured) {
|
||||
// Initialize demo data and get profile
|
||||
DemoStorageService.initializeDemoData();
|
||||
|
|
@ -127,7 +148,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||
setProfile(userProfile);
|
||||
return userProfile;
|
||||
} catch (error) {
|
||||
console.error('Error fetching user profile:', error);
|
||||
console.error("Error fetching user profile:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
|
@ -135,13 +156,14 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||
const signIn = async (email: string, password: string) => {
|
||||
if (!isSupabaseConfigured) {
|
||||
aethexToast.warning({
|
||||
title: 'Demo Mode',
|
||||
description: 'Supabase not configured. This is a demo - please set up your Supabase project.'
|
||||
title: "Demo Mode",
|
||||
description:
|
||||
"Supabase not configured. This is a demo - please set up your Supabase project.",
|
||||
});
|
||||
// Simulate successful login for demo
|
||||
setTimeout(() => {
|
||||
setUser({ id: 'demo-user', email } as User);
|
||||
setSession({ user: { id: 'demo-user', email } } as Session);
|
||||
setUser({ id: "demo-user", email } as User);
|
||||
setSession({ user: { id: "demo-user", email } } as Session);
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
|
|
@ -155,18 +177,23 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||
if (error) throw error;
|
||||
} catch (error: any) {
|
||||
aethexToast.error({
|
||||
title: 'Sign in failed',
|
||||
description: error.message
|
||||
title: "Sign in failed",
|
||||
description: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const signUp = async (email: string, password: string, userData?: Partial<AethexUserProfile>) => {
|
||||
const signUp = async (
|
||||
email: string,
|
||||
password: string,
|
||||
userData?: Partial<AethexUserProfile>,
|
||||
) => {
|
||||
if (!isSupabaseConfigured) {
|
||||
aethexToast.warning({
|
||||
title: 'Demo Mode',
|
||||
description: 'Supabase not configured. This is a demo - please set up your Supabase project.'
|
||||
title: "Demo Mode",
|
||||
description:
|
||||
"Supabase not configured. This is a demo - please set up your Supabase project.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -187,24 +214,25 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||
});
|
||||
|
||||
aethexToast.success({
|
||||
title: 'Account created!',
|
||||
description: 'Please check your email to verify your account'
|
||||
title: "Account created!",
|
||||
description: "Please check your email to verify your account",
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
aethexToast.error({
|
||||
title: 'Sign up failed',
|
||||
description: error.message
|
||||
title: "Sign up failed",
|
||||
description: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const signInWithOAuth = async (provider: 'github' | 'google') => {
|
||||
const signInWithOAuth = async (provider: "github" | "google") => {
|
||||
if (!isSupabaseConfigured) {
|
||||
aethexToast.warning({
|
||||
title: 'Demo Mode',
|
||||
description: 'OAuth login requires Supabase to be configured. Currently in demo mode.'
|
||||
title: "Demo Mode",
|
||||
description:
|
||||
"OAuth login requires Supabase to be configured. Currently in demo mode.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -220,13 +248,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||
if (error) throw error;
|
||||
|
||||
aethexToast.success({
|
||||
title: 'Redirecting...',
|
||||
description: `Signing in with ${provider}`
|
||||
title: "Redirecting...",
|
||||
description: `Signing in with ${provider}`,
|
||||
});
|
||||
} catch (error: any) {
|
||||
aethexToast.error({
|
||||
title: `${provider} sign in failed`,
|
||||
description: error.message
|
||||
description: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
|
@ -238,8 +266,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||
setSession(null);
|
||||
setProfile(null);
|
||||
aethexToast.info({
|
||||
title: 'Signed out',
|
||||
description: 'Demo session ended'
|
||||
title: "Signed out",
|
||||
description: "Demo session ended",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -249,38 +277,43 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||
if (error) throw error;
|
||||
} catch (error: any) {
|
||||
aethexToast.error({
|
||||
title: 'Sign out failed',
|
||||
description: error.message
|
||||
title: "Sign out failed",
|
||||
description: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const updateProfile = async (updates: Partial<AethexUserProfile>) => {
|
||||
if (!user) throw new Error('No user logged in');
|
||||
if (!user) throw new Error("No user logged in");
|
||||
|
||||
if (!isSupabaseConfigured) {
|
||||
// Use demo storage
|
||||
const updatedProfile = DemoStorageService.updateUserProfile(updates as any);
|
||||
const updatedProfile = DemoStorageService.updateUserProfile(
|
||||
updates as any,
|
||||
);
|
||||
setProfile(updatedProfile as AethexUserProfile);
|
||||
aethexToast.success({
|
||||
title: 'Profile updated',
|
||||
description: 'Your profile has been updated successfully'
|
||||
title: "Profile updated",
|
||||
description: "Your profile has been updated successfully",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedProfile = await aethexUserService.updateProfile(user.id, updates);
|
||||
const updatedProfile = await aethexUserService.updateProfile(
|
||||
user.id,
|
||||
updates,
|
||||
);
|
||||
setProfile(updatedProfile);
|
||||
aethexToast.success({
|
||||
title: 'Profile updated',
|
||||
description: 'Your profile has been updated successfully'
|
||||
title: "Profile updated",
|
||||
description: "Your profile has been updated successfully",
|
||||
});
|
||||
} catch (error: any) {
|
||||
aethexToast.error({
|
||||
title: 'Update failed',
|
||||
description: error.message
|
||||
title: "Update failed",
|
||||
description: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
|
@ -298,9 +331,5 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||
updateProfile,
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// Database adapter for existing AeThex community platform
|
||||
// Maps existing schema to our application needs
|
||||
|
||||
import { supabase } from './supabase';
|
||||
import type { Database } from './database.types';
|
||||
import { aethexToast } from './aethex-toast';
|
||||
import { supabase } from "./supabase";
|
||||
import type { Database } from "./database.types";
|
||||
import { aethexToast } from "./aethex-toast";
|
||||
|
||||
// Extended user profile type that matches existing + new schema
|
||||
export interface AethexUserProfile {
|
||||
|
|
@ -21,8 +21,8 @@ export interface AethexUserProfile {
|
|||
created_at: string;
|
||||
updated_at: string;
|
||||
// New AeThex app fields
|
||||
user_type?: 'game_developer' | 'client' | 'community_member' | 'customer';
|
||||
experience_level?: 'beginner' | 'intermediate' | 'advanced' | 'expert';
|
||||
user_type?: "game_developer" | "client" | "community_member" | "customer";
|
||||
experience_level?: "beginner" | "intermediate" | "advanced" | "expert";
|
||||
full_name?: string;
|
||||
location?: string;
|
||||
website_url?: string;
|
||||
|
|
@ -38,7 +38,7 @@ export interface AethexProject {
|
|||
user_id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status: 'planning' | 'in_progress' | 'completed' | 'on_hold';
|
||||
status: "planning" | "in_progress" | "completed" | "on_hold";
|
||||
technologies?: string[];
|
||||
github_url?: string;
|
||||
demo_url?: string;
|
||||
|
|
@ -69,56 +69,64 @@ export interface AethexUserAchievement {
|
|||
// User Profile Services
|
||||
export const aethexUserService = {
|
||||
async getCurrentUser(): Promise<AethexUserProfile | null> {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user) return null;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', user.id)
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("id", user.id)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching user profile:', error);
|
||||
console.error("Error fetching user profile:", error);
|
||||
return null;
|
||||
}
|
||||
|
||||
return data as AethexUserProfile;
|
||||
},
|
||||
|
||||
async updateProfile(userId: string, updates: Partial<AethexUserProfile>): Promise<AethexUserProfile | null> {
|
||||
async updateProfile(
|
||||
userId: string,
|
||||
updates: Partial<AethexUserProfile>,
|
||||
): Promise<AethexUserProfile | null> {
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.from("profiles")
|
||||
.update(updates)
|
||||
.eq('id', userId)
|
||||
.eq("id", userId)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error updating profile:', error);
|
||||
console.error("Error updating profile:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data as AethexUserProfile;
|
||||
},
|
||||
|
||||
async createInitialProfile(userId: string, profileData: Partial<AethexUserProfile>): Promise<AethexUserProfile | null> {
|
||||
async createInitialProfile(
|
||||
userId: string,
|
||||
profileData: Partial<AethexUserProfile>,
|
||||
): Promise<AethexUserProfile | null> {
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.from("profiles")
|
||||
.insert({
|
||||
id: userId,
|
||||
username: profileData.username || `user_${Date.now()}`,
|
||||
user_type: profileData.user_type || 'community_member',
|
||||
experience_level: profileData.experience_level || 'beginner',
|
||||
user_type: profileData.user_type || "community_member",
|
||||
experience_level: profileData.experience_level || "beginner",
|
||||
full_name: profileData.full_name,
|
||||
email: profileData.email,
|
||||
...profileData
|
||||
...profileData,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating profile:', error);
|
||||
console.error("Error creating profile:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
|
@ -127,39 +135,36 @@ export const aethexUserService = {
|
|||
|
||||
async addUserInterests(userId: string, interests: string[]): Promise<void> {
|
||||
// First, delete existing interests
|
||||
await supabase
|
||||
.from('user_interests')
|
||||
.delete()
|
||||
.eq('user_id', userId);
|
||||
await supabase.from("user_interests").delete().eq("user_id", userId);
|
||||
|
||||
// Insert new interests
|
||||
const interestRows = interests.map(interest => ({
|
||||
const interestRows = interests.map((interest) => ({
|
||||
user_id: userId,
|
||||
interest,
|
||||
}));
|
||||
|
||||
const { error } = await supabase
|
||||
.from('user_interests')
|
||||
.from("user_interests")
|
||||
.insert(interestRows);
|
||||
|
||||
if (error) {
|
||||
console.error('Error adding interests:', error);
|
||||
console.error("Error adding interests:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getUserInterests(userId: string): Promise<string[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('user_interests')
|
||||
.select('interest')
|
||||
.eq('user_id', userId);
|
||||
.from("user_interests")
|
||||
.select("interest")
|
||||
.eq("user_id", userId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching interests:', error);
|
||||
console.error("Error fetching interests:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.map(item => item.interest);
|
||||
return data.map((item) => item.interest);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -167,44 +172,49 @@ export const aethexUserService = {
|
|||
export const aethexProjectService = {
|
||||
async getUserProjects(userId: string): Promise<AethexProject[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false });
|
||||
.from("projects")
|
||||
.select("*")
|
||||
.eq("user_id", userId)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching projects:', error);
|
||||
console.error("Error fetching projects:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data as AethexProject[];
|
||||
},
|
||||
|
||||
async createProject(project: Omit<AethexProject, 'id' | 'created_at' | 'updated_at'>): Promise<AethexProject | null> {
|
||||
async createProject(
|
||||
project: Omit<AethexProject, "id" | "created_at" | "updated_at">,
|
||||
): Promise<AethexProject | null> {
|
||||
const { data, error } = await supabase
|
||||
.from('projects')
|
||||
.from("projects")
|
||||
.insert(project)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating project:', error);
|
||||
console.error("Error creating project:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data as AethexProject;
|
||||
},
|
||||
|
||||
async updateProject(projectId: string, updates: Partial<AethexProject>): Promise<AethexProject | null> {
|
||||
async updateProject(
|
||||
projectId: string,
|
||||
updates: Partial<AethexProject>,
|
||||
): Promise<AethexProject | null> {
|
||||
const { data, error } = await supabase
|
||||
.from('projects')
|
||||
.from("projects")
|
||||
.update(updates)
|
||||
.eq('id', projectId)
|
||||
.eq("id", projectId)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error updating project:', error);
|
||||
console.error("Error updating project:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
|
@ -213,12 +223,12 @@ export const aethexProjectService = {
|
|||
|
||||
async deleteProject(projectId: string): Promise<boolean> {
|
||||
const { error } = await supabase
|
||||
.from('projects')
|
||||
.from("projects")
|
||||
.delete()
|
||||
.eq('id', projectId);
|
||||
.eq("id", projectId);
|
||||
|
||||
if (error) {
|
||||
console.error('Error deleting project:', error);
|
||||
console.error("Error deleting project:", error);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -227,21 +237,23 @@ export const aethexProjectService = {
|
|||
|
||||
async getAllProjects(limit = 10): Promise<AethexProject[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('projects')
|
||||
.select(`
|
||||
.from("projects")
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
profiles!projects_user_id_fkey (
|
||||
username,
|
||||
full_name,
|
||||
avatar_url
|
||||
)
|
||||
`)
|
||||
.eq('status', 'completed')
|
||||
.order('created_at', { ascending: false })
|
||||
`,
|
||||
)
|
||||
.eq("status", "completed")
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(limit);
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching all projects:', error);
|
||||
console.error("Error fetching all projects:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -253,12 +265,12 @@ export const aethexProjectService = {
|
|||
export const aethexAchievementService = {
|
||||
async getAllAchievements(): Promise<AethexAchievement[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('achievements')
|
||||
.select('*')
|
||||
.order('points_reward', { ascending: false });
|
||||
.from("achievements")
|
||||
.select("*")
|
||||
.order("points_reward", { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching achievements:', error);
|
||||
console.error("Error fetching achievements:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -267,45 +279,48 @@ export const aethexAchievementService = {
|
|||
|
||||
async getUserAchievements(userId: string): Promise<AethexAchievement[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('user_achievements')
|
||||
.select(`
|
||||
.from("user_achievements")
|
||||
.select(
|
||||
`
|
||||
unlocked_at,
|
||||
achievements (*)
|
||||
`)
|
||||
.eq('user_id', userId)
|
||||
.order('unlocked_at', { ascending: false });
|
||||
`,
|
||||
)
|
||||
.eq("user_id", userId)
|
||||
.order("unlocked_at", { ascending: false });
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching user achievements:', error);
|
||||
console.error("Error fetching user achievements:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.map(item => item.achievements).filter(Boolean) as AethexAchievement[];
|
||||
return data
|
||||
.map((item) => item.achievements)
|
||||
.filter(Boolean) as AethexAchievement[];
|
||||
},
|
||||
|
||||
async awardAchievement(userId: string, achievementId: string): Promise<void> {
|
||||
const { error } = await supabase
|
||||
.from('user_achievements')
|
||||
.insert({
|
||||
const { error } = await supabase.from("user_achievements").insert({
|
||||
user_id: userId,
|
||||
achievement_id: achievementId,
|
||||
});
|
||||
|
||||
if (error && error.code !== '23505') { // Ignore duplicate key error
|
||||
console.error('Error awarding achievement:', error);
|
||||
if (error && error.code !== "23505") {
|
||||
// Ignore duplicate key error
|
||||
console.error("Error awarding achievement:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Get achievement details for toast
|
||||
const { data: achievement } = await supabase
|
||||
.from('achievements')
|
||||
.select('*')
|
||||
.eq('id', achievementId)
|
||||
.from("achievements")
|
||||
.select("*")
|
||||
.eq("id", achievementId)
|
||||
.single();
|
||||
|
||||
if (achievement) {
|
||||
aethexToast.aethex({
|
||||
title: 'Achievement Unlocked! 🎉',
|
||||
title: "Achievement Unlocked! 🎉",
|
||||
description: `${achievement.icon} ${achievement.name} - ${achievement.description}`,
|
||||
duration: 8000,
|
||||
});
|
||||
|
|
@ -318,9 +333,9 @@ export const aethexAchievementService = {
|
|||
async updateUserXPAndLevel(userId: string, xpGained: number): Promise<void> {
|
||||
// Get current user data
|
||||
const { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select('total_xp, level, loyalty_points')
|
||||
.eq('id', userId)
|
||||
.from("profiles")
|
||||
.select("total_xp, level, loyalty_points")
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
|
||||
if (!profile) return;
|
||||
|
|
@ -331,21 +346,21 @@ export const aethexAchievementService = {
|
|||
|
||||
// Update profile
|
||||
await supabase
|
||||
.from('profiles')
|
||||
.from("profiles")
|
||||
.update({
|
||||
total_xp: newTotalXP,
|
||||
level: newLevel,
|
||||
loyalty_points: newLoyaltyPoints,
|
||||
})
|
||||
.eq('id', userId);
|
||||
.eq("id", userId);
|
||||
|
||||
// Check for level-up achievements
|
||||
if (newLevel > (profile.level || 1)) {
|
||||
if (newLevel >= 5) {
|
||||
const levelUpAchievement = await supabase
|
||||
.from('achievements')
|
||||
.select('id')
|
||||
.eq('name', 'Level Master')
|
||||
.from("achievements")
|
||||
.select("id")
|
||||
.eq("name", "Level Master")
|
||||
.single();
|
||||
|
||||
if (levelUpAchievement.data) {
|
||||
|
|
@ -357,9 +372,9 @@ export const aethexAchievementService = {
|
|||
|
||||
async checkAndAwardOnboardingAchievement(userId: string): Promise<void> {
|
||||
const { data: achievement } = await supabase
|
||||
.from('achievements')
|
||||
.select('id')
|
||||
.eq('name', 'AeThex Explorer')
|
||||
.from("achievements")
|
||||
.select("id")
|
||||
.eq("name", "AeThex Explorer")
|
||||
.single();
|
||||
|
||||
if (achievement) {
|
||||
|
|
@ -373,9 +388,9 @@ export const aethexAchievementService = {
|
|||
// First project achievement
|
||||
if (projects.length >= 1) {
|
||||
const { data: achievement } = await supabase
|
||||
.from('achievements')
|
||||
.select('id')
|
||||
.eq('name', 'Portfolio Creator')
|
||||
.from("achievements")
|
||||
.select("id")
|
||||
.eq("name", "Portfolio Creator")
|
||||
.single();
|
||||
|
||||
if (achievement) {
|
||||
|
|
@ -384,12 +399,12 @@ export const aethexAchievementService = {
|
|||
}
|
||||
|
||||
// Project master achievement
|
||||
const completedProjects = projects.filter(p => p.status === 'completed');
|
||||
const completedProjects = projects.filter((p) => p.status === "completed");
|
||||
if (completedProjects.length >= 10) {
|
||||
const { data: achievement } = await supabase
|
||||
.from('achievements')
|
||||
.select('id')
|
||||
.eq('name', 'Project Master')
|
||||
.from("achievements")
|
||||
.select("id")
|
||||
.eq("name", "Project Master")
|
||||
.single();
|
||||
|
||||
if (achievement) {
|
||||
|
|
@ -403,14 +418,14 @@ export const aethexAchievementService = {
|
|||
export const aethexNotificationService = {
|
||||
async getUserNotifications(userId: string): Promise<any[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('notifications')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false })
|
||||
.from("notifications")
|
||||
.select("*")
|
||||
.eq("user_id", userId)
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching notifications:', error);
|
||||
console.error("Error fetching notifications:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -419,15 +434,17 @@ export const aethexNotificationService = {
|
|||
|
||||
async markAsRead(notificationId: string): Promise<void> {
|
||||
await supabase
|
||||
.from('notifications')
|
||||
.from("notifications")
|
||||
.update({ is_read: true })
|
||||
.eq('id', notificationId);
|
||||
.eq("id", notificationId);
|
||||
},
|
||||
|
||||
async createNotification(userId: string, type: string, data: any): Promise<void> {
|
||||
await supabase
|
||||
.from('notifications')
|
||||
.insert({
|
||||
async createNotification(
|
||||
userId: string,
|
||||
type: string,
|
||||
data: any,
|
||||
): Promise<void> {
|
||||
await supabase.from("notifications").insert({
|
||||
user_id: userId,
|
||||
type,
|
||||
data,
|
||||
|
|
@ -437,33 +454,36 @@ export const aethexNotificationService = {
|
|||
|
||||
// Real-time subscriptions
|
||||
export const aethexRealtimeService = {
|
||||
subscribeToUserNotifications(userId: string, callback: (notification: any) => void) {
|
||||
subscribeToUserNotifications(
|
||||
userId: string,
|
||||
callback: (notification: any) => void,
|
||||
) {
|
||||
return supabase
|
||||
.channel(`notifications:${userId}`)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
"postgres_changes",
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'notifications',
|
||||
event: "INSERT",
|
||||
schema: "public",
|
||||
table: "notifications",
|
||||
filter: `user_id=eq.${userId}`,
|
||||
},
|
||||
callback
|
||||
callback,
|
||||
)
|
||||
.subscribe();
|
||||
},
|
||||
|
||||
subscribeToProjects(callback: (project: any) => void) {
|
||||
return supabase
|
||||
.channel('projects')
|
||||
.channel("projects")
|
||||
.on(
|
||||
'postgres_changes',
|
||||
"postgres_changes",
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'projects',
|
||||
event: "INSERT",
|
||||
schema: "public",
|
||||
table: "projects",
|
||||
},
|
||||
callback
|
||||
callback,
|
||||
)
|
||||
.subscribe();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,378 +4,394 @@ export type Json =
|
|||
| boolean
|
||||
| null
|
||||
| { [key: string]: Json | undefined }
|
||||
| Json[]
|
||||
| Json[];
|
||||
|
||||
export type Database = {
|
||||
public: {
|
||||
Tables: {
|
||||
achievements: {
|
||||
Row: {
|
||||
badge_color: string | null
|
||||
created_at: string
|
||||
description: string | null
|
||||
icon: string | null
|
||||
id: string
|
||||
name: string
|
||||
xp_reward: number | null
|
||||
}
|
||||
badge_color: string | null;
|
||||
created_at: string;
|
||||
description: string | null;
|
||||
icon: string | null;
|
||||
id: string;
|
||||
name: string;
|
||||
xp_reward: number | null;
|
||||
};
|
||||
Insert: {
|
||||
badge_color?: string | null
|
||||
created_at?: string
|
||||
description?: string | null
|
||||
icon?: string | null
|
||||
id?: string
|
||||
name: string
|
||||
xp_reward?: number | null
|
||||
}
|
||||
badge_color?: string | null;
|
||||
created_at?: string;
|
||||
description?: string | null;
|
||||
icon?: string | null;
|
||||
id?: string;
|
||||
name: string;
|
||||
xp_reward?: number | null;
|
||||
};
|
||||
Update: {
|
||||
badge_color?: string | null
|
||||
created_at?: string
|
||||
description?: string | null
|
||||
icon?: string | null
|
||||
id?: string
|
||||
name?: string
|
||||
xp_reward?: number | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
badge_color?: string | null;
|
||||
created_at?: string;
|
||||
description?: string | null;
|
||||
icon?: string | null;
|
||||
id?: string;
|
||||
name?: string;
|
||||
xp_reward?: number | null;
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
comments: {
|
||||
Row: {
|
||||
author_id: string
|
||||
content: string
|
||||
created_at: string
|
||||
id: string
|
||||
post_id: string
|
||||
}
|
||||
author_id: string;
|
||||
content: string;
|
||||
created_at: string;
|
||||
id: string;
|
||||
post_id: string;
|
||||
};
|
||||
Insert: {
|
||||
author_id: string
|
||||
content: string
|
||||
created_at?: string
|
||||
id?: string
|
||||
post_id: string
|
||||
}
|
||||
author_id: string;
|
||||
content: string;
|
||||
created_at?: string;
|
||||
id?: string;
|
||||
post_id: string;
|
||||
};
|
||||
Update: {
|
||||
author_id?: string
|
||||
content?: string
|
||||
created_at?: string
|
||||
id?: string
|
||||
post_id?: string
|
||||
}
|
||||
author_id?: string;
|
||||
content?: string;
|
||||
created_at?: string;
|
||||
id?: string;
|
||||
post_id?: string;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "comments_author_id_fkey"
|
||||
columns: ["author_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "user_profiles"
|
||||
referencedColumns: ["id"]
|
||||
foreignKeyName: "comments_author_id_fkey";
|
||||
columns: ["author_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "user_profiles";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
{
|
||||
foreignKeyName: "comments_post_id_fkey"
|
||||
columns: ["post_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "community_posts"
|
||||
referencedColumns: ["id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
foreignKeyName: "comments_post_id_fkey";
|
||||
columns: ["post_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "community_posts";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
];
|
||||
};
|
||||
community_posts: {
|
||||
Row: {
|
||||
author_id: string
|
||||
category: string | null
|
||||
comments_count: number | null
|
||||
content: string
|
||||
created_at: string
|
||||
id: string
|
||||
is_published: boolean | null
|
||||
likes_count: number | null
|
||||
tags: string[] | null
|
||||
title: string
|
||||
updated_at: string
|
||||
}
|
||||
author_id: string;
|
||||
category: string | null;
|
||||
comments_count: number | null;
|
||||
content: string;
|
||||
created_at: string;
|
||||
id: string;
|
||||
is_published: boolean | null;
|
||||
likes_count: number | null;
|
||||
tags: string[] | null;
|
||||
title: string;
|
||||
updated_at: string;
|
||||
};
|
||||
Insert: {
|
||||
author_id: string
|
||||
category?: string | null
|
||||
comments_count?: number | null
|
||||
content: string
|
||||
created_at?: string
|
||||
id?: string
|
||||
is_published?: boolean | null
|
||||
likes_count?: number | null
|
||||
tags?: string[] | null
|
||||
title: string
|
||||
updated_at?: string
|
||||
}
|
||||
author_id: string;
|
||||
category?: string | null;
|
||||
comments_count?: number | null;
|
||||
content: string;
|
||||
created_at?: string;
|
||||
id?: string;
|
||||
is_published?: boolean | null;
|
||||
likes_count?: number | null;
|
||||
tags?: string[] | null;
|
||||
title: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
Update: {
|
||||
author_id?: string
|
||||
category?: string | null
|
||||
comments_count?: number | null
|
||||
content?: string
|
||||
created_at?: string
|
||||
id?: string
|
||||
is_published?: boolean | null
|
||||
likes_count?: number | null
|
||||
tags?: string[] | null
|
||||
title?: string
|
||||
updated_at?: string
|
||||
}
|
||||
author_id?: string;
|
||||
category?: string | null;
|
||||
comments_count?: number | null;
|
||||
content?: string;
|
||||
created_at?: string;
|
||||
id?: string;
|
||||
is_published?: boolean | null;
|
||||
likes_count?: number | null;
|
||||
tags?: string[] | null;
|
||||
title?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "community_posts_author_id_fkey"
|
||||
columns: ["author_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "user_profiles"
|
||||
referencedColumns: ["id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
foreignKeyName: "community_posts_author_id_fkey";
|
||||
columns: ["author_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "user_profiles";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
];
|
||||
};
|
||||
notifications: {
|
||||
Row: {
|
||||
created_at: string
|
||||
id: string
|
||||
message: string | null
|
||||
read: boolean | null
|
||||
title: string
|
||||
type: string | null
|
||||
user_id: string
|
||||
}
|
||||
created_at: string;
|
||||
id: string;
|
||||
message: string | null;
|
||||
read: boolean | null;
|
||||
title: string;
|
||||
type: string | null;
|
||||
user_id: string;
|
||||
};
|
||||
Insert: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
message?: string | null
|
||||
read?: boolean | null
|
||||
title: string
|
||||
type?: string | null
|
||||
user_id: string
|
||||
}
|
||||
created_at?: string;
|
||||
id?: string;
|
||||
message?: string | null;
|
||||
read?: boolean | null;
|
||||
title: string;
|
||||
type?: string | null;
|
||||
user_id: string;
|
||||
};
|
||||
Update: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
message?: string | null
|
||||
read?: boolean | null
|
||||
title?: string
|
||||
type?: string | null
|
||||
user_id?: string
|
||||
}
|
||||
created_at?: string;
|
||||
id?: string;
|
||||
message?: string | null;
|
||||
read?: boolean | null;
|
||||
title?: string;
|
||||
type?: string | null;
|
||||
user_id?: string;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "notifications_user_id_fkey"
|
||||
columns: ["user_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "user_profiles"
|
||||
referencedColumns: ["id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
foreignKeyName: "notifications_user_id_fkey";
|
||||
columns: ["user_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "user_profiles";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
];
|
||||
};
|
||||
projects: {
|
||||
Row: {
|
||||
created_at: string
|
||||
demo_url: string | null
|
||||
description: string | null
|
||||
end_date: string | null
|
||||
github_url: string | null
|
||||
id: string
|
||||
image_url: string | null
|
||||
start_date: string | null
|
||||
status: Database["public"]["Enums"]["project_status_enum"] | null
|
||||
technologies: string[] | null
|
||||
title: string
|
||||
updated_at: string
|
||||
user_id: string
|
||||
}
|
||||
created_at: string;
|
||||
demo_url: string | null;
|
||||
description: string | null;
|
||||
end_date: string | null;
|
||||
github_url: string | null;
|
||||
id: string;
|
||||
image_url: string | null;
|
||||
start_date: string | null;
|
||||
status: Database["public"]["Enums"]["project_status_enum"] | null;
|
||||
technologies: string[] | null;
|
||||
title: string;
|
||||
updated_at: string;
|
||||
user_id: string;
|
||||
};
|
||||
Insert: {
|
||||
created_at?: string
|
||||
demo_url?: string | null
|
||||
description?: string | null
|
||||
end_date?: string | null
|
||||
github_url?: string | null
|
||||
id?: string
|
||||
image_url?: string | null
|
||||
start_date?: string | null
|
||||
status?: Database["public"]["Enums"]["project_status_enum"] | null
|
||||
technologies?: string[] | null
|
||||
title: string
|
||||
updated_at?: string
|
||||
user_id: string
|
||||
}
|
||||
created_at?: string;
|
||||
demo_url?: string | null;
|
||||
description?: string | null;
|
||||
end_date?: string | null;
|
||||
github_url?: string | null;
|
||||
id?: string;
|
||||
image_url?: string | null;
|
||||
start_date?: string | null;
|
||||
status?: Database["public"]["Enums"]["project_status_enum"] | null;
|
||||
technologies?: string[] | null;
|
||||
title: string;
|
||||
updated_at?: string;
|
||||
user_id: string;
|
||||
};
|
||||
Update: {
|
||||
created_at?: string
|
||||
demo_url?: string | null
|
||||
description?: string | null
|
||||
end_date?: string | null
|
||||
github_url?: string | null
|
||||
id?: string
|
||||
image_url?: string | null
|
||||
start_date?: string | null
|
||||
status?: Database["public"]["Enums"]["project_status_enum"] | null
|
||||
technologies?: string[] | null
|
||||
title?: string
|
||||
updated_at?: string
|
||||
user_id?: string
|
||||
}
|
||||
created_at?: string;
|
||||
demo_url?: string | null;
|
||||
description?: string | null;
|
||||
end_date?: string | null;
|
||||
github_url?: string | null;
|
||||
id?: string;
|
||||
image_url?: string | null;
|
||||
start_date?: string | null;
|
||||
status?: Database["public"]["Enums"]["project_status_enum"] | null;
|
||||
technologies?: string[] | null;
|
||||
title?: string;
|
||||
updated_at?: string;
|
||||
user_id?: string;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "projects_user_id_fkey"
|
||||
columns: ["user_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "user_profiles"
|
||||
referencedColumns: ["id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
foreignKeyName: "projects_user_id_fkey";
|
||||
columns: ["user_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "user_profiles";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
];
|
||||
};
|
||||
user_achievements: {
|
||||
Row: {
|
||||
achievement_id: string
|
||||
earned_at: string
|
||||
id: string
|
||||
user_id: string
|
||||
}
|
||||
achievement_id: string;
|
||||
earned_at: string;
|
||||
id: string;
|
||||
user_id: string;
|
||||
};
|
||||
Insert: {
|
||||
achievement_id: string
|
||||
earned_at?: string
|
||||
id?: string
|
||||
user_id: string
|
||||
}
|
||||
achievement_id: string;
|
||||
earned_at?: string;
|
||||
id?: string;
|
||||
user_id: string;
|
||||
};
|
||||
Update: {
|
||||
achievement_id?: string
|
||||
earned_at?: string
|
||||
id?: string
|
||||
user_id?: string
|
||||
}
|
||||
achievement_id?: string;
|
||||
earned_at?: string;
|
||||
id?: string;
|
||||
user_id?: string;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "user_achievements_achievement_id_fkey"
|
||||
columns: ["achievement_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "achievements"
|
||||
referencedColumns: ["id"]
|
||||
foreignKeyName: "user_achievements_achievement_id_fkey";
|
||||
columns: ["achievement_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "achievements";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
{
|
||||
foreignKeyName: "user_achievements_user_id_fkey"
|
||||
columns: ["user_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "user_profiles"
|
||||
referencedColumns: ["id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
foreignKeyName: "user_achievements_user_id_fkey";
|
||||
columns: ["user_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "user_profiles";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
];
|
||||
};
|
||||
user_interests: {
|
||||
Row: {
|
||||
created_at: string
|
||||
id: string
|
||||
interest: string
|
||||
user_id: string
|
||||
}
|
||||
created_at: string;
|
||||
id: string;
|
||||
interest: string;
|
||||
user_id: string;
|
||||
};
|
||||
Insert: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
interest: string
|
||||
user_id: string
|
||||
}
|
||||
created_at?: string;
|
||||
id?: string;
|
||||
interest: string;
|
||||
user_id: string;
|
||||
};
|
||||
Update: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
interest?: string
|
||||
user_id?: string
|
||||
}
|
||||
created_at?: string;
|
||||
id?: string;
|
||||
interest?: string;
|
||||
user_id?: string;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "user_interests_user_id_fkey"
|
||||
columns: ["user_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "user_profiles"
|
||||
referencedColumns: ["id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
foreignKeyName: "user_interests_user_id_fkey";
|
||||
columns: ["user_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "user_profiles";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
];
|
||||
};
|
||||
user_profiles: {
|
||||
Row: {
|
||||
avatar_url: string | null
|
||||
bio: string | null
|
||||
created_at: string
|
||||
experience_level: Database["public"]["Enums"]["experience_level_enum"] | null
|
||||
full_name: string | null
|
||||
github_url: string | null
|
||||
id: string
|
||||
level: number | null
|
||||
linkedin_url: string | null
|
||||
location: string | null
|
||||
total_xp: number | null
|
||||
twitter_url: string | null
|
||||
updated_at: string
|
||||
user_type: Database["public"]["Enums"]["user_type_enum"]
|
||||
username: string | null
|
||||
website_url: string | null
|
||||
}
|
||||
avatar_url: string | null;
|
||||
bio: string | null;
|
||||
created_at: string;
|
||||
experience_level:
|
||||
| Database["public"]["Enums"]["experience_level_enum"]
|
||||
| null;
|
||||
full_name: string | null;
|
||||
github_url: string | null;
|
||||
id: string;
|
||||
level: number | null;
|
||||
linkedin_url: string | null;
|
||||
location: string | null;
|
||||
total_xp: number | null;
|
||||
twitter_url: string | null;
|
||||
updated_at: string;
|
||||
user_type: Database["public"]["Enums"]["user_type_enum"];
|
||||
username: string | null;
|
||||
website_url: string | null;
|
||||
};
|
||||
Insert: {
|
||||
avatar_url?: string | null
|
||||
bio?: string | null
|
||||
created_at?: string
|
||||
experience_level?: Database["public"]["Enums"]["experience_level_enum"] | null
|
||||
full_name?: string | null
|
||||
github_url?: string | null
|
||||
id: string
|
||||
level?: number | null
|
||||
linkedin_url?: string | null
|
||||
location?: string | null
|
||||
total_xp?: number | null
|
||||
twitter_url?: string | null
|
||||
updated_at?: string
|
||||
user_type: Database["public"]["Enums"]["user_type_enum"]
|
||||
username?: string | null
|
||||
website_url?: string | null
|
||||
}
|
||||
avatar_url?: string | null;
|
||||
bio?: string | null;
|
||||
created_at?: string;
|
||||
experience_level?:
|
||||
| Database["public"]["Enums"]["experience_level_enum"]
|
||||
| null;
|
||||
full_name?: string | null;
|
||||
github_url?: string | null;
|
||||
id: string;
|
||||
level?: number | null;
|
||||
linkedin_url?: string | null;
|
||||
location?: string | null;
|
||||
total_xp?: number | null;
|
||||
twitter_url?: string | null;
|
||||
updated_at?: string;
|
||||
user_type: Database["public"]["Enums"]["user_type_enum"];
|
||||
username?: string | null;
|
||||
website_url?: string | null;
|
||||
};
|
||||
Update: {
|
||||
avatar_url?: string | null
|
||||
bio?: string | null
|
||||
created_at?: string
|
||||
experience_level?: Database["public"]["Enums"]["experience_level_enum"] | null
|
||||
full_name?: string | null
|
||||
github_url?: string | null
|
||||
id?: string
|
||||
level?: number | null
|
||||
linkedin_url?: string | null
|
||||
location?: string | null
|
||||
total_xp?: number | null
|
||||
twitter_url?: string | null
|
||||
updated_at?: string
|
||||
user_type?: Database["public"]["Enums"]["user_type_enum"]
|
||||
username?: string | null
|
||||
website_url?: string | null
|
||||
}
|
||||
avatar_url?: string | null;
|
||||
bio?: string | null;
|
||||
created_at?: string;
|
||||
experience_level?:
|
||||
| Database["public"]["Enums"]["experience_level_enum"]
|
||||
| null;
|
||||
full_name?: string | null;
|
||||
github_url?: string | null;
|
||||
id?: string;
|
||||
level?: number | null;
|
||||
linkedin_url?: string | null;
|
||||
location?: string | null;
|
||||
total_xp?: number | null;
|
||||
twitter_url?: string | null;
|
||||
updated_at?: string;
|
||||
user_type?: Database["public"]["Enums"]["user_type_enum"];
|
||||
username?: string | null;
|
||||
website_url?: string | null;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "user_profiles_id_fkey"
|
||||
columns: ["id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "users"
|
||||
referencedColumns: ["id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
foreignKeyName: "user_profiles_id_fkey";
|
||||
columns: ["id"];
|
||||
isOneToOne: true;
|
||||
referencedRelation: "users";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
];
|
||||
};
|
||||
};
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
[_ in never]: never;
|
||||
};
|
||||
Functions: {
|
||||
[_ in never]: never
|
||||
}
|
||||
[_ in never]: never;
|
||||
};
|
||||
Enums: {
|
||||
experience_level_enum: "beginner" | "intermediate" | "advanced" | "expert"
|
||||
project_status_enum: "planning" | "in_progress" | "completed" | "on_hold"
|
||||
user_type_enum: "game_developer" | "client" | "community_member" | "customer"
|
||||
}
|
||||
experience_level_enum:
|
||||
| "beginner"
|
||||
| "intermediate"
|
||||
| "advanced"
|
||||
| "expert";
|
||||
project_status_enum: "planning" | "in_progress" | "completed" | "on_hold";
|
||||
user_type_enum:
|
||||
| "game_developer"
|
||||
| "client"
|
||||
| "community_member"
|
||||
| "customer";
|
||||
};
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
}
|
||||
[_ in never]: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type UserProfile = Database['public']['Tables']['user_profiles']['Row'];
|
||||
export type Project = Database['public']['Tables']['projects']['Row'];
|
||||
export type Achievement = Database['public']['Tables']['achievements']['Row'];
|
||||
export type CommunityPost = Database['public']['Tables']['community_posts']['Row'];
|
||||
export type Notification = Database['public']['Tables']['notifications']['Row'];
|
||||
export type UserProfile = Database["public"]["Tables"]["user_profiles"]["Row"];
|
||||
export type Project = Database["public"]["Tables"]["projects"]["Row"];
|
||||
export type Achievement = Database["public"]["Tables"]["achievements"]["Row"];
|
||||
export type CommunityPost =
|
||||
Database["public"]["Tables"]["community_posts"]["Row"];
|
||||
export type Notification = Database["public"]["Tables"]["notifications"]["Row"];
|
||||
|
||||
export type UserType = Database['public']['Enums']['user_type_enum'];
|
||||
export type ExperienceLevel = Database['public']['Enums']['experience_level_enum'];
|
||||
export type ProjectStatus = Database['public']['Enums']['project_status_enum'];
|
||||
export type UserType = Database["public"]["Enums"]["user_type_enum"];
|
||||
export type ExperienceLevel =
|
||||
Database["public"]["Enums"]["experience_level_enum"];
|
||||
export type ProjectStatus = Database["public"]["Enums"]["project_status_enum"];
|
||||
|
|
|
|||
|
|
@ -1,74 +1,74 @@
|
|||
// Demo storage service - simulates backend functionality using localStorage
|
||||
import type { UserProfile, Project, Achievement } from './database.types';
|
||||
import { aethexToast } from './aethex-toast';
|
||||
import type { UserProfile, Project, Achievement } from "./database.types";
|
||||
import { aethexToast } from "./aethex-toast";
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
USER_PROFILE: 'aethex_demo_user_profile',
|
||||
PROJECTS: 'aethex_demo_projects',
|
||||
ACHIEVEMENTS: 'aethex_demo_achievements',
|
||||
NOTIFICATIONS: 'aethex_demo_notifications',
|
||||
INTERESTS: 'aethex_demo_interests',
|
||||
USER_PROFILE: "aethex_demo_user_profile",
|
||||
PROJECTS: "aethex_demo_projects",
|
||||
ACHIEVEMENTS: "aethex_demo_achievements",
|
||||
NOTIFICATIONS: "aethex_demo_notifications",
|
||||
INTERESTS: "aethex_demo_interests",
|
||||
};
|
||||
|
||||
// Demo user data
|
||||
const DEMO_USER_PROFILE: Partial<UserProfile> = {
|
||||
id: 'demo-user-123',
|
||||
username: 'demo_developer',
|
||||
full_name: 'Demo Developer',
|
||||
user_type: 'game_developer',
|
||||
experience_level: 'intermediate',
|
||||
id: "demo-user-123",
|
||||
username: "demo_developer",
|
||||
full_name: "Demo Developer",
|
||||
user_type: "game_developer",
|
||||
experience_level: "intermediate",
|
||||
total_xp: 1250,
|
||||
level: 5,
|
||||
bio: 'Passionate game developer exploring the AeThex ecosystem',
|
||||
location: 'Digital Realm',
|
||||
github_url: 'https://github.com/demo-developer',
|
||||
twitter_url: 'https://twitter.com/demo_dev',
|
||||
bio: "Passionate game developer exploring the AeThex ecosystem",
|
||||
location: "Digital Realm",
|
||||
github_url: "https://github.com/demo-developer",
|
||||
twitter_url: "https://twitter.com/demo_dev",
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const DEMO_PROJECTS: Project[] = [
|
||||
{
|
||||
id: 'proj-1',
|
||||
user_id: 'demo-user-123',
|
||||
title: 'Quantum Quest',
|
||||
description: 'A sci-fi adventure game built with AeThex Engine',
|
||||
status: 'in_progress',
|
||||
technologies: ['AeThex Engine', 'TypeScript', 'WebGL'],
|
||||
github_url: 'https://github.com/demo/quantum-quest',
|
||||
demo_url: 'https://quantum-quest-demo.com',
|
||||
id: "proj-1",
|
||||
user_id: "demo-user-123",
|
||||
title: "Quantum Quest",
|
||||
description: "A sci-fi adventure game built with AeThex Engine",
|
||||
status: "in_progress",
|
||||
technologies: ["AeThex Engine", "TypeScript", "WebGL"],
|
||||
github_url: "https://github.com/demo/quantum-quest",
|
||||
demo_url: "https://quantum-quest-demo.com",
|
||||
image_url: null,
|
||||
start_date: '2024-01-15',
|
||||
start_date: "2024-01-15",
|
||||
end_date: null,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'proj-2',
|
||||
user_id: 'demo-user-123',
|
||||
title: 'Neon Runner',
|
||||
description: 'Fast-paced endless runner with cyberpunk aesthetics',
|
||||
status: 'completed',
|
||||
technologies: ['AeThex Engine', 'JavaScript', 'CSS3'],
|
||||
github_url: 'https://github.com/demo/neon-runner',
|
||||
demo_url: 'https://neon-runner-demo.com',
|
||||
id: "proj-2",
|
||||
user_id: "demo-user-123",
|
||||
title: "Neon Runner",
|
||||
description: "Fast-paced endless runner with cyberpunk aesthetics",
|
||||
status: "completed",
|
||||
technologies: ["AeThex Engine", "JavaScript", "CSS3"],
|
||||
github_url: "https://github.com/demo/neon-runner",
|
||||
demo_url: "https://neon-runner-demo.com",
|
||||
image_url: null,
|
||||
start_date: '2023-08-01',
|
||||
end_date: '2023-12-15',
|
||||
start_date: "2023-08-01",
|
||||
end_date: "2023-12-15",
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'proj-3',
|
||||
user_id: 'demo-user-123',
|
||||
title: 'Pixel Physics',
|
||||
description: 'Educational physics simulation game',
|
||||
status: 'planning',
|
||||
technologies: ['AeThex Engine', 'React', 'Physics Engine'],
|
||||
id: "proj-3",
|
||||
user_id: "demo-user-123",
|
||||
title: "Pixel Physics",
|
||||
description: "Educational physics simulation game",
|
||||
status: "planning",
|
||||
technologies: ["AeThex Engine", "React", "Physics Engine"],
|
||||
github_url: null,
|
||||
demo_url: null,
|
||||
image_url: null,
|
||||
start_date: '2024-03-01',
|
||||
start_date: "2024-03-01",
|
||||
end_date: null,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
|
|
@ -77,39 +77,39 @@ const DEMO_PROJECTS: Project[] = [
|
|||
|
||||
const DEMO_ACHIEVEMENTS: Achievement[] = [
|
||||
{
|
||||
id: 'ach-1',
|
||||
name: 'Welcome to AeThex',
|
||||
description: 'Complete your profile setup',
|
||||
icon: '🎉',
|
||||
id: "ach-1",
|
||||
name: "Welcome to AeThex",
|
||||
description: "Complete your profile setup",
|
||||
icon: "🎉",
|
||||
xp_reward: 100,
|
||||
badge_color: '#10B981',
|
||||
badge_color: "#10B981",
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'ach-2',
|
||||
name: 'First Project',
|
||||
description: 'Create your first project',
|
||||
icon: '🚀',
|
||||
id: "ach-2",
|
||||
name: "First Project",
|
||||
description: "Create your first project",
|
||||
icon: "🚀",
|
||||
xp_reward: 150,
|
||||
badge_color: '#3B82F6',
|
||||
badge_color: "#3B82F6",
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'ach-3',
|
||||
name: 'Community Contributor',
|
||||
description: 'Make your first community post',
|
||||
icon: '💬',
|
||||
id: "ach-3",
|
||||
name: "Community Contributor",
|
||||
description: "Make your first community post",
|
||||
icon: "💬",
|
||||
xp_reward: 75,
|
||||
badge_color: '#8B5CF6',
|
||||
badge_color: "#8B5CF6",
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'ach-4',
|
||||
name: 'Experienced Developer',
|
||||
description: 'Complete 5 projects',
|
||||
icon: '👨💻',
|
||||
id: "ach-4",
|
||||
name: "Experienced Developer",
|
||||
description: "Complete 5 projects",
|
||||
icon: "👨💻",
|
||||
xp_reward: 300,
|
||||
badge_color: '#EF4444',
|
||||
badge_color: "#EF4444",
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
|
@ -123,7 +123,11 @@ export class DemoStorageService {
|
|||
|
||||
static updateUserProfile(updates: Partial<UserProfile>): UserProfile {
|
||||
const current = this.getUserProfile() || DEMO_USER_PROFILE;
|
||||
const updated = { ...current, ...updates, updated_at: new Date().toISOString() };
|
||||
const updated = {
|
||||
...current,
|
||||
...updates,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
localStorage.setItem(STORAGE_KEYS.USER_PROFILE, JSON.stringify(updated));
|
||||
return updated as UserProfile;
|
||||
}
|
||||
|
|
@ -134,7 +138,9 @@ export class DemoStorageService {
|
|||
return stored ? JSON.parse(stored) : DEMO_PROJECTS;
|
||||
}
|
||||
|
||||
static createProject(project: Omit<Project, 'id' | 'created_at' | 'updated_at'>): Project {
|
||||
static createProject(
|
||||
project: Omit<Project, "id" | "created_at" | "updated_at">,
|
||||
): Project {
|
||||
const projects = this.getUserProjects();
|
||||
const newProject: Project = {
|
||||
...project,
|
||||
|
|
@ -147,19 +153,26 @@ export class DemoStorageService {
|
|||
return newProject;
|
||||
}
|
||||
|
||||
static updateProject(projectId: string, updates: Partial<Project>): Project | null {
|
||||
static updateProject(
|
||||
projectId: string,
|
||||
updates: Partial<Project>,
|
||||
): Project | null {
|
||||
const projects = this.getUserProjects();
|
||||
const index = projects.findIndex(p => p.id === projectId);
|
||||
const index = projects.findIndex((p) => p.id === projectId);
|
||||
if (index === -1) return null;
|
||||
|
||||
projects[index] = { ...projects[index], ...updates, updated_at: new Date().toISOString() };
|
||||
projects[index] = {
|
||||
...projects[index],
|
||||
...updates,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
localStorage.setItem(STORAGE_KEYS.PROJECTS, JSON.stringify(projects));
|
||||
return projects[index];
|
||||
}
|
||||
|
||||
static deleteProject(projectId: string): boolean {
|
||||
const projects = this.getUserProjects();
|
||||
const filtered = projects.filter(p => p.id !== projectId);
|
||||
const filtered = projects.filter((p) => p.id !== projectId);
|
||||
localStorage.setItem(STORAGE_KEYS.PROJECTS, JSON.stringify(filtered));
|
||||
return filtered.length < projects.length;
|
||||
}
|
||||
|
|
@ -171,22 +184,25 @@ export class DemoStorageService {
|
|||
|
||||
static getUserAchievements(): Achievement[] {
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.ACHIEVEMENTS);
|
||||
const earnedIds = stored ? JSON.parse(stored) : ['ach-1', 'ach-2', 'ach-3'];
|
||||
return DEMO_ACHIEVEMENTS.filter(ach => earnedIds.includes(ach.id));
|
||||
const earnedIds = stored ? JSON.parse(stored) : ["ach-1", "ach-2", "ach-3"];
|
||||
return DEMO_ACHIEVEMENTS.filter((ach) => earnedIds.includes(ach.id));
|
||||
}
|
||||
|
||||
static awardAchievement(achievementId: string): void {
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.ACHIEVEMENTS);
|
||||
const earnedIds = stored ? JSON.parse(stored) : ['ach-1', 'ach-2', 'ach-3'];
|
||||
const earnedIds = stored ? JSON.parse(stored) : ["ach-1", "ach-2", "ach-3"];
|
||||
|
||||
if (!earnedIds.includes(achievementId)) {
|
||||
earnedIds.push(achievementId);
|
||||
localStorage.setItem(STORAGE_KEYS.ACHIEVEMENTS, JSON.stringify(earnedIds));
|
||||
localStorage.setItem(
|
||||
STORAGE_KEYS.ACHIEVEMENTS,
|
||||
JSON.stringify(earnedIds),
|
||||
);
|
||||
|
||||
const achievement = DEMO_ACHIEVEMENTS.find(a => a.id === achievementId);
|
||||
const achievement = DEMO_ACHIEVEMENTS.find((a) => a.id === achievementId);
|
||||
if (achievement) {
|
||||
aethexToast.aethex({
|
||||
title: 'Achievement Unlocked!',
|
||||
title: "Achievement Unlocked!",
|
||||
description: `${achievement.icon} ${achievement.name} - ${achievement.description}`,
|
||||
duration: 8000,
|
||||
});
|
||||
|
|
@ -197,7 +213,9 @@ export class DemoStorageService {
|
|||
// Interests Management
|
||||
static getUserInterests(): string[] {
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.INTERESTS);
|
||||
return stored ? JSON.parse(stored) : ['Game Development', 'AI/ML', 'Web3', 'Mobile Apps'];
|
||||
return stored
|
||||
? JSON.parse(stored)
|
||||
: ["Game Development", "AI/ML", "Web3", "Mobile Apps"];
|
||||
}
|
||||
|
||||
static updateUserInterests(interests: string[]): void {
|
||||
|
|
@ -208,22 +226,34 @@ export class DemoStorageService {
|
|||
static initializeDemoData(): void {
|
||||
// Only initialize if no data exists
|
||||
if (!localStorage.getItem(STORAGE_KEYS.USER_PROFILE)) {
|
||||
localStorage.setItem(STORAGE_KEYS.USER_PROFILE, JSON.stringify(DEMO_USER_PROFILE));
|
||||
localStorage.setItem(
|
||||
STORAGE_KEYS.USER_PROFILE,
|
||||
JSON.stringify(DEMO_USER_PROFILE),
|
||||
);
|
||||
}
|
||||
if (!localStorage.getItem(STORAGE_KEYS.PROJECTS)) {
|
||||
localStorage.setItem(STORAGE_KEYS.PROJECTS, JSON.stringify(DEMO_PROJECTS));
|
||||
localStorage.setItem(
|
||||
STORAGE_KEYS.PROJECTS,
|
||||
JSON.stringify(DEMO_PROJECTS),
|
||||
);
|
||||
}
|
||||
if (!localStorage.getItem(STORAGE_KEYS.ACHIEVEMENTS)) {
|
||||
localStorage.setItem(STORAGE_KEYS.ACHIEVEMENTS, JSON.stringify(['ach-1', 'ach-2', 'ach-3']));
|
||||
localStorage.setItem(
|
||||
STORAGE_KEYS.ACHIEVEMENTS,
|
||||
JSON.stringify(["ach-1", "ach-2", "ach-3"]),
|
||||
);
|
||||
}
|
||||
if (!localStorage.getItem(STORAGE_KEYS.INTERESTS)) {
|
||||
localStorage.setItem(STORAGE_KEYS.INTERESTS, JSON.stringify(['Game Development', 'AI/ML', 'Web3', 'Mobile Apps']));
|
||||
localStorage.setItem(
|
||||
STORAGE_KEYS.INTERESTS,
|
||||
JSON.stringify(["Game Development", "AI/ML", "Web3", "Mobile Apps"]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all demo data
|
||||
static clearDemoData(): void {
|
||||
Object.values(STORAGE_KEYS).forEach(key => {
|
||||
Object.values(STORAGE_KEYS).forEach((key) => {
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,36 @@
|
|||
import { supabase } from './supabase';
|
||||
import type { Database, UserProfile, Project, Achievement, CommunityPost } from './database.types';
|
||||
import { supabase } from "./supabase";
|
||||
import type {
|
||||
Database,
|
||||
UserProfile,
|
||||
Project,
|
||||
Achievement,
|
||||
CommunityPost,
|
||||
} from "./database.types";
|
||||
|
||||
// User Profile Services
|
||||
export const userProfileService = {
|
||||
async getProfile(userId: string): Promise<UserProfile | null> {
|
||||
const { data, error } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('*')
|
||||
.eq('id', userId)
|
||||
.from("user_profiles")
|
||||
.select("*")
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
|
||||
if (error && error.code !== 'PGRST116') {
|
||||
if (error && error.code !== "PGRST116") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
async updateProfile(userId: string, updates: Partial<UserProfile>): Promise<UserProfile> {
|
||||
async updateProfile(
|
||||
userId: string,
|
||||
updates: Partial<UserProfile>,
|
||||
): Promise<UserProfile> {
|
||||
const { data, error } = await supabase
|
||||
.from('user_profiles')
|
||||
.from("user_profiles")
|
||||
.update(updates)
|
||||
.eq('id', userId)
|
||||
.eq("id", userId)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
|
|
@ -29,9 +38,11 @@ export const userProfileService = {
|
|||
return data;
|
||||
},
|
||||
|
||||
async createProfile(profile: Omit<UserProfile, 'created_at' | 'updated_at'>): Promise<UserProfile> {
|
||||
async createProfile(
|
||||
profile: Omit<UserProfile, "created_at" | "updated_at">,
|
||||
): Promise<UserProfile> {
|
||||
const { data, error } = await supabase
|
||||
.from('user_profiles')
|
||||
.from("user_profiles")
|
||||
.insert(profile)
|
||||
.select()
|
||||
.single();
|
||||
|
|
@ -41,13 +52,13 @@ export const userProfileService = {
|
|||
},
|
||||
|
||||
async addInterests(userId: string, interests: string[]): Promise<void> {
|
||||
const interestRows = interests.map(interest => ({
|
||||
const interestRows = interests.map((interest) => ({
|
||||
user_id: userId,
|
||||
interest,
|
||||
}));
|
||||
|
||||
const { error } = await supabase
|
||||
.from('user_interests')
|
||||
.from("user_interests")
|
||||
.insert(interestRows);
|
||||
|
||||
if (error) throw error;
|
||||
|
|
@ -55,12 +66,12 @@ export const userProfileService = {
|
|||
|
||||
async getUserInterests(userId: string): Promise<string[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('user_interests')
|
||||
.select('interest')
|
||||
.eq('user_id', userId);
|
||||
.from("user_interests")
|
||||
.select("interest")
|
||||
.eq("user_id", userId);
|
||||
|
||||
if (error) throw error;
|
||||
return data.map(item => item.interest);
|
||||
return data.map((item) => item.interest);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -68,18 +79,20 @@ export const userProfileService = {
|
|||
export const projectService = {
|
||||
async getUserProjects(userId: string): Promise<Project[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false });
|
||||
.from("projects")
|
||||
.select("*")
|
||||
.eq("user_id", userId)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
|
||||
async createProject(project: Omit<Project, 'id' | 'created_at' | 'updated_at'>): Promise<Project> {
|
||||
async createProject(
|
||||
project: Omit<Project, "id" | "created_at" | "updated_at">,
|
||||
): Promise<Project> {
|
||||
const { data, error } = await supabase
|
||||
.from('projects')
|
||||
.from("projects")
|
||||
.insert(project)
|
||||
.select()
|
||||
.single();
|
||||
|
|
@ -88,11 +101,14 @@ export const projectService = {
|
|||
return data;
|
||||
},
|
||||
|
||||
async updateProject(projectId: string, updates: Partial<Project>): Promise<Project> {
|
||||
async updateProject(
|
||||
projectId: string,
|
||||
updates: Partial<Project>,
|
||||
): Promise<Project> {
|
||||
const { data, error } = await supabase
|
||||
.from('projects')
|
||||
.from("projects")
|
||||
.update(updates)
|
||||
.eq('id', projectId)
|
||||
.eq("id", projectId)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
|
|
@ -102,26 +118,28 @@ export const projectService = {
|
|||
|
||||
async deleteProject(projectId: string): Promise<void> {
|
||||
const { error } = await supabase
|
||||
.from('projects')
|
||||
.from("projects")
|
||||
.delete()
|
||||
.eq('id', projectId);
|
||||
.eq("id", projectId);
|
||||
|
||||
if (error) throw error;
|
||||
},
|
||||
|
||||
async getAllProjects(limit = 10): Promise<Project[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('projects')
|
||||
.select(`
|
||||
.from("projects")
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
user_profiles (
|
||||
username,
|
||||
full_name,
|
||||
avatar_url
|
||||
)
|
||||
`)
|
||||
.eq('status', 'completed')
|
||||
.order('created_at', { ascending: false })
|
||||
`,
|
||||
)
|
||||
.eq("status", "completed")
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(limit);
|
||||
|
||||
if (error) throw error;
|
||||
|
|
@ -133,9 +151,9 @@ export const projectService = {
|
|||
export const achievementService = {
|
||||
async getAllAchievements(): Promise<Achievement[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('achievements')
|
||||
.select('*')
|
||||
.order('xp_reward', { ascending: false });
|
||||
.from("achievements")
|
||||
.select("*")
|
||||
.order("xp_reward", { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
|
|
@ -143,27 +161,30 @@ export const achievementService = {
|
|||
|
||||
async getUserAchievements(userId: string): Promise<Achievement[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('user_achievements')
|
||||
.select(`
|
||||
.from("user_achievements")
|
||||
.select(
|
||||
`
|
||||
earned_at,
|
||||
achievements (*)
|
||||
`)
|
||||
.eq('user_id', userId)
|
||||
.order('earned_at', { ascending: false });
|
||||
`,
|
||||
)
|
||||
.eq("user_id", userId)
|
||||
.order("earned_at", { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
return data.map(item => item.achievements).filter(Boolean) as Achievement[];
|
||||
return data
|
||||
.map((item) => item.achievements)
|
||||
.filter(Boolean) as Achievement[];
|
||||
},
|
||||
|
||||
async awardAchievement(userId: string, achievementId: string): Promise<void> {
|
||||
const { error } = await supabase
|
||||
.from('user_achievements')
|
||||
.insert({
|
||||
const { error } = await supabase.from("user_achievements").insert({
|
||||
user_id: userId,
|
||||
achievement_id: achievementId,
|
||||
});
|
||||
|
||||
if (error && error.code !== '23505') { // Ignore duplicate key error
|
||||
if (error && error.code !== "23505") {
|
||||
// Ignore duplicate key error
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
|
@ -179,7 +200,9 @@ export const achievementService = {
|
|||
|
||||
// Welcome achievement
|
||||
if (profile.full_name && profile.user_type) {
|
||||
const welcomeAchievement = achievements.find(a => a.name === 'Welcome to AeThex');
|
||||
const welcomeAchievement = achievements.find(
|
||||
(a) => a.name === "Welcome to AeThex",
|
||||
);
|
||||
if (welcomeAchievement) {
|
||||
await this.awardAchievement(userId, welcomeAchievement.id);
|
||||
}
|
||||
|
|
@ -187,16 +210,20 @@ export const achievementService = {
|
|||
|
||||
// First project achievement
|
||||
if (projects.length >= 1) {
|
||||
const firstProjectAchievement = achievements.find(a => a.name === 'First Project');
|
||||
const firstProjectAchievement = achievements.find(
|
||||
(a) => a.name === "First Project",
|
||||
);
|
||||
if (firstProjectAchievement) {
|
||||
await this.awardAchievement(userId, firstProjectAchievement.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Experienced developer achievement
|
||||
const completedProjects = projects.filter(p => p.status === 'completed');
|
||||
const completedProjects = projects.filter((p) => p.status === "completed");
|
||||
if (completedProjects.length >= 5) {
|
||||
const experiencedAchievement = achievements.find(a => a.name === 'Experienced Developer');
|
||||
const experiencedAchievement = achievements.find(
|
||||
(a) => a.name === "Experienced Developer",
|
||||
);
|
||||
if (experiencedAchievement) {
|
||||
await this.awardAchievement(userId, experiencedAchievement.id);
|
||||
}
|
||||
|
|
@ -208,26 +235,33 @@ export const achievementService = {
|
|||
export const communityService = {
|
||||
async getPosts(limit = 10): Promise<CommunityPost[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('community_posts')
|
||||
.select(`
|
||||
.from("community_posts")
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
user_profiles (
|
||||
username,
|
||||
full_name,
|
||||
avatar_url
|
||||
)
|
||||
`)
|
||||
.eq('is_published', true)
|
||||
.order('created_at', { ascending: false })
|
||||
`,
|
||||
)
|
||||
.eq("is_published", true)
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(limit);
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
|
||||
async createPost(post: Omit<CommunityPost, 'id' | 'created_at' | 'updated_at' | 'likes_count' | 'comments_count'>): Promise<CommunityPost> {
|
||||
async createPost(
|
||||
post: Omit<
|
||||
CommunityPost,
|
||||
"id" | "created_at" | "updated_at" | "likes_count" | "comments_count"
|
||||
>,
|
||||
): Promise<CommunityPost> {
|
||||
const { data, error } = await supabase
|
||||
.from('community_posts')
|
||||
.from("community_posts")
|
||||
.insert(post)
|
||||
.select()
|
||||
.single();
|
||||
|
|
@ -238,10 +272,10 @@ export const communityService = {
|
|||
|
||||
async getUserPosts(userId: string): Promise<CommunityPost[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('community_posts')
|
||||
.select('*')
|
||||
.eq('author_id', userId)
|
||||
.order('created_at', { ascending: false });
|
||||
.from("community_posts")
|
||||
.select("*")
|
||||
.eq("author_id", userId)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
return data;
|
||||
|
|
@ -252,10 +286,10 @@ export const communityService = {
|
|||
export const notificationService = {
|
||||
async getUserNotifications(userId: string): Promise<any[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('notifications')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.order('created_at', { ascending: false })
|
||||
.from("notifications")
|
||||
.select("*")
|
||||
.eq("user_id", userId)
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
if (error) throw error;
|
||||
|
|
@ -264,17 +298,20 @@ export const notificationService = {
|
|||
|
||||
async markAsRead(notificationId: string): Promise<void> {
|
||||
const { error } = await supabase
|
||||
.from('notifications')
|
||||
.from("notifications")
|
||||
.update({ read: true })
|
||||
.eq('id', notificationId);
|
||||
.eq("id", notificationId);
|
||||
|
||||
if (error) throw error;
|
||||
},
|
||||
|
||||
async createNotification(userId: string, title: string, message?: string, type = 'info'): Promise<void> {
|
||||
const { error } = await supabase
|
||||
.from('notifications')
|
||||
.insert({
|
||||
async createNotification(
|
||||
userId: string,
|
||||
title: string,
|
||||
message?: string,
|
||||
type = "info",
|
||||
): Promise<void> {
|
||||
const { error } = await supabase.from("notifications").insert({
|
||||
user_id: userId,
|
||||
title,
|
||||
message,
|
||||
|
|
@ -287,34 +324,37 @@ export const notificationService = {
|
|||
|
||||
// Real-time subscriptions
|
||||
export const realtimeService = {
|
||||
subscribeToUserNotifications(userId: string, callback: (notification: any) => void) {
|
||||
subscribeToUserNotifications(
|
||||
userId: string,
|
||||
callback: (notification: any) => void,
|
||||
) {
|
||||
return supabase
|
||||
.channel(`notifications:${userId}`)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
"postgres_changes",
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'notifications',
|
||||
event: "INSERT",
|
||||
schema: "public",
|
||||
table: "notifications",
|
||||
filter: `user_id=eq.${userId}`,
|
||||
},
|
||||
callback
|
||||
callback,
|
||||
)
|
||||
.subscribe();
|
||||
},
|
||||
|
||||
subscribeToCommunityPosts(callback: (post: any) => void) {
|
||||
return supabase
|
||||
.channel('community_posts')
|
||||
.channel("community_posts")
|
||||
.on(
|
||||
'postgres_changes',
|
||||
"postgres_changes",
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'community_posts',
|
||||
filter: 'is_published=eq.true',
|
||||
event: "INSERT",
|
||||
schema: "public",
|
||||
table: "community_posts",
|
||||
filter: "is_published=eq.true",
|
||||
},
|
||||
callback
|
||||
callback,
|
||||
)
|
||||
.subscribe();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
import type { Database } from './database.types';
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import type { Database } from "./database.types";
|
||||
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
|
||||
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
||||
|
||||
// Check if Supabase is configured
|
||||
export const isSupabaseConfigured = !!(supabaseUrl && supabaseAnonKey &&
|
||||
supabaseUrl !== 'https://your-project-ref.supabase.co' &&
|
||||
supabaseAnonKey !== 'your-anon-key-here');
|
||||
export const isSupabaseConfigured = !!(
|
||||
supabaseUrl &&
|
||||
supabaseAnonKey &&
|
||||
supabaseUrl !== "https://your-project-ref.supabase.co" &&
|
||||
supabaseAnonKey !== "your-anon-key-here"
|
||||
);
|
||||
|
||||
// Use fallback values for development if not configured
|
||||
const fallbackUrl = 'https://demo.supabase.co';
|
||||
const fallbackKey = 'demo-key';
|
||||
const fallbackUrl = "https://demo.supabase.co";
|
||||
const fallbackKey = "demo-key";
|
||||
|
||||
export const supabase = createClient<Database>(
|
||||
supabaseUrl || fallbackUrl,
|
||||
|
|
@ -20,9 +23,9 @@ export const supabase = createClient<Database>(
|
|||
auth: {
|
||||
autoRefreshToken: isSupabaseConfigured,
|
||||
persistSession: isSupabaseConfigured,
|
||||
detectSessionInUrl: isSupabaseConfigured
|
||||
}
|
||||
}
|
||||
detectSessionInUrl: isSupabaseConfigured,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Auth helpers
|
||||
|
|
|
|||
|
|
@ -56,7 +56,8 @@ export default function Login() {
|
|||
});
|
||||
aethexToast.success({
|
||||
title: "Account created!",
|
||||
description: "Please check your email to verify your account, then sign in."
|
||||
description:
|
||||
"Please check your email to verify your account, then sign in.",
|
||||
});
|
||||
setIsSignUp(false);
|
||||
} else {
|
||||
|
|
@ -70,7 +71,7 @@ export default function Login() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleSocialLogin = async (provider: 'github' | 'google') => {
|
||||
const handleSocialLogin = async (provider: "github" | "google") => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await signInWithOAuth(provider);
|
||||
|
|
@ -125,8 +126,7 @@ export default function Login() {
|
|||
<CardDescription>
|
||||
{isSignUp
|
||||
? "Create your AeThex account to get started"
|
||||
: "Sign in to your AeThex account to access the dashboard"
|
||||
}
|
||||
: "Sign in to your AeThex account to access the dashboard"}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge
|
||||
|
|
@ -221,7 +221,9 @@ export default function Login() {
|
|||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={isSignUp ? "Create a password" : "Enter your password"}
|
||||
placeholder={
|
||||
isSignUp ? "Create a password" : "Enter your password"
|
||||
}
|
||||
className="pl-10 bg-background/50 border-border/50 focus:border-aethex-400"
|
||||
required
|
||||
minLength={isSignUp ? 6 : undefined}
|
||||
|
|
@ -255,7 +257,9 @@ export default function Login() {
|
|||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-gradient-to-r from-aethex-500 to-neon-blue hover:from-aethex-600 hover:to-neon-blue/90 hover-lift interactive-scale glow-blue"
|
||||
disabled={!email || !password || (isSignUp && !fullName) || isLoading}
|
||||
disabled={
|
||||
!email || !password || (isSignUp && !fullName) || isLoading
|
||||
}
|
||||
>
|
||||
<LogIn className="h-4 w-4 mr-2" />
|
||||
{isSignUp ? "Create Account" : "Sign In to Dashboard"}
|
||||
|
|
@ -265,7 +269,9 @@ export default function Login() {
|
|||
|
||||
<div className="text-center pt-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isSignUp ? "Already have an account?" : "Don't have an account?"}{" "}
|
||||
{isSignUp
|
||||
? "Already have an account?"
|
||||
: "Don't have an account?"}{" "}
|
||||
<button
|
||||
onClick={() => setIsSignUp(!isSignUp)}
|
||||
className="text-aethex-400 hover:underline font-medium"
|
||||
|
|
|
|||
Loading…
Reference in a new issue