Prettier format pending files
This commit is contained in:
parent
c00bdfcc2d
commit
9e8163d005
9 changed files with 773 additions and 336 deletions
|
|
@ -48,10 +48,7 @@ const CORE_ACHIEVEMENTS = [
|
|||
const DEFAULT_TARGET_EMAIL = "mrpiglr@gmail.com";
|
||||
const DEFAULT_TARGET_USERNAME = "mrpiglr";
|
||||
|
||||
export default async function handler(
|
||||
req: VercelRequest,
|
||||
res: VercelResponse,
|
||||
) {
|
||||
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
|
@ -68,20 +65,18 @@ export default async function handler(
|
|||
// Ensure core achievements exist
|
||||
const achievementResults = await Promise.all(
|
||||
CORE_ACHIEVEMENTS.map(async (achievement) => {
|
||||
const { error } = await admin
|
||||
.from("achievements")
|
||||
.upsert(
|
||||
{
|
||||
id: achievement.id,
|
||||
name: achievement.name,
|
||||
description: achievement.description,
|
||||
icon: achievement.icon,
|
||||
badge_color: achievement.badgeColor,
|
||||
xp_reward: achievement.xpReward,
|
||||
created_at: nowIso,
|
||||
},
|
||||
{ onConflict: "id" },
|
||||
);
|
||||
const { error } = await admin.from("achievements").upsert(
|
||||
{
|
||||
id: achievement.id,
|
||||
name: achievement.name,
|
||||
description: achievement.description,
|
||||
icon: achievement.icon,
|
||||
badge_color: achievement.badgeColor,
|
||||
xp_reward: achievement.xpReward,
|
||||
created_at: nowIso,
|
||||
},
|
||||
{ onConflict: "id" },
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
|
|
@ -92,14 +87,8 @@ export default async function handler(
|
|||
|
||||
// Normalise profile progression defaults
|
||||
await Promise.all([
|
||||
admin
|
||||
.from("user_profiles")
|
||||
.update({ level: 1 })
|
||||
.is("level", null),
|
||||
admin
|
||||
.from("user_profiles")
|
||||
.update({ total_xp: 0 })
|
||||
.is("total_xp", null),
|
||||
admin.from("user_profiles").update({ level: 1 }).is("level", null),
|
||||
admin.from("user_profiles").update({ total_xp: 0 }).is("total_xp", null),
|
||||
admin
|
||||
.from("user_profiles")
|
||||
.update({ user_type: "game_developer" })
|
||||
|
|
@ -108,7 +97,9 @@ export default async function handler(
|
|||
|
||||
// Locate target user
|
||||
const normalizedEmail = (targetEmail || DEFAULT_TARGET_EMAIL).toLowerCase();
|
||||
const normalizedUsername = (targetUsername || DEFAULT_TARGET_USERNAME).toLowerCase();
|
||||
const normalizedUsername = (
|
||||
targetUsername || DEFAULT_TARGET_USERNAME
|
||||
).toLowerCase();
|
||||
|
||||
let targetUserId: string | null = null;
|
||||
|
||||
|
|
@ -171,7 +162,9 @@ export default async function handler(
|
|||
throw existingError;
|
||||
}
|
||||
|
||||
const existingIds = new Set((existingRows ?? []).map((row: any) => row.achievement_id));
|
||||
const existingIds = new Set(
|
||||
(existingRows ?? []).map((row: any) => row.achievement_id),
|
||||
);
|
||||
|
||||
for (const achievement of CORE_ACHIEVEMENTS) {
|
||||
if (existingIds.has(achievement.id)) {
|
||||
|
|
|
|||
|
|
@ -67,12 +67,18 @@ const App = () => (
|
|||
<Route path="/profile/me" element={<Profile />} />
|
||||
|
||||
<Route path="/developers" element={<DevelopersDirectory />} />
|
||||
<Route path="/developers/me" element={<LegacyPassportRedirect />} />
|
||||
<Route
|
||||
path="/developers/me"
|
||||
element={<LegacyPassportRedirect />}
|
||||
/>
|
||||
<Route
|
||||
path="/developers/:id"
|
||||
element={<LegacyPassportRedirect />}
|
||||
/>
|
||||
<Route path="/profiles" element={<Navigate to="/developers" replace />} />
|
||||
<Route
|
||||
path="/profiles"
|
||||
element={<Navigate to="/developers" replace />}
|
||||
/>
|
||||
<Route path="/profiles/me" element={<LegacyPassportRedirect />} />
|
||||
<Route
|
||||
path="/profiles/:id"
|
||||
|
|
|
|||
|
|
@ -150,10 +150,14 @@ const PassportSummary = ({
|
|||
<div className="w-full max-w-xs space-y-3 rounded-xl bg-slate-900/60 p-4 border border-slate-800">
|
||||
<div className="flex items-center justify-between text-slate-200">
|
||||
<span className="text-sm font-medium uppercase tracking-wider">
|
||||
{isLegendary ? "Legendary Status" : `Progress to Level ${level + 1}`}
|
||||
{isLegendary
|
||||
? "Legendary Status"
|
||||
: `Progress to Level ${level + 1}`}
|
||||
</span>
|
||||
<span className="text-sm text-slate-300">
|
||||
{isLegendary ? "MAX" : `${(progressToNextLevel || 0).toFixed(0)}%`}
|
||||
{isLegendary
|
||||
? "MAX"
|
||||
: `${(progressToNextLevel || 0).toFixed(0)}%`}
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={progressToNextLevel} className="h-2" />
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ const ensureSupabase = () => {
|
|||
const MS_PER_DAY = 1000 * 60 * 60 * 24;
|
||||
|
||||
const startOfUTC = (date: Date) =>
|
||||
new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
||||
new Date(
|
||||
Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()),
|
||||
);
|
||||
|
||||
const isoDate = (date: Date) => date.toISOString().slice(0, 10);
|
||||
|
||||
|
|
@ -50,14 +52,14 @@ const normalizeProfile = (
|
|||
): AethexUserProfile => ({
|
||||
...(row as AethexUserProfile),
|
||||
email: email ?? (row as any)?.email,
|
||||
username:
|
||||
(row as any)?.username ?? email?.split("@")[0] ?? "user",
|
||||
username: (row as any)?.username ?? email?.split("@")[0] ?? "user",
|
||||
onboarded: true,
|
||||
role: (row as any)?.role ?? "developer",
|
||||
loyalty_points: (row as any)?.loyalty_points ?? 0,
|
||||
current_streak: (row as any)?.current_streak ?? 0,
|
||||
longest_streak:
|
||||
(row as any)?.longest_streak ?? Math.max((row as any)?.current_streak ?? 0, 0),
|
||||
(row as any)?.longest_streak ??
|
||||
Math.max((row as any)?.current_streak ?? 0, 0),
|
||||
last_streak_at: (row as any)?.last_streak_at ?? null,
|
||||
});
|
||||
|
||||
|
|
@ -302,16 +304,15 @@ export const aethexUserService = {
|
|||
return normalizeProfile(data);
|
||||
},
|
||||
|
||||
async getProfileByUsername(username: string): Promise<AethexUserProfile | null> {
|
||||
async getProfileByUsername(
|
||||
username: string,
|
||||
): Promise<AethexUserProfile | null> {
|
||||
const normalized = username?.trim();
|
||||
if (!normalized) return null;
|
||||
|
||||
ensureSupabase();
|
||||
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
} = await supabase
|
||||
const { data, error } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("*")
|
||||
.eq("username", normalized)
|
||||
|
|
@ -332,10 +333,7 @@ export const aethexUserService = {
|
|||
return normalizeProfile(data);
|
||||
}
|
||||
|
||||
const {
|
||||
data: fallback,
|
||||
error: fallbackError,
|
||||
} = await supabase
|
||||
const { data: fallback, error: fallbackError } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("*")
|
||||
.ilike("username", normalized)
|
||||
|
|
@ -379,13 +377,11 @@ export const aethexUserService = {
|
|||
}
|
||||
|
||||
return ((data as any[]) || []).map((row) =>
|
||||
normalizeProfile(
|
||||
{
|
||||
...(row as AethexUserProfile),
|
||||
user_type: (row as any).user_type || "game_developer",
|
||||
experience_level: (row as any).experience_level || "beginner",
|
||||
},
|
||||
),
|
||||
normalizeProfile({
|
||||
...(row as AethexUserProfile),
|
||||
user_type: (row as any).user_type || "game_developer",
|
||||
experience_level: (row as any).experience_level || "beginner",
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
|
|
@ -693,7 +689,10 @@ export const aethexAchievementService = {
|
|||
}
|
||||
},
|
||||
|
||||
async updateUserXPAndLevel(userId: string, xpGained: number | null = null): Promise<void> {
|
||||
async updateUserXPAndLevel(
|
||||
userId: string,
|
||||
xpGained: number | null = null,
|
||||
): Promise<void> {
|
||||
ensureSupabase();
|
||||
|
||||
const { data: profile, error } = await supabase
|
||||
|
|
@ -719,7 +718,8 @@ export const aethexAchievementService = {
|
|||
const updates: Record<string, number> = {};
|
||||
if ("total_xp" in currentProfile) updates.total_xp = newTotalXP;
|
||||
if ("level" in currentProfile) updates.level = newLevel;
|
||||
if ("loyalty_points" in currentProfile) updates.loyalty_points = newLoyaltyPoints;
|
||||
if ("loyalty_points" in currentProfile)
|
||||
updates.loyalty_points = newLoyaltyPoints;
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
const { error: updateError } = await supabase
|
||||
|
|
@ -748,11 +748,16 @@ export const aethexAchievementService = {
|
|||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Edge function award failed, attempting direct Supabase insert", error);
|
||||
console.warn(
|
||||
"Edge function award failed, attempting direct Supabase insert",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
const achievements = await this.getAllAchievements();
|
||||
const byName = new Map(achievements.map((item) => [item.name, item.id] as const));
|
||||
const byName = new Map(
|
||||
achievements.map((item) => [item.name, item.id] as const),
|
||||
);
|
||||
const names = ["Welcome to AeThex", "AeThex Explorer"];
|
||||
|
||||
for (const name of names) {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ const fetchMock = fetch as unknown as Mock;
|
|||
|
||||
vi.mock("@/lib/supabase", () => {
|
||||
const userProfiles = new Map<string, any>();
|
||||
const userAchievements: Array<{ user_id: string; achievement_id: string }> = [];
|
||||
const userAchievements: Array<{ user_id: string; achievement_id: string }> =
|
||||
[];
|
||||
|
||||
const achievementsCatalog = [
|
||||
{
|
||||
|
|
@ -59,8 +60,12 @@ vi.mock("@/lib/supabase", () => {
|
|||
},
|
||||
];
|
||||
|
||||
const achievementsById = new Map(achievementsCatalog.map((item) => [item.id, item] as const));
|
||||
const achievementsByName = new Map(achievementsCatalog.map((item) => [item.name, item] as const));
|
||||
const achievementsById = new Map(
|
||||
achievementsCatalog.map((item) => [item.id, item] as const),
|
||||
);
|
||||
const achievementsByName = new Map(
|
||||
achievementsCatalog.map((item) => [item.name, item] as const),
|
||||
);
|
||||
|
||||
const profileDefaults = (id: string) => ({
|
||||
id,
|
||||
|
|
@ -201,7 +206,8 @@ vi.mock("@/lib/supabase", () => {
|
|||
for (const entry of entries) {
|
||||
const exists = userAchievements.some(
|
||||
(item) =>
|
||||
item.user_id === entry.user_id && item.achievement_id === entry.achievement_id,
|
||||
item.user_id === entry.user_id &&
|
||||
item.achievement_id === entry.achievement_id,
|
||||
);
|
||||
if (exists) {
|
||||
error = { code: "23505" };
|
||||
|
|
@ -215,10 +221,12 @@ vi.mock("@/lib/supabase", () => {
|
|||
return {
|
||||
eq(_column: string, userId: string) {
|
||||
return {
|
||||
data: userAchievements.map((entry) => ({
|
||||
...entry,
|
||||
achievements: achievementsById.get(entry.achievement_id),
|
||||
})).filter((entry) => entry.user_id === userId),
|
||||
data: userAchievements
|
||||
.map((entry) => ({
|
||||
...entry,
|
||||
achievements: achievementsById.get(entry.achievement_id),
|
||||
}))
|
||||
.filter((entry) => entry.user_id === userId),
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
|
|
@ -251,9 +259,11 @@ vi.mock("@/lib/supabase", () => {
|
|||
}),
|
||||
},
|
||||
from(table: string) {
|
||||
return tableMap[table] ?? {
|
||||
select: () => ({ data: [], error: null }),
|
||||
};
|
||||
return (
|
||||
tableMap[table] ?? {
|
||||
select: () => ({ data: [], error: null }),
|
||||
}
|
||||
);
|
||||
},
|
||||
channel: () => ({
|
||||
on: () => ({}),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -58,7 +58,9 @@ const DeveloperCard = ({ profile }: DeveloperCardProps) => {
|
|||
<CardHeader className="space-y-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className={cn("text-xs uppercase tracking-wider", realmStyle)}>
|
||||
<Badge
|
||||
className={cn("text-xs uppercase tracking-wider", realmStyle)}
|
||||
>
|
||||
{profile.user_type.replace("_", " ")}
|
||||
</Badge>
|
||||
{isGodMode && (
|
||||
|
|
@ -124,7 +126,10 @@ const DeveloperCard = ({ profile }: DeveloperCardProps) => {
|
|||
variant="outline"
|
||||
className="w-full border-slate-700/70 text-slate-100 transition-colors hover:border-aethex-400/60 hover:text-white"
|
||||
>
|
||||
<Link to={passportHref} className="flex items-center justify-center gap-2">
|
||||
<Link
|
||||
to={passportHref}
|
||||
className="flex items-center justify-center gap-2"
|
||||
>
|
||||
<UserRound className="h-4 w-4" />
|
||||
View Passport
|
||||
</Link>
|
||||
|
|
@ -193,8 +198,8 @@ const DevelopersDirectory = () => {
|
|||
Discover AeThex developers
|
||||
</h1>
|
||||
<p className="text-slate-300">
|
||||
Browse verified builders, clients, and community members across
|
||||
every AeThex realm.
|
||||
Browse verified builders, clients, and community members
|
||||
across every AeThex realm.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ import {
|
|||
interface SiteStatus {
|
||||
name: string;
|
||||
url: string;
|
||||
status: 'online' | 'offline' | 'maintenance';
|
||||
status: "online" | "offline" | "maintenance";
|
||||
lastCheck: string;
|
||||
responseTime: number;
|
||||
version: string;
|
||||
|
|
@ -61,7 +61,7 @@ interface CrossSiteCommunication {
|
|||
siteName: string;
|
||||
lastSync: string;
|
||||
dataExchanged: number;
|
||||
status: 'connected' | 'disconnected' | 'syncing';
|
||||
status: "connected" | "disconnected" | "syncing";
|
||||
}
|
||||
|
||||
export default function Profile() {
|
||||
|
|
@ -70,17 +70,17 @@ export default function Profile() {
|
|||
const { success: toastSuccess, error: toastError } = useAethexToast();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
|
||||
// Profile settings state
|
||||
const [profileData, setProfileData] = useState({
|
||||
displayName: '',
|
||||
bio: '',
|
||||
company: '',
|
||||
location: '',
|
||||
website: '',
|
||||
githubUsername: '',
|
||||
twitterUsername: '',
|
||||
linkedinUrl: '',
|
||||
displayName: "",
|
||||
bio: "",
|
||||
company: "",
|
||||
location: "",
|
||||
website: "",
|
||||
githubUsername: "",
|
||||
twitterUsername: "",
|
||||
linkedinUrl: "",
|
||||
});
|
||||
|
||||
// Notification settings
|
||||
|
|
@ -94,7 +94,7 @@ export default function Profile() {
|
|||
|
||||
// Privacy settings
|
||||
const [privacy, setPrivacy] = useState({
|
||||
profileVisibility: 'public',
|
||||
profileVisibility: "public",
|
||||
showEmail: false,
|
||||
showProjects: true,
|
||||
allowAnalytics: true,
|
||||
|
|
@ -114,50 +114,50 @@ export default function Profile() {
|
|||
// Cross-site communication data
|
||||
const [crossSiteData, setCrossSiteData] = useState<CrossSiteCommunication[]>([
|
||||
{
|
||||
siteName: 'AeThex Labs',
|
||||
lastSync: '2 minutes ago',
|
||||
siteName: "AeThex Labs",
|
||||
lastSync: "2 minutes ago",
|
||||
dataExchanged: 1.2,
|
||||
status: 'connected',
|
||||
status: "connected",
|
||||
},
|
||||
{
|
||||
siteName: 'Development Portal',
|
||||
lastSync: '5 minutes ago',
|
||||
siteName: "Development Portal",
|
||||
lastSync: "5 minutes ago",
|
||||
dataExchanged: 0.8,
|
||||
status: 'syncing',
|
||||
status: "syncing",
|
||||
},
|
||||
{
|
||||
siteName: 'Community Hub',
|
||||
lastSync: '12 minutes ago',
|
||||
siteName: "Community Hub",
|
||||
lastSync: "12 minutes ago",
|
||||
dataExchanged: 2.1,
|
||||
status: 'connected',
|
||||
status: "connected",
|
||||
},
|
||||
]);
|
||||
|
||||
// Site monitoring data
|
||||
const [siteStatuses, setSiteStatuses] = useState<SiteStatus[]>([
|
||||
{
|
||||
name: 'Main Site',
|
||||
url: 'https://core.aethex.biz',
|
||||
status: 'online',
|
||||
lastCheck: 'Just now',
|
||||
name: "Main Site",
|
||||
url: "https://core.aethex.biz",
|
||||
status: "online",
|
||||
lastCheck: "Just now",
|
||||
responseTime: 245,
|
||||
version: '1.0.0',
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
name: 'API Gateway',
|
||||
url: 'https://api.aethex.biz',
|
||||
status: 'online',
|
||||
lastCheck: '30 seconds ago',
|
||||
name: "API Gateway",
|
||||
url: "https://api.aethex.biz",
|
||||
status: "online",
|
||||
lastCheck: "30 seconds ago",
|
||||
responseTime: 123,
|
||||
version: '2.1.3',
|
||||
version: "2.1.3",
|
||||
},
|
||||
{
|
||||
name: 'Labs Portal',
|
||||
url: 'https://labs.aethex.biz',
|
||||
status: 'maintenance',
|
||||
lastCheck: '2 minutes ago',
|
||||
name: "Labs Portal",
|
||||
url: "https://labs.aethex.biz",
|
||||
status: "maintenance",
|
||||
lastCheck: "2 minutes ago",
|
||||
responseTime: 0,
|
||||
version: '1.5.2',
|
||||
version: "1.5.2",
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
@ -166,20 +166,20 @@ export default function Profile() {
|
|||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !user) {
|
||||
navigate('/login');
|
||||
navigate("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
if (profile) {
|
||||
setProfileData({
|
||||
displayName: profile.full_name || '',
|
||||
bio: profile.bio || '',
|
||||
company: (profile as any).company || '',
|
||||
location: profile.location || '',
|
||||
website: (profile as any).website || '',
|
||||
githubUsername: (profile as any).github_username || '',
|
||||
twitterUsername: (profile as any).twitter_username || '',
|
||||
linkedinUrl: profile.linkedin_url || '',
|
||||
displayName: profile.full_name || "",
|
||||
bio: profile.bio || "",
|
||||
company: (profile as any).company || "",
|
||||
location: profile.location || "",
|
||||
website: (profile as any).website || "",
|
||||
githubUsername: (profile as any).github_username || "",
|
||||
twitterUsername: (profile as any).twitter_username || "",
|
||||
linkedinUrl: profile.linkedin_url || "",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -193,7 +193,7 @@ export default function Profile() {
|
|||
console.log("Starting profile save...", {
|
||||
user: user.id,
|
||||
profile: !!profile,
|
||||
profileData
|
||||
profileData,
|
||||
});
|
||||
|
||||
try {
|
||||
|
|
@ -204,13 +204,20 @@ export default function Profile() {
|
|||
try {
|
||||
// Try using the AuthContext updateProfile (which works with mock too)
|
||||
await updateProfile({
|
||||
username: profileData.displayName || user.email?.split('@')[0] || `user_${Date.now()}`,
|
||||
username:
|
||||
profileData.displayName ||
|
||||
user.email?.split("@")[0] ||
|
||||
`user_${Date.now()}`,
|
||||
full_name: profileData.displayName,
|
||||
bio: profileData.bio,
|
||||
location: profileData.location,
|
||||
linkedin_url: profileData.linkedinUrl,
|
||||
github_url: profileData.githubUsername ? `https://github.com/${profileData.githubUsername}` : null,
|
||||
twitter_url: profileData.twitterUsername ? `https://twitter.com/${profileData.twitterUsername}` : null,
|
||||
github_url: profileData.githubUsername
|
||||
? `https://github.com/${profileData.githubUsername}`
|
||||
: null,
|
||||
twitter_url: profileData.twitterUsername
|
||||
? `https://twitter.com/${profileData.twitterUsername}`
|
||||
: null,
|
||||
user_type: "game_developer",
|
||||
experience_level: "beginner",
|
||||
level: 1,
|
||||
|
|
@ -219,22 +226,32 @@ export default function Profile() {
|
|||
|
||||
console.log("Profile created successfully via AuthContext");
|
||||
} catch (authError) {
|
||||
console.warn("AuthContext creation failed, trying direct database:", authError);
|
||||
console.warn(
|
||||
"AuthContext creation failed, trying direct database:",
|
||||
authError,
|
||||
);
|
||||
|
||||
// Fallback to direct database call
|
||||
const { data, error } = await supabase
|
||||
.from("user_profiles")
|
||||
.insert({
|
||||
id: user.id,
|
||||
username: profileData.displayName || user.email?.split('@')[0] || `user_${Date.now()}`,
|
||||
username:
|
||||
profileData.displayName ||
|
||||
user.email?.split("@")[0] ||
|
||||
`user_${Date.now()}`,
|
||||
user_type: "game_developer",
|
||||
experience_level: "beginner",
|
||||
full_name: profileData.displayName,
|
||||
bio: profileData.bio,
|
||||
location: profileData.location,
|
||||
linkedin_url: profileData.linkedinUrl,
|
||||
github_url: profileData.githubUsername ? `https://github.com/${profileData.githubUsername}` : null,
|
||||
twitter_url: profileData.twitterUsername ? `https://twitter.com/${profileData.twitterUsername}` : null,
|
||||
github_url: profileData.githubUsername
|
||||
? `https://github.com/${profileData.githubUsername}`
|
||||
: null,
|
||||
twitter_url: profileData.twitterUsername
|
||||
? `https://twitter.com/${profileData.twitterUsername}`
|
||||
: null,
|
||||
level: 1,
|
||||
total_xp: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
|
|
@ -257,8 +274,12 @@ export default function Profile() {
|
|||
bio: profileData.bio,
|
||||
location: profileData.location,
|
||||
linkedin_url: profileData.linkedinUrl,
|
||||
github_url: profileData.githubUsername ? `https://github.com/${profileData.githubUsername}` : null,
|
||||
twitter_url: profileData.twitterUsername ? `https://twitter.com/${profileData.twitterUsername}` : null,
|
||||
github_url: profileData.githubUsername
|
||||
? `https://github.com/${profileData.githubUsername}`
|
||||
: null,
|
||||
twitter_url: profileData.twitterUsername
|
||||
? `https://twitter.com/${profileData.twitterUsername}`
|
||||
: null,
|
||||
updated_at: new Date().toISOString(),
|
||||
} as any);
|
||||
}
|
||||
|
|
@ -271,7 +292,8 @@ export default function Profile() {
|
|||
console.error("Profile save error:", error);
|
||||
toastError({
|
||||
title: "Update Failed",
|
||||
description: error.message || "Failed to update profile. Please try again.",
|
||||
description:
|
||||
error.message || "Failed to update profile. Please try again.",
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
|
|
@ -280,14 +302,14 @@ export default function Profile() {
|
|||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
case 'connected':
|
||||
case "online":
|
||||
case "connected":
|
||||
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
||||
case 'offline':
|
||||
case 'disconnected':
|
||||
case "offline":
|
||||
case "disconnected":
|
||||
return <XCircle className="h-4 w-4 text-red-500" />;
|
||||
case 'maintenance':
|
||||
case 'syncing':
|
||||
case "maintenance":
|
||||
case "syncing":
|
||||
return <RefreshCw className="h-4 w-4 text-yellow-500 animate-spin" />;
|
||||
default:
|
||||
return <Circle className="h-4 w-4 text-gray-400" />;
|
||||
|
|
@ -296,17 +318,17 @@ export default function Profile() {
|
|||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
case 'connected':
|
||||
return 'bg-green-500';
|
||||
case 'offline':
|
||||
case 'disconnected':
|
||||
return 'bg-red-500';
|
||||
case 'maintenance':
|
||||
case 'syncing':
|
||||
return 'bg-yellow-500';
|
||||
case "online":
|
||||
case "connected":
|
||||
return "bg-green-500";
|
||||
case "offline":
|
||||
case "disconnected":
|
||||
return "bg-red-500";
|
||||
case "maintenance":
|
||||
case "syncing":
|
||||
return "bg-yellow-500";
|
||||
default:
|
||||
return 'bg-gray-400';
|
||||
return "bg-gray-400";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -329,14 +351,20 @@ export default function Profile() {
|
|||
</Avatar>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">
|
||||
{profile?.full_name || 'User Profile'}
|
||||
{profile?.full_name || "User Profile"}
|
||||
</h1>
|
||||
<p className="text-gray-300">{user?.email}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<Badge variant="outline" className="border-purple-400/40 text-purple-200">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-purple-400/40 text-purple-200"
|
||||
>
|
||||
Current streak: {currentStreak}d
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-purple-400/40 text-purple-200">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-purple-400/40 text-purple-200"
|
||||
>
|
||||
Longest streak: {longestStreak}d
|
||||
</Badge>
|
||||
</div>
|
||||
|
|
@ -346,19 +374,31 @@ export default function Profile() {
|
|||
|
||||
<Tabs defaultValue="profile" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-4 bg-slate-800/50 border border-slate-700">
|
||||
<TabsTrigger value="profile" className="text-white data-[state=active]:bg-purple-600">
|
||||
<TabsTrigger
|
||||
value="profile"
|
||||
className="text-white data-[state=active]:bg-purple-600"
|
||||
>
|
||||
<User className="h-4 w-4 mr-2" />
|
||||
Profile
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="settings" className="text-white data-[state=active]:bg-purple-600">
|
||||
<TabsTrigger
|
||||
value="settings"
|
||||
className="text-white data-[state=active]:bg-purple-600"
|
||||
>
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
Settings
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="overseer" className="text-white data-[state=active]:bg-purple-600">
|
||||
<TabsTrigger
|
||||
value="overseer"
|
||||
className="text-white data-[state=active]:bg-purple-600"
|
||||
>
|
||||
<Monitor className="h-4 w-4 mr-2" />
|
||||
Overseer
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="network" className="text-white data-[state=active]:bg-purple-600">
|
||||
<TabsTrigger
|
||||
value="network"
|
||||
className="text-white data-[state=active]:bg-purple-600"
|
||||
>
|
||||
<Network className="h-4 w-4 mr-2" />
|
||||
Network
|
||||
</TabsTrigger>
|
||||
|
|
@ -379,48 +419,80 @@ export default function Profile() {
|
|||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="displayName" className="text-white">Display Name</Label>
|
||||
<Label htmlFor="displayName" className="text-white">
|
||||
Display Name
|
||||
</Label>
|
||||
<Input
|
||||
id="displayName"
|
||||
value={profileData.displayName}
|
||||
onChange={(e) => setProfileData({ ...profileData, displayName: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setProfileData({
|
||||
...profileData,
|
||||
displayName: e.target.value,
|
||||
})
|
||||
}
|
||||
className="bg-slate-900/50 border-slate-600 text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="company" className="text-white">Company</Label>
|
||||
<Label htmlFor="company" className="text-white">
|
||||
Company
|
||||
</Label>
|
||||
<Input
|
||||
id="company"
|
||||
value={profileData.company}
|
||||
onChange={(e) => setProfileData({ ...profileData, company: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setProfileData({
|
||||
...profileData,
|
||||
company: e.target.value,
|
||||
})
|
||||
}
|
||||
className="bg-slate-900/50 border-slate-600 text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="location" className="text-white">Location</Label>
|
||||
<Label htmlFor="location" className="text-white">
|
||||
Location
|
||||
</Label>
|
||||
<Input
|
||||
id="location"
|
||||
value={profileData.location}
|
||||
onChange={(e) => setProfileData({ ...profileData, location: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setProfileData({
|
||||
...profileData,
|
||||
location: e.target.value,
|
||||
})
|
||||
}
|
||||
className="bg-slate-900/50 border-slate-600 text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="website" className="text-white">Website</Label>
|
||||
<Label htmlFor="website" className="text-white">
|
||||
Website
|
||||
</Label>
|
||||
<Input
|
||||
id="website"
|
||||
value={profileData.website}
|
||||
onChange={(e) => setProfileData({ ...profileData, website: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setProfileData({
|
||||
...profileData,
|
||||
website: e.target.value,
|
||||
})
|
||||
}
|
||||
className="bg-slate-900/50 border-slate-600 text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="bio" className="text-white">Bio</Label>
|
||||
<Label htmlFor="bio" className="text-white">
|
||||
Bio
|
||||
</Label>
|
||||
<Textarea
|
||||
id="bio"
|
||||
value={profileData.bio}
|
||||
onChange={(e) => setProfileData({ ...profileData, bio: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setProfileData({ ...profileData, bio: e.target.value })
|
||||
}
|
||||
className="bg-slate-900/50 border-slate-600 text-white"
|
||||
rows={3}
|
||||
/>
|
||||
|
|
@ -428,35 +500,60 @@ export default function Profile() {
|
|||
<Separator className="bg-slate-600" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="github" className="text-white">GitHub Username</Label>
|
||||
<Label htmlFor="github" className="text-white">
|
||||
GitHub Username
|
||||
</Label>
|
||||
<Input
|
||||
id="github"
|
||||
value={profileData.githubUsername}
|
||||
onChange={(e) => setProfileData({ ...profileData, githubUsername: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setProfileData({
|
||||
...profileData,
|
||||
githubUsername: e.target.value,
|
||||
})
|
||||
}
|
||||
className="bg-slate-900/50 border-slate-600 text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="twitter" className="text-white">Twitter Username</Label>
|
||||
<Label htmlFor="twitter" className="text-white">
|
||||
Twitter Username
|
||||
</Label>
|
||||
<Input
|
||||
id="twitter"
|
||||
value={profileData.twitterUsername}
|
||||
onChange={(e) => setProfileData({ ...profileData, twitterUsername: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setProfileData({
|
||||
...profileData,
|
||||
twitterUsername: e.target.value,
|
||||
})
|
||||
}
|
||||
className="bg-slate-900/50 border-slate-600 text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="linkedin" className="text-white">LinkedIn URL</Label>
|
||||
<Label htmlFor="linkedin" className="text-white">
|
||||
LinkedIn URL
|
||||
</Label>
|
||||
<Input
|
||||
id="linkedin"
|
||||
value={profileData.linkedinUrl}
|
||||
onChange={(e) => setProfileData({ ...profileData, linkedinUrl: e.target.value })}
|
||||
onChange={(e) =>
|
||||
setProfileData({
|
||||
...profileData,
|
||||
linkedinUrl: e.target.value,
|
||||
})
|
||||
}
|
||||
className="bg-slate-900/50 border-slate-600 text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleProfileSave} disabled={isSaving} className="bg-purple-600 hover:bg-purple-700">
|
||||
{isSaving ? 'Saving...' : 'Save Profile'}
|
||||
<Button
|
||||
onClick={handleProfileSave}
|
||||
disabled={isSaving}
|
||||
className="bg-purple-600 hover:bg-purple-700"
|
||||
>
|
||||
{isSaving ? "Saving..." : "Save Profile"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -474,14 +571,20 @@ export default function Profile() {
|
|||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{Object.entries(notifications).map(([key, value]) => (
|
||||
<div key={key} className="flex items-center justify-between">
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<Label className="text-white capitalize">
|
||||
{key.replace(/([A-Z])/g, ' $1').trim()}
|
||||
{key.replace(/([A-Z])/g, " $1").trim()}
|
||||
</Label>
|
||||
<Switch
|
||||
checked={value}
|
||||
onCheckedChange={(checked) =>
|
||||
setNotifications({ ...notifications, [key]: checked })
|
||||
onCheckedChange={(checked) =>
|
||||
setNotifications({
|
||||
...notifications,
|
||||
[key]: checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -498,16 +601,28 @@ export default function Profile() {
|
|||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{Object.entries(privacy).map(([key, value]) => (
|
||||
<div key={key} className="flex items-center justify-between">
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<Label className="text-white capitalize">
|
||||
{key.replace(/([A-Z])/g, ' $1').trim()}
|
||||
{key.replace(/([A-Z])/g, " $1").trim()}
|
||||
</Label>
|
||||
<Switch
|
||||
checked={typeof value === 'boolean' ? value : value === 'public'}
|
||||
onCheckedChange={(checked) =>
|
||||
setPrivacy({
|
||||
...privacy,
|
||||
[key]: typeof value === 'boolean' ? checked : (checked ? 'public' : 'private')
|
||||
checked={
|
||||
typeof value === "boolean"
|
||||
? value
|
||||
: value === "public"
|
||||
}
|
||||
onCheckedChange={(checked) =>
|
||||
setPrivacy({
|
||||
...privacy,
|
||||
[key]:
|
||||
typeof value === "boolean"
|
||||
? checked
|
||||
: checked
|
||||
? "public"
|
||||
: "private",
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
|
@ -526,7 +641,9 @@ export default function Profile() {
|
|||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">System Health</p>
|
||||
<p className="text-2xl font-bold text-green-400">{overseerData.systemHealth}%</p>
|
||||
<p className="text-2xl font-bold text-green-400">
|
||||
{overseerData.systemHealth}%
|
||||
</p>
|
||||
</div>
|
||||
<Activity className="h-8 w-8 text-green-400" />
|
||||
</div>
|
||||
|
|
@ -538,7 +655,9 @@ export default function Profile() {
|
|||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Active Users</p>
|
||||
<p className="text-2xl font-bold text-blue-400">{overseerData.activeUsers}</p>
|
||||
<p className="text-2xl font-bold text-blue-400">
|
||||
{overseerData.activeUsers}
|
||||
</p>
|
||||
</div>
|
||||
<Users className="h-8 w-8 text-blue-400" />
|
||||
</div>
|
||||
|
|
@ -550,7 +669,9 @@ export default function Profile() {
|
|||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Server Load</p>
|
||||
<p className="text-2xl font-bold text-yellow-400">{overseerData.serverLoad}%</p>
|
||||
<p className="text-2xl font-bold text-yellow-400">
|
||||
{overseerData.serverLoad}%
|
||||
</p>
|
||||
</div>
|
||||
<Server className="h-8 w-8 text-yellow-400" />
|
||||
</div>
|
||||
|
|
@ -562,7 +683,9 @@ export default function Profile() {
|
|||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Error Rate</p>
|
||||
<p className="text-2xl font-bold text-red-400">{overseerData.errorRate}%</p>
|
||||
<p className="text-2xl font-bold text-red-400">
|
||||
{overseerData.errorRate}%
|
||||
</p>
|
||||
</div>
|
||||
<AlertTriangle className="h-8 w-8 text-red-400" />
|
||||
</div>
|
||||
|
|
@ -583,19 +706,29 @@ export default function Profile() {
|
|||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{siteStatuses.map((site, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-4 bg-slate-900/50 rounded-lg border border-slate-600">
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-4 bg-slate-900/50 rounded-lg border border-slate-600"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
{getStatusIcon(site.status)}
|
||||
<div>
|
||||
<h4 className="text-white font-medium">{site.name}</h4>
|
||||
<h4 className="text-white font-medium">
|
||||
{site.name}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-400">{site.url}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<Badge variant="outline" className={`${getStatusColor(site.status)} text-white border-0`}>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`${getStatusColor(site.status)} text-white border-0`}
|
||||
>
|
||||
{site.status}
|
||||
</Badge>
|
||||
<p className="text-sm text-gray-400 mt-1">{site.responseTime}ms</p>
|
||||
<p className="text-sm text-gray-400 mt-1">
|
||||
{site.responseTime}ms
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -619,17 +752,29 @@ export default function Profile() {
|
|||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{crossSiteData.map((site, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-4 bg-slate-900/50 rounded-lg border border-slate-600">
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-4 bg-slate-900/50 rounded-lg border border-slate-600"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
{getStatusIcon(site.status)}
|
||||
<div>
|
||||
<h4 className="text-white font-medium">{site.siteName}</h4>
|
||||
<p className="text-sm text-gray-400">Last sync: {site.lastSync}</p>
|
||||
<h4 className="text-white font-medium">
|
||||
{site.siteName}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-400">
|
||||
Last sync: {site.lastSync}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-white font-medium">{site.dataExchanged} MB</p>
|
||||
<Badge variant="outline" className={`${getStatusColor(site.status)} text-white border-0 mt-1`}>
|
||||
<p className="text-white font-medium">
|
||||
{site.dataExchanged} MB
|
||||
</p>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`${getStatusColor(site.status)} text-white border-0 mt-1`}
|
||||
>
|
||||
{site.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
|
@ -653,28 +798,42 @@ export default function Profile() {
|
|||
<h4 className="text-white font-medium">Auto-Sync</h4>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">Automatically sync data between sites</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
Automatically sync data between sites
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-slate-900/50 rounded-lg border border-slate-600">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-white font-medium">Real-time Updates</h4>
|
||||
<h4 className="text-white font-medium">
|
||||
Real-time Updates
|
||||
</h4>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">Enable real-time cross-site communication</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
Enable real-time cross-site communication
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-slate-900/50 rounded-lg border border-slate-600">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-white font-medium">Data Encryption</h4>
|
||||
<h4 className="text-white font-medium">
|
||||
Data Encryption
|
||||
</h4>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">Encrypt data during transmission</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
Encrypt data during transmission
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-slate-900/50 rounded-lg border border-slate-600">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-white font-medium">Monitoring Alerts</h4>
|
||||
<h4 className="text-white font-medium">
|
||||
Monitoring Alerts
|
||||
</h4>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">Get notified of connection issues</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
Get notified of connection issues
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -75,8 +75,9 @@ const ProfilePassport = () => {
|
|||
const loadPassport = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
let resolvedProfile: (AethexUserProfile & { email?: string | null }) | null =
|
||||
null;
|
||||
let resolvedProfile:
|
||||
| (AethexUserProfile & { email?: string | null })
|
||||
| null = null;
|
||||
let resolvedId: string | null = null;
|
||||
|
||||
if (isSelfRoute) {
|
||||
|
|
@ -89,7 +90,8 @@ const ProfilePassport = () => {
|
|||
}
|
||||
|
||||
if (
|
||||
currentUser.username.toLowerCase() === requestedUsername.toLowerCase() &&
|
||||
currentUser.username.toLowerCase() ===
|
||||
requestedUsername.toLowerCase() &&
|
||||
currentUser.username !== requestedUsername
|
||||
) {
|
||||
navigate(`/passport/${currentUser.username}`, { replace: true });
|
||||
|
|
@ -215,7 +217,7 @@ const ProfilePassport = () => {
|
|||
...resolvedProfile,
|
||||
email:
|
||||
resolvedProfile.email ??
|
||||
(viewingSelf ? user?.email ?? authProfile?.email ?? null : null),
|
||||
(viewingSelf ? (user?.email ?? authProfile?.email ?? null) : null),
|
||||
});
|
||||
setAchievements(achievementList ?? []);
|
||||
setInterests(interestList ?? []);
|
||||
|
|
@ -261,7 +263,8 @@ const ProfilePassport = () => {
|
|||
((user?.id && profile.id && user.id === profile.id) ||
|
||||
(authProfile?.username &&
|
||||
profile.username &&
|
||||
authProfile.username.toLowerCase() === profile.username.toLowerCase())),
|
||||
authProfile.username.toLowerCase() ===
|
||||
profile.username.toLowerCase())),
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue