improve SEO on UGC, proxy S3
This commit is contained in:
parent
6e4e8a4eaa
commit
107c78a5f2
13 changed files with 149 additions and 34 deletions
|
|
@ -5,7 +5,7 @@
|
||||||
title = 'Rugplay',
|
title = 'Rugplay',
|
||||||
description = 'Experience realistic cryptocurrency trading simulation game with AI-powered markets, rug pull mechanics, and virtual currencies. Learn crypto trading without financial risk in this educational game.',
|
description = 'Experience realistic cryptocurrency trading simulation game with AI-powered markets, rug pull mechanics, and virtual currencies. Learn crypto trading without financial risk in this educational game.',
|
||||||
type = 'website',
|
type = 'website',
|
||||||
image = '/rugplay.svg',
|
image = '/apple-touch-icon.png',
|
||||||
imageAlt = 'Rugplay Logo',
|
imageAlt = 'Rugplay Logo',
|
||||||
keywords = '',
|
keywords = '',
|
||||||
author = 'Outpoot',
|
author = 'Outpoot',
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export function getTimeBasedGreeting(name: string): string {
|
||||||
|
|
||||||
export function getPublicUrl(key: string | null): string | null {
|
export function getPublicUrl(key: string | null): string | null {
|
||||||
if (!key) return null;
|
if (!key) return null;
|
||||||
return `${PUBLIC_B2_ENDPOINT}/${PUBLIC_B2_BUCKET}/${key}`;
|
return `/api/proxy/s3/${key}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function debounce(func: (...args: any[]) => void, wait: number) {
|
export function debounce(func: (...args: any[]) => void, wait: number) {
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
|
|
||||||
<SEO
|
<SEO
|
||||||
title="Rugplay"
|
title="Rugplay"
|
||||||
description="Experience realistic cryptocurrency trading simulation with AI-powered markets, rug pull mechanics, and virtual currencies. Learn crypto trading without financial risk in this educational trading game."
|
description="A realistic crypto trading simulator that lets you experience the risks and mechanics of decentralized exchanges without real financial consequences. Create coins, trade with liquidity pools, and learn about 'rug pulls' in a... relatively safe environment :)"
|
||||||
keywords="crypto simulation game, trading practice game, rug pull simulation, virtual cryptocurrency game"
|
keywords="crypto simulation game, trading practice game, rug pull simulation, virtual cryptocurrency game"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
35
website/src/routes/api/proxy/s3/[...path]/+server.ts
Normal file
35
website/src/routes/api/proxy/s3/[...path]/+server.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { PUBLIC_B2_BUCKET, PUBLIC_B2_ENDPOINT } from "$env/static/public";
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export async function GET({ params, request }) {
|
||||||
|
const path = params.path;
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
throw error(400, 'Path is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const s3Url = `${PUBLIC_B2_ENDPOINT}/${PUBLIC_B2_BUCKET}/${path}`;
|
||||||
|
const response = await fetch(s3Url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw error(response.status, 'Failed to fetch from S3');
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = response.headers.get('content-type') || 'application/octet-stream';
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
|
||||||
|
return new Response(buffer, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': contentType,
|
||||||
|
'Cache-Control': 'public, max-age=60',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Proxy error:', e);
|
||||||
|
throw error(500, 'Failed to proxy S3 request');
|
||||||
|
}
|
||||||
|
}
|
||||||
30
website/src/routes/coin/[coinSymbol]/+page.server.ts
Normal file
30
website/src/routes/coin/[coinSymbol]/+page.server.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export async function load({ params, url, fetch }) {
|
||||||
|
const { coinSymbol } = params;
|
||||||
|
const timeframe = url.searchParams.get('timeframe') || '1m';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/coin/${coinSymbol}?timeframe=${timeframe}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
throw error(404, 'Coin not found');
|
||||||
|
}
|
||||||
|
throw error(500, 'Failed to load coin data');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
coinSymbol,
|
||||||
|
coin: result.coin,
|
||||||
|
chartData: result.candlestickData || [],
|
||||||
|
volumeData: result.volumeData || [],
|
||||||
|
timeframe
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch coin data:', e);
|
||||||
|
throw error(500, 'Failed to load coin data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,14 +31,14 @@
|
||||||
|
|
||||||
const { data } = $props();
|
const { data } = $props();
|
||||||
let coinSymbol = $derived(data.coinSymbol);
|
let coinSymbol = $derived(data.coinSymbol);
|
||||||
let coin = $state<any>(null);
|
let coin = $state(data.coin);
|
||||||
let loading = $state(true);
|
let loading = $state(false);
|
||||||
let chartData = $state<any[]>([]);
|
let chartData = $state(data.chartData);
|
||||||
let volumeData = $state<any[]>([]);
|
let volumeData = $state(data.volumeData);
|
||||||
let userHolding = $state(0);
|
let userHolding = $state(0);
|
||||||
let buyModalOpen = $state(false);
|
let buyModalOpen = $state(false);
|
||||||
let sellModalOpen = $state(false);
|
let sellModalOpen = $state(false);
|
||||||
let selectedTimeframe = $state('1m');
|
let selectedTimeframe = $state(data.timeframe || '1m');
|
||||||
let lastPriceUpdateTime = 0;
|
let lastPriceUpdateTime = 0;
|
||||||
let shouldSignIn = $state(false);
|
let shouldSignIn = $state(false);
|
||||||
|
|
||||||
|
|
@ -54,7 +54,6 @@
|
||||||
];
|
];
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await loadCoinData();
|
|
||||||
await loadUserHolding();
|
await loadUserHolding();
|
||||||
|
|
||||||
websocketController.setCoin(coinSymbol.toUpperCase());
|
websocketController.setCoin(coinSymbol.toUpperCase());
|
||||||
|
|
@ -92,6 +91,7 @@
|
||||||
|
|
||||||
async function loadCoinData() {
|
async function loadCoinData() {
|
||||||
try {
|
try {
|
||||||
|
loading = true;
|
||||||
const response = await fetch(`/api/coin/${coinSymbol}?timeframe=${selectedTimeframe}`);
|
const response = await fetch(`/api/coin/${coinSymbol}?timeframe=${selectedTimeframe}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -274,7 +274,7 @@
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
|
||||||
const processedChartData = chartData.map((candle) => {
|
const processedChartData = chartData.map((candle: { open: any; close: any; high: number; low: number; }) => {
|
||||||
if (candle.open === candle.close) {
|
if (candle.open === candle.close) {
|
||||||
const basePrice = candle.open;
|
const basePrice = candle.open;
|
||||||
const variation = basePrice * 0.001;
|
const variation = basePrice * 0.001;
|
||||||
|
|
@ -361,14 +361,14 @@
|
||||||
<SEO
|
<SEO
|
||||||
title={coin
|
title={coin
|
||||||
? `${coin.name} (*${coin.symbol}) - Rugplay`
|
? `${coin.name} (*${coin.symbol}) - Rugplay`
|
||||||
: `Loading ${coinSymbol.toUpperCase()} - Rugplay Game`}
|
: `Loading ${coinSymbol.toUpperCase()} - Rugplay`}
|
||||||
description={coin
|
description={coin
|
||||||
? `Trade ${coin.name} (*${coin.symbol}) in the Rugplay simulation game. Current price: $${formatPrice(coin.currentPrice)}, Market cap: ${formatMarketCap(coin.marketCap)}, 24h change: ${coin.change24h >= 0 ? '+' : ''}${coin.change24h.toFixed(2)}%.`
|
? `Trade ${coin.name} (*${coin.symbol}) in the Rugplay simulation game. Current price: $${formatPrice(coin.currentPrice)}, Market cap: ${formatMarketCap(coin.marketCap)}, 24h change: ${coin.change24h >= 0 ? '+' : ''}${coin.change24h.toFixed(2)}%.`
|
||||||
: `Virtual cryptocurrency trading page for ${coinSymbol.toUpperCase()} in the Rugplay simulation game.`}
|
: `Virtual cryptocurrency trading page for ${coinSymbol.toUpperCase()} in the Rugplay simulation game.`}
|
||||||
keywords={coin
|
keywords={coin
|
||||||
? `${coin.name} cryptocurrency game, *${coin.symbol} virtual trading, ${coin.symbol} price simulation, cryptocurrency trading game, virtual coin ${coin.symbol}`
|
? `${coin.name} cryptocurrency game, *${coin.symbol} virtual trading, ${coin.symbol} price simulation, cryptocurrency trading game, virtual coin ${coin.symbol}`
|
||||||
: `${coinSymbol} virtual cryptocurrency, crypto trading simulation, virtual coin trading`}
|
: `${coinSymbol} virtual cryptocurrency, crypto trading simulation, virtual coin trading`}
|
||||||
image={coin?.icon ? getPublicUrl(coin.icon) : '/rugplay.svg'}
|
image={coin?.icon ? getPublicUrl(coin.icon) : '/apple-touch-icon.png'}
|
||||||
imageAlt={coin ? `${coin.name} (${coin.symbol}) logo` : `${coinSymbol} cryptocurrency logo`}
|
imageAlt={coin ? `${coin.name} (${coin.symbol}) logo` : `${coinSymbol} cryptocurrency logo`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
export async function load({ params }) {
|
|
||||||
return {
|
|
||||||
coinSymbol: params.coinSymbol
|
|
||||||
};
|
|
||||||
}
|
|
||||||
31
website/src/routes/hopium/[id]/+page.server.ts
Normal file
31
website/src/routes/hopium/[id]/+page.server.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export async function load({ params, fetch }) {
|
||||||
|
const questionId = parseInt(params.id);
|
||||||
|
|
||||||
|
if (isNaN(questionId)) {
|
||||||
|
throw error(400, 'Invalid question ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/hopium/questions/${questionId}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
throw error(404, 'Question not found');
|
||||||
|
}
|
||||||
|
throw error(500, 'Failed to load question');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
questionId,
|
||||||
|
question: result.question || result,
|
||||||
|
probabilityData: result.probabilityHistory || []
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch question:', e);
|
||||||
|
throw error(500, 'Failed to load question');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,9 +28,10 @@
|
||||||
import type { PredictionQuestion } from '$lib/types/prediction';
|
import type { PredictionQuestion } from '$lib/types/prediction';
|
||||||
import HopiumQuestionSkeleton from '$lib/components/self/skeletons/HopiumQuestionSkeleton.svelte';
|
import HopiumQuestionSkeleton from '$lib/components/self/skeletons/HopiumQuestionSkeleton.svelte';
|
||||||
|
|
||||||
let question = $state<PredictionQuestion | null>(null);
|
const { data } = $props();
|
||||||
let loading = $state(true);
|
let question = $state(data.question);
|
||||||
let probabilityData = $state<any[]>([]);
|
let loading = $state(false);
|
||||||
|
let probabilityData = $state(data.probabilityData);
|
||||||
|
|
||||||
// Betting form
|
// Betting form
|
||||||
let betSide = $state<boolean>(true);
|
let betSide = $state<boolean>(true);
|
||||||
|
|
@ -38,7 +39,7 @@
|
||||||
let customBetAmount = $state('');
|
let customBetAmount = $state('');
|
||||||
|
|
||||||
let userBalance = $derived($PORTFOLIO_SUMMARY ? $PORTFOLIO_SUMMARY.baseCurrencyBalance : 0);
|
let userBalance = $derived($PORTFOLIO_SUMMARY ? $PORTFOLIO_SUMMARY.baseCurrencyBalance : 0);
|
||||||
let questionId = $derived(parseInt(page.params.id));
|
let questionId = $derived(data.questionId);
|
||||||
|
|
||||||
// Chart related
|
// Chart related
|
||||||
let chartContainer = $state<HTMLDivElement>();
|
let chartContainer = $state<HTMLDivElement>();
|
||||||
|
|
@ -46,7 +47,6 @@
|
||||||
let lineSeries: any = null;
|
let lineSeries: any = null;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
fetchQuestion();
|
|
||||||
if ($USER_DATA) {
|
if ($USER_DATA) {
|
||||||
fetchPortfolioSummary();
|
fetchPortfolioSummary();
|
||||||
}
|
}
|
||||||
|
|
@ -54,6 +54,7 @@
|
||||||
|
|
||||||
async function fetchQuestion() {
|
async function fetchQuestion() {
|
||||||
try {
|
try {
|
||||||
|
loading = true;
|
||||||
const response = await fetch(`/api/hopium/questions/${questionId}`);
|
const response = await fetch(`/api/hopium/questions/${questionId}`);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
|
||||||
|
|
@ -288,7 +288,6 @@
|
||||||
<SEO
|
<SEO
|
||||||
title="Settings - Rugplay"
|
title="Settings - Rugplay"
|
||||||
description="Manage your Rugplay account settings, profile information, audio preferences, and privacy options."
|
description="Manage your Rugplay account settings, profile information, audio preferences, and privacy options."
|
||||||
noindex={true}
|
|
||||||
keywords="game account settings, profile settings game, privacy settings, audio settings game"
|
keywords="game account settings, profile settings game, privacy settings, audio settings game"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
27
website/src/routes/user/[username]/+page.server.ts
Normal file
27
website/src/routes/user/[username]/+page.server.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export async function load({ params, fetch }) {
|
||||||
|
const { username } = params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/user/${username}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
throw error(404, 'User not found');
|
||||||
|
}
|
||||||
|
throw error(500, 'Failed to load user profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileData = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
username,
|
||||||
|
profileData,
|
||||||
|
recentTransactions: profileData.recentTransactions || []
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch user profile:', e);
|
||||||
|
throw error(500, 'Failed to load user profile');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,15 +20,14 @@
|
||||||
Activity
|
Activity
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import type { UserProfileData } from '$lib/types/user-profile';
|
|
||||||
import { USER_DATA } from '$lib/stores/user-data';
|
import { USER_DATA } from '$lib/stores/user-data';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let username = $derived(data.username);
|
let username = $derived(data.username);
|
||||||
|
|
||||||
let profileData = $state<UserProfileData | null>(null);
|
let profileData = $state(data.profileData);
|
||||||
let recentTransactions = $state<any[]>([]);
|
let recentTransactions = $state(data.recentTransactions);
|
||||||
let loading = $state(true);
|
let loading = $state(false);
|
||||||
|
|
||||||
let previousUsername = $state<string | null>(null);
|
let previousUsername = $state<string | null>(null);
|
||||||
|
|
||||||
|
|
@ -37,8 +36,11 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await fetchProfileData();
|
|
||||||
previousUsername = username;
|
previousUsername = username;
|
||||||
|
|
||||||
|
if (isOwnProfile) {
|
||||||
|
await fetchTransactions();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|
@ -350,7 +352,7 @@
|
||||||
? `${profileData.profile.bio} - View ${profileData.profile.name}'s simulated trading activity and virtual portfolio in the Rugplay cryptocurrency simulation game.`
|
? `${profileData.profile.bio} - View ${profileData.profile.name}'s simulated trading activity and virtual portfolio in the Rugplay cryptocurrency simulation game.`
|
||||||
: `View @${username}'s profile and simulated trading activity in Rugplay - cryptocurrency trading simulation game platform.`}
|
: `View @${username}'s profile and simulated trading activity in Rugplay - cryptocurrency trading simulation game platform.`}
|
||||||
type="profile"
|
type="profile"
|
||||||
image={profileData?.profile?.image ? getPublicUrl(profileData.profile.image) : '/rugplay.svg'}
|
image={profileData?.profile?.image ? getPublicUrl(profileData.profile.image) : '/apple-touch-icon.png'}
|
||||||
imageAlt={profileData?.profile?.name
|
imageAlt={profileData?.profile?.name
|
||||||
? `${profileData.profile.name}'s profile picture`
|
? `${profileData.profile.name}'s profile picture`
|
||||||
: `@${username}'s profile`}
|
: `@${username}'s profile`}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
export async function load({ params }) {
|
|
||||||
return {
|
|
||||||
username: params.username
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in a new issue