diff --git a/server/package-lock.json b/server/package-lock.json index e86df2a..fea874c 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -188,7 +188,6 @@ "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -246,7 +245,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1629,7 +1627,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", @@ -2302,7 +2299,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/server/src/models/index.ts b/server/src/models/index.ts index 7d9ce6b..b4405b3 100644 --- a/server/src/models/index.ts +++ b/server/src/models/index.ts @@ -1,4 +1,4 @@ -import mongoose, { Schema, Types } from 'mongoose'; +import mongoose, { Schema, Types } from "mongoose"; type PlainObject = Record; @@ -17,12 +17,22 @@ const commonSchemaOptions = { const UserSchema = new Schema( { - email: { type: String, required: true, unique: true, lowercase: true, trim: true }, + email: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true, + }, password_hash: { type: String, default: null }, display_name: { type: String, required: true, trim: true, maxlength: 100 }, avatar_url: { type: String, default: null }, bio: { type: String, default: null }, - role: { type: String, enum: ['writer', 'editor', 'reader', 'admin'], default: 'writer' }, + role: { + type: String, + enum: ["writer", "editor", "reader", "admin"], + default: "writer", + }, genres: { type: [String], default: [] }, trust_score: { type: Number, default: 50 }, streak_days: { type: Number, default: 0 }, @@ -31,27 +41,42 @@ const UserSchema = new Schema( oauth_provider: { type: String, default: null }, oauth_id: { type: String, default: null }, refresh_token: { type: String, default: null }, + tagline: { type: String, default: null, maxlength: 120 }, + social_links: { + type: { + website: { type: String, default: null }, + twitter: { type: String, default: null }, + instagram: { type: String, default: null }, + goodreads: { type: String, default: null }, + }, + default: {}, + }, }, - commonSchemaOptions + commonSchemaOptions, ); const DraftSchema = new Schema( { - author_id: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true }, + author_id: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + index: true, + }, title: { type: String, required: true, maxlength: 500 }, content: { type: Schema.Types.Mixed, default: {} }, - content_text: { type: String, default: '' }, + content_text: { type: String, default: "" }, genre: { type: String, default: null }, status: { type: String, - enum: ['draft', 'shared', 'under_review', 'accepted', 'published'], - default: 'draft', + enum: ["draft", "shared", "under_review", "accepted", "published"], + default: "draft", index: true, }, visibility: { type: String, - enum: ['private', 'editors_only', 'public'], - default: 'private', + enum: ["private", "editors_only", "public"], + default: "private", index: true, }, ai_critique: { type: Schema.Types.Mixed, default: null }, @@ -62,30 +87,45 @@ const DraftSchema = new Schema( plagiarism_score: { type: Number, default: 0 }, last_plagiarism_check: { type: Date, default: null }, }, - commonSchemaOptions + commonSchemaOptions, ); // Optimize Agora/explore queries DraftSchema.index({ visibility: 1, createdAt: -1 }); // Optimize AI critique sorting -DraftSchema.index({ 'ai_critique.scores.plot': -1, createdAt: -1 }); -DraftSchema.index({ 'ai_critique.scores.character': -1, createdAt: -1 }); +DraftSchema.index({ "ai_critique.scores.plot": -1, createdAt: -1 }); +DraftSchema.index({ "ai_critique.scores.character": -1, createdAt: -1 }); const DraftVersionSchema = new Schema( { - draft_id: { type: Schema.Types.ObjectId, ref: 'Draft', required: true, index: true }, + draft_id: { + type: Schema.Types.ObjectId, + ref: "Draft", + required: true, + index: true, + }, content: { type: Schema.Types.Mixed, required: true }, - content_text: { type: String, default: '' }, + content_text: { type: String, default: "" }, word_count: { type: Number, default: 0 }, version_number: { type: Number, required: true }, }, - commonSchemaOptions + commonSchemaOptions, ); const FeedbackSchema = new Schema( { - draft_id: { type: Schema.Types.ObjectId, ref: 'Draft', required: true, index: true }, - reviewer_id: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true }, + draft_id: { + type: Schema.Types.ObjectId, + ref: "Draft", + required: true, + index: true, + }, + reviewer_id: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + index: true, + }, scores: { plot: { type: Number, required: true }, pacing: { type: Number, required: true }, @@ -96,26 +136,40 @@ const FeedbackSchema = new Schema( helpfulness_score: { type: Number, default: 0 }, is_anonymous: { type: Boolean, default: false }, }, - commonSchemaOptions + commonSchemaOptions, ); FeedbackSchema.index({ draft_id: 1, reviewer_id: 1 }, { unique: true }); const InlineCommentSchema = new Schema( { - draft_id: { type: Schema.Types.ObjectId, ref: 'Draft', required: true, index: true }, - user_id: { type: Schema.Types.ObjectId, ref: 'User', required: true }, + draft_id: { + type: Schema.Types.ObjectId, + ref: "Draft", + required: true, + index: true, + }, + user_id: { type: Schema.Types.ObjectId, ref: "User", required: true }, anchor_from: { type: Number, required: true }, anchor_to: { type: Number, required: true }, comment_text: { type: String, required: true }, is_resolved: { type: Boolean, default: false }, - parent_id: { type: Schema.Types.ObjectId, ref: 'InlineComment', default: null }, + parent_id: { + type: Schema.Types.ObjectId, + ref: "InlineComment", + default: null, + }, }, - commonSchemaOptions + commonSchemaOptions, ); const OpportunitySchema = new Schema( { - publisher_id: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true }, + publisher_id: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + index: true, + }, title: { type: String, required: true, maxlength: 300 }, description: { type: String, default: null }, genres: { type: [String], default: [] }, @@ -126,33 +180,59 @@ const OpportunitySchema = new Schema( is_featured: { type: Boolean, default: false }, is_active: { type: Boolean, default: true }, }, - commonSchemaOptions + commonSchemaOptions, ); const SubmissionSchema = new Schema( { - draft_id: { type: Schema.Types.ObjectId, ref: 'Draft', required: true }, - opportunity_id: { type: Schema.Types.ObjectId, ref: 'Opportunity', required: true }, - submitter_id: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true }, + draft_id: { type: Schema.Types.ObjectId, ref: "Draft", required: true }, + opportunity_id: { + type: Schema.Types.ObjectId, + ref: "Opportunity", + required: true, + }, + submitter_id: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + index: true, + }, status: { type: String, - enum: ['submitted', 'under_review', 'shortlisted', 'accepted', 'rejected'], - default: 'submitted', + enum: [ + "submitted", + "under_review", + "shortlisted", + "accepted", + "rejected", + ], + default: "submitted", index: true, }, editor_feedback: { type: String, default: null }, reviewed_at: { type: Date, default: null }, }, - commonSchemaOptions + commonSchemaOptions, ); SubmissionSchema.index({ draft_id: 1, opportunity_id: 1 }, { unique: true }); const NotificationSchema = new Schema( { - user_id: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true }, + user_id: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + index: true, + }, type: { type: String, - enum: ['feedback_received', 'submission_update', 'ai_ready', 'achievement_unlocked', 'system'], + enum: [ + "feedback_received", + "submission_update", + "ai_ready", + "achievement_unlocked", + "system", + ], required: true, }, title: { type: String, required: true }, @@ -160,7 +240,7 @@ const NotificationSchema = new Schema( metadata: { type: Schema.Types.Mixed, default: {} }, is_read: { type: Boolean, default: false }, }, - commonSchemaOptions + commonSchemaOptions, ); const AchievementSchema = new Schema( @@ -171,30 +251,48 @@ const AchievementSchema = new Schema( icon: { type: String, required: true }, requirement_value: { type: Number, required: true, default: 1 }, }, - commonSchemaOptions + commonSchemaOptions, ); const UserAchievementSchema = new Schema( { - user_id: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true }, - achievement_id: { type: Schema.Types.ObjectId, ref: 'Achievement', required: true, index: true }, + user_id: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + index: true, + }, + achievement_id: { + type: Schema.Types.ObjectId, + ref: "Achievement", + required: true, + index: true, + }, progress: { type: Number, default: 0 }, earned: { type: Boolean, default: false }, earned_at: { type: Date, default: null }, }, - commonSchemaOptions + commonSchemaOptions, +); +UserAchievementSchema.index( + { user_id: 1, achievement_id: 1 }, + { unique: true }, ); -UserAchievementSchema.index({ user_id: 1, achievement_id: 1 }, { unique: true }); const WritingSessionSchema = new Schema( { - user_id: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true }, - draft_id: { type: Schema.Types.ObjectId, ref: 'Draft', default: null }, + user_id: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + index: true, + }, + draft_id: { type: Schema.Types.ObjectId, ref: "Draft", default: null }, started_at: { type: Date, default: () => new Date() }, ended_at: { type: Date, default: null }, words_written: { type: Number, default: 0 }, }, - commonSchemaOptions + commonSchemaOptions, ); const AIEmbeddingSchema = new Schema( @@ -204,22 +302,42 @@ const AIEmbeddingSchema = new Schema( embedding: { type: [Number], default: [] }, updated_at: { type: Date, default: () => new Date() }, }, - commonSchemaOptions + commonSchemaOptions, ); AIEmbeddingSchema.index({ entity_type: 1, entity_id: 1 }, { unique: true }); -export const UserModel = mongoose.models.User || mongoose.model('User', UserSchema); -export const DraftModel = mongoose.models.Draft || mongoose.model('Draft', DraftSchema); -export const DraftVersionModel = mongoose.models.DraftVersion || mongoose.model('DraftVersion', DraftVersionSchema); -export const FeedbackModel = mongoose.models.Feedback || mongoose.model('Feedback', FeedbackSchema); -export const InlineCommentModel = mongoose.models.InlineComment || mongoose.model('InlineComment', InlineCommentSchema); -export const OpportunityModel = mongoose.models.Opportunity || mongoose.model('Opportunity', OpportunitySchema); -export const SubmissionModel = mongoose.models.Submission || mongoose.model('Submission', SubmissionSchema); -export const NotificationModel = mongoose.models.Notification || mongoose.model('Notification', NotificationSchema); -export const AchievementModel = mongoose.models.Achievement || mongoose.model('Achievement', AchievementSchema); -export const UserAchievementModel = mongoose.models.UserAchievement || mongoose.model('UserAchievement', UserAchievementSchema); -export const WritingSessionModel = mongoose.models.WritingSession || mongoose.model('WritingSession', WritingSessionSchema); -export const AIEmbeddingModel = mongoose.models.AIEmbedding || mongoose.model('AIEmbedding', AIEmbeddingSchema); +export const UserModel = + mongoose.models.User || mongoose.model("User", UserSchema); +export const DraftModel = + mongoose.models.Draft || mongoose.model("Draft", DraftSchema); +export const DraftVersionModel = + mongoose.models.DraftVersion || + mongoose.model("DraftVersion", DraftVersionSchema); +export const FeedbackModel = + mongoose.models.Feedback || mongoose.model("Feedback", FeedbackSchema); +export const InlineCommentModel = + mongoose.models.InlineComment || + mongoose.model("InlineComment", InlineCommentSchema); +export const OpportunityModel = + mongoose.models.Opportunity || + mongoose.model("Opportunity", OpportunitySchema); +export const SubmissionModel = + mongoose.models.Submission || mongoose.model("Submission", SubmissionSchema); +export const NotificationModel = + mongoose.models.Notification || + mongoose.model("Notification", NotificationSchema); +export const AchievementModel = + mongoose.models.Achievement || + mongoose.model("Achievement", AchievementSchema); +export const UserAchievementModel = + mongoose.models.UserAchievement || + mongoose.model("UserAchievement", UserAchievementSchema); +export const WritingSessionModel = + mongoose.models.WritingSession || + mongoose.model("WritingSession", WritingSessionSchema); +export const AIEmbeddingModel = + mongoose.models.AIEmbedding || + mongoose.model("AIEmbedding", AIEmbeddingSchema); export function isValidObjectId(id: string) { return Types.ObjectId.isValid(id); @@ -231,7 +349,7 @@ export function toObjectId(id: string) { export function normalize(doc: any) { if (!doc) return doc; - if (typeof doc.toJSON === 'function') return doc.toJSON(); + if (typeof doc.toJSON === "function") return doc.toJSON(); return doc; } @@ -241,7 +359,11 @@ export function ensureObjectId(id: string, fieldName: string) { } } -export function averageFeedbackScore(scores: { plot: number; pacing: number; character: number }) { +export function averageFeedbackScore(scores: { + plot: number; + pacing: number; + character: number; +}) { return (scores.plot + scores.pacing + scores.character) / 3; } diff --git a/server/src/routes/users.routes.ts b/server/src/routes/users.routes.ts index c7c2f7e..21238ae 100644 --- a/server/src/routes/users.routes.ts +++ b/server/src/routes/users.routes.ts @@ -6,11 +6,20 @@ import { z } from 'zod'; const router = Router(); +const socialLinksSchema = z.object({ + website: z.string().url().optional().nullable(), + twitter: z.string().url().optional().nullable(), + instagram: z.string().url().optional().nullable(), + goodreads: z.string().url().optional().nullable(), +}); + const updateProfileSchema = z.object({ display_name: z.string().min(1).max(100).optional(), - bio: z.string().max(1000).optional(), - genres: z.array(z.string()).optional(), - avatar_url: z.string().url().optional(), + bio: z.string().max(1000).optional(), + tagline: z.string().max(120).optional().nullable(), + genres: z.array(z.string()).optional(), + avatar_url: z.string().url().optional().nullable(), + social_links: socialLinksSchema.optional(), }); router.get('/leaderboard', usersController.getLeaderboard); diff --git a/server/src/services/users.service.ts b/server/src/services/users.service.ts index bb4f5d5..bda7328 100644 --- a/server/src/services/users.service.ts +++ b/server/src/services/users.service.ts @@ -26,6 +26,8 @@ export const usersService = { display_name: user.display_name, avatar_url: user.avatar_url, bio: user.bio, + tagline: (user as any).tagline ?? null, + social_links: (user as any).social_links ?? {}, role: user.role, genres: user.genres || [], trust_score: user.trust_score, @@ -44,11 +46,13 @@ export const usersService = { }; }, - async updateProfile(userId: string, data: { display_name?: string; bio?: string; genres?: string[]; avatar_url?: string }) { - const update: any = {}; +async updateProfile(userId: string, data: { display_name?: string; bio?: string; tagline?: string | null; genres?: string[]; avatar_url?: string; social_links?: { website?: string | null; twitter?: string | null; instagram?: string | null; goodreads?: string | null } }) { + const update: any = {}; if (data.display_name !== undefined) update.display_name = data.display_name; if (data.bio !== undefined) update.bio = data.bio; if (data.genres !== undefined) update.genres = data.genres; + if (data.tagline !== undefined) update.tagline = data.tagline; + if (data.social_links !== undefined) update.social_links = data.social_links; if (data.avatar_url !== undefined) update.avatar_url = data.avatar_url; if (!Object.keys(update).length) throw new AppError('No fields to update', 400); @@ -62,6 +66,8 @@ export const usersService = { display_name: user.display_name, avatar_url: user.avatar_url, bio: user.bio, + tagline: (user as any).tagline ?? null, + social_links: (user as any).social_links ?? {}, role: user.role, genres: user.genres || [], trust_score: user.trust_score, diff --git a/server/tsconfig.json b/server/tsconfig.json index d4d879c..7eb2ad8 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -13,7 +13,6 @@ "declarationMap": true, "sourceMap": true, "moduleResolution": "node", - "ignoreDeprecations": "6.0", "strict": false, "skipLibCheck": true, "noImplicitAny": false diff --git a/web/package-lock.json b/web/package-lock.json index 33c6ae4..8e42f07 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -72,7 +72,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1589,7 +1588,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1656,7 +1654,6 @@ "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", @@ -2182,7 +2179,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2536,7 +2532,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -3166,7 +3161,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3352,7 +3346,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5654,7 +5647,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5664,7 +5656,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6413,7 +6404,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6576,7 +6566,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6862,7 +6851,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/web/src/app/page.tsx b/web/src/app/profile/[id]/page.tsx similarity index 100% rename from web/src/app/page.tsx rename to web/src/app/profile/[id]/page.tsx diff --git a/web/src/app/profile/page.tsx b/web/src/app/profile/page.tsx deleted file mode 100644 index 5efface..0000000 --- a/web/src/app/profile/page.tsx +++ /dev/null @@ -1,126 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { MainLayout } from "@/components/layout/MainLayout"; -import { GlassCard } from "@/components/ui/GlassCard"; -import { InkButton } from "@/components/ui/InkButton"; -import { Badge } from "@/components/ui/Badge"; -import { api } from "@/lib/api"; - -interface ProfileData { - id: string; - display_name: string; - bio: string; - role: string; - genres: string[]; - trust_score: number; - streak_days: number; - stats: { - reviews_given: number; - feedback_received: number; - avg_rating: number; - drafts_published: number; - total_words: number; - rank: number; - }; -} - -interface DraftHistory { - id: string; - title: string; - genre: string; - status: string; - word_count: number; - feedback_count: number; - updated_at: string; -} - -export default function ProfilePage() { - const [profile, setProfile] = useState(null); - const [history, setHistory] = useState([]); - const [editing, setEditing] = useState(false); - const [bio, setBio] = useState(""); - - useEffect(() => { - const load = async () => { - const [profileRes, historyRes] = await Promise.all([ - api.get("/users/me/profile"), - api.get("/users/me/history"), - ]); - if (profileRes.data) { - setProfile(profileRes.data); - setBio(profileRes.data.bio || ""); - } - if (historyRes.data) setHistory(historyRes.data); - }; - load().catch(console.error); - }, []); - - const saveBio = async () => { - await api.patch("/users/me", { bio }); - setProfile((prev) => prev ? { ...prev, bio } : prev); - setEditing(false); - }; - - return ( - -
- -
-
-

{profile?.display_name || "Profile"}

-

{profile?.role || "writer"}

-
- Trust {Math.round(profile?.trust_score || 0)} -
- {editing ? ( -
-