Prettier format pending files
This commit is contained in:
parent
fd79f442c2
commit
cd41527a58
9 changed files with 663 additions and 191 deletions
|
|
@ -53,7 +53,10 @@ const App = () => (
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route path="/admin" element={<Admin />} />
|
<Route path="/admin" element={<Admin />} />
|
||||||
<Route path="/feed" element={<Feed />} />
|
<Route path="/feed" element={<Feed />} />
|
||||||
<Route path="/network" element={<Navigate to="/feed" replace />} />
|
<Route
|
||||||
|
path="/network"
|
||||||
|
element={<Navigate to="/feed" replace />}
|
||||||
|
/>
|
||||||
<Route path="/projects/new" element={<ProjectsNew />} />
|
<Route path="/projects/new" element={<ProjectsNew />} />
|
||||||
<Route
|
<Route
|
||||||
path="/profile"
|
path="/profile"
|
||||||
|
|
|
||||||
|
|
@ -235,13 +235,20 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
user.id,
|
user.id,
|
||||||
updates,
|
updates,
|
||||||
);
|
);
|
||||||
setProfile((prev) => ({ ...(prev || {} as any), ...(updatedProfile || {} as any), ...updates } as any));
|
setProfile(
|
||||||
|
(prev) =>
|
||||||
|
({
|
||||||
|
...(prev || ({} as any)),
|
||||||
|
...(updatedProfile || ({} as any)),
|
||||||
|
...updates,
|
||||||
|
}) as any,
|
||||||
|
);
|
||||||
aethexToast.success({
|
aethexToast.success({
|
||||||
title: "Profile updated",
|
title: "Profile updated",
|
||||||
description: "Your profile has been updated successfully",
|
description: "Your profile has been updated successfully",
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setProfile((prev) => ({ ...(prev || {} as any), ...updates } as any));
|
setProfile((prev) => ({ ...(prev || ({} as any)), ...updates }) as any);
|
||||||
aethexToast.error({
|
aethexToast.error({
|
||||||
title: "Update failed",
|
title: "Update failed",
|
||||||
description: error.message,
|
description: error.message,
|
||||||
|
|
|
||||||
|
|
@ -81,13 +81,17 @@ export const aethexUserService = {
|
||||||
// If table missing, fall back to mock for local dev only
|
// If table missing, fall back to mock for local dev only
|
||||||
if (isTableMissing(error)) {
|
if (isTableMissing(error)) {
|
||||||
const mock = await mockAuth.getUserProfile(user.id as any);
|
const mock = await mockAuth.getUserProfile(user.id as any);
|
||||||
if (mock) return { ...(mock as any), email: user.email } as AethexUserProfile;
|
if (mock)
|
||||||
const created = await mockAuth.updateProfile(user.id as any, {
|
return { ...(mock as any), email: user.email } as AethexUserProfile;
|
||||||
username: user.email?.split("@")[0] || "user",
|
const created = await mockAuth.updateProfile(
|
||||||
email: user.email || "",
|
user.id as any,
|
||||||
role: "member",
|
{
|
||||||
onboarded: true,
|
username: user.email?.split("@")[0] || "user",
|
||||||
} as any);
|
email: user.email || "",
|
||||||
|
role: "member",
|
||||||
|
onboarded: true,
|
||||||
|
} as any,
|
||||||
|
);
|
||||||
return { ...(created as any), email: user.email } as AethexUserProfile;
|
return { ...(created as any), email: user.email } as AethexUserProfile;
|
||||||
}
|
}
|
||||||
// If no row, create initial DB profile instead of mock
|
// If no row, create initial DB profile instead of mock
|
||||||
|
|
@ -132,13 +136,19 @@ export const aethexUserService = {
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
if (isTableMissing(error)) {
|
if (isTableMissing(error)) {
|
||||||
const mock = await mockAuth.updateProfile(userId as any, updates as any);
|
const mock = await mockAuth.updateProfile(
|
||||||
|
userId as any,
|
||||||
|
updates as any,
|
||||||
|
);
|
||||||
return mock as unknown as AethexUserProfile;
|
return mock as unknown as AethexUserProfile;
|
||||||
}
|
}
|
||||||
if ((error as any)?.code === "PGRST116") {
|
if ((error as any)?.code === "PGRST116") {
|
||||||
const { data: upserted, error: upsertError } = await supabase
|
const { data: upserted, error: upsertError } = await supabase
|
||||||
.from("user_profiles")
|
.from("user_profiles")
|
||||||
.upsert({ id: userId, user_type: "community_member", ...updates } as any, { onConflict: "id" })
|
.upsert(
|
||||||
|
{ id: userId, user_type: "community_member", ...updates } as any,
|
||||||
|
{ onConflict: "id" },
|
||||||
|
)
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
if (upsertError) throw upsertError;
|
if (upsertError) throw upsertError;
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,59 @@
|
||||||
import Layout from "@/components/Layout";
|
import Layout from "@/components/Layout";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Rocket, Cpu, Users, Shield, Zap, GitBranch } from "lucide-react";
|
import { Rocket, Cpu, Users, Shield, Zap, GitBranch } from "lucide-react";
|
||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
const values = [
|
const values = [
|
||||||
{ icon: <Shield className="h-5 w-5" />, title: "Integrity", desc: "Transparent processes, honest communication, dependable delivery." },
|
{
|
||||||
{ icon: <Zap className="h-5 w-5" />, title: "Excellence", desc: "Relentless attention to quality, performance, and user experience." },
|
icon: <Shield className="h-5 w-5" />,
|
||||||
{ icon: <Users className="h-5 w-5" />, title: "Partnership", desc: "We win with our customers, not at their expense." },
|
title: "Integrity",
|
||||||
|
desc: "Transparent processes, honest communication, dependable delivery.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Zap className="h-5 w-5" />,
|
||||||
|
title: "Excellence",
|
||||||
|
desc: "Relentless attention to quality, performance, and user experience.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Users className="h-5 w-5" />,
|
||||||
|
title: "Partnership",
|
||||||
|
desc: "We win with our customers, not at their expense.",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const capabilities = [
|
const capabilities = [
|
||||||
{ title: "Product Engineering", points: ["Web & Mobile Apps", "Realtime & AI Systems", "3D & Game Experiences"] },
|
{
|
||||||
{ title: "Platform & Infra", points: ["Cloud-native Architecture", "DevOps & Observability", "Security & Compliance"] },
|
title: "Product Engineering",
|
||||||
{ title: "Advisory & Enablement", points: ["Technical Strategy", "Codebase Modernization", "Team Upskilling"] },
|
points: [
|
||||||
|
"Web & Mobile Apps",
|
||||||
|
"Realtime & AI Systems",
|
||||||
|
"3D & Game Experiences",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Platform & Infra",
|
||||||
|
points: [
|
||||||
|
"Cloud-native Architecture",
|
||||||
|
"DevOps & Observability",
|
||||||
|
"Security & Compliance",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Advisory & Enablement",
|
||||||
|
points: [
|
||||||
|
"Technical Strategy",
|
||||||
|
"Codebase Modernization",
|
||||||
|
"Team Upskilling",
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const milestones = [
|
const milestones = [
|
||||||
|
|
@ -30,9 +69,13 @@ export default function About() {
|
||||||
<div className="container mx-auto px-4 max-w-6xl space-y-10">
|
<div className="container mx-auto px-4 max-w-6xl space-y-10">
|
||||||
<div className="grid md:grid-cols-2 gap-8 items-start">
|
<div className="grid md:grid-cols-2 gap-8 items-start">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h1 className="text-4xl font-bold text-gradient-purple">About AeThex</h1>
|
<h1 className="text-4xl font-bold text-gradient-purple">
|
||||||
|
About AeThex
|
||||||
|
</h1>
|
||||||
<p className="text-muted-foreground text-lg">
|
<p className="text-muted-foreground text-lg">
|
||||||
We craft reliable, scalable software—shipping fast without compromising quality. From prototypes to global platforms, we partner end-to-end: strategy, design, engineering, and growth.
|
We craft reliable, scalable software—shipping fast without
|
||||||
|
compromising quality. From prototypes to global platforms, we
|
||||||
|
partner end-to-end: strategy, design, engineering, and growth.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Badge variant="outline">TypeScript</Badge>
|
<Badge variant="outline">TypeScript</Badge>
|
||||||
|
|
@ -42,21 +85,31 @@ export default function About() {
|
||||||
<Badge variant="outline">Edge</Badge>
|
<Badge variant="outline">Edge</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 pt-2">
|
<div className="flex gap-3 pt-2">
|
||||||
<Button asChild><a href="/contact">Start a project</a></Button>
|
<Button asChild>
|
||||||
<Button asChild variant="outline"><a href="/dashboard">Explore dashboard</a></Button>
|
<a href="/contact">Start a project</a>
|
||||||
|
</Button>
|
||||||
|
<Button asChild variant="outline">
|
||||||
|
<a href="/dashboard">Explore dashboard</a>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Card className="bg-card/50 border-border/50">
|
<Card className="bg-card/50 border-border/50">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2"><Rocket className="h-5 w-5" /> Mission</CardTitle>
|
<CardTitle className="flex items-center gap-2">
|
||||||
<CardDescription>Turn bold ideas into useful, loved software.</CardDescription>
|
<Rocket className="h-5 w-5" /> Mission
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Turn bold ideas into useful, loved software.
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid sm:grid-cols-2 gap-4">
|
<CardContent className="grid sm:grid-cols-2 gap-4">
|
||||||
{capabilities.map((c) => (
|
{capabilities.map((c) => (
|
||||||
<div key={c.title}>
|
<div key={c.title}>
|
||||||
<div className="font-medium mb-1">{c.title}</div>
|
<div className="font-medium mb-1">{c.title}</div>
|
||||||
<ul className="text-sm text-muted-foreground list-disc list-inside space-y-1">
|
<ul className="text-sm text-muted-foreground list-disc list-inside space-y-1">
|
||||||
{c.points.map((p) => (<li key={p}>{p}</li>))}
|
{c.points.map((p) => (
|
||||||
|
<li key={p}>{p}</li>
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -66,10 +119,17 @@ export default function About() {
|
||||||
|
|
||||||
<div className="grid md:grid-cols-4 gap-4">
|
<div className="grid md:grid-cols-4 gap-4">
|
||||||
{milestones.map((m) => (
|
{milestones.map((m) => (
|
||||||
<Card key={m.label} className="bg-card/50 border-border/50 text-center">
|
<Card
|
||||||
|
key={m.label}
|
||||||
|
className="bg-card/50 border-border/50 text-center"
|
||||||
|
>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="text-3xl font-bold text-gradient">{m.kpi}</div>
|
<div className="text-3xl font-bold text-gradient">
|
||||||
<div className="text-sm text-muted-foreground mt-1">{m.label}</div>
|
{m.kpi}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground mt-1">
|
||||||
|
{m.label}
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|
@ -77,13 +137,23 @@ export default function About() {
|
||||||
|
|
||||||
<Card className="bg-card/50 border-border/50">
|
<Card className="bg-card/50 border-border/50">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2"><Cpu className="h-5 w-5" /> Our Approach</CardTitle>
|
<CardTitle className="flex items-center gap-2">
|
||||||
<CardDescription>Opinionated engineering, measurable outcomes.</CardDescription>
|
<Cpu className="h-5 w-5" /> Our Approach
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Opinionated engineering, measurable outcomes.
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid md:grid-cols-3 gap-6">
|
<CardContent className="grid md:grid-cols-3 gap-6">
|
||||||
{values.map((v) => (
|
{values.map((v) => (
|
||||||
<div key={v.title} className="p-4 rounded-lg border border-border/50">
|
<div
|
||||||
<div className="flex items-center gap-2 font-semibold">{v.icon}{v.title}</div>
|
key={v.title}
|
||||||
|
className="p-4 rounded-lg border border-border/50"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 font-semibold">
|
||||||
|
{v.icon}
|
||||||
|
{v.title}
|
||||||
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mt-1">{v.desc}</p>
|
<p className="text-sm text-muted-foreground mt-1">{v.desc}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -92,22 +162,32 @@ export default function About() {
|
||||||
|
|
||||||
<Card className="bg-card/50 border-border/50">
|
<Card className="bg-card/50 border-border/50">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2"><GitBranch className="h-5 w-5" /> Timeline</CardTitle>
|
<CardTitle className="flex items-center gap-2">
|
||||||
<CardDescription>Highlights from recent builds and launches.</CardDescription>
|
<GitBranch className="h-5 w-5" /> Timeline
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Highlights from recent builds and launches.
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="h-2 w-2 rounded-full bg-aethex-400 mt-2" />
|
<div className="h-2 w-2 rounded-full bg-aethex-400 mt-2" />
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">2024 • Network + Dashboard</div>
|
<div className="font-medium">2024 • Network + Dashboard</div>
|
||||||
<div className="text-sm text-muted-foreground">Shipped a creator-centric dashboard and social graph foundations.</div>
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Shipped a creator-centric dashboard and social graph
|
||||||
|
foundations.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="h-2 w-2 rounded-full bg-aethex-400 mt-2" />
|
<div className="h-2 w-2 rounded-full bg-aethex-400 mt-2" />
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">2025 • Realtime Feed</div>
|
<div className="font-medium">2025 • Realtime Feed</div>
|
||||||
<div className="text-sm text-muted-foreground">Vertical feed with follow, reactions, and frictionless profile onboarding.</div>
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Vertical feed with follow, reactions, and frictionless
|
||||||
|
profile onboarding.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import Layout from "@/components/Layout";
|
import Layout from "@/components/Layout";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
|
@ -17,16 +23,27 @@ export default function Contact() {
|
||||||
const submit = async (e: React.FormEvent) => {
|
const submit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!name.trim() || !email.trim() || !message.trim()) {
|
if (!name.trim() || !email.trim() || !message.trim()) {
|
||||||
aethexToast.error({ title: "Missing info", description: "Please fill out all fields." });
|
aethexToast.error({
|
||||||
|
title: "Missing info",
|
||||||
|
description: "Please fill out all fields.",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsSending(true);
|
setIsSending(true);
|
||||||
try {
|
try {
|
||||||
// In production, send to your backend or a function endpoint
|
// In production, send to your backend or a function endpoint
|
||||||
aethexToast.success({ title: "Message sent", description: "We’ll get back to you within 1–2 business days." });
|
aethexToast.success({
|
||||||
setName(""); setEmail(""); setMessage("");
|
title: "Message sent",
|
||||||
|
description: "We’ll get back to you within 1–2 business days.",
|
||||||
|
});
|
||||||
|
setName("");
|
||||||
|
setEmail("");
|
||||||
|
setMessage("");
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
aethexToast.error({ title: "Failed to send", description: err?.message || "Try again." });
|
aethexToast.error({
|
||||||
|
title: "Failed to send",
|
||||||
|
description: err?.message || "Try again.",
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsSending(false);
|
setIsSending(false);
|
||||||
}
|
}
|
||||||
|
|
@ -38,13 +55,24 @@ export default function Contact() {
|
||||||
<div className="container mx-auto px-4 max-w-5xl space-y-10">
|
<div className="container mx-auto px-4 max-w-5xl space-y-10">
|
||||||
<div className="grid md:grid-cols-2 gap-8 items-start">
|
<div className="grid md:grid-cols-2 gap-8 items-start">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h1 className="text-4xl font-bold text-gradient-purple">Contact Us</h1>
|
<h1 className="text-4xl font-bold text-gradient-purple">
|
||||||
<p className="text-muted-foreground">Have a project or question? We typically respond within 1–2 business days.</p>
|
Contact Us
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Have a project or question? We typically respond within 1–2
|
||||||
|
business days.
|
||||||
|
</p>
|
||||||
<Card className="bg-card/50 border-border/50">
|
<Card className="bg-card/50 border-border/50">
|
||||||
<CardContent className="p-6 space-y-3">
|
<CardContent className="p-6 space-y-3">
|
||||||
<div className="flex items-center gap-2 text-sm"><Mail className="h-4 w-4" /> support@aethex.biz</div>
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<div className="flex items-center gap-2 text-sm"><Phone className="h-4 w-4" /> (530) 784-1287</div>
|
<Mail className="h-4 w-4" /> support@aethex.biz
|
||||||
<div className="flex items-center gap-2 text-sm"><MessageSquare className="h-4 w-4" /> Community hub</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Phone className="h-4 w-4" /> (530) 784-1287
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<MessageSquare className="h-4 w-4" /> Community hub
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -52,24 +80,49 @@ export default function Contact() {
|
||||||
<Card className="bg-card/50 border-border/50">
|
<Card className="bg-card/50 border-border/50">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Send a message</CardTitle>
|
<CardTitle>Send a message</CardTitle>
|
||||||
<CardDescription>Tell us about your goals and timeline.</CardDescription>
|
<CardDescription>
|
||||||
|
Tell us about your goals and timeline.
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={submit} className="space-y-4">
|
<form onSubmit={submit} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="name">Name</Label>
|
<Label htmlFor="name">Name</Label>
|
||||||
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} placeholder="Your name" />
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="Your name"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="email">Email</Label>
|
<Label htmlFor="email">Email</Label>
|
||||||
<Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="you@example.com" />
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="message">Message</Label>
|
<Label htmlFor="message">Message</Label>
|
||||||
<Textarea id="message" rows={6} value={message} onChange={(e) => setMessage(e.target.value)} placeholder="What can we help you build?" />
|
<Textarea
|
||||||
|
id="message"
|
||||||
|
rows={6}
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
placeholder="What can we help you build?"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" disabled={isSending} className="hover-lift">{isSending ? "Sending..." : "Send"}</Button>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSending}
|
||||||
|
className="hover-lift"
|
||||||
|
>
|
||||||
|
{isSending ? "Sending..." : "Send"}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,9 @@ export default function Dashboard() {
|
||||||
!!p?.banner_url,
|
!!p?.banner_url,
|
||||||
!!(p?.website_url || p?.github_url || p?.linkedin_url || p?.twitter_url),
|
!!(p?.website_url || p?.github_url || p?.linkedin_url || p?.twitter_url),
|
||||||
];
|
];
|
||||||
const pct = Math.round((checks.filter(Boolean).length / checks.length) * 100);
|
const pct = Math.round(
|
||||||
|
(checks.filter(Boolean).length / checks.length) * 100,
|
||||||
|
);
|
||||||
setProfileCompletion(pct);
|
setProfileCompletion(pct);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -157,7 +159,9 @@ export default function Dashboard() {
|
||||||
|
|
||||||
// Check and award project-related achievements, then load achievements
|
// Check and award project-related achievements, then load achievements
|
||||||
try {
|
try {
|
||||||
await aethexAchievementService.checkAndAwardProjectAchievements(user!.id);
|
await aethexAchievementService.checkAndAwardProjectAchievements(
|
||||||
|
user!.id,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("checkAndAwardProjectAchievements failed:", e);
|
console.warn("checkAndAwardProjectAchievements failed:", e);
|
||||||
}
|
}
|
||||||
|
|
@ -364,7 +368,11 @@ export default function Dashboard() {
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => document.getElementById("settings")?.scrollIntoView({ behavior: "smooth" })}
|
onClick={() =>
|
||||||
|
document
|
||||||
|
.getElementById("settings")
|
||||||
|
?.scrollIntoView({ behavior: "smooth" })
|
||||||
|
}
|
||||||
className="bg-orange-600 hover:bg-orange-700"
|
className="bg-orange-600 hover:bg-orange-700"
|
||||||
>
|
>
|
||||||
Setup Profile
|
Setup Profile
|
||||||
|
|
@ -398,7 +406,9 @@ export default function Dashboard() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="text-sm text-muted-foreground">Profile {profileCompletion}% complete</div>
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Profile {profileCompletion}% complete
|
||||||
|
</div>
|
||||||
<Button variant="outline" size="sm" className="hover-lift">
|
<Button variant="outline" size="sm" className="hover-lift">
|
||||||
<Bell className="h-4 w-4 mr-2" />
|
<Bell className="h-4 w-4 mr-2" />
|
||||||
Notifications
|
Notifications
|
||||||
|
|
@ -407,7 +417,11 @@ export default function Dashboard() {
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="hover-lift"
|
className="hover-lift"
|
||||||
onClick={() => document.getElementById("settings")?.scrollIntoView({ behavior: "smooth" })}
|
onClick={() =>
|
||||||
|
document
|
||||||
|
.getElementById("settings")
|
||||||
|
?.scrollIntoView({ behavior: "smooth" })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Settings className="h-4 w-4 mr-2" />
|
<Settings className="h-4 w-4 mr-2" />
|
||||||
Settings
|
Settings
|
||||||
|
|
@ -535,16 +549,25 @@ export default function Dashboard() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Settings Section */}
|
{/* Settings Section */}
|
||||||
<Card className="bg-card/50 border-border/50 animate-fade-in" id="settings">
|
<Card
|
||||||
|
className="bg-card/50 border-border/50 animate-fade-in"
|
||||||
|
id="settings"
|
||||||
|
>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-gradient">Account Settings</CardTitle>
|
<CardTitle className="text-gradient">
|
||||||
<CardDescription>Manage your profile, notifications, and privacy</CardDescription>
|
Account Settings
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Manage your profile, notifications, and privacy
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Tabs defaultValue="profile">
|
<Tabs defaultValue="profile">
|
||||||
<TabsList className="mb-4">
|
<TabsList className="mb-4">
|
||||||
<TabsTrigger value="profile">Profile</TabsTrigger>
|
<TabsTrigger value="profile">Profile</TabsTrigger>
|
||||||
<TabsTrigger value="notifications">Notifications</TabsTrigger>
|
<TabsTrigger value="notifications">
|
||||||
|
Notifications
|
||||||
|
</TabsTrigger>
|
||||||
<TabsTrigger value="privacy">Privacy</TabsTrigger>
|
<TabsTrigger value="privacy">Privacy</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
|
|
@ -552,97 +575,189 @@ export default function Dashboard() {
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="displayName">Display Name</Label>
|
<Label htmlFor="displayName">Display Name</Label>
|
||||||
<Input id="displayName" value={displayName} onChange={(e) => setDisplayName(e.target.value)} />
|
<Input
|
||||||
|
id="displayName"
|
||||||
|
value={displayName}
|
||||||
|
onChange={(e) => setDisplayName(e.target.value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="location">Location</Label>
|
<Label htmlFor="location">Location</Label>
|
||||||
<Input id="location" value={locationInput} onChange={(e) => setLocationInput(e.target.value)} />
|
<Input
|
||||||
|
id="location"
|
||||||
|
value={locationInput}
|
||||||
|
onChange={(e) => setLocationInput(e.target.value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<Label htmlFor="avatar">Profile Image</Label>
|
<Label htmlFor="avatar">Profile Image</Label>
|
||||||
<Input id="avatar" type="file" accept="image/*" onChange={async (e) => {
|
<Input
|
||||||
const file = e.target.files?.[0];
|
id="avatar"
|
||||||
if (!file || !user) return;
|
type="file"
|
||||||
const storeDataUrl = () => new Promise<string>((resolve, reject) => {
|
accept="image/*"
|
||||||
const reader = new FileReader();
|
onChange={async (e) => {
|
||||||
reader.onload = () => resolve(reader.result as string);
|
const file = e.target.files?.[0];
|
||||||
reader.onerror = () => reject(new Error("Failed to read file"));
|
if (!file || !user) return;
|
||||||
reader.readAsDataURL(file);
|
const storeDataUrl = () =>
|
||||||
});
|
new Promise<string>((resolve, reject) => {
|
||||||
try {
|
const reader = new FileReader();
|
||||||
const path = `${user.id}/avatar-${Date.now()}-${file.name}`;
|
reader.onload = () =>
|
||||||
const { error } = await supabase.storage.from("avatars").upload(path, file, { upsert: true });
|
resolve(reader.result as string);
|
||||||
if (error) throw error;
|
reader.onerror = () =>
|
||||||
const { data } = supabase.storage.from("avatars").getPublicUrl(path);
|
reject(new Error("Failed to read file"));
|
||||||
await updateProfile({ avatar_url: data.publicUrl } as any);
|
reader.readAsDataURL(file);
|
||||||
computeProfileCompletion({ ...(profile as any), avatar_url: data.publicUrl });
|
});
|
||||||
aethexToast.success({ title: "Avatar updated" });
|
|
||||||
} catch (err: any) {
|
|
||||||
try {
|
try {
|
||||||
const dataUrl = await storeDataUrl();
|
const path = `${user.id}/avatar-${Date.now()}-${file.name}`;
|
||||||
await updateProfile({ avatar_url: dataUrl } as any);
|
const { error } = await supabase.storage
|
||||||
computeProfileCompletion({ ...(profile as any), avatar_url: dataUrl });
|
.from("avatars")
|
||||||
aethexToast.success({ title: "Avatar saved (local)" });
|
.upload(path, file, { upsert: true });
|
||||||
} catch (e:any) {
|
if (error) throw error;
|
||||||
aethexToast.error({ title: "Upload failed", description: err?.message || "Unable to upload image" });
|
const { data } = supabase.storage
|
||||||
|
.from("avatars")
|
||||||
|
.getPublicUrl(path);
|
||||||
|
await updateProfile({
|
||||||
|
avatar_url: data.publicUrl,
|
||||||
|
} as any);
|
||||||
|
computeProfileCompletion({
|
||||||
|
...(profile as any),
|
||||||
|
avatar_url: data.publicUrl,
|
||||||
|
});
|
||||||
|
aethexToast.success({
|
||||||
|
title: "Avatar updated",
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
try {
|
||||||
|
const dataUrl = await storeDataUrl();
|
||||||
|
await updateProfile({
|
||||||
|
avatar_url: dataUrl,
|
||||||
|
} as any);
|
||||||
|
computeProfileCompletion({
|
||||||
|
...(profile as any),
|
||||||
|
avatar_url: dataUrl,
|
||||||
|
});
|
||||||
|
aethexToast.success({
|
||||||
|
title: "Avatar saved (local)",
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
aethexToast.error({
|
||||||
|
title: "Upload failed",
|
||||||
|
description:
|
||||||
|
err?.message || "Unable to upload image",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}} />
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<Label htmlFor="banner">Banner Image</Label>
|
<Label htmlFor="banner">Banner Image</Label>
|
||||||
<Input id="banner" type="file" accept="image/*" onChange={async (e) => {
|
<Input
|
||||||
const file = e.target.files?.[0];
|
id="banner"
|
||||||
if (!file || !user) return;
|
type="file"
|
||||||
const storeDataUrl = () => new Promise<string>((resolve, reject) => {
|
accept="image/*"
|
||||||
const reader = new FileReader();
|
onChange={async (e) => {
|
||||||
reader.onload = () => resolve(reader.result as string);
|
const file = e.target.files?.[0];
|
||||||
reader.onerror = () => reject(new Error("Failed to read file"));
|
if (!file || !user) return;
|
||||||
reader.readAsDataURL(file);
|
const storeDataUrl = () =>
|
||||||
});
|
new Promise<string>((resolve, reject) => {
|
||||||
try {
|
const reader = new FileReader();
|
||||||
const path = `${user.id}/banner-${Date.now()}-${file.name}`;
|
reader.onload = () =>
|
||||||
const { error } = await supabase.storage.from("banners").upload(path, file, { upsert: true });
|
resolve(reader.result as string);
|
||||||
if (error) throw error;
|
reader.onerror = () =>
|
||||||
const { data } = supabase.storage.from("banners").getPublicUrl(path);
|
reject(new Error("Failed to read file"));
|
||||||
await updateProfile({ banner_url: data.publicUrl } as any);
|
reader.readAsDataURL(file);
|
||||||
computeProfileCompletion({ ...(profile as any), banner_url: data.publicUrl });
|
});
|
||||||
aethexToast.success({ title: "Banner updated" });
|
|
||||||
} catch (err: any) {
|
|
||||||
try {
|
try {
|
||||||
const dataUrl = await storeDataUrl();
|
const path = `${user.id}/banner-${Date.now()}-${file.name}`;
|
||||||
await updateProfile({ banner_url: dataUrl } as any);
|
const { error } = await supabase.storage
|
||||||
computeProfileCompletion({ ...(profile as any), banner_url: dataUrl });
|
.from("banners")
|
||||||
aethexToast.success({ title: "Banner saved (local)" });
|
.upload(path, file, { upsert: true });
|
||||||
} catch (e:any) {
|
if (error) throw error;
|
||||||
aethexToast.error({ title: "Upload failed", description: err?.message || "Unable to upload image" });
|
const { data } = supabase.storage
|
||||||
|
.from("banners")
|
||||||
|
.getPublicUrl(path);
|
||||||
|
await updateProfile({
|
||||||
|
banner_url: data.publicUrl,
|
||||||
|
} as any);
|
||||||
|
computeProfileCompletion({
|
||||||
|
...(profile as any),
|
||||||
|
banner_url: data.publicUrl,
|
||||||
|
});
|
||||||
|
aethexToast.success({
|
||||||
|
title: "Banner updated",
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
try {
|
||||||
|
const dataUrl = await storeDataUrl();
|
||||||
|
await updateProfile({
|
||||||
|
banner_url: dataUrl,
|
||||||
|
} as any);
|
||||||
|
computeProfileCompletion({
|
||||||
|
...(profile as any),
|
||||||
|
banner_url: dataUrl,
|
||||||
|
});
|
||||||
|
aethexToast.success({
|
||||||
|
title: "Banner saved (local)",
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
aethexToast.error({
|
||||||
|
title: "Upload failed",
|
||||||
|
description:
|
||||||
|
err?.message || "Unable to upload image",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}} />
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<Label htmlFor="bio">Bio</Label>
|
<Label htmlFor="bio">Bio</Label>
|
||||||
<Textarea id="bio" value={bio} onChange={(e) => setBio(e.target.value)} />
|
<Textarea
|
||||||
|
id="bio"
|
||||||
|
value={bio}
|
||||||
|
onChange={(e) => setBio(e.target.value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="website">Website</Label>
|
<Label htmlFor="website">Website</Label>
|
||||||
<Input id="website" value={website} onChange={(e) => setWebsite(e.target.value)} />
|
<Input
|
||||||
|
id="website"
|
||||||
|
value={website}
|
||||||
|
onChange={(e) => setWebsite(e.target.value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="linkedin">LinkedIn URL</Label>
|
<Label htmlFor="linkedin">LinkedIn URL</Label>
|
||||||
<Input id="linkedin" value={linkedin} onChange={(e) => setLinkedin(e.target.value)} />
|
<Input
|
||||||
|
id="linkedin"
|
||||||
|
value={linkedin}
|
||||||
|
onChange={(e) => setLinkedin(e.target.value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="github">GitHub URL</Label>
|
<Label htmlFor="github">GitHub URL</Label>
|
||||||
<Input id="github" value={github} onChange={(e) => setGithub(e.target.value)} />
|
<Input
|
||||||
|
id="github"
|
||||||
|
value={github}
|
||||||
|
onChange={(e) => setGithub(e.target.value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="twitter">Twitter URL</Label>
|
<Label htmlFor="twitter">Twitter URL</Label>
|
||||||
<Input id="twitter" value={twitter} onChange={(e) => setTwitter(e.target.value)} />
|
<Input
|
||||||
|
id="twitter"
|
||||||
|
value={twitter}
|
||||||
|
onChange={(e) => setTwitter(e.target.value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button onClick={saveProfile} disabled={savingProfile} className="hover-lift">
|
<Button
|
||||||
|
onClick={saveProfile}
|
||||||
|
disabled={savingProfile}
|
||||||
|
className="hover-lift"
|
||||||
|
>
|
||||||
{savingProfile ? "Saving..." : "Save Changes"}
|
{savingProfile ? "Saving..." : "Save Changes"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -652,14 +767,18 @@ export default function Dashboard() {
|
||||||
<div className="flex items-center justify-between border rounded-lg p-3">
|
<div className="flex items-center justify-between border rounded-lg p-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">Email notifications</div>
|
<div className="font-medium">Email notifications</div>
|
||||||
<div className="text-sm text-muted-foreground">Get updates in your inbox</div>
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Get updates in your inbox
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Switch defaultChecked />
|
<Switch defaultChecked />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between border rounded-lg p-3">
|
<div className="flex items-center justify-between border rounded-lg p-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">Push notifications</div>
|
<div className="font-medium">Push notifications</div>
|
||||||
<div className="text-sm text-muted-foreground">Receive alerts in the app</div>
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Receive alerts in the app
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Switch />
|
<Switch />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -669,14 +788,18 @@ export default function Dashboard() {
|
||||||
<div className="flex items-center justify-between border rounded-lg p-3">
|
<div className="flex items-center justify-between border rounded-lg p-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">Public profile</div>
|
<div className="font-medium">Public profile</div>
|
||||||
<div className="text-sm text-muted-foreground">Show your profile to everyone</div>
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Show your profile to everyone
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Switch defaultChecked />
|
<Switch defaultChecked />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between border rounded-lg p-3">
|
<div className="flex items-center justify-between border rounded-lg p-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">Show email</div>
|
<div className="font-medium">Show email</div>
|
||||||
<div className="text-sm text-muted-foreground">Display email on your profile</div>
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Display email on your profile
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Switch />
|
<Switch />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,15 @@ import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { aethexSocialService } from "@/lib/aethex-social-service";
|
import { aethexSocialService } from "@/lib/aethex-social-service";
|
||||||
import { Heart, MessageCircle, Share2, UserPlus, UserCheck, Volume2, VolumeX } from "lucide-react";
|
import {
|
||||||
|
Heart,
|
||||||
|
MessageCircle,
|
||||||
|
Share2,
|
||||||
|
UserPlus,
|
||||||
|
UserCheck,
|
||||||
|
Volume2,
|
||||||
|
VolumeX,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
interface FeedItem {
|
interface FeedItem {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -45,7 +53,11 @@ export default function Feed() {
|
||||||
authorAvatar: r.avatar_url,
|
authorAvatar: r.avatar_url,
|
||||||
caption: r.bio || "",
|
caption: r.bio || "",
|
||||||
mediaUrl: r.banner_url || r.avatar_url || null,
|
mediaUrl: r.banner_url || r.avatar_url || null,
|
||||||
mediaType: r.banner_url?.match(/\.(mp4|webm|mov)(\?.*)?$/i) ? "video" : r.banner_url || r.avatar_url ? "image" : "none",
|
mediaType: r.banner_url?.match(/\.(mp4|webm|mov)(\?.*)?$/i)
|
||||||
|
? "video"
|
||||||
|
: r.banner_url || r.avatar_url
|
||||||
|
? "image"
|
||||||
|
: "none",
|
||||||
likes: Math.floor(Math.random() * 200) + 5,
|
likes: Math.floor(Math.random() * 200) + 5,
|
||||||
comments: Math.floor(Math.random() * 30),
|
comments: Math.floor(Math.random() * 30),
|
||||||
}));
|
}));
|
||||||
|
|
@ -72,7 +84,11 @@ export default function Feed() {
|
||||||
if (!user && !loading) return <Navigate to="/login" replace />;
|
if (!user && !loading) return <Navigate to="/login" replace />;
|
||||||
if (loading || isLoading) {
|
if (loading || isLoading) {
|
||||||
return (
|
return (
|
||||||
<LoadingScreen message="Loading your feed..." showProgress duration={1000} />
|
<LoadingScreen
|
||||||
|
message="Loading your feed..."
|
||||||
|
showProgress
|
||||||
|
duration={1000}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,17 +97,33 @@ export default function Feed() {
|
||||||
<div className="min-h-screen bg-aethex-gradient">
|
<div className="min-h-screen bg-aethex-gradient">
|
||||||
<div className="h-[calc(100vh-64px)] overflow-y-auto snap-y snap-mandatory no-scrollbar">
|
<div className="h-[calc(100vh-64px)] overflow-y-auto snap-y snap-mandatory no-scrollbar">
|
||||||
{items.length === 0 && (
|
{items.length === 0 && (
|
||||||
<div className="flex items-center justify-center h-full text-muted-foreground">No posts yet. Follow people to populate your feed.</div>
|
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||||
|
No posts yet. Follow people to populate your feed.
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<section key={item.id} className="snap-start h-[calc(100vh-64px)] relative flex items-center justify-center">
|
<section
|
||||||
|
key={item.id}
|
||||||
|
className="snap-start h-[calc(100vh-64px)] relative flex items-center justify-center"
|
||||||
|
>
|
||||||
<Card className="w-full h-full bg-black/60 border-border/30 overflow-hidden">
|
<Card className="w-full h-full bg-black/60 border-border/30 overflow-hidden">
|
||||||
<CardContent className="w-full h-full p-0 relative">
|
<CardContent className="w-full h-full p-0 relative">
|
||||||
{/* Media */}
|
{/* Media */}
|
||||||
{item.mediaType === "video" && item.mediaUrl ? (
|
{item.mediaType === "video" && item.mediaUrl ? (
|
||||||
<video src={item.mediaUrl} className="w-full h-full object-cover" autoPlay loop muted={muted} playsInline />
|
<video
|
||||||
|
src={item.mediaUrl}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted={muted}
|
||||||
|
playsInline
|
||||||
|
/>
|
||||||
) : item.mediaType === "image" && item.mediaUrl ? (
|
) : item.mediaType === "image" && item.mediaUrl ? (
|
||||||
<img src={item.mediaUrl} alt={item.caption || item.authorName} className="w-full h-full object-cover" />
|
<img
|
||||||
|
src={item.mediaUrl}
|
||||||
|
alt={item.caption || item.authorName}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full bg-gradient-to-br from-aethex-500/20 to-neon-blue/20" />
|
<div className="w-full h-full bg-gradient-to-br from-aethex-500/20 to-neon-blue/20" />
|
||||||
)}
|
)}
|
||||||
|
|
@ -101,16 +133,37 @@ export default function Feed() {
|
||||||
|
|
||||||
{/* Right rail actions */}
|
{/* Right rail actions */}
|
||||||
<div className="absolute right-4 bottom-24 flex flex-col items-center gap-4">
|
<div className="absolute right-4 bottom-24 flex flex-col items-center gap-4">
|
||||||
<Button size="icon" variant="secondary" className="rounded-full bg-white/20 hover:bg-white/30" onClick={() => setMuted((m) => !m)}>
|
<Button
|
||||||
{muted ? <VolumeX className="h-5 w-5" /> : <Volume2 className="h-5 w-5" />}
|
size="icon"
|
||||||
|
variant="secondary"
|
||||||
|
className="rounded-full bg-white/20 hover:bg-white/30"
|
||||||
|
onClick={() => setMuted((m) => !m)}
|
||||||
|
>
|
||||||
|
{muted ? (
|
||||||
|
<VolumeX className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<Volume2 className="h-5 w-5" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="icon" variant="secondary" className="rounded-full bg-white/20 hover:bg-white/30">
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="secondary"
|
||||||
|
className="rounded-full bg-white/20 hover:bg-white/30"
|
||||||
|
>
|
||||||
<Heart className="h-5 w-5" />
|
<Heart className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="icon" variant="secondary" className="rounded-full bg-white/20 hover:bg-white/30">
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="secondary"
|
||||||
|
className="rounded-full bg-white/20 hover:bg-white/30"
|
||||||
|
>
|
||||||
<MessageCircle className="h-5 w-5" />
|
<MessageCircle className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="icon" variant="secondary" className="rounded-full bg-white/20 hover:bg-white/30">
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="secondary"
|
||||||
|
className="rounded-full bg-white/20 hover:bg-white/30"
|
||||||
|
>
|
||||||
<Share2 className="h-5 w-5" />
|
<Share2 className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -120,15 +173,37 @@ export default function Feed() {
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Avatar className="h-10 w-10">
|
<Avatar className="h-10 w-10">
|
||||||
<AvatarImage src={item.authorAvatar || undefined} />
|
<AvatarImage src={item.authorAvatar || undefined} />
|
||||||
<AvatarFallback>{item.authorName[0] || "U"}</AvatarFallback>
|
<AvatarFallback>
|
||||||
|
{item.authorName[0] || "U"}
|
||||||
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-semibold text-white">{item.authorName}</div>
|
<div className="font-semibold text-white">
|
||||||
{item.caption && <div className="text-xs text-white/80 max-w-[60vw] line-clamp-2">{item.caption}</div>}
|
{item.authorName}
|
||||||
|
</div>
|
||||||
|
{item.caption && (
|
||||||
|
<div className="text-xs text-white/80 max-w-[60vw] line-clamp-2">
|
||||||
|
{item.caption}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button size="sm" variant={isFollowingAuthor(item.authorId) ? "outline" : "default"} onClick={() => toggleFollow(item.authorId)}>
|
<Button
|
||||||
{isFollowingAuthor(item.authorId) ? (<span className="flex items-center gap-1"><UserCheck className="h-4 w-4" /> Following</span>) : (<span className="flex items-center gap-1"><UserPlus className="h-4 w-4" /> Follow</span>)}
|
size="sm"
|
||||||
|
variant={
|
||||||
|
isFollowingAuthor(item.authorId) ? "outline" : "default"
|
||||||
|
}
|
||||||
|
onClick={() => toggleFollow(item.authorId)}
|
||||||
|
>
|
||||||
|
{isFollowingAuthor(item.authorId) ? (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<UserCheck className="h-4 w-4" /> Following
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<UserPlus className="h-4 w-4" /> Follow
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,10 @@ import { Textarea } from "@/components/ui/textarea";
|
||||||
import LoadingScreen from "@/components/LoadingScreen";
|
import LoadingScreen from "@/components/LoadingScreen";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { aethexToast } from "@/lib/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
import { aethexProjectService, aethexAchievementService } from "@/lib/aethex-database-adapter";
|
import {
|
||||||
|
aethexProjectService,
|
||||||
|
aethexAchievementService,
|
||||||
|
} from "@/lib/aethex-database-adapter";
|
||||||
|
|
||||||
export default function ProjectsNew() {
|
export default function ProjectsNew() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -18,7 +21,9 @@ export default function ProjectsNew() {
|
||||||
const [technologies, setTechnologies] = useState("");
|
const [technologies, setTechnologies] = useState("");
|
||||||
const [githubUrl, setGithubUrl] = useState("");
|
const [githubUrl, setGithubUrl] = useState("");
|
||||||
const [liveUrl, setLiveUrl] = useState("");
|
const [liveUrl, setLiveUrl] = useState("");
|
||||||
const [status, setStatus] = useState<"planning" | "in_progress" | "completed" | "on_hold">("planning");
|
const [status, setStatus] = useState<
|
||||||
|
"planning" | "in_progress" | "completed" | "on_hold"
|
||||||
|
>("planning");
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|
@ -34,7 +39,10 @@ export default function ProjectsNew() {
|
||||||
const handleSubmit = async (e: any) => {
|
const handleSubmit = async (e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!title.trim()) {
|
if (!title.trim()) {
|
||||||
aethexToast.error({ title: "Title required", description: "Please provide a project title." });
|
aethexToast.error({
|
||||||
|
title: "Title required",
|
||||||
|
description: "Please provide a project title.",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,16 +66,24 @@ export default function ProjectsNew() {
|
||||||
|
|
||||||
if (project) {
|
if (project) {
|
||||||
try {
|
try {
|
||||||
await aethexAchievementService.checkAndAwardProjectAchievements(user.id);
|
await aethexAchievementService.checkAndAwardProjectAchievements(
|
||||||
|
user.id,
|
||||||
|
);
|
||||||
} catch {}
|
} catch {}
|
||||||
aethexToast.success({ title: "Project created", description: "Your project has been created." });
|
aethexToast.success({
|
||||||
|
title: "Project created",
|
||||||
|
description: "Your project has been created.",
|
||||||
|
});
|
||||||
navigate("/dashboard");
|
navigate("/dashboard");
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Project creation failed");
|
throw new Error("Project creation failed");
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Error creating project:", err);
|
console.error("Error creating project:", err);
|
||||||
aethexToast.error({ title: "Failed to create project", description: err?.message || "Please try again." });
|
aethexToast.error({
|
||||||
|
title: "Failed to create project",
|
||||||
|
description: err?.message || "Please try again.",
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|
@ -79,37 +95,72 @@ export default function ProjectsNew() {
|
||||||
<div className="container mx-auto px-4 max-w-3xl">
|
<div className="container mx-auto px-4 max-w-3xl">
|
||||||
<h1 className="text-2xl font-bold mb-4">Start a New Project</h1>
|
<h1 className="text-2xl font-bold mb-4">Start a New Project</h1>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4 bg-card/50 border-border/50 p-6 rounded-lg">
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="space-y-4 bg-card/50 border-border/50 p-6 rounded-lg"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="title">Project Title</Label>
|
<Label htmlFor="title">Project Title</Label>
|
||||||
<Input id="title" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="e.g. Decentralized Chat App" />
|
<Input
|
||||||
|
id="title"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
placeholder="e.g. Decentralized Chat App"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="description">Description</Label>
|
<Label htmlFor="description">Description</Label>
|
||||||
<Textarea id="description" value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Short description of the project" />
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
placeholder="Short description of the project"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="technologies">Technologies (comma separated)</Label>
|
<Label htmlFor="technologies">
|
||||||
<Input id="technologies" value={technologies} onChange={(e) => setTechnologies(e.target.value)} placeholder="React, Node.js, Supabase, Typescript" />
|
Technologies (comma separated)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="technologies"
|
||||||
|
value={technologies}
|
||||||
|
onChange={(e) => setTechnologies(e.target.value)}
|
||||||
|
placeholder="React, Node.js, Supabase, Typescript"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="github">GitHub URL</Label>
|
<Label htmlFor="github">GitHub URL</Label>
|
||||||
<Input id="github" value={githubUrl} onChange={(e) => setGithubUrl(e.target.value)} placeholder="https://github.com/your/repo" />
|
<Input
|
||||||
|
id="github"
|
||||||
|
value={githubUrl}
|
||||||
|
onChange={(e) => setGithubUrl(e.target.value)}
|
||||||
|
placeholder="https://github.com/your/repo"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="live">Live URL</Label>
|
<Label htmlFor="live">Live URL</Label>
|
||||||
<Input id="live" value={liveUrl} onChange={(e) => setLiveUrl(e.target.value)} placeholder="https://example.com" />
|
<Input
|
||||||
|
id="live"
|
||||||
|
value={liveUrl}
|
||||||
|
onChange={(e) => setLiveUrl(e.target.value)}
|
||||||
|
placeholder="https://example.com"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="status">Status</Label>
|
<Label htmlFor="status">Status</Label>
|
||||||
<select id="status" value={status} onChange={(e) => setStatus(e.target.value as any)} className="w-full p-2 rounded border border-border/30 bg-background">
|
<select
|
||||||
|
id="status"
|
||||||
|
value={status}
|
||||||
|
onChange={(e) => setStatus(e.target.value as any)}
|
||||||
|
className="w-full p-2 rounded border border-border/30 bg-background"
|
||||||
|
>
|
||||||
<option value="planning">Planning</option>
|
<option value="planning">Planning</option>
|
||||||
<option value="in_progress">In Progress</option>
|
<option value="in_progress">In Progress</option>
|
||||||
<option value="completed">Completed</option>
|
<option value="completed">Completed</option>
|
||||||
|
|
@ -118,10 +169,18 @@ export default function ProjectsNew() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<Button type="submit" disabled={isSubmitting} className="hover-lift">
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="hover-lift"
|
||||||
|
>
|
||||||
{isSubmitting ? "Creating..." : "Create Project"}
|
{isSubmitting ? "Creating..." : "Create Project"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" variant="outline" onClick={() => navigate(-1)}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
128
index.html
128
index.html
|
|
@ -5,12 +5,21 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
<title>AeThex — Developer Platform, Projects, Community</title>
|
<title>AeThex — Developer Platform, Projects, Community</title>
|
||||||
<meta name="description" content="AeThex: an advanced development platform and community for builders. Collaborate on projects, learn, and ship innovation." />
|
<meta
|
||||||
<meta name="keywords" content="AeThex, developer platform, projects, community, mentorship, research labs, consulting, tutorials" />
|
name="description"
|
||||||
|
content="AeThex: an advanced development platform and community for builders. Collaborate on projects, learn, and ship innovation."
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="keywords"
|
||||||
|
content="AeThex, developer platform, projects, community, mentorship, research labs, consulting, tutorials"
|
||||||
|
/>
|
||||||
<meta name="application-name" content="AeThex" />
|
<meta name="application-name" content="AeThex" />
|
||||||
<meta name="theme-color" content="#0a0aff" />
|
<meta name="theme-color" content="#0a0aff" />
|
||||||
<meta name="color-scheme" content="dark light" />
|
<meta name="color-scheme" content="dark light" />
|
||||||
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
|
<meta
|
||||||
|
name="robots"
|
||||||
|
content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1"
|
||||||
|
/>
|
||||||
<meta name="googlebot" content="index, follow" />
|
<meta name="googlebot" content="index, follow" />
|
||||||
|
|
||||||
<!-- Geo/Audience -->
|
<!-- Geo/Audience -->
|
||||||
|
|
@ -22,30 +31,66 @@
|
||||||
<link rel="canonical" href="/" />
|
<link rel="canonical" href="/" />
|
||||||
|
|
||||||
<!-- Favicons / Icons -->
|
<!-- Favicons / Icons -->
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F3979ec9a8a28471d900a80e94e2c45fe?format=png&width=32" />
|
<link
|
||||||
<link rel="icon" type="image/png" sizes="192x192" href="https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F3979ec9a8a28471d900a80e94e2c45fe?format=png&width=192" />
|
rel="icon"
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F3979ec9a8a28471d900a80e94e2c45fe?format=png&width=180" />
|
type="image/png"
|
||||||
|
sizes="32x32"
|
||||||
|
href="https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F3979ec9a8a28471d900a80e94e2c45fe?format=png&width=32"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="192x192"
|
||||||
|
href="https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F3979ec9a8a28471d900a80e94e2c45fe?format=png&width=192"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="180x180"
|
||||||
|
href="https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F3979ec9a8a28471d900a80e94e2c45fe?format=png&width=180"
|
||||||
|
/>
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
<meta name="msapplication-TileColor" content="#0a0aff" />
|
<meta name="msapplication-TileColor" content="#0a0aff" />
|
||||||
|
|
||||||
<!-- Open Graph -->
|
<!-- Open Graph -->
|
||||||
<meta property="og:site_name" content="AeThex" />
|
<meta property="og:site_name" content="AeThex" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:title" content="AeThex — Developer Platform, Projects, Community" />
|
<meta
|
||||||
<meta property="og:description" content="Join AeThex to build, learn, and connect. Tutorials, mentorship, research labs, and a thriving developer community." />
|
property="og:title"
|
||||||
<meta property="og:image" content="https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F3979ec9a8a28471d900a80e94e2c45fe?format=png&width=1200" />
|
content="AeThex — Developer Platform, Projects, Community"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="Join AeThex to build, learn, and connect. Tutorials, mentorship, research labs, and a thriving developer community."
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="og:image"
|
||||||
|
content="https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F3979ec9a8a28471d900a80e94e2c45fe?format=png&width=1200"
|
||||||
|
/>
|
||||||
<meta property="og:image:width" content="1200" />
|
<meta property="og:image:width" content="1200" />
|
||||||
<meta property="og:image:height" content="630" />
|
<meta property="og:image:height" content="630" />
|
||||||
<meta property="og:url" content="/" />
|
<meta property="og:url" content="/" />
|
||||||
|
|
||||||
<!-- Twitter -->
|
<!-- Twitter -->
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:title" content="AeThex — Developer Platform, Projects, Community" />
|
<meta
|
||||||
<meta name="twitter:description" content="Build and innovate with AeThex. Projects, mentorship, research labs, and tutorials for modern developers." />
|
name="twitter:title"
|
||||||
<meta name="twitter:image" content="https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F3979ec9a8a28471d900a80e94e2c45fe?format=png&width=1200" />
|
content="AeThex — Developer Platform, Projects, Community"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="twitter:description"
|
||||||
|
content="Build and innovate with AeThex. Projects, mentorship, research labs, and tutorials for modern developers."
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="twitter:image"
|
||||||
|
content="https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F3979ec9a8a28471d900a80e94e2c45fe?format=png&width=1200"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Preconnects -->
|
<!-- Preconnects -->
|
||||||
<link rel="preconnect" href="https://kmdeisowhtsalsekkzqd.supabase.co" crossorigin />
|
<link
|
||||||
|
rel="preconnect"
|
||||||
|
href="https://kmdeisowhtsalsekkzqd.supabase.co"
|
||||||
|
crossorigin
|
||||||
|
/>
|
||||||
<link rel="preconnect" href="https://cdn.builder.io" crossorigin />
|
<link rel="preconnect" href="https://cdn.builder.io" crossorigin />
|
||||||
|
|
||||||
<!-- Structured Data + dynamic canonical/og:url -->
|
<!-- Structured Data + dynamic canonical/og:url -->
|
||||||
|
|
@ -53,8 +98,8 @@
|
||||||
(function () {
|
(function () {
|
||||||
var origin = location.origin;
|
var origin = location.origin;
|
||||||
function addJSONLD(obj) {
|
function addJSONLD(obj) {
|
||||||
var s = document.createElement('script');
|
var s = document.createElement("script");
|
||||||
s.type = 'application/ld+json';
|
s.type = "application/ld+json";
|
||||||
s.text = JSON.stringify(obj);
|
s.text = JSON.stringify(obj);
|
||||||
document.head.appendChild(s);
|
document.head.appendChild(s);
|
||||||
}
|
}
|
||||||
|
|
@ -62,47 +107,64 @@
|
||||||
addJSONLD({
|
addJSONLD({
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "Organization",
|
"@type": "Organization",
|
||||||
"name": "AeThex",
|
name: "AeThex",
|
||||||
"url": origin,
|
url: origin,
|
||||||
"logo": "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F3979ec9a8a28471d900a80e94e2c45fe?format=png&width=512",
|
logo: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2F3979ec9a8a28471d900a80e94e2c45fe?format=png&width=512",
|
||||||
"areaServed": "Worldwide"
|
areaServed: "Worldwide",
|
||||||
});
|
});
|
||||||
// Website
|
// Website
|
||||||
addJSONLD({
|
addJSONLD({
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "WebSite",
|
"@type": "WebSite",
|
||||||
"name": "AeThex",
|
name: "AeThex",
|
||||||
"url": origin
|
url: origin,
|
||||||
});
|
});
|
||||||
// FAQ for AEO
|
// FAQ for AEO
|
||||||
addJSONLD({
|
addJSONLD({
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "FAQPage",
|
"@type": "FAQPage",
|
||||||
"mainEntity": [
|
mainEntity: [
|
||||||
{
|
{
|
||||||
"@type": "Question",
|
"@type": "Question",
|
||||||
"name": "What is AeThex?",
|
name: "What is AeThex?",
|
||||||
"acceptedAnswer": {"@type": "Answer", "text": "AeThex is an advanced development platform and community where developers collaborate on projects, learn through tutorials, and access mentorship and research labs."}
|
acceptedAnswer: {
|
||||||
|
"@type": "Answer",
|
||||||
|
text: "AeThex is an advanced development platform and community where developers collaborate on projects, learn through tutorials, and access mentorship and research labs.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@type": "Question",
|
"@type": "Question",
|
||||||
"name": "How do I get started?",
|
name: "How do I get started?",
|
||||||
"acceptedAnswer": {"@type": "Answer", "text": "Visit the Get Started and Onboarding flows to create your profile and join projects."}
|
acceptedAnswer: {
|
||||||
|
"@type": "Answer",
|
||||||
|
text: "Visit the Get Started and Onboarding flows to create your profile and join projects.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@type": "Question",
|
"@type": "Question",
|
||||||
"name": "Does AeThex offer mentorship programs?",
|
name: "Does AeThex offer mentorship programs?",
|
||||||
"acceptedAnswer": {"@type": "Answer", "text": "Yes. AeThex provides mentorship programs and a community feed to help you grow and connect."}
|
acceptedAnswer: {
|
||||||
}
|
"@type": "Answer",
|
||||||
]
|
text: "Yes. AeThex provides mentorship programs and a community feed to help you grow and connect.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
// Set canonical and og:url to the current URL
|
// Set canonical and og:url to the current URL
|
||||||
var link = document.querySelector('link[rel="canonical"]');
|
var link = document.querySelector('link[rel="canonical"]');
|
||||||
if (!link) { link = document.createElement('link'); link.rel = 'canonical'; document.head.appendChild(link); }
|
if (!link) {
|
||||||
|
link = document.createElement("link");
|
||||||
|
link.rel = "canonical";
|
||||||
|
document.head.appendChild(link);
|
||||||
|
}
|
||||||
link.href = location.href;
|
link.href = location.href;
|
||||||
var ogUrl = document.querySelector('meta[property="og:url"]');
|
var ogUrl = document.querySelector('meta[property="og:url"]');
|
||||||
if (!ogUrl) { ogUrl = document.createElement('meta'); ogUrl.setAttribute('property','og:url'); document.head.appendChild(ogUrl); }
|
if (!ogUrl) {
|
||||||
ogUrl.setAttribute('content', location.href);
|
ogUrl = document.createElement("meta");
|
||||||
|
ogUrl.setAttribute("property", "og:url");
|
||||||
|
document.head.appendChild(ogUrl);
|
||||||
|
}
|
||||||
|
ogUrl.setAttribute("content", location.href);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue