feat: add username availability check API endpoint

feat: create user image retrieval API endpoint

feat: enhance coin page with dynamic data fetching and improved UI

feat: implement coin creation form with validation and submission logic

feat: add user settings page with profile update functionality
This commit is contained in:
Face 2025-05-23 16:26:02 +03:00
parent 9aa4ba157b
commit 16ad425bb5
48 changed files with 3030 additions and 326 deletions

View file

@ -0,0 +1,73 @@
import { error, json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { coin, user, priceHistory } from '$lib/server/db/schema';
import { eq, desc } from 'drizzle-orm';
export async function GET({ params }) {
const { coinSymbol } = params;
if (!coinSymbol) {
throw error(400, 'Coin symbol is required');
}
const normalizedSymbol = coinSymbol.toUpperCase();
const [coinData] = await db
.select({
id: coin.id,
name: coin.name,
symbol: coin.symbol,
creatorId: coin.creatorId,
creatorName: user.name,
creatorUsername: user.username,
creatorBio: user.bio,
creatorImage: user.image,
initialSupply: coin.initialSupply,
circulatingSupply: coin.circulatingSupply,
currentPrice: coin.currentPrice,
marketCap: coin.marketCap,
icon: coin.icon,
volume24h: coin.volume24h,
change24h: coin.change24h,
poolCoinAmount: coin.poolCoinAmount,
poolBaseCurrencyAmount: coin.poolBaseCurrencyAmount,
createdAt: coin.createdAt,
isListed: coin.isListed
})
.from(coin)
.leftJoin(user, eq(coin.creatorId, user.id))
.where(eq(coin.symbol, normalizedSymbol))
.limit(1);
if (!coinData) {
throw error(404, 'Coin not found');
}
const priceHistoryData = await db
.select({
price: priceHistory.price,
timestamp: priceHistory.timestamp
})
.from(priceHistory)
.where(eq(priceHistory.coinId, coinData.id))
.orderBy(desc(priceHistory.timestamp))
.limit(720);
return json({
coin: {
...coinData,
currentPrice: Number(coinData.currentPrice),
marketCap: Number(coinData.marketCap),
volume24h: Number(coinData.volume24h || 0),
change24h: Number(coinData.change24h || 0),
initialSupply: Number(coinData.initialSupply),
circulatingSupply: Number(coinData.circulatingSupply),
poolCoinAmount: Number(coinData.poolCoinAmount),
poolBaseCurrencyAmount: Number(coinData.poolBaseCurrencyAmount)
},
priceHistory: priceHistoryData.map(p => ({
price: Number(p.price),
timestamp: p.timestamp
}))
});
}

View file

@ -0,0 +1,143 @@
import { auth } from '$lib/auth';
import { error, json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { coin, userPortfolio, user, priceHistory } from '$lib/server/db/schema';
import { eq } from 'drizzle-orm';
import { uploadCoinIcon } from '$lib/server/s3';
import { CREATION_FEE, FIXED_SUPPLY, STARTING_PRICE, INITIAL_LIQUIDITY, TOTAL_COST, MAX_FILE_SIZE } from '$lib/data/constants';
function validateInputs(name: string, symbol: string, iconFile: File | null) {
if (!name || name.length < 2 || name.length > 255) {
throw error(400, 'Name must be between 2 and 255 characters');
}
if (!symbol || symbol.length < 2 || symbol.length > 10) {
throw error(400, 'Symbol must be between 2 and 10 characters');
}
if (iconFile && iconFile.size > MAX_FILE_SIZE) {
throw error(400, 'Icon file must be smaller than 1MB');
}
}
async function validateUserBalance(userId: number) {
const [userData] = await db
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
.from(user)
.where(eq(user.id, userId))
.limit(1);
if (!userData) {
throw error(404, 'User not found');
}
const currentBalance = Number(userData.baseCurrencyBalance);
if (currentBalance < TOTAL_COST) {
throw error(400, `Insufficient funds. You need $${TOTAL_COST.toFixed(2)} but only have $${currentBalance.toFixed(2)}.`);
}
return currentBalance;
}
async function validateSymbolUnique(symbol: string) {
const existingCoin = await db.select().from(coin).where(eq(coin.symbol, symbol)).limit(1);
if (existingCoin.length > 0) {
throw error(400, 'A coin with this symbol already exists');
}
}
async function handleIconUpload(iconFile: File | null, symbol: string): Promise<string | null> {
if (!iconFile || iconFile.size === 0) {
return null;
}
const arrayBuffer = await iconFile.arrayBuffer();
return await uploadCoinIcon(
symbol,
new Uint8Array(arrayBuffer),
iconFile.type,
iconFile.size
);
}
export async function POST({ request }) {
const session = await auth.api.getSession({
headers: request.headers
});
if (!session?.user) {
throw error(401, 'Not authenticated');
}
const formData = await request.formData();
const name = formData.get('name') as string;
const symbol = formData.get('symbol') as string;
const iconFile = formData.get('icon') as File | null;
const normalizedSymbol = symbol?.toUpperCase();
const userId = Number(session.user.id);
validateInputs(name, normalizedSymbol, iconFile);
const [currentBalance] = await Promise.all([
validateUserBalance(userId),
validateSymbolUnique(normalizedSymbol)
]);
let iconKey: string | null = null;
try {
iconKey = await handleIconUpload(iconFile, normalizedSymbol);
} catch (e) {
console.error('Icon upload failed, continuing without icon:', e);
}
let createdCoin: any;
await db.transaction(async (tx) => {
await tx.update(user)
.set({
baseCurrencyBalance: (currentBalance - TOTAL_COST).toString(),
updatedAt: new Date()
})
.where(eq(user.id, userId));
const [newCoin] = await tx.insert(coin).values({
name,
symbol: normalizedSymbol,
icon: iconKey,
creatorId: userId,
initialSupply: FIXED_SUPPLY.toString(),
circulatingSupply: FIXED_SUPPLY.toString(),
currentPrice: STARTING_PRICE.toString(),
marketCap: (FIXED_SUPPLY * STARTING_PRICE).toString(),
poolCoinAmount: FIXED_SUPPLY.toString(),
poolBaseCurrencyAmount: INITIAL_LIQUIDITY.toString()
}).returning();
createdCoin = newCoin;
await tx.insert(userPortfolio).values({
userId,
coinId: newCoin.id,
quantity: FIXED_SUPPLY.toString()
});
await tx.insert(priceHistory).values({
coinId: newCoin.id,
price: STARTING_PRICE.toString()
});
});
return json({
success: true,
coin: {
id: createdCoin.id,
name: createdCoin.name,
symbol: createdCoin.symbol,
icon: createdCoin.icon
},
feePaid: CREATION_FEE,
liquidityDeposited: INITIAL_LIQUIDITY,
initialPrice: STARTING_PRICE,
supply: FIXED_SUPPLY
});
}

View file

@ -0,0 +1,37 @@
import { json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { coin } from '$lib/server/db/schema';
import { desc, eq } from 'drizzle-orm';
export async function GET() {
const topCoins = await db
.select({
id: coin.id,
name: coin.name,
symbol: coin.symbol,
icon: coin.icon,
currentPrice: coin.currentPrice,
marketCap: coin.marketCap,
volume24h: coin.volume24h,
change24h: coin.change24h,
isListed: coin.isListed
})
.from(coin)
.where(eq(coin.isListed, true))
.orderBy(desc(coin.marketCap))
.limit(20);
return json({
coins: topCoins.map(c => ({
id: c.id,
name: c.name,
symbol: c.symbol,
icon: c.icon,
price: Number(c.currentPrice),
marketCap: Number(c.marketCap),
volume24h: Number(c.volume24h || 0),
change24h: Number(c.change24h || 0),
isListed: c.isListed
}))
});
}

View file

@ -0,0 +1,62 @@
import { auth } from '$lib/auth';
import { error, json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { user, userPortfolio, coin } from '$lib/server/db/schema';
import { eq } from 'drizzle-orm';
export async function GET({ request }) {
const session = await auth.api.getSession({
headers: request.headers
});
if (!session?.user) {
throw error(401, 'Not authenticated');
}
const userId = Number(session.user.id);
const [userData] = await db
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
.from(user)
.where(eq(user.id, userId))
.limit(1);
if (!userData) {
throw error(404, 'User not found');
}
const holdings = await db
.select({
quantity: userPortfolio.quantity,
currentPrice: coin.currentPrice,
symbol: coin.symbol
})
.from(userPortfolio)
.innerJoin(coin, eq(userPortfolio.coinId, coin.id))
.where(eq(userPortfolio.userId, userId));
let totalCoinValue = 0;
const coinHoldings = holdings.map(holding => {
const quantity = Number(holding.quantity);
const price = Number(holding.currentPrice);
const value = quantity * price;
totalCoinValue += value;
return {
symbol: holding.symbol,
quantity,
currentPrice: price,
value
};
});
const baseCurrencyBalance = Number(userData.baseCurrencyBalance);
return json({
baseCurrencyBalance,
totalCoinValue,
totalValue: baseCurrencyBalance + totalCoinValue,
coinHoldings,
currency: '$'
});
}

View file

@ -0,0 +1,71 @@
import { auth } from '$lib/auth';
import { uploadProfilePicture } from '$lib/server/s3';
import { error, json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { user } from '$lib/server/db/schema';
import { eq } from 'drizzle-orm';
import { MAX_FILE_SIZE } from '$lib/data/constants';
function validateInputs(name: string, bio: string, username: string, avatarFile: File | null) {
if (name && name.length < 1) {
throw error(400, 'Name cannot be empty');
}
if (bio && bio.length > 160) {
throw error(400, 'Bio must be 160 characters or less');
}
if (username && (username.length < 3 || username.length > 30)) {
throw error(400, 'Username must be between 3 and 30 characters');
}
if (avatarFile && avatarFile.size > MAX_FILE_SIZE) {
throw error(400, 'Avatar file must be smaller than 1MB');
}
}
export async function POST({ request }) {
const session = await auth.api.getSession({
headers: request.headers
});
if (!session?.user) {
throw error(401, 'Not authenticated');
}
const formData = await request.formData();
const name = formData.get('name') as string;
const bio = formData.get('bio') as string;
const username = formData.get('username') as string;
const avatarFile = formData.get('avatar') as File | null;
validateInputs(name, bio, username, avatarFile);
const updates: Record<string, any> = {
name,
bio,
username,
updatedAt: new Date()
};
if (avatarFile && avatarFile.size > 0) {
try {
const arrayBuffer = await avatarFile.arrayBuffer();
const key = await uploadProfilePicture(
session.user.id,
new Uint8Array(arrayBuffer),
avatarFile.type,
avatarFile.size
);
updates.image = key;
} catch (e) {
console.error('Avatar upload failed, continuing without update:', e);
}
}
await db.update(user)
.set(updates)
.where(eq(user.id, Number(session.user.id)));
return json({ success: true });
}

View file

@ -0,0 +1,17 @@
import { json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { user } from '$lib/server/db/schema';
import { eq } from 'drizzle-orm';
export async function GET({ url }) {
const username = url.searchParams.get('username');
if (!username) {
return json({ available: false });
}
const exists = await db.query.user.findFirst({
where: eq(user.username, username)
});
return json({ available: !exists });
}

View file

@ -0,0 +1,28 @@
import { error, json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { user } from '$lib/server/db/schema';
import { eq } from 'drizzle-orm';
import { getPublicUrl } from '$lib/utils';
export async function GET({ params }) {
const { userId } = params;
try {
const [userData] = await db
.select({ image: user.image })
.from(user)
.where(eq(user.id, Number(userId)))
.limit(1);
if (!userData) {
throw error(404, 'User not found');
}
const url = getPublicUrl(userData.image);
return json({ url });
} catch (e) {
console.error('Failed to get user image:', e);
throw error(500, 'Failed to get user image');
}
}