Prettier format pending files

This commit is contained in:
Builder.io 2025-10-18 03:49:38 +00:00
parent e5a5607728
commit df2b52af71
9 changed files with 531 additions and 148 deletions

View file

@ -74,7 +74,10 @@ const App = () => (
<Route path="/feed" element={<Feed />} />
<Route path="/teams" element={<Teams />} />
<Route path="/projects/new" element={<ProjectsNew />} />
<Route path="/projects/:projectId/board" element={<ProjectBoard />} />
<Route
path="/projects/:projectId/board"
element={<ProjectBoard />}
/>
<Route path="/profile" element={<Profile />} />
<Route path="/profile/me" element={<Profile />} />

View file

@ -1,5 +1,8 @@
import { supabase } from "@/lib/supabase";
import { aethexUserService, aethexNotificationService } from "@/lib/aethex-database-adapter";
import {
aethexUserService,
aethexNotificationService,
} from "@/lib/aethex-database-adapter";
export type TeamVisibility = "public" | "private";
export type MembershipRole = "owner" | "admin" | "member";
@ -11,14 +14,21 @@ export const aethexCollabService = {
async listMyTeams(userId: string) {
const { data, error } = await supabase
.from("team_memberships")
.select("team_id, teams:team_id ( id, name, slug, description, visibility, created_at )")
.select(
"team_id, teams:team_id ( id, name, slug, description, visibility, created_at )",
)
.eq("user_id", userId)
.order("created_at", { ascending: false });
if (error) return [] as any[];
return (data || []) as any[];
},
async createTeam(ownerId: string, name: string, description?: string | null, visibility: TeamVisibility = "private") {
async createTeam(
ownerId: string,
name: string,
description?: string | null,
visibility: TeamVisibility = "private",
) {
// Ensure the owner has a user_profiles row to satisfy FK
try {
await aethexUserService.getCurrentUser();
@ -26,7 +36,12 @@ export const aethexCollabService = {
const { data, error } = await supabase
.from("teams")
.insert({ owner_id: ownerId, name, description: description || null, visibility })
.insert({
owner_id: ownerId,
name,
description: description || null,
visibility,
})
.select()
.single();
if (error) throw new Error(error.message || "Unable to create team");
@ -52,7 +67,11 @@ export const aethexCollabService = {
return team;
},
async addTeamMember(teamId: string, userId: string, role: MembershipRole = "member") {
async addTeamMember(
teamId: string,
userId: string,
role: MembershipRole = "member",
) {
const { error } = await supabase
.from("team_memberships")
.insert({ team_id: teamId, user_id: userId, role });
@ -60,7 +79,11 @@ export const aethexCollabService = {
},
// Projects
async addProjectMember(projectId: string, userId: string, role: ProjectRole = "contributor") {
async addProjectMember(
projectId: string,
userId: string,
role: ProjectRole = "contributor",
) {
const { error } = await supabase
.from("project_members")
.insert({ project_id: projectId, user_id: userId, role });
@ -70,7 +93,9 @@ export const aethexCollabService = {
async listProjectMembers(projectId: string) {
const { data, error } = await supabase
.from("project_members")
.select("user_id, role, user:user_id ( id, full_name, username, avatar_url )")
.select(
"user_id, role, user:user_id ( id, full_name, username, avatar_url )",
)
.eq("project_id", projectId);
if (error) return [] as any[];
return (data || []) as any[];
@ -87,10 +112,22 @@ export const aethexCollabService = {
return (data || []) as any[];
},
async createTask(projectId: string, title: string, description?: string | null, assigneeId?: string | null, dueDate?: string | null) {
async createTask(
projectId: string,
title: string,
description?: string | null,
assigneeId?: string | null,
dueDate?: string | null,
) {
const { data, error } = await supabase
.from("project_tasks")
.insert({ project_id: projectId, title, description: description || null, assignee_id: assigneeId || null, due_date: dueDate || null })
.insert({
project_id: projectId,
title,
description: description || null,
assignee_id: assigneeId || null,
due_date: dueDate || null,
})
.select()
.single();
if (error) throw new Error(error.message || "Unable to create task");

View file

@ -56,7 +56,10 @@ export const aethexSocialService = {
const resp = await fetch("/api/social/follow", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ follower_id: followerId, following_id: followingId }),
body: JSON.stringify({
follower_id: followerId,
following_id: followingId,
}),
});
if (!resp.ok) throw new Error(await resp.text());
},
@ -65,7 +68,10 @@ export const aethexSocialService = {
const resp = await fetch("/api/social/unfollow", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ follower_id: followerId, following_id: followingId }),
body: JSON.stringify({
follower_id: followerId,
following_id: followingId,
}),
});
if (!resp.ok) throw new Error(await resp.text());
},
@ -148,7 +154,11 @@ export const aethexSocialService = {
const resp = await fetch("/api/social/endorse", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ endorser_id: endorserId, endorsed_id: endorsedId, skill }),
body: JSON.stringify({
endorser_id: endorserId,
endorsed_id: endorsedId,
skill,
}),
});
if (!resp.ok) throw new Error(await resp.text());
},

View file

@ -509,10 +509,30 @@ export default function Dashboard() {
const showProfileSetup = !profileComplete;
const statsDisplay = [
{ label: "Active Projects", value: stats.activeProjects, icon: Rocket, color: "from-blue-500 to-purple-600" },
{ label: "Completed Tasks", value: stats.completedTasks, icon: Trophy, color: "from-green-500 to-blue-600" },
{ label: "Team Members", value: stats.teamMembers, icon: Users, color: "from-purple-500 to-pink-600" },
{ label: "Performance Score", value: stats.performanceScore, icon: TrendingUp, color: "from-orange-500 to-red-600" },
{
label: "Active Projects",
value: stats.activeProjects,
icon: Rocket,
color: "from-blue-500 to-purple-600",
},
{
label: "Completed Tasks",
value: stats.completedTasks,
icon: Trophy,
color: "from-green-500 to-blue-600",
},
{
label: "Team Members",
value: stats.teamMembers,
icon: Users,
color: "from-purple-500 to-pink-600",
},
{
label: "Performance Score",
value: stats.performanceScore,
icon: TrendingUp,
color: "from-orange-500 to-red-600",
},
];
const getProgressPercentage = (project: any) => {
@ -561,10 +581,18 @@ export default function Dashboard() {
};
const quickActions = [
{ title: "Start New Project", icon: Rocket, color: "from-blue-500 to-purple-600" },
{
title: "Start New Project",
icon: Rocket,
color: "from-blue-500 to-purple-600",
},
{ title: "Create Team", icon: Users, color: "from-green-500 to-blue-600" },
{ title: "Access Labs", icon: Zap, color: "from-yellow-500 to-orange-600" },
{ title: "View Analytics", icon: BarChart3, color: "from-purple-500 to-pink-600" },
{
title: "View Analytics",
icon: BarChart3,
color: "from-purple-500 to-pink-600",
},
];
if (isLoading) {
@ -1200,7 +1228,12 @@ export default function Dashboard() {
Your active development projects
</CardDescription>
</div>
<Button variant="outline" size="sm" className="hover-lift" onClick={() => navigate("/projects")}>
<Button
variant="outline"
size="sm"
className="hover-lift"
onClick={() => navigate("/projects")}
>
View All
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
@ -1266,7 +1299,13 @@ export default function Dashboard() {
>
{getPriorityFromTech(project.technologies || [])}
</Badge>
<Button variant="outline" size="sm" onClick={() => navigate(`/projects/${project.id}/board`)}>
<Button
variant="outline"
size="sm"
onClick={() =>
navigate(`/projects/${project.id}/board`)
}
>
Open Board
</Button>
</div>
@ -1406,14 +1445,27 @@ export default function Dashboard() {
</CardHeader>
<CardContent className="space-y-3">
{teams.length === 0 ? (
<div className="text-sm text-muted-foreground">No teams yet.</div>
<div className="text-sm text-muted-foreground">
No teams yet.
</div>
) : (
teams.slice(0, 6).map((t: any) => {
const team = (t as any).teams || t;
return (
<div key={team.id} className="flex items-center justify-between p-3 rounded border border-border/40">
<div className="font-medium text-sm">{team.name}</div>
<Button variant="ghost" size="sm" onClick={() => navigate("/teams")}>Open</Button>
<div
key={team.id}
className="flex items-center justify-between p-3 rounded border border-border/40"
>
<div className="font-medium text-sm">
{team.name}
</div>
<Button
variant="ghost"
size="sm"
onClick={() => navigate("/teams")}
>
Open
</Button>
</div>
);
})
@ -1428,15 +1480,34 @@ export default function Dashboard() {
</CardHeader>
<CardContent className="space-y-3">
{invites.length === 0 ? (
<div className="text-sm text-muted-foreground">No invites yet.</div>
<div className="text-sm text-muted-foreground">
No invites yet.
</div>
) : (
invites.slice(0, 6).map((inv: any) => (
<div key={inv.id} className="flex items-center justify-between p-3 rounded border border-border/40">
<div
key={inv.id}
className="flex items-center justify-between p-3 rounded border border-border/40"
>
<div className="text-sm">
<div className="font-medium">{inv.invitee_email}</div>
<div className="text-xs text-muted-foreground">{inv.status}</div>
<div className="font-medium">
{inv.invitee_email}
</div>
<div className="text-xs text-muted-foreground">
{inv.status}
</div>
</div>
<Button variant="ghost" size="sm" onClick={() => navigator.clipboard.writeText(`${location.origin}/login?invite=${inv.token}`)}>Copy link</Button>
<Button
variant="ghost"
size="sm"
onClick={() =>
navigator.clipboard.writeText(
`${location.origin}/login?invite=${inv.token}`,
)
}
>
Copy link
</Button>
</div>
))
)}

View file

@ -6,39 +6,68 @@ export default function Privacy() {
<div className="min-h-screen bg-aethex-gradient py-12">
<div className="container mx-auto px-4 max-w-4xl space-y-8">
<header className="space-y-2">
<h1 className="text-3xl font-bold text-gradient-purple">Privacy Policy</h1>
<p className="text-sm text-muted-foreground">Effective date: 2025-10-18</p>
<h1 className="text-3xl font-bold text-gradient-purple">
Privacy Policy
</h1>
<p className="text-sm text-muted-foreground">
This Privacy Policy explains how AeThex ("we", "us") collects, uses, shares, and protects
information when you use our products, sites, and services (the "Services").
Effective date: 2025-10-18
</p>
<p className="text-sm text-muted-foreground">
This Privacy Policy explains how AeThex ("we", "us") collects,
uses, shares, and protects information when you use our products,
sites, and services (the "Services").
</p>
</header>
<section className="space-y-3">
<h2 className="font-semibold">Information We Collect</h2>
<ul className="list-disc pl-5 text-sm text-muted-foreground space-y-1">
<li>Account data: name, username, email, profile details, social links.</li>
<li>Content: posts, comments, projects, teams, endorsements, activity metadata.</li>
<li>Usage data: device/browser information, pages visited, interactions, approximate location.</li>
<li>Cookies & similar: session and preference cookies for authentication and settings.</li>
<li>
Account data: name, username, email, profile details, social
links.
</li>
<li>
Content: posts, comments, projects, teams, endorsements,
activity metadata.
</li>
<li>
Usage data: device/browser information, pages visited,
interactions, approximate location.
</li>
<li>
Cookies & similar: session and preference cookies for
authentication and settings.
</li>
</ul>
</section>
<section className="space-y-3">
<h2 className="font-semibold">How We Use Information</h2>
<ul className="list-disc pl-5 text-sm text-muted-foreground space-y-1">
<li>Provide and improve the Services, including social, projects, teams, and notifications.</li>
<li>Security, abuse prevention, fraud detection, and diagnostics.</li>
<li>Personalization (e.g., recommendations, feed ranking) and aggregated analytics.</li>
<li>Communications: transactional emails (verification, invites, alerts) and product updates.</li>
<li>
Provide and improve the Services, including social, projects,
teams, and notifications.
</li>
<li>
Security, abuse prevention, fraud detection, and diagnostics.
</li>
<li>
Personalization (e.g., recommendations, feed ranking) and
aggregated analytics.
</li>
<li>
Communications: transactional emails (verification, invites,
alerts) and product updates.
</li>
</ul>
</section>
<section className="space-y-3">
<h2 className="font-semibold">Legal Bases (EEA/UK)</h2>
<p className="text-sm text-muted-foreground">
We process data under: (i) Performance of a contract (providing core features), (ii) Legitimate
interests (security, analytics, product improvement), (iii) Consent (where required), and (iv)
We process data under: (i) Performance of a contract (providing
core features), (ii) Legitimate interests (security, analytics,
product improvement), (iii) Consent (where required), and (iv)
Compliance with legal obligations.
</p>
</section>
@ -46,8 +75,9 @@ export default function Privacy() {
<section className="space-y-3">
<h2 className="font-semibold">Sharing & Service Providers</h2>
<p className="text-sm text-muted-foreground">
We do not sell your personal information. We use trusted sub-processors to operate the platform:
Supabase (auth, database, storage), Vercel/Netlify (hosting/CDN), and Resend (email). These
We do not sell your personal information. We use trusted
sub-processors to operate the platform: Supabase (auth, database,
storage), Vercel/Netlify (hosting/CDN), and Resend (email). These
providers process data on our behalf under appropriate agreements.
</p>
</section>
@ -55,24 +85,31 @@ export default function Privacy() {
<section className="space-y-3">
<h2 className="font-semibold">International Transfers</h2>
<p className="text-sm text-muted-foreground">
Data may be processed in the United States and other countries. Where applicable, we rely on
appropriate safeguards (e.g., SCCs) for cross-border transfers.
Data may be processed in the United States and other countries.
Where applicable, we rely on appropriate safeguards (e.g., SCCs)
for cross-border transfers.
</p>
</section>
<section className="space-y-3">
<h2 className="font-semibold">Data Retention</h2>
<p className="text-sm text-muted-foreground">
We retain data for as long as needed to provide Services, comply with law, resolve disputes,
and enforce agreements. You may request deletion of your account data, subject to legal holds.
We retain data for as long as needed to provide Services, comply
with law, resolve disputes, and enforce agreements. You may
request deletion of your account data, subject to legal holds.
</p>
</section>
<section className="space-y-3">
<h2 className="font-semibold">Your Rights</h2>
<ul className="list-disc pl-5 text-sm text-muted-foreground space-y-1">
<li>Access, correction, deletion, and portability of your data.</li>
<li>Object to or restrict certain processing; withdraw consent where applicable.</li>
<li>
Access, correction, deletion, and portability of your data.
</li>
<li>
Object to or restrict certain processing; withdraw consent where
applicable.
</li>
<li>Manage notifications and email preferences in-app.</li>
</ul>
</section>
@ -80,31 +117,35 @@ export default function Privacy() {
<section className="space-y-3">
<h2 className="font-semibold">Security</h2>
<p className="text-sm text-muted-foreground">
We use industry-standard measures to protect data in transit and at rest. No method of
transmission or storage is 100% secure; you are responsible for safeguarding credentials.
We use industry-standard measures to protect data in transit and
at rest. No method of transmission or storage is 100% secure; you
are responsible for safeguarding credentials.
</p>
</section>
<section className="space-y-3">
<h2 className="font-semibold">Children</h2>
<p className="text-sm text-muted-foreground">
Our Services are not directed to children under 13 (or as defined by local law). We do not
knowingly collect data from children. If you believe a child has provided data, contact us.
Our Services are not directed to children under 13 (or as defined
by local law). We do not knowingly collect data from children. If
you believe a child has provided data, contact us.
</p>
</section>
<section className="space-y-3">
<h2 className="font-semibold">Changes</h2>
<p className="text-sm text-muted-foreground">
We may update this Policy. Material changes will be announced via the app or email. Your
continued use after changes constitutes acceptance.
We may update this Policy. Material changes will be announced via
the app or email. Your continued use after changes constitutes
acceptance.
</p>
</section>
<section className="space-y-3">
<h2 className="font-semibold">Contact</h2>
<p className="text-sm text-muted-foreground">
For privacy inquiries: privacy@aethex.biz. For support: support@aethex.biz.
For privacy inquiries: privacy@aethex.biz. For support:
support@aethex.biz.
</p>
</section>
</div>

View file

@ -2,7 +2,13 @@ import Layout from "@/components/Layout";
import { useAuth } from "@/contexts/AuthContext";
import { useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
@ -10,7 +16,11 @@ import { Badge } from "@/components/ui/badge";
import { aethexCollabService } from "@/lib/aethex-collab-service";
import LoadingScreen from "@/components/LoadingScreen";
const columns: { key: "todo"|"doing"|"done"|"blocked"; title: string; hint: string }[] = [
const columns: {
key: "todo" | "doing" | "done" | "blocked";
title: string;
hint: string;
}[] = [
{ key: "todo", title: "To do", hint: "Planned" },
{ key: "doing", title: "In progress", hint: "Active" },
{ key: "done", title: "Done", hint: "Completed" },
@ -47,7 +57,12 @@ export default function ProjectBoard() {
}, [projectId]);
const grouped = useMemo(() => {
const map: Record<string, any[]> = { todo: [], doing: [], done: [], blocked: [] };
const map: Record<string, any[]> = {
todo: [],
doing: [],
done: [],
blocked: [],
};
for (const t of tasks) {
map[t.status || "todo"].push(t);
}
@ -59,7 +74,13 @@ export default function ProjectBoard() {
if (!title.trim()) return;
setCreating(true);
try {
await aethexCollabService.createTask(projectId, title.trim(), description.trim() || null, null, null);
await aethexCollabService.createTask(
projectId,
title.trim(),
description.trim() || null,
null,
null,
);
setTitle("");
setDescription("");
await load();
@ -68,12 +89,18 @@ export default function ProjectBoard() {
}
};
const move = async (taskId: string, status: "todo"|"doing"|"done"|"blocked") => {
const move = async (
taskId: string,
status: "todo" | "doing" | "done" | "blocked",
) => {
await aethexCollabService.updateTaskStatus(taskId, status);
await load();
};
if (loading || isLoading) return <LoadingScreen message="Loading project..." showProgress duration={800} />;
if (loading || isLoading)
return (
<LoadingScreen message="Loading project..." showProgress duration={800} />
);
if (!user) return null;
return (
@ -81,17 +108,29 @@ export default function ProjectBoard() {
<div className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(110,141,255,0.12),transparent_60%)] py-10">
<div className="mx-auto w-full max-w-6xl px-4 lg:px-6 space-y-6">
<section className="rounded-3xl border border-border/40 bg-background/80 p-6 shadow-2xl backdrop-blur">
<h1 className="text-3xl font-semibold text-foreground">Project Board</h1>
<p className="mt-1 text-sm text-muted-foreground">Track tasks by status. Drag-and-drop coming next.</p>
<h1 className="text-3xl font-semibold text-foreground">
Project Board
</h1>
<p className="mt-1 text-sm text-muted-foreground">
Track tasks by status. Drag-and-drop coming next.
</p>
</section>
<div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)]">
{columns.map((col) => (
<Card key={col.key} className="rounded-3xl border-border/40 bg-background/70 shadow-xl backdrop-blur-lg">
<Card
key={col.key}
className="rounded-3xl border-border/40 bg-background/70 shadow-xl backdrop-blur-lg"
>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
{col.title}
<Badge variant="outline" className="border-border/50 text-xs">{grouped[col.key].length}</Badge>
<Badge
variant="outline"
className="border-border/50 text-xs"
>
{grouped[col.key].length}
</Badge>
</CardTitle>
<CardDescription>{col.hint}</CardDescription>
</CardHeader>
@ -100,14 +139,26 @@ export default function ProjectBoard() {
<p className="text-sm text-muted-foreground">No tasks.</p>
) : (
grouped[col.key].map((t) => (
<div key={t.id} className="rounded-2xl border border-border/30 bg-background/60 p-3">
<div className="font-medium text-foreground">{t.title}</div>
<div
key={t.id}
className="rounded-2xl border border-border/30 bg-background/60 p-3"
>
<div className="font-medium text-foreground">
{t.title}
</div>
{t.description ? (
<p className="text-xs text-muted-foreground mt-1">{t.description}</p>
<p className="text-xs text-muted-foreground mt-1">
{t.description}
</p>
) : null}
<div className="mt-2 flex flex-wrap gap-2">
{columns.map((k) => (
<Button key={`${t.id}-${k.key}`} size="xs" variant="outline" onClick={() => move(t.id, k.key)}>
<Button
key={`${t.id}-${k.key}`}
size="xs"
variant="outline"
onClick={() => move(t.id, k.key)}
>
{k.title}
</Button>
))}
@ -123,13 +174,27 @@ export default function ProjectBoard() {
<Card className="rounded-3xl border-border/40 bg-background/70 shadow-xl backdrop-blur-lg">
<CardHeader>
<CardTitle className="text-lg">Add task</CardTitle>
<CardDescription>Keep titles concise; details optional.</CardDescription>
<CardDescription>
Keep titles concise; details optional.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<Input placeholder="Task title" value={title} onChange={(e) => setTitle(e.target.value)} />
<Textarea placeholder="Description (optional)" value={description} onChange={(e) => setDescription(e.target.value)} />
<Input
placeholder="Task title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<Textarea
placeholder="Description (optional)"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<div className="flex justify-end">
<Button onClick={handleCreate} disabled={creating || !title.trim()} className="rounded-full bg-gradient-to-r from-aethex-500 to-neon-blue text-white">
<Button
onClick={handleCreate}
disabled={creating || !title.trim()}
className="rounded-full bg-gradient-to-r from-aethex-500 to-neon-blue text-white"
>
{creating ? "Creating..." : "Create task"}
</Button>
</div>

View file

@ -2,7 +2,13 @@ import Layout from "@/components/Layout";
import { useAuth } from "@/contexts/AuthContext";
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
@ -39,31 +45,57 @@ export default function Teams() {
load();
}, [user?.id]);
const canCreate = useMemo(() => name.trim().length > 2 && !creating, [name, creating]);
const canCreate = useMemo(
() => name.trim().length > 2 && !creating,
[name, creating],
);
const handleCreate = async () => {
if (!user?.id) return;
if (!canCreate) return;
setCreating(true);
const tempId = `temp-${Date.now()}`;
const optimistic = { team_id: tempId, teams: { id: tempId, name: name.trim(), description: description.trim() || null, visibility: "private" } } as any;
const optimistic = {
team_id: tempId,
teams: {
id: tempId,
name: name.trim(),
description: description.trim() || null,
visibility: "private",
},
} as any;
setTeams((prev) => [optimistic, ...prev]);
setName("");
setDescription("");
let created: any | null = null;
try {
created = await aethexCollabService.createTeam(user.id, optimistic.teams.name, optimistic.teams.description, "private");
setTeams((prev) => prev.map((t: any) => (t.team_id === tempId ? { team_id: created.id, teams: created } : t)));
created = await aethexCollabService.createTeam(
user.id,
optimistic.teams.name,
optimistic.teams.description,
"private",
);
setTeams((prev) =>
prev.map((t: any) =>
t.team_id === tempId ? { team_id: created.id, teams: created } : t,
),
);
aethexToast.success({ title: "Team created" });
} catch (e: any) {
setTeams((prev) => prev.filter((t: any) => t.team_id !== tempId));
aethexToast.error({ title: "Failed to create team", description: e?.message || "Try again later." });
aethexToast.error({
title: "Failed to create team",
description: e?.message || "Try again later.",
});
} finally {
setCreating(false);
}
};
if (loading || isLoading) return <LoadingScreen message="Loading teams..." showProgress duration={800} />;
if (loading || isLoading)
return (
<LoadingScreen message="Loading teams..." showProgress duration={800} />
);
if (!user) return null;
@ -73,7 +105,9 @@ export default function Teams() {
<div className="mx-auto w-full max-w-6xl px-4 lg:px-6 space-y-6">
<section className="rounded-3xl border border-border/40 bg-background/80 p-6 shadow-2xl backdrop-blur">
<h1 className="text-3xl font-semibold text-foreground">Teams</h1>
<p className="mt-1 text-sm text-muted-foreground">Create a team and collaborate with members across projects.</p>
<p className="mt-1 text-sm text-muted-foreground">
Create a team and collaborate with members across projects.
</p>
</section>
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
@ -85,25 +119,47 @@ export default function Teams() {
</CardHeader>
<CardContent className="space-y-3">
{teams.length === 0 ? (
<p className="text-sm text-muted-foreground">No teams yet. Create one to get started.</p>
<p className="text-sm text-muted-foreground">
No teams yet. Create one to get started.
</p>
) : (
teams.map((t) => {
const team = (t as any).teams || t;
return (
<div key={team.id} className="flex items-center justify-between rounded-2xl border border-border/30 bg-background/60 p-4">
<div
key={team.id}
className="flex items-center justify-between rounded-2xl border border-border/30 bg-background/60 p-4"
>
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10">
<AvatarImage src={undefined} />
<AvatarFallback>{team.name?.[0]?.toUpperCase() || "T"}</AvatarFallback>
<AvatarFallback>
{team.name?.[0]?.toUpperCase() || "T"}
</AvatarFallback>
</Avatar>
<div>
<div className="font-medium text-foreground">{team.name}</div>
<div className="text-xs text-muted-foreground">{team.visibility || "private"}</div>
<div className="font-medium text-foreground">
{team.name}
</div>
<div className="text-xs text-muted-foreground">
{team.visibility || "private"}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="border-border/50">Team</Badge>
<Button variant="outline" size="sm" onClick={() => navigate(`/projects/new`)}>New project</Button>
<Badge
variant="outline"
className="border-border/50"
>
Team
</Badge>
<Button
variant="outline"
size="sm"
onClick={() => navigate(`/projects/new`)}
>
New project
</Button>
</div>
</div>
);
@ -117,13 +173,27 @@ export default function Teams() {
<Card className="rounded-3xl border-border/40 bg-background/70 shadow-xl backdrop-blur-lg">
<CardHeader>
<CardTitle className="text-lg">Create a team</CardTitle>
<CardDescription>Private by default; you can invite members later.</CardDescription>
<CardDescription>
Private by default; you can invite members later.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<Input placeholder="Team name" value={name} onChange={(e) => setName(e.target.value)} />
<Textarea placeholder="Short description (optional)" value={description} onChange={(e) => setDescription(e.target.value)} />
<Input
placeholder="Team name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Textarea
placeholder="Short description (optional)"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<div className="flex justify-end">
<Button onClick={handleCreate} disabled={!canCreate} className="rounded-full bg-gradient-to-r from-aethex-500 to-neon-blue text-white">
<Button
onClick={handleCreate}
disabled={!canCreate}
className="rounded-full bg-gradient-to-r from-aethex-500 to-neon-blue text-white"
>
{creating ? "Creating..." : "Create team"}
</Button>
</div>

View file

@ -6,29 +6,36 @@ export default function Terms() {
<div className="min-h-screen bg-aethex-gradient py-12">
<div className="container mx-auto px-4 max-w-4xl space-y-8">
<header className="space-y-2">
<h1 className="text-3xl font-bold text-gradient-purple">Terms of Service</h1>
<p className="text-sm text-muted-foreground">Effective date: 2025-10-18</p>
<h1 className="text-3xl font-bold text-gradient-purple">
Terms of Service
</h1>
<p className="text-sm text-muted-foreground">
These Terms govern your access to and use of the AeThex Services. By using the Services,
you agree to these Terms.
Effective date: 2025-10-18
</p>
<p className="text-sm text-muted-foreground">
These Terms govern your access to and use of the AeThex Services.
By using the Services, you agree to these Terms.
</p>
</header>
<section className="space-y-3">
<h2 className="font-semibold">Accounts & Eligibility</h2>
<p className="text-sm text-muted-foreground">
You must be at least 13 years old (or the age required by your jurisdiction). You are
responsible for maintaining the confidentiality of your credentials and for all activity
under your account.
You must be at least 13 years old (or the age required by your
jurisdiction). You are responsible for maintaining the
confidentiality of your credentials and for all activity under
your account.
</p>
</section>
<section className="space-y-3">
<h2 className="font-semibold">User Content & License</h2>
<p className="text-sm text-muted-foreground">
You retain ownership of content you submit. You grant AeThex a worldwide, non-exclusive,
royalty-free license to host, store, reproduce, and display your content solely to operate
and improve the Services. You represent you have the rights to submit such content.
You retain ownership of content you submit. You grant AeThex a
worldwide, non-exclusive, royalty-free license to host, store,
reproduce, and display your content solely to operate and improve
the Services. You represent you have the rights to submit such
content.
</p>
</section>
@ -38,31 +45,37 @@ export default function Terms() {
<li>No unlawful, infringing, or deceptive activity.</li>
<li>No harassment, hate, or exploitation.</li>
<li>No malware, spam, scraping, or abuse of API limits.</li>
<li>No attempts to access others accounts or data without permission.</li>
<li>
No attempts to access others accounts or data without
permission.
</li>
</ul>
</section>
<section className="space-y-3">
<h2 className="font-semibold">Projects, Teams, and Matching</h2>
<p className="text-sm text-muted-foreground">
Collaboration features (projects, teams, tasks, endorsements) are provided as-is. Team owners
and project owners are responsible for membership, roles, and shared materials.
Collaboration features (projects, teams, tasks, endorsements) are
provided as-is. Team owners and project owners are responsible for
membership, roles, and shared materials.
</p>
</section>
<section className="space-y-3">
<h2 className="font-semibold">Third-Party Services</h2>
<p className="text-sm text-muted-foreground">
We may integrate third-party services (e.g., Supabase, Resend, hosting/CDN). Your use of
those services is subject to their terms and policies.
We may integrate third-party services (e.g., Supabase, Resend,
hosting/CDN). Your use of those services is subject to their terms
and policies.
</p>
</section>
<section className="space-y-3">
<h2 className="font-semibold">Intellectual Property</h2>
<p className="text-sm text-muted-foreground">
AeThex and its licensors retain all rights to the platform, branding, and software. You may
not copy, modify, or distribute platform code or assets except as permitted by law or written
AeThex and its licensors retain all rights to the platform,
branding, and software. You may not copy, modify, or distribute
platform code or assets except as permitted by law or written
authorization.
</p>
</section>
@ -70,16 +83,19 @@ export default function Terms() {
<section className="space-y-3">
<h2 className="font-semibold">Termination</h2>
<p className="text-sm text-muted-foreground">
We may suspend or terminate your access for violations or risk to the platform. You may
discontinue use at any time.
We may suspend or terminate your access for violations or risk to
the platform. You may discontinue use at any time.
</p>
</section>
<section className="space-y-3">
<h2 className="font-semibold">Disclaimers & Limitation of Liability</h2>
<h2 className="font-semibold">
Disclaimers & Limitation of Liability
</h2>
<p className="text-sm text-muted-foreground">
The Services are provided as-is without warranties. To the maximum extent permitted by law,
AeThex will not be liable for indirect, incidental, or consequential damages arising from or
The Services are provided as-is without warranties. To the
maximum extent permitted by law, AeThex will not be liable for
indirect, incidental, or consequential damages arising from or
related to your use of the Services.
</p>
</section>
@ -87,24 +103,25 @@ export default function Terms() {
<section className="space-y-3">
<h2 className="font-semibold">Indemnification</h2>
<p className="text-sm text-muted-foreground">
You agree to indemnify and hold AeThex harmless from claims arising out of your content or
misuse of the Services.
You agree to indemnify and hold AeThex harmless from claims
arising out of your content or misuse of the Services.
</p>
</section>
<section className="space-y-3">
<h2 className="font-semibold">Changes to Terms</h2>
<p className="text-sm text-muted-foreground">
We may update these Terms. Material changes will be announced via the app or email. Your
continued use constitutes acceptance of updated Terms.
We may update these Terms. Material changes will be announced via
the app or email. Your continued use constitutes acceptance of
updated Terms.
</p>
</section>
<section className="space-y-3">
<h2 className="font-semibold">Governing Law & Contact</h2>
<p className="text-sm text-muted-foreground">
These Terms are governed by applicable laws of the United States. For questions, contact
legal@aethex.biz.
These Terms are governed by applicable laws of the United States.
For questions, contact legal@aethex.biz.
</p>
</section>
</div>

View file

@ -720,7 +720,9 @@ export function createServer() {
}
}
await accrue(inviter_id, "loyalty", 5, "invite_sent", { invitee: email });
await accrue(inviter_id, "loyalty", 5, "invite_sent", {
invitee: email,
});
try {
await adminSupabase.from("notifications").insert({
user_id: inviter_id,
@ -799,7 +801,9 @@ export function createServer() {
if (inviterId) {
await accrue(inviterId, "xp", 100, "invite_accepted", { token });
await accrue(inviterId, "loyalty", 50, "invite_accepted", { token });
await accrue(inviterId, "reputation", 2, "invite_accepted", { token });
await accrue(inviterId, "reputation", 2, "invite_accepted", {
token,
});
try {
await adminSupabase.from("notifications").insert({
user_id: inviterId,
@ -810,7 +814,9 @@ export function createServer() {
} catch {}
}
await accrue(acceptor_id, "xp", 50, "invite_accepted", { token });
await accrue(acceptor_id, "reputation", 1, "invite_accepted", { token });
await accrue(acceptor_id, "reputation", 1, "invite_accepted", {
token,
});
try {
await adminSupabase.from("notifications").insert({
user_id: acceptor_id,
@ -828,20 +834,32 @@ export function createServer() {
// Follow/unfollow with notifications
app.post("/api/social/follow", async (req, res) => {
const { follower_id, following_id } = (req.body || {}) as { follower_id?: string; following_id?: string };
const { follower_id, following_id } = (req.body || {}) as {
follower_id?: string;
following_id?: string;
};
if (!follower_id || !following_id)
return res.status(400).json({ error: "follower_id and following_id required" });
return res
.status(400)
.json({ error: "follower_id and following_id required" });
try {
await adminSupabase
.from("user_follows")
.upsert({ follower_id, following_id } as any, { onConflict: "follower_id,following_id" as any });
await accrue(follower_id, "loyalty", 5, "follow_user", { following_id });
.upsert({ follower_id, following_id } as any, {
onConflict: "follower_id,following_id" as any,
});
await accrue(follower_id, "loyalty", 5, "follow_user", {
following_id,
});
const { data: follower } = await adminSupabase
.from("user_profiles")
.select("full_name, username")
.eq("id", follower_id)
.maybeSingle();
const followerName = (follower as any)?.full_name || (follower as any)?.username || "Someone";
const followerName =
(follower as any)?.full_name ||
(follower as any)?.username ||
"Someone";
await adminSupabase.from("notifications").insert({
user_id: following_id,
type: "info",
@ -855,9 +873,14 @@ export function createServer() {
});
app.post("/api/social/unfollow", async (req, res) => {
const { follower_id, following_id } = (req.body || {}) as { follower_id?: string; following_id?: string };
const { follower_id, following_id } = (req.body || {}) as {
follower_id?: string;
following_id?: string;
};
if (!follower_id || !following_id)
return res.status(400).json({ error: "follower_id and following_id required" });
return res
.status(400)
.json({ error: "follower_id and following_id required" });
try {
await adminSupabase
.from("user_follows")
@ -872,18 +895,32 @@ export function createServer() {
// Endorse with notification
app.post("/api/social/endorse", async (req, res) => {
const { endorser_id, endorsed_id, skill } = (req.body || {}) as { endorser_id?: string; endorsed_id?: string; skill?: string };
const { endorser_id, endorsed_id, skill } = (req.body || {}) as {
endorser_id?: string;
endorsed_id?: string;
skill?: string;
};
if (!endorser_id || !endorsed_id || !skill)
return res.status(400).json({ error: "endorser_id, endorsed_id, skill required" });
return res
.status(400)
.json({ error: "endorser_id, endorsed_id, skill required" });
try {
await adminSupabase.from("endorsements").insert({ endorser_id, endorsed_id, skill } as any);
await accrue(endorsed_id, "reputation", 2, "endorsement_received", { skill, from: endorser_id });
await adminSupabase
.from("endorsements")
.insert({ endorser_id, endorsed_id, skill } as any);
await accrue(endorsed_id, "reputation", 2, "endorsement_received", {
skill,
from: endorser_id,
});
const { data: endorser } = await adminSupabase
.from("user_profiles")
.select("full_name, username")
.eq("id", endorser_id)
.maybeSingle();
const endorserName = (endorser as any)?.full_name || (endorser as any)?.username || "Someone";
const endorserName =
(endorser as any)?.full_name ||
(endorser as any)?.username ||
"Someone";
await adminSupabase.from("notifications").insert({
user_id: endorsed_id,
type: "success",
@ -909,24 +946,48 @@ export function createServer() {
metadata,
} = (req.body || {}) as any;
if (!actor_id || !verb || !object_type) {
return res.status(400).json({ error: "actor_id, verb, object_type required" });
return res
.status(400)
.json({ error: "actor_id, verb, object_type required" });
}
try {
const { data: eventRow, error: evErr } = await adminSupabase
.from("activity_events")
.insert({ actor_id, verb, object_type, object_id: object_id || null, target_id: target_team_id || target_project_id || null, metadata: metadata || null } as any)
.insert({
actor_id,
verb,
object_type,
object_id: object_id || null,
target_id: target_team_id || target_project_id || null,
metadata: metadata || null,
} as any)
.select()
.single();
if (evErr) return res.status(500).json({ error: evErr.message });
const notify = async (userId: string, title: string, message?: string) => {
await adminSupabase.from("notifications").insert({ user_id: userId, type: "info", title, message: message || null });
const notify = async (
userId: string,
title: string,
message?: string,
) => {
await adminSupabase
.from("notifications")
.insert({
user_id: userId,
type: "info",
title,
message: message || null,
});
};
// Notify explicit targets
if (Array.isArray(target_user_ids) && target_user_ids.length) {
for (const uid of target_user_ids) {
await notify(uid, `${verb} · ${object_type}`, (metadata && metadata.summary) || null);
await notify(
uid,
`${verb} · ${object_type}`,
(metadata && metadata.summary) || null,
);
}
}
@ -936,8 +997,12 @@ export function createServer() {
.from("team_memberships")
.select("user_id")
.eq("team_id", target_team_id);
for (const m of (members || [])) {
await notify((m as any).user_id, `${verb} · ${object_type}`, (metadata && metadata.summary) || null);
for (const m of members || []) {
await notify(
(m as any).user_id,
`${verb} · ${object_type}`,
(metadata && metadata.summary) || null,
);
}
}
@ -947,8 +1012,12 @@ export function createServer() {
.from("project_members")
.select("user_id")
.eq("project_id", target_project_id);
for (const m of (members || [])) {
await notify((m as any).user_id, `${verb} · ${object_type}`, (metadata && metadata.summary) || null);
for (const m of members || []) {
await notify(
(m as any).user_id,
`${verb} · ${object_type}`,
(metadata && metadata.summary) || null,
);
}
}