Initialer Laravel Commit für BetiX
This commit is contained in:
1472
resources/js/pages/Social/Hub.vue
Normal file
1472
resources/js/pages/Social/Hub.vue
Normal file
File diff suppressed because it is too large
Load Diff
930
resources/js/pages/Social/Profile.vue
Normal file
930
resources/js/pages/Social/Profile.vue
Normal file
@@ -0,0 +1,930 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, useForm, usePage } from '@inertiajs/vue3';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import html2canvas from 'html2canvas';
|
||||
import { useNotifications } from '@/composables/useNotifications';
|
||||
import {
|
||||
Flag, X, CheckCircle2, AlertCircle, ChevronRight, Loader2,
|
||||
MessageSquareOff, UserX, ShieldAlert, BadgeDollarSign, HelpCircle
|
||||
} from 'lucide-vue-next';
|
||||
import UserLayout from '@/layouts/user/userlayout.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
profile: any;
|
||||
isOwnProfile: boolean;
|
||||
isFriend: boolean;
|
||||
isPending?: boolean;
|
||||
theyRequestedMe?: boolean;
|
||||
friendRowId?: number | null;
|
||||
hasLiked: boolean;
|
||||
}>();
|
||||
|
||||
const { notify } = useNotifications();
|
||||
const stats = props.profile.stats;
|
||||
const bestWins = props.profile.best_wins || [];
|
||||
const comments = ref(props.profile.comments || []);
|
||||
|
||||
// Animation for stats
|
||||
const animatedWager = ref(0);
|
||||
onMounted(() => {
|
||||
const target = parseFloat(stats.wagered || 0);
|
||||
const duration = 1500;
|
||||
const start = performance.now();
|
||||
|
||||
const step = (timestamp: number) => {
|
||||
const progress = Math.min((timestamp - start) / duration, 1);
|
||||
animatedWager.value = target * (1 - Math.pow(1 - progress, 3));
|
||||
if (progress < 1) requestAnimationFrame(step);
|
||||
};
|
||||
requestAnimationFrame(step);
|
||||
});
|
||||
|
||||
// Rank & Role Logic
|
||||
const vipLevelsConfig = [
|
||||
{ name: 'Newbie', color: '#888888' },
|
||||
{ name: 'Bronze', color: '#cd7f32' },
|
||||
{ name: 'Silver', color: '#c0c0c0' },
|
||||
{ name: 'Gold', color: '#ffd700' },
|
||||
{ name: 'Platinum', color: '#00f2ff' },
|
||||
{ name: 'Diamond', color: '#ff007a' },
|
||||
{ name: 'Obsidian', color: '#ff3e3e' }
|
||||
];
|
||||
|
||||
const currentVipStyle = computed(() => {
|
||||
const level = props.profile.vip_level || 0;
|
||||
const idx = Math.min(Math.max(level, 0), vipLevelsConfig.length - 1);
|
||||
return vipLevelsConfig[idx];
|
||||
});
|
||||
|
||||
const roleConfig = computed(() => {
|
||||
const role = (props.profile.role || 'User').toLowerCase(); // Case insensitive check
|
||||
let color = '#888';
|
||||
let effectClass = '';
|
||||
let displayName = props.profile.role || 'User';
|
||||
|
||||
if (role === 'admin') {
|
||||
color = '#ff3e3e';
|
||||
effectClass = 'role-admin';
|
||||
displayName = 'ADMIN';
|
||||
} else if (role === 'mod' || role === 'staff') {
|
||||
color = '#00f2ff';
|
||||
effectClass = 'role-staff';
|
||||
displayName = 'STAFF';
|
||||
} else if (role === 'streamer') {
|
||||
color = '#a855f7';
|
||||
effectClass = 'role-streamer';
|
||||
displayName = 'STREAMER';
|
||||
}
|
||||
|
||||
return { name: displayName, color, effectClass };
|
||||
});
|
||||
|
||||
// Comment Logic
|
||||
const commentForm = useForm({ content: '' });
|
||||
const submitComment = () => {
|
||||
if (!commentForm.content.trim()) return;
|
||||
commentForm.post(`/profile/${props.profile.id}/comment`, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
commentForm.reset();
|
||||
notify({ type: 'green', title: 'Posted', desc: 'Comment added.', icon: 'message-circle' });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Report Logic
|
||||
const isReportOpen = ref(false);
|
||||
const reportReason = ref('');
|
||||
const reportSubmitting = ref(false);
|
||||
const reportDone = ref(false);
|
||||
|
||||
const reportReasons = [
|
||||
{ value: 'spam', label: 'Spam', desc: 'Unerwünschte Werbung oder Wiederholungen', icon: MessageSquareOff, color: '#f59e0b' },
|
||||
{ value: 'harassment', label: 'Belästigung', desc: 'Belästigendes oder feindseliges Verhalten', icon: UserX, color: '#ef4444' },
|
||||
{ value: 'inappropriate', label: 'Unangemessenes Profil', desc: 'Anstößige Profilbilder oder Inhalte', icon: ShieldAlert, color: '#f97316' },
|
||||
{ value: 'fake', label: 'Fake-Profil', desc: 'Vortäuschung einer anderen Identität', icon: BadgeDollarSign, color: '#a855f7' },
|
||||
{ value: 'other', label: 'Sonstiges', desc: 'Anderer Verstoß gegen die Nutzungsregeln', icon: HelpCircle, color: '#6b7280' },
|
||||
];
|
||||
|
||||
function openReportModal() {
|
||||
isReportOpen.value = true;
|
||||
reportReason.value = '';
|
||||
reportDone.value = false;
|
||||
}
|
||||
|
||||
function closeReportModal() {
|
||||
isReportOpen.value = false;
|
||||
}
|
||||
|
||||
const submitReport = async () => {
|
||||
if (!reportReason.value || reportSubmitting.value) return;
|
||||
reportSubmitting.value = true;
|
||||
|
||||
// Build snapshot + screenshot
|
||||
const snapshot = {
|
||||
id: props.profile.id,
|
||||
username: props.profile.username,
|
||||
avatar: props.profile.avatar,
|
||||
banner: props.profile.banner,
|
||||
bio: props.profile.bio,
|
||||
role: props.profile.role,
|
||||
vip_level: props.profile.vip_level,
|
||||
clan_tag: props.profile.clan_tag,
|
||||
stats: {
|
||||
wagered: props.profile.stats?.wagered,
|
||||
wins: props.profile.stats?.wins,
|
||||
biggest_win: props.profile.stats?.biggest_win,
|
||||
likes_count: props.profile.stats?.likes_count,
|
||||
join_date: props.profile.stats?.join_date,
|
||||
},
|
||||
best_wins: bestWins.slice(0, 5),
|
||||
comments: comments.value.slice(0, 10).map((c: any) => ({
|
||||
id: c.id,
|
||||
content: c.content,
|
||||
created_at: c.created_at,
|
||||
user: { id: c.user?.id, username: c.user?.username, avatar: c.user?.avatar },
|
||||
})),
|
||||
captured_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
let screenshot: string | null = null;
|
||||
try {
|
||||
const profileEl = document.querySelector('.profile-page') as HTMLElement | null;
|
||||
if (profileEl) {
|
||||
const canvas = await html2canvas(profileEl, {
|
||||
useCORS: true, allowTaint: false, scale: 1, backgroundColor: '#0a0a0c',
|
||||
});
|
||||
screenshot = canvas.toDataURL('image/png');
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
const csrf = (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content || '';
|
||||
try {
|
||||
const res = await fetch(`/profile/${props.profile.id}/report`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-TOKEN': csrf,
|
||||
},
|
||||
body: JSON.stringify({ reason: reportReason.value, snapshot, screenshot }),
|
||||
});
|
||||
if (res.ok) {
|
||||
reportDone.value = true;
|
||||
setTimeout(closeReportModal, 1800);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
reportSubmitting.value = false;
|
||||
};
|
||||
|
||||
// Like Logic
|
||||
const toggleLike = () => {
|
||||
useForm({}).post(`/profile/${props.profile.id}/like`, { preserveScroll: true });
|
||||
};
|
||||
|
||||
// Tip Logic
|
||||
const isTipOpen = ref(false);
|
||||
const page = usePage();
|
||||
const myWallets = computed(() => (page.props as any)?.auth?.user?.wallets || []);
|
||||
const currencies = computed<string[]>(() => myWallets.value.map((w: any) => w.currency));
|
||||
const tipForm = useForm<{ currency: string; amount: string | number; note?: string }>({
|
||||
currency: '',
|
||||
amount: '',
|
||||
note: ''
|
||||
});
|
||||
|
||||
// Friend request
|
||||
const localPending = ref(!!props.isPending);
|
||||
const localIsFriend = ref(props.isFriend);
|
||||
const localTheyRequested = ref(!!props.theyRequestedMe);
|
||||
|
||||
const sendFriendRequest = () => {
|
||||
if (localIsFriend.value || localPending.value) return;
|
||||
useForm({ user_id: props.profile.id }).post('/friends/request', {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
localPending.value = true;
|
||||
notify({ type: 'magenta', title: 'Friend request', desc: 'Request sent to ' + props.profile.username, icon: 'user-plus' });
|
||||
},
|
||||
onError: (e) => {
|
||||
notify({ type: 'red', title: 'Error', desc: (e && Object.values(e)[0]) as any || 'Failed to send request', icon: 'alert-triangle' });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const acceptFriendRequest = () => {
|
||||
if (!props.friendRowId) return;
|
||||
useForm({}).post(`/friends/${props.friendRowId}/accept`, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
localTheyRequested.value = false;
|
||||
localIsFriend.value = true;
|
||||
notify({ type: 'green', title: 'Friends!', desc: `You and ${props.profile.username} are now friends.`, icon: 'user-check' });
|
||||
},
|
||||
onError: () => {
|
||||
notify({ type: 'red', title: 'Error', desc: 'Could not accept request.', icon: 'x' });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const declineFriendRequest = () => {
|
||||
if (!props.friendRowId) return;
|
||||
useForm({}).post(`/friends/${props.friendRowId}/decline`, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
localTheyRequested.value = false;
|
||||
notify({ type: 'gray', title: 'Declined', desc: 'Friend request declined.', icon: 'x' });
|
||||
},
|
||||
onError: () => {
|
||||
notify({ type: 'red', title: 'Error', desc: 'Could not decline request.', icon: 'x' });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// Default to first available currency
|
||||
if (currencies.value.length && !tipForm.currency) {
|
||||
tipForm.currency = currencies.value[0];
|
||||
}
|
||||
});
|
||||
|
||||
const submitTip = () => {
|
||||
// Basic client validation
|
||||
const amt = parseFloat(String(tipForm.amount).replace(',', '.'));
|
||||
if (!amt || amt <= 0) {
|
||||
notify({ type: 'red', title: 'Invalid amount', desc: 'Please enter a positive amount.', icon: 'alert-triangle' });
|
||||
return;
|
||||
}
|
||||
tipForm.amount = amt;
|
||||
tipForm.post(`/profile/${props.profile.id}/tip`, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
isTipOpen.value = false;
|
||||
tipForm.reset('amount', 'note');
|
||||
notify({ type: 'green', title: 'Tip sent', desc: 'Your tip was sent successfully.', icon: 'send' });
|
||||
},
|
||||
onError: (errs) => {
|
||||
const first = errs?.amount || Object.values(errs)[0] || 'Transfer failed';
|
||||
notify({ type: 'red', title: 'Error', desc: String(first), icon: 'x' });
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UserLayout>
|
||||
<Head :title="`${profile.username}'s Profile`" />
|
||||
|
||||
<div class="profile-page" :class="roleConfig.effectClass">
|
||||
<div class="bg-fx"></div>
|
||||
|
||||
<!-- Banner -->
|
||||
<div class="banner" :style="{ backgroundImage: `url(${profile.banner || '/img/default-banner.jpg'})` }">
|
||||
<div class="banner-overlay"></div>
|
||||
</div>
|
||||
|
||||
<div class="content-container">
|
||||
<!-- Header Section -->
|
||||
<div class="profile-header">
|
||||
<div class="avatar-wrapper">
|
||||
<div class="avatar" :style="{ borderColor: roleConfig.effectClass ? roleConfig.color : currentVipStyle.color, boxShadow: `0 0 20px ${roleConfig.effectClass ? roleConfig.color : currentVipStyle.color}40` }">
|
||||
<img v-if="profile.avatar" :src="profile.avatar" alt="Avatar">
|
||||
<span v-else>{{ profile.username.charAt(0) }}</span>
|
||||
</div>
|
||||
<div class="online-status"></div>
|
||||
</div>
|
||||
|
||||
<div class="header-info">
|
||||
<div class="name-row">
|
||||
<h1 class="username" :class="roleConfig.effectClass" :style="{ color: roleConfig.effectClass ? roleConfig.color : '#fff' }">
|
||||
<span v-if="profile.clan_tag" class="clan-tag">[{{ profile.clan_tag }}]</span>
|
||||
{{ profile.username }}
|
||||
</h1>
|
||||
<div class="badges">
|
||||
<span class="badge vip" :style="{ color: currentVipStyle.color, borderColor: currentVipStyle.color, background: `${currentVipStyle.color}15` }">
|
||||
VIP {{ profile.vip_level }}
|
||||
</span>
|
||||
<span class="badge role" :class="roleConfig.effectClass" :style="{ color: roleConfig.color, borderColor: roleConfig.color, background: `${roleConfig.color}15` }">
|
||||
{{ roleConfig.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="bio">{{ profile.bio || 'No bio provided.' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<button v-if="!isOwnProfile" class="btn-action like" :class="{ active: hasLiked }" @click="toggleLike">
|
||||
<i data-lucide="heart" :class="{ 'fill-current': hasLiked }"></i>
|
||||
<span>{{ stats.likes_count }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Incoming friend request: show Accept / Decline -->
|
||||
<template v-if="!isOwnProfile && localTheyRequested">
|
||||
<button class="btn-action friend accept" @click="acceptFriendRequest">
|
||||
<i data-lucide="user-check"></i> Accept
|
||||
</button>
|
||||
<button class="btn-action friend decline" @click="declineFriendRequest">
|
||||
<i data-lucide="x"></i> Decline
|
||||
</button>
|
||||
</template>
|
||||
<!-- Already friends / outgoing request / no request -->
|
||||
<button v-else-if="!isOwnProfile" class="btn-action friend" :class="{ added: localIsFriend || localPending }" @click="sendFriendRequest" :disabled="localIsFriend || localPending">
|
||||
<i data-lucide="user-plus" v-if="!localIsFriend && !localPending"></i>
|
||||
<i data-lucide="check" v-else></i>
|
||||
{{ localIsFriend ? 'Friends' : (localPending ? 'Pending' : 'Add') }}
|
||||
</button>
|
||||
|
||||
<button v-if="!isOwnProfile && isFriend" class="btn-action tip" @click="isTipOpen = true" title="Send Tip">
|
||||
<i data-lucide="coins"></i>
|
||||
Tip
|
||||
</button>
|
||||
|
||||
<button v-if="!isOwnProfile" class="btn-action report" @click="openReportModal" title="Report Profile">
|
||||
<i data-lucide="flag"></i>
|
||||
</button>
|
||||
|
||||
<a :href="isOwnProfile ? '/trophy' : `/trophy/${profile.username}`" class="btn-action trophy" title="Trophy Room">
|
||||
<i data-lucide="trophy"></i>
|
||||
</a>
|
||||
|
||||
<a href="/settings" v-if="isOwnProfile" class="btn-edit">
|
||||
<i data-lucide="edit-2"></i> Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card highlight">
|
||||
<div class="stat-icon"><i data-lucide="coins"></i></div>
|
||||
<div class="stat-data">
|
||||
<div class="stat-label">Total Wagered</div>
|
||||
<div class="stat-value">${{ animatedWager.toFixed(2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon"><i data-lucide="trophy"></i></div>
|
||||
<div class="stat-data">
|
||||
<div class="stat-label">Total Wins</div>
|
||||
<div class="stat-value">{{ stats.wins }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon"><i data-lucide="star"></i></div>
|
||||
<div class="stat-data">
|
||||
<div class="stat-label">Biggest Win</div>
|
||||
<div class="stat-value text-gold">${{ parseFloat(stats.biggest_win || 0).toFixed(2) }}</div>
|
||||
<div class="stat-sub" v-if="stats.biggest_win_game">in {{ stats.biggest_win_game }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon"><i data-lucide="calendar"></i></div>
|
||||
<div class="stat-data">
|
||||
<div class="stat-label">Joined</div>
|
||||
<div class="stat-value">{{ stats.join_date }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Columns -->
|
||||
<div class="profile-cols">
|
||||
<!-- Left: Wins -->
|
||||
<div class="col-wins">
|
||||
<h3><i data-lucide="crown" class="text-gold"></i> Best Wins</h3>
|
||||
<div class="wins-grid">
|
||||
<div v-for="(win, index) in bestWins" :key="index" class="win-card">
|
||||
<div class="win-img" :style="{ backgroundImage: `url('${win.image}')` }"></div>
|
||||
<div class="win-info">
|
||||
<div class="win-amount text-gold">${{ win.amount.toFixed(2) }}</div>
|
||||
<div class="win-game">{{ win.game }}</div>
|
||||
<div class="win-multi">{{ win.multiplier }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="bestWins.length === 0" class="win-card empty">
|
||||
<i data-lucide="ghost"></i>
|
||||
<span>No big wins yet</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Comments -->
|
||||
<div class="col-comments">
|
||||
<h3><i data-lucide="message-square"></i> Comments</h3>
|
||||
|
||||
<div class="comment-input" v-if="!isOwnProfile">
|
||||
<textarea v-model="commentForm.content" placeholder="Write a comment..." rows="2"></textarea>
|
||||
<button @click="submitComment" :disabled="commentForm.processing"><i data-lucide="send"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="comments-list">
|
||||
<div v-for="comment in comments" :key="comment.id" class="comment-item">
|
||||
<div class="c-avatar">
|
||||
<img v-if="comment.user?.avatar" :src="comment.user.avatar">
|
||||
<span v-else>{{ (comment.user?.username || '?').charAt(0) }}</span>
|
||||
</div>
|
||||
<div class="c-content">
|
||||
<div class="c-head">
|
||||
<span class="c-name">{{ comment.user?.username || '?' }}</span>
|
||||
<span class="c-time">Just now</span>
|
||||
</div>
|
||||
<div class="c-text">{{ comment.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="comments.length === 0" class="no-comments">No comments yet. Be the first!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Tip Modal -->
|
||||
<div v-if="isTipOpen" class="modal-overlay" @click.self="isTipOpen = false">
|
||||
<div class="modal-card">
|
||||
<h3>Send Tip to {{ profile.username }}</h3>
|
||||
<div class="form-row" style="display:grid; grid-template-columns: 120px 1fr; gap:10px; align-items:center; margin-bottom:10px;">
|
||||
<label class="lbl">Currency</label>
|
||||
<select v-model="tipForm.currency">
|
||||
<option v-for="c in currencies" :key="c" :value="c">{{ c }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row" style="display:grid; grid-template-columns: 120px 1fr; gap:10px; align-items:center; margin-bottom:10px;">
|
||||
<label class="lbl">Amount</label>
|
||||
<input v-model="tipForm.amount" type="number" min="0" step="0.00000001" placeholder="0.00" class="input" />
|
||||
</div>
|
||||
<div class="form-row" style="margin-bottom:10px;">
|
||||
<textarea v-model="tipForm.note" rows="2" placeholder="Add a note (optional)" class="input"></textarea>
|
||||
</div>
|
||||
<div v-if="tipForm.errors && Object.keys(tipForm.errors).length" class="msg err">{{ tipForm.errors.amount || Object.values(tipForm.errors)[0] }}</div>
|
||||
<div class="modal-actions">
|
||||
<button @click="isTipOpen = false" class="btn-cancel">Cancel</button>
|
||||
<button @click="submitTip" class="btn-confirm" :disabled="tipForm.processing">{{ tipForm.processing ? 'Sending...' : 'Send Tip' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Report Modal (teleported to body) -->
|
||||
<teleport to="body">
|
||||
<transition name="modal-fade">
|
||||
<div v-if="isReportOpen" class="gc-report-overlay" @click.self="closeReportModal">
|
||||
<div class="gc-report-modal" role="dialog" aria-modal="true" aria-labelledby="pr-title">
|
||||
|
||||
<!-- Success -->
|
||||
<div v-if="reportDone" class="report-success">
|
||||
<div class="success-ring">
|
||||
<CheckCircle2 :size="36" />
|
||||
</div>
|
||||
<h3>Erfolgreich gemeldet</h3>
|
||||
<p>Danke für deinen Hinweis.<br>Unser Moderationsteam wird den Fall prüfen.</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- Header -->
|
||||
<div class="rm-head">
|
||||
<div class="rm-head-left">
|
||||
<div class="rm-head-icon">
|
||||
<Flag :size="16" />
|
||||
</div>
|
||||
<span id="pr-title" class="rm-title">Profil melden</span>
|
||||
</div>
|
||||
<button class="rm-close" @click="closeReportModal" aria-label="Schließen">
|
||||
<X :size="15" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- User card -->
|
||||
<div class="rm-user">
|
||||
<div class="rm-avatar-wrap">
|
||||
<div class="rm-avatar">
|
||||
<img v-if="profile.avatar" :src="profile.avatar" :alt="profile.username" />
|
||||
<div v-else class="rm-avatar-fallback">{{ profile.username[0].toUpperCase() }}</div>
|
||||
</div>
|
||||
<div class="rm-avatar-glow"></div>
|
||||
</div>
|
||||
<div class="rm-user-info">
|
||||
<span class="rm-username">@{{ profile.username }}</span>
|
||||
<div class="rm-badges">
|
||||
<span v-if="profile.role === 'admin'" class="rm-badge admin">ADMIN</span>
|
||||
<span v-else-if="profile.role === 'staff' || profile.role === 'mod'" class="rm-badge staff">STAFF</span>
|
||||
<span v-else-if="profile.role === 'streamer'" class="rm-badge streamer">STREAMER</span>
|
||||
<span v-else class="rm-badge user">USER</span>
|
||||
<span v-if="profile.vip_level > 0" class="rm-badge vip">★ VIP {{ profile.vip_level }}</span>
|
||||
<span v-if="profile.clan_tag" class="rm-badge clan">[{{ profile.clan_tag }}]</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reason grid -->
|
||||
<div class="rm-section">
|
||||
<div class="rm-section-label">
|
||||
<Flag :size="12" />
|
||||
Meldegrund auswählen
|
||||
</div>
|
||||
<div class="rm-reasons">
|
||||
<button
|
||||
v-for="r in reportReasons"
|
||||
:key="r.value"
|
||||
class="rm-reason-btn"
|
||||
:class="{ selected: reportReason === r.value }"
|
||||
:style="reportReason === r.value ? `--reason-color: ${r.color}` : ''"
|
||||
@click="reportReason = r.value"
|
||||
>
|
||||
<div class="reason-icon-wrap" :style="`color: ${r.color}`">
|
||||
<component :is="r.icon" :size="18" />
|
||||
</div>
|
||||
<div class="reason-text">
|
||||
<span class="reason-label">{{ r.label }}</span>
|
||||
<span class="reason-desc">{{ r.desc }}</span>
|
||||
</div>
|
||||
<div class="reason-check-wrap" v-if="reportReason === r.value">
|
||||
<CheckCircle2 :size="16" />
|
||||
</div>
|
||||
<ChevronRight v-else :size="14" class="reason-chevron" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="rm-actions">
|
||||
<button class="rm-btn-cancel" @click="closeReportModal">
|
||||
<X :size="14" />
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
class="rm-btn-submit"
|
||||
:disabled="!reportReason || reportSubmitting"
|
||||
@click="submitReport"
|
||||
>
|
||||
<Loader2 v-if="reportSubmitting" :size="15" class="rm-spin" />
|
||||
<Flag v-else :size="15" />
|
||||
{{ reportSubmitting ? 'Wird gesendet…' : 'Jetzt melden' }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</teleport>
|
||||
|
||||
</div>
|
||||
</UserLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.profile-page { min-height: 100vh; background: #050505; padding-bottom: 60px; position: relative; overflow: hidden; }
|
||||
|
||||
/* Role Effects & Backgrounds */
|
||||
.bg-fx { position: absolute; inset: 0; pointer-events: none; z-index: 0; opacity: 0; transition: opacity 0.5s; }
|
||||
|
||||
.role-admin .bg-fx {
|
||||
opacity: 1;
|
||||
background:
|
||||
radial-gradient(circle at 20% 30%, rgba(255, 62, 62, 0.15), transparent 60%),
|
||||
radial-gradient(circle at 80% 70%, rgba(255, 62, 62, 0.1), transparent 60%);
|
||||
animation: pulse-bg-red 4s infinite alternate;
|
||||
}
|
||||
.role-staff .bg-fx {
|
||||
opacity: 1;
|
||||
background:
|
||||
radial-gradient(circle at 20% 30%, rgba(0, 242, 255, 0.15), transparent 60%),
|
||||
radial-gradient(circle at 80% 70%, rgba(0, 242, 255, 0.1), transparent 60%);
|
||||
animation: pulse-bg-cyan 4s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes pulse-bg-red { 0% { opacity: 0.6; transform: scale(1); } 100% { opacity: 1; transform: scale(1.05); } }
|
||||
@keyframes pulse-bg-cyan { 0% { opacity: 0.6; transform: scale(1); } 100% { opacity: 1; transform: scale(1.05); } }
|
||||
|
||||
/* Glitter Text */
|
||||
.username.role-admin {
|
||||
text-shadow: 0 0 20px rgba(255, 62, 62, 0.8);
|
||||
animation: glitter-red 2s infinite alternate;
|
||||
}
|
||||
.username.role-staff {
|
||||
text-shadow: 0 0 20px rgba(0, 242, 255, 0.8);
|
||||
animation: glitter-cyan 2s infinite alternate;
|
||||
}
|
||||
|
||||
/* Badge Glitter */
|
||||
.badge.role.role-admin {
|
||||
box-shadow: 0 0 15px rgba(255, 62, 62, 0.5);
|
||||
animation: border-pulse-red 1.5s infinite;
|
||||
background: rgba(255, 62, 62, 0.15) !important;
|
||||
}
|
||||
.badge.role.role-staff {
|
||||
box-shadow: 0 0 15px rgba(0, 242, 255, 0.5);
|
||||
animation: border-pulse-cyan 1.5s infinite;
|
||||
background: rgba(0, 242, 255, 0.15) !important;
|
||||
}
|
||||
|
||||
@keyframes glitter-red { 0% { filter: brightness(1); text-shadow: 0 0 10px rgba(255,62,62,0.5); } 100% { filter: brightness(1.5); text-shadow: 0 0 25px rgba(255,62,62,1); } }
|
||||
@keyframes glitter-cyan { 0% { filter: brightness(1); text-shadow: 0 0 10px rgba(0,242,255,0.5); } 100% { filter: brightness(1.5); text-shadow: 0 0 25px rgba(0,242,255,1); } }
|
||||
@keyframes border-pulse-red { 0% { border-color: rgba(255,62,62,0.4); box-shadow: 0 0 5px rgba(255,62,62,0.2); } 100% { border-color: rgba(255,62,62,1); box-shadow: 0 0 20px rgba(255,62,62,0.6); } }
|
||||
@keyframes border-pulse-cyan { 0% { border-color: rgba(0,242,255,0.4); box-shadow: 0 0 5px rgba(0,242,255,0.2); } 100% { border-color: rgba(0,242,255,1); box-shadow: 0 0 20px rgba(0,242,255,0.6); } }
|
||||
|
||||
.banner { height: 280px; background-size: cover; background-position: center; position: relative; z-index: 1; }
|
||||
.banner-overlay { position: absolute; inset: 0; background: linear-gradient(to bottom, transparent 0%, #050505 100%); }
|
||||
|
||||
.content-container { max-width: 1100px; margin: -100px auto 0; padding: 0 20px; position: relative; z-index: 10; }
|
||||
|
||||
/* Header */
|
||||
.profile-header { display: flex; align-items: flex-end; gap: 30px; margin-bottom: 50px; }
|
||||
|
||||
.avatar-wrapper { position: relative; }
|
||||
.avatar {
|
||||
width: 160px; height: 160px; border-radius: 50%; border: 6px solid #050505; background: #111;
|
||||
overflow: hidden; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 56px; font-weight: 900; color: #333; transition: 0.3s;
|
||||
}
|
||||
.avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.online-status { position: absolute; bottom: 12px; right: 12px; width: 24px; height: 24px; background: #00ff9d; border: 4px solid #050505; border-radius: 50%; box-shadow: 0 0 10px #00ff9d; }
|
||||
|
||||
.header-info { flex: 1; padding-bottom: 15px; }
|
||||
.name-row { display: flex; align-items: center; gap: 16px; margin-bottom: 10px; flex-wrap: wrap; }
|
||||
.username { font-size: 36px; font-weight: 900; color: #fff; letter-spacing: -1px; line-height: 1; display: flex; align-items: center; gap: 10px; }
|
||||
.clan-tag { color: inherit; opacity: 0.8; font-size: 0.6em; background: rgba(255,255,255,0.1); padding: 2px 8px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.1); vertical-align: middle; }
|
||||
|
||||
.badges { display: flex; gap: 8px; }
|
||||
.badge { font-size: 11px; font-weight: 900; padding: 4px 10px; border-radius: 6px; text-transform: uppercase; border: 1px solid transparent; letter-spacing: 0.5px; }
|
||||
|
||||
.bio { color: #888; font-size: 15px; max-width: 600px; line-height: 1.6; }
|
||||
|
||||
.header-actions { display: flex; gap: 12px; padding-bottom: 20px; }
|
||||
.btn-action, .btn-edit { display: flex; align-items: center; gap: 8px; padding: 12px 20px; border-radius: 12px; font-weight: 800; font-size: 13px; cursor: pointer; transition: 0.2s; border: none; }
|
||||
.btn-action { background: #111; border: 1px solid #333; color: #fff; }
|
||||
.btn-action:hover { background: #1a1a1a; border-color: #555; }
|
||||
.btn-action.like.active { color: #ff007a; border-color: #ff007a; background: rgba(255,0,122,0.1); }
|
||||
.btn-action.friend { background: #ff007a; color: #fff; border-color: #ff007a; }
|
||||
.btn-action.friend:hover { background: #d40065; }
|
||||
.btn-action.friend.added { background: #111; border-color: #333; color: #fff; }
|
||||
.btn-action.friend.accept { background: #00c853; border-color: #00c853; }
|
||||
.btn-action.friend.accept:hover { background: #00a844; }
|
||||
.btn-action.friend.decline { background: #111; border-color: #ff3e3e; color: #ff3e3e; }
|
||||
.btn-action.friend.decline:hover { background: rgba(255,62,62,0.1); }
|
||||
.btn-action.report:hover { color: #ff3e3e; border-color: #ff3e3e; }
|
||||
.btn-action.trophy:hover { color: #ffd700; border-color: #ffd700; }
|
||||
|
||||
.btn-edit { background: #111; border: 1px solid #333; color: #fff; text-decoration: none; }
|
||||
.btn-edit:hover { background: #1a1a1a; border-color: #444; }
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 50px; }
|
||||
.stat-card { background: #0f0f0f; border: 1px solid #1f1f1f; padding: 24px; border-radius: 20px; display: flex; align-items: center; gap: 16px; transition: 0.3s; position: relative; overflow: hidden; }
|
||||
.stat-card:hover { transform: translateY(-4px); border-color: #333; box-shadow: 0 10px 30px -10px rgba(0,0,0,0.5); }
|
||||
.stat-card.highlight { background: linear-gradient(135deg, rgba(255,0,122,0.05), transparent); border-color: rgba(255,0,122,0.2); }
|
||||
.stat-icon { width: 52px; height: 52px; background: #18181b; border-radius: 14px; display: flex; align-items: center; justify-content: center; color: #666; flex-shrink: 0; }
|
||||
.stat-card.highlight .stat-icon { color: #ff007a; background: rgba(255,0,122,0.1); }
|
||||
.stat-label { font-size: 11px; font-weight: 700; color: #666; text-transform: uppercase; margin-bottom: 4px; letter-spacing: 0.5px; }
|
||||
.stat-value { font-size: 20px; font-weight: 900; color: #fff; }
|
||||
.stat-sub { font-size: 11px; color: #555; margin-top: 2px; }
|
||||
.text-magenta { color: #ff007a; }
|
||||
.text-gold { color: #ffd700; }
|
||||
|
||||
/* Columns */
|
||||
.profile-cols { display: grid; grid-template-columns: 1fr 350px; gap: 30px; }
|
||||
|
||||
/* Wins Section */
|
||||
.col-wins h3, .col-comments h3 { font-size: 20px; font-weight: 800; color: #fff; margin-bottom: 24px; display: flex; align-items: center; gap: 10px; }
|
||||
.wins-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 20px; }
|
||||
|
||||
.win-card { background: #0f0f0f; border: 1px solid #1f1f1f; border-radius: 16px; overflow: hidden; transition: 0.3s; position: relative; }
|
||||
.win-card:hover { transform: translateY(-4px); border-color: #ffd700; box-shadow: 0 10px 40px -10px rgba(255, 215, 0, 0.15); }
|
||||
.win-img { height: 140px; background-size: cover; background-position: center; position: relative; }
|
||||
.win-img::after { content: ''; position: absolute; inset: 0; background: linear-gradient(to bottom, transparent, #0f0f0f); }
|
||||
|
||||
.win-info { padding: 16px; position: relative; z-index: 2; margin-top: -40px; }
|
||||
.win-amount { font-size: 20px; font-weight: 900; margin-bottom: 4px; text-shadow: 0 2px 10px rgba(0,0,0,0.8); }
|
||||
.win-game { font-size: 12px; color: #ccc; font-weight: 600; }
|
||||
.win-multi { position: absolute; top: 16px; right: 16px; background: rgba(255, 215, 0, 0.1); color: #ffd700; border: 1px solid rgba(255, 215, 0, 0.3); padding: 4px 8px; border-radius: 6px; font-size: 11px; font-weight: 800; }
|
||||
|
||||
.win-card.empty { height: 200px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; color: #333; border: 1px dashed #222; }
|
||||
.win-card.empty i { width: 32px; height: 32px; }
|
||||
|
||||
/* Comments */
|
||||
.comment-input { display: flex; gap: 10px; background: #111; padding: 10px; border-radius: 12px; border: 1px solid #222; margin-bottom: 20px; }
|
||||
.comment-input textarea { flex: 1; background: transparent; border: none; color: #fff; font-size: 13px; resize: none; outline: none; }
|
||||
.comment-input button { background: #ff007a; color: #fff; border: none; width: 36px; height: 36px; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
|
||||
.comment-input button:hover { background: #d40065; }
|
||||
|
||||
.comments-list { display: flex; flex-direction: column; gap: 16px; }
|
||||
.comment-item { display: flex; gap: 12px; }
|
||||
.c-avatar { width: 36px; height: 36px; border-radius: 10px; background: #222; display: flex; align-items: center; justify-content: center; font-weight: 700; color: #fff; overflow: hidden; flex-shrink: 0; }
|
||||
.c-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.c-content { background: #111; padding: 12px; border-radius: 12px; border: 1px solid #222; flex: 1; }
|
||||
.c-head { display: flex; justify-content: space-between; margin-bottom: 4px; }
|
||||
.c-name { font-size: 12px; font-weight: 700; color: #fff; }
|
||||
.c-time { font-size: 10px; color: #555; }
|
||||
.c-text { font-size: 13px; color: #ccc; line-height: 1.4; }
|
||||
.no-comments { text-align: center; color: #444; font-size: 12px; padding: 20px; }
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.8); z-index: 100; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(5px); }
|
||||
.modal-card { background: #111; border: 1px solid #333; padding: 24px; border-radius: 16px; width: 100%; max-width: 400px; }
|
||||
.modal-card h3 { color: #fff; font-size: 18px; font-weight: 800; margin-bottom: 16px; }
|
||||
.modal-card select { width: 100%; background: #000; border: 1px solid #333; color: #fff; padding: 12px; border-radius: 8px; margin-bottom: 20px; outline: none; }
|
||||
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
||||
.btn-cancel { background: transparent; border: 1px solid #333; color: #ccc; padding: 10px 16px; border-radius: 8px; cursor: pointer; }
|
||||
.btn-confirm { background: #ff3e3e; border: none; color: #fff; padding: 10px 16px; border-radius: 8px; cursor: pointer; font-weight: 700; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.stats-grid { grid-template-columns: 1fr 1fr; }
|
||||
.profile-cols { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.profile-header { flex-direction: column; align-items: center; text-align: center; margin-top: -80px; }
|
||||
.header-info { padding-bottom: 0; width: 100%; }
|
||||
.name-row { justify-content: center; }
|
||||
.header-actions { width: 100%; justify-content: center; flex-wrap: wrap; }
|
||||
.avatar { width: 120px; height: 120px; font-size: 40px; }
|
||||
.banner { height: 200px; }
|
||||
}
|
||||
|
||||
/* Extra small phones */
|
||||
@media (max-width: 420px) {
|
||||
.username { font-size: clamp(18px, 7vw, 24px); word-break: break-word; }
|
||||
.bio { font-size: 14px; }
|
||||
.wins-grid { grid-template-columns: 1fr; }
|
||||
.stats-grid { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
|
||||
/* ===== Report Modal ===== */
|
||||
.gc-report-overlay {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(8px) saturate(0.8);
|
||||
-webkit-backdrop-filter: blur(8px) saturate(0.8);
|
||||
z-index: 2147483647;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
.gc-report-modal {
|
||||
background: #0c0c0e;
|
||||
border: 1px solid rgba(255,255,255,0.07);
|
||||
border-radius: 22px;
|
||||
width: 100%; max-width: 460px;
|
||||
box-shadow: 0 40px 100px rgba(0,0,0,0.9), 0 0 0 1px rgba(223,0,106,0.05);
|
||||
overflow: hidden;
|
||||
animation: modal-pop 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
@keyframes modal-pop {
|
||||
from { opacity: 0; transform: scale(0.9) translateY(16px); }
|
||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
.modal-fade-enter-active { transition: opacity 0.25s ease; }
|
||||
.modal-fade-leave-active { transition: opacity 0.15s ease; }
|
||||
.modal-fade-enter-from, .modal-fade-leave-to { opacity: 0; }
|
||||
|
||||
.rm-head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 16px 18px;
|
||||
background: linear-gradient(135deg, rgba(223,0,106,0.12), rgba(0,0,0,0));
|
||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||
}
|
||||
.rm-head-left { display: flex; align-items: center; gap: 10px; }
|
||||
.rm-head-icon {
|
||||
width: 30px; height: 30px; border-radius: 9px;
|
||||
background: rgba(223,0,106,0.18); border: 1px solid rgba(223,0,106,0.35);
|
||||
color: #df006a; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.rm-title { font-size: 14px; font-weight: 800; color: #fff; letter-spacing: 0.2px; }
|
||||
.rm-close {
|
||||
width: 28px; height: 28px; border-radius: 8px;
|
||||
background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.08);
|
||||
color: #555; cursor: pointer; display: flex; align-items: center; justify-content: center;
|
||||
transition: 0.2s; flex-shrink: 0;
|
||||
}
|
||||
.rm-close:hover { background: rgba(255,255,255,0.1); color: #fff; }
|
||||
|
||||
.rm-user {
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
padding: 14px 18px;
|
||||
background: rgba(255,255,255,0.02);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
.rm-avatar-wrap { position: relative; flex-shrink: 0; }
|
||||
.rm-avatar {
|
||||
width: 52px; height: 52px; border-radius: 14px; overflow: hidden;
|
||||
border: 2px solid rgba(255,255,255,0.08); background: #151515; position: relative; z-index: 1;
|
||||
}
|
||||
.rm-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.rm-avatar-fallback {
|
||||
width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 20px; font-weight: 900; color: #555;
|
||||
}
|
||||
.rm-avatar-glow {
|
||||
position: absolute; inset: -4px; border-radius: 18px; z-index: 0;
|
||||
background: radial-gradient(circle, rgba(223,0,106,0.25), transparent 70%);
|
||||
filter: blur(6px);
|
||||
}
|
||||
.rm-user-info { display: flex; flex-direction: column; gap: 7px; min-width: 0; }
|
||||
.rm-username { font-size: 15px; font-weight: 800; color: #fff; }
|
||||
.rm-badges { display: flex; gap: 5px; flex-wrap: wrap; }
|
||||
.rm-badge {
|
||||
font-size: 9px; font-weight: 800; padding: 2px 8px; border-radius: 5px;
|
||||
text-transform: uppercase; letter-spacing: 0.5px; border: 1px solid;
|
||||
}
|
||||
.rm-badge.admin { color: #ff3e3e; border-color: rgba(255,62,62,0.35); background: rgba(255,62,62,0.08); }
|
||||
.rm-badge.staff { color: #3b82f6; border-color: rgba(59,130,246,0.35); background: rgba(59,130,246,0.08); }
|
||||
.rm-badge.streamer { color: #a855f7; border-color: rgba(168,85,247,0.35); background: rgba(168,85,247,0.08); }
|
||||
.rm-badge.user { color: #555; border-color: rgba(255,255,255,0.08); background: rgba(255,255,255,0.03); }
|
||||
.rm-badge.vip { color: #ffd700; border-color: rgba(255,215,0,0.35); background: rgba(255,215,0,0.08); }
|
||||
.rm-badge.clan { color: #00f2ff; border-color: rgba(0,242,255,0.35); background: rgba(0,242,255,0.08); }
|
||||
|
||||
.rm-section { padding: 14px 18px; border-bottom: 1px solid rgba(255,255,255,0.05); }
|
||||
.rm-section-label {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
font-size: 10px; font-weight: 700; text-transform: uppercase;
|
||||
letter-spacing: 0.7px; color: #444; margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.rm-reasons { display: flex; flex-direction: column; gap: 7px; }
|
||||
.rm-reason-btn {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 11px 14px; border-radius: 12px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
color: #888; cursor: pointer; transition: all 0.18s ease;
|
||||
text-align: left; width: 100%;
|
||||
}
|
||||
.rm-reason-btn:hover {
|
||||
background: rgba(255,255,255,0.06);
|
||||
border-color: rgba(255,255,255,0.12);
|
||||
color: #ddd;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
.rm-reason-btn.selected {
|
||||
background: rgba(var(--reason-color, 223 0 106) / 0.1);
|
||||
border-color: color-mix(in srgb, var(--reason-color, #df006a) 50%, transparent);
|
||||
color: #fff;
|
||||
}
|
||||
.reason-icon-wrap {
|
||||
width: 34px; height: 34px; border-radius: 10px;
|
||||
background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.06);
|
||||
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||||
transition: 0.18s;
|
||||
}
|
||||
.rm-reason-btn.selected .reason-icon-wrap {
|
||||
background: rgba(var(--reason-color, 223 0 106) / 0.15);
|
||||
border-color: color-mix(in srgb, var(--reason-color, #df006a) 40%, transparent);
|
||||
}
|
||||
.reason-text { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
|
||||
.reason-label { font-size: 13px; font-weight: 700; color: inherit; }
|
||||
.reason-desc { font-size: 11px; color: #555; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.rm-reason-btn.selected .reason-desc { color: #888; }
|
||||
.reason-check-wrap { color: #22c55e; flex-shrink: 0; }
|
||||
.reason-chevron { color: #333; flex-shrink: 0; transition: 0.18s; }
|
||||
.rm-reason-btn:hover .reason-chevron { color: #555; transform: translateX(2px); }
|
||||
|
||||
.rm-actions {
|
||||
display: flex; gap: 10px;
|
||||
padding: 14px 18px 18px;
|
||||
}
|
||||
.rm-btn-cancel {
|
||||
flex: 1; padding: 11px 14px; border-radius: 11px;
|
||||
background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08);
|
||||
color: #888; cursor: pointer; font-size: 13px; font-weight: 700;
|
||||
transition: 0.2s; display: flex; align-items: center; justify-content: center; gap: 7px;
|
||||
}
|
||||
.rm-btn-cancel:hover { background: rgba(255,255,255,0.08); color: #ccc; }
|
||||
.rm-btn-submit {
|
||||
flex: 2; padding: 11px 14px; border-radius: 11px;
|
||||
background: linear-gradient(135deg, #df006a, #a8004e);
|
||||
border: 1px solid rgba(223,0,106,0.5);
|
||||
color: #fff; cursor: pointer; font-size: 13px; font-weight: 800;
|
||||
transition: 0.2s; display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
box-shadow: 0 4px 20px rgba(223,0,106,0.25);
|
||||
}
|
||||
.rm-btn-submit:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #f2007a, #c0005c);
|
||||
box-shadow: 0 6px 30px rgba(223,0,106,0.45);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.rm-btn-submit:disabled { opacity: 0.35; cursor: not-allowed; box-shadow: none; }
|
||||
.rm-spin { animation: rm-spin 0.8s linear infinite; }
|
||||
@keyframes rm-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.report-success {
|
||||
padding: 44px 24px 40px;
|
||||
display: flex; flex-direction: column; align-items: center; gap: 14px; text-align: center;
|
||||
}
|
||||
.success-ring {
|
||||
width: 64px; height: 64px; border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(34,197,94,0.2), rgba(34,197,94,0.05));
|
||||
border: 2px solid rgba(34,197,94,0.4);
|
||||
color: #22c55e; display: flex; align-items: center; justify-content: center;
|
||||
box-shadow: 0 0 30px rgba(34,197,94,0.25);
|
||||
animation: success-bounce 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
@keyframes success-bounce {
|
||||
0% { transform: scale(0); opacity: 0; }
|
||||
60% { transform: scale(1.12); }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
.report-success h3 { font-size: 18px; font-weight: 900; color: #fff; margin: 0; }
|
||||
.report-success p { font-size: 13px; color: #666; margin: 0; line-height: 1.6; }
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.gc-report-overlay { align-items: flex-end; padding: 0; }
|
||||
.gc-report-modal { border-radius: 22px 22px 0 0; max-width: 100%; }
|
||||
.reason-desc { display: none; }
|
||||
}
|
||||
</style>
|
||||
508
resources/js/pages/Social/Settings.vue
Normal file
508
resources/js/pages/Social/Settings.vue
Normal file
@@ -0,0 +1,508 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useForm, Head } from '@inertiajs/vue3';
|
||||
import UserLayout from '@/layouts/user/userlayout.vue';
|
||||
import { useNotifications } from '@/composables/useNotifications';
|
||||
|
||||
// Declare route globally for TypeScript if needed, or assume it's available
|
||||
declare function route(name: string, params?: any): string;
|
||||
|
||||
const props = defineProps<{
|
||||
user: any;
|
||||
}>();
|
||||
|
||||
const { notify } = useNotifications();
|
||||
|
||||
// Helper to access global route function safely with fallback
|
||||
const getRoute = (name: string, params?: any) => {
|
||||
// @ts-ignore
|
||||
if (typeof window.route === 'function') {
|
||||
// @ts-ignore
|
||||
return window.route(name, params);
|
||||
}
|
||||
|
||||
// Fallback for known routes if Ziggy fails
|
||||
if (name === 'profile.update') return '/profile/update';
|
||||
if (name === 'profile.upload') return '/profile/upload';
|
||||
|
||||
console.error('Ziggy route function not found on window and no fallback for:', name);
|
||||
return '';
|
||||
};
|
||||
|
||||
const form = useForm({
|
||||
is_public: props.user.is_public || false,
|
||||
bio: props.user.bio || '',
|
||||
avatar: props.user.avatar || '',
|
||||
banner: props.user.banner || '',
|
||||
});
|
||||
|
||||
const isEditingBio = ref(false);
|
||||
const isEditingAvatar = ref(false);
|
||||
const isEditingBanner = ref(false);
|
||||
const isUploading = ref(false);
|
||||
|
||||
const save = () => {
|
||||
const url = getRoute('profile.update');
|
||||
if (!url) return;
|
||||
|
||||
form.post(url, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
isEditingBio.value = false;
|
||||
isEditingAvatar.value = false;
|
||||
isEditingBanner.value = false;
|
||||
notify({ type: 'green', title: 'Saved', desc: 'Profile updated successfully.', icon: 'check' });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const togglePublic = () => {
|
||||
form.is_public = !form.is_public;
|
||||
save();
|
||||
};
|
||||
|
||||
const handleUpload = async (event: Event, type: 'avatar' | 'banner') => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
isUploading.value = true;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('type', type);
|
||||
|
||||
try {
|
||||
const url = getRoute('profile.upload');
|
||||
if (!url) throw new Error('Route not found');
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content || '',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
let errorMsg = 'Upload failed';
|
||||
try {
|
||||
const errorData = await res.json();
|
||||
errorMsg = errorData.message || errorMsg;
|
||||
} catch {}
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
if (type === 'avatar') form.avatar = data.url;
|
||||
else form.banner = data.url;
|
||||
notify({ type: 'green', title: 'Uploaded', desc: `${type} updated successfully.`, icon: 'upload' });
|
||||
|
||||
// Close popover
|
||||
if (type === 'avatar') isEditingAvatar.value = false;
|
||||
if (type === 'banner') isEditingBanner.value = false;
|
||||
} catch (e: any) {
|
||||
notify({ type: 'red', title: 'Error', desc: e.message, icon: 'alert-triangle' });
|
||||
} finally {
|
||||
isUploading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Rank Logic
|
||||
const vipLevelsConfig = [
|
||||
{ name: 'Newbie', color: '#888888' },
|
||||
{ name: 'Bronze', color: '#cd7f32' },
|
||||
{ name: 'Silver', color: '#c0c0c0' },
|
||||
{ name: 'Gold', color: '#ffd700' },
|
||||
{ name: 'Platinum', color: '#00f2ff' },
|
||||
{ name: 'Diamond', color: '#ff007a' },
|
||||
{ name: 'Obsidian', color: '#ff3e3e' }
|
||||
];
|
||||
|
||||
const currentVipStyle = computed(() => {
|
||||
const level = props.user.vip_level || 0;
|
||||
const idx = Math.min(Math.max(level, 0), vipLevelsConfig.length - 1);
|
||||
return vipLevelsConfig[idx];
|
||||
});
|
||||
|
||||
// Role Logic
|
||||
const roleConfig = computed(() => {
|
||||
const role = props.user.role || 'User';
|
||||
let color = '#888';
|
||||
let effectClass = '';
|
||||
|
||||
if (role === 'Admin') {
|
||||
color = '#ff3e3e';
|
||||
effectClass = 'role-admin';
|
||||
} else if (role === 'Mod' || role === 'Staff') {
|
||||
color = '#00f2ff';
|
||||
effectClass = 'role-staff';
|
||||
} else if (role === 'Streamer') {
|
||||
color = '#a855f7';
|
||||
effectClass = 'role-streamer';
|
||||
}
|
||||
|
||||
return { name: role, color, effectClass };
|
||||
});
|
||||
|
||||
// Profile URL Logic
|
||||
const profileUrl = computed(() => {
|
||||
return `${window.location.origin}/profile/${props.user.username}`;
|
||||
});
|
||||
|
||||
const copyProfileUrl = () => {
|
||||
navigator.clipboard.writeText(profileUrl.value);
|
||||
notify({ type: 'green', title: 'Copied', desc: 'Profile URL copied to clipboard.', icon: 'copy' });
|
||||
};
|
||||
|
||||
// Mock stats for preview
|
||||
const stats = {
|
||||
wagered: 12500.50,
|
||||
wins: 450,
|
||||
losses: 320,
|
||||
favorite_game: 'Gates of Olympus',
|
||||
last_played: 'Sweet Bonanza',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UserLayout>
|
||||
<Head title="Edit Profile" />
|
||||
|
||||
<div class="edit-profile-page" :class="roleConfig.effectClass">
|
||||
<div class="bg-fx"></div>
|
||||
|
||||
<!-- Banner Editor -->
|
||||
<div class="banner-editor group" :style="{ backgroundImage: `url(${form.banner || '/img/default-banner.jpg'})` }">
|
||||
<div class="banner-overlay"></div>
|
||||
|
||||
<div class="edit-trigger" @click="isEditingBanner = !isEditingBanner">
|
||||
<i data-lucide="camera"></i>
|
||||
<span>Edit Banner</span>
|
||||
</div>
|
||||
|
||||
<div v-if="isEditingBanner" class="edit-popover banner-pop">
|
||||
<div class="pop-tabs">
|
||||
<label class="pop-tab">
|
||||
<input type="file" class="hidden" accept=".jpg,.jpeg,.png,.gif,.webp,.bmp,image/jpeg,image/png,image/gif,image/webp,image/bmp" @change="handleUpload($event, 'banner')">
|
||||
<i data-lucide="upload"></i> Upload
|
||||
</label>
|
||||
<div class="pop-divider">or</div>
|
||||
<input type="text" v-model="form.banner" placeholder="Paste URL..." class="edit-input" @keyup.enter="save">
|
||||
<button @click="save" class="save-btn"><i data-lucide="check"></i></button>
|
||||
</div>
|
||||
<p class="upload-hint">✓ JPG ✓ PNG ✓ GIF (animiert) ✓ WebP ✓ BMP · Max. <b style="color:#888">5 MB</b></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-container">
|
||||
<!-- Header Section -->
|
||||
<div class="profile-header">
|
||||
|
||||
<!-- Avatar Editor -->
|
||||
<div class="avatar-wrapper group">
|
||||
<div class="avatar" :style="{ borderColor: roleConfig.name !== 'User' ? roleConfig.color : currentVipStyle.color, boxShadow: `0 0 20px ${roleConfig.name !== 'User' ? roleConfig.color : currentVipStyle.color}40` }">
|
||||
<img v-if="form.avatar" :src="form.avatar" alt="Avatar">
|
||||
<span v-else>{{ user.username.charAt(0) }}</span>
|
||||
<div v-if="isUploading" class="upload-overlay"><span class="spinner"></span></div>
|
||||
</div>
|
||||
<div class="avatar-edit-overlay" @click="isEditingAvatar = !isEditingAvatar">
|
||||
<i data-lucide="camera"></i>
|
||||
</div>
|
||||
|
||||
<div v-if="isEditingAvatar" class="edit-popover avatar-pop">
|
||||
<div class="pop-tabs">
|
||||
<label class="pop-tab">
|
||||
<input type="file" class="hidden" accept=".jpg,.jpeg,.png,.gif,.webp,.bmp,image/jpeg,image/png,image/gif,image/webp,image/bmp" @change="handleUpload($event, 'avatar')">
|
||||
<i data-lucide="upload"></i>
|
||||
</label>
|
||||
<input type="text" v-model="form.avatar" placeholder="URL..." class="edit-input" @keyup.enter="save">
|
||||
<button @click="save" class="save-btn"><i data-lucide="check"></i></button>
|
||||
</div>
|
||||
<p class="upload-hint">✓ JPG ✓ PNG ✓ GIF (animiert) ✓ WebP ✓ BMP · Max. <b style="color:#888">5 MB</b></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-info">
|
||||
<div class="name-row">
|
||||
<h1 class="username" :class="roleConfig.effectClass" :style="{ color: roleConfig.name !== 'User' ? roleConfig.color : '#fff' }">
|
||||
<span v-if="user.clan_tag" class="clan-tag">[{{ user.clan_tag }}]</span>
|
||||
{{ user.username }}
|
||||
</h1>
|
||||
<div class="badges">
|
||||
<span class="badge vip" :style="{ color: currentVipStyle.color, borderColor: currentVipStyle.color, background: `${currentVipStyle.color}15` }">
|
||||
VIP {{ user.vip_level || 0 }}
|
||||
</span>
|
||||
<span class="badge role" :class="roleConfig.effectClass" :style="{ color: roleConfig.color, borderColor: roleConfig.color, background: `${roleConfig.color}15` }">
|
||||
{{ roleConfig.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bio Editor -->
|
||||
<div class="bio-editor">
|
||||
<div v-if="!isEditingBio" class="bio-display" @click="isEditingBio = true">
|
||||
{{ form.bio || 'Click here to add a bio...' }}
|
||||
<i data-lucide="edit-2" class="edit-icon"></i>
|
||||
</div>
|
||||
<div v-else class="bio-input-wrap">
|
||||
<textarea v-model="form.bio" rows="2" class="bio-input" placeholder="Tell us about yourself..." maxlength="160"></textarea>
|
||||
<div class="bio-actions">
|
||||
<span class="char-count">{{ form.bio.length }}/160</span>
|
||||
<button @click="save" class="bio-save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<div class="public-toggle" @click="togglePublic">
|
||||
<span class="toggle-label">Public Profile</span>
|
||||
<div class="toggle-switch" :class="{ active: form.is_public }">
|
||||
<div class="toggle-knob"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Copy Profile URL -->
|
||||
<div class="url-copy-box" @click="copyProfileUrl">
|
||||
<i data-lucide="link"></i>
|
||||
<span>Copy Link</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Grid (Preview) -->
|
||||
<div class="stats-grid opacity-50 pointer-events-none grayscale">
|
||||
<div class="stat-card highlight">
|
||||
<div class="stat-icon"><i data-lucide="coins"></i></div>
|
||||
<div class="stat-data">
|
||||
<div class="stat-label">Total Wagered</div>
|
||||
<div class="stat-value">${{ stats.wagered.toFixed(2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon"><i data-lucide="trophy"></i></div>
|
||||
<div class="stat-data">
|
||||
<div class="stat-label">Total Wins</div>
|
||||
<div class="stat-value">{{ stats.wins }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon"><i data-lucide="heart"></i></div>
|
||||
<div class="stat-data">
|
||||
<div class="stat-label">Favorite Game</div>
|
||||
<div class="stat-value text-magenta">{{ stats.favorite_game }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon"><i data-lucide="clock"></i></div>
|
||||
<div class="stat-data">
|
||||
<div class="stat-label">Last Played</div>
|
||||
<div class="stat-value">{{ stats.last_played }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-hint">
|
||||
<i data-lucide="info"></i> Stats are just a preview here.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</UserLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.edit-profile-page { min-height: 100vh; background: #050505; padding-bottom: 100px; position: relative; overflow: hidden; }
|
||||
|
||||
/* Role Effects & Backgrounds */
|
||||
.bg-fx { position: absolute; inset: 0; pointer-events: none; z-index: 0; opacity: 0; transition: opacity 0.5s; }
|
||||
|
||||
.role-admin .bg-fx {
|
||||
opacity: 1;
|
||||
background:
|
||||
radial-gradient(circle at 20% 30%, rgba(255, 62, 62, 0.08), transparent 50%),
|
||||
radial-gradient(circle at 80% 70%, rgba(255, 62, 62, 0.05), transparent 50%);
|
||||
animation: pulse-bg-red 5s infinite alternate;
|
||||
}
|
||||
.role-staff .bg-fx {
|
||||
opacity: 1;
|
||||
background:
|
||||
radial-gradient(circle at 20% 30%, rgba(0, 242, 255, 0.08), transparent 50%),
|
||||
radial-gradient(circle at 80% 70%, rgba(0, 242, 255, 0.05), transparent 50%);
|
||||
animation: pulse-bg-cyan 5s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes pulse-bg-red { 0% { opacity: 0.8; } 100% { opacity: 1; } }
|
||||
@keyframes pulse-bg-cyan { 0% { opacity: 0.8; } 100% { opacity: 1; } }
|
||||
|
||||
/* Glitter Text */
|
||||
.username.role-admin { text-shadow: 0 0 15px rgba(255, 62, 62, 0.6); animation: glitter-red 3s infinite; }
|
||||
.username.role-staff { text-shadow: 0 0 15px rgba(0, 242, 255, 0.6); animation: glitter-cyan 3s infinite; }
|
||||
|
||||
/* Badge Glitter */
|
||||
.badge.role.role-admin { box-shadow: 0 0 10px rgba(255, 62, 62, 0.4); animation: border-pulse-red 2s infinite; }
|
||||
.badge.role.role-staff { box-shadow: 0 0 10px rgba(0, 242, 255, 0.4); animation: border-pulse-cyan 2s infinite; }
|
||||
|
||||
@keyframes glitter-red { 0%, 100% { filter: brightness(1); } 50% { filter: brightness(1.3); } }
|
||||
@keyframes glitter-cyan { 0%, 100% { filter: brightness(1); } 50% { filter: brightness(1.3); } }
|
||||
@keyframes border-pulse-red { 0%, 100% { border-color: rgba(255,62,62,0.3); } 50% { border-color: rgba(255,62,62,0.8); } }
|
||||
@keyframes border-pulse-cyan { 0%, 100% { border-color: rgba(0,242,255,0.3); } 50% { border-color: rgba(0,242,255,0.8); } }
|
||||
|
||||
/* Banner Editor */
|
||||
.banner-editor {
|
||||
height: 300px; background-size: cover; background-position: center; position: relative;
|
||||
border-bottom: 1px solid #222; transition: 0.3s; z-index: 1;
|
||||
}
|
||||
.banner-editor:hover .banner-overlay { opacity: 0.4; }
|
||||
.banner-overlay { position: absolute; inset: 0; background: #000; opacity: 0.2; transition: 0.3s; }
|
||||
|
||||
.edit-trigger {
|
||||
position: absolute; top: 20px; right: 20px; background: rgba(0,0,0,0.6); backdrop-filter: blur(10px);
|
||||
padding: 10px 16px; border-radius: 12px; color: #fff; font-weight: 700; font-size: 13px;
|
||||
display: flex; align-items: center; gap: 8px; cursor: pointer; border: 1px solid rgba(255,255,255,0.1);
|
||||
transition: 0.2s; opacity: 0; transform: translateY(-10px);
|
||||
}
|
||||
.banner-editor:hover .edit-trigger { opacity: 1; transform: translateY(0); }
|
||||
.edit-trigger:hover { background: rgba(255,0,122,0.8); border-color: #ff007a; }
|
||||
.edit-trigger i { width: 16px; height: 16px; }
|
||||
|
||||
/* Edit Popover */
|
||||
.edit-popover {
|
||||
position: absolute; background: #111; border: 1px solid #333; padding: 8px; border-radius: 12px;
|
||||
display: flex; flex-direction: column; gap: 8px; box-shadow: 0 10px 40px rgba(0,0,0,0.5); z-index: 20;
|
||||
animation: popIn 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.banner-pop { top: 70px; right: 20px; width: 320px; }
|
||||
.avatar-pop { bottom: -60px; left: 50%; transform: translateX(-50%); width: 280px; }
|
||||
|
||||
.pop-tabs { display: flex; align-items: center; gap: 8px; }
|
||||
.pop-tab {
|
||||
background: #222; color: #ccc; padding: 8px 12px; border-radius: 8px; font-size: 12px; font-weight: 700;
|
||||
cursor: pointer; display: flex; align-items: center; gap: 6px; transition: 0.2s;
|
||||
}
|
||||
.pop-tab:hover { background: #333; color: #fff; }
|
||||
.pop-tab i { width: 14px; }
|
||||
.pop-divider { font-size: 10px; color: #555; font-weight: 700; text-transform: uppercase; }
|
||||
.upload-hint { font-size: 10px; color: #555; margin: 0; text-align: center; }
|
||||
|
||||
.edit-input {
|
||||
flex: 1; background: #000; border: 1px solid #222; color: #fff; padding: 8px 12px; border-radius: 8px; font-size: 13px; width: 100%;
|
||||
}
|
||||
.edit-input:focus { border-color: #ff007a; outline: none; }
|
||||
.save-btn {
|
||||
background: #ff007a; color: #fff; border: none; width: 36px; height: 36px; border-radius: 8px; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||||
}
|
||||
.save-btn:hover { background: #d40065; }
|
||||
|
||||
/* Content Container */
|
||||
.content-container { max-width: 1100px; margin: -80px auto 0; padding: 0 20px; position: relative; z-index: 10; }
|
||||
|
||||
/* Header */
|
||||
.profile-header { display: flex; align-items: flex-end; gap: 30px; margin-bottom: 40px; }
|
||||
|
||||
/* Avatar Editor */
|
||||
.avatar-wrapper { position: relative; cursor: pointer; }
|
||||
.avatar {
|
||||
width: 160px; height: 160px; border-radius: 50%; border: 6px solid #050505; background: #111;
|
||||
overflow: hidden; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 48px; font-weight: 900; color: #333; box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
||||
transition: 0.3s; position: relative;
|
||||
}
|
||||
.avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.upload-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; }
|
||||
.spinner { width: 24px; height: 24px; border: 3px solid rgba(255,255,255,0.3); border-top-color: #fff; border-radius: 50%; animation: spin 1s linear infinite; }
|
||||
|
||||
.avatar-edit-overlay {
|
||||
position: absolute; inset: 6px; border-radius: 50%; background: rgba(0,0,0,0.6);
|
||||
display: flex; align-items: center; justify-content: center; color: #fff; opacity: 0;
|
||||
transition: 0.2s; backdrop-filter: blur(2px);
|
||||
}
|
||||
.avatar-wrapper:hover .avatar-edit-overlay { opacity: 1; }
|
||||
.avatar-edit-overlay i { width: 32px; height: 32px; }
|
||||
|
||||
/* Info */
|
||||
.header-info { flex: 1; padding-bottom: 10px; }
|
||||
.name-row { display: flex; align-items: center; gap: 16px; margin-bottom: 12px; flex-wrap: wrap; }
|
||||
.username { font-size: 32px; font-weight: 900; color: #fff; display: flex; align-items: center; gap: 10px; }
|
||||
.clan-tag { color: inherit; opacity: 0.8; font-size: 0.6em; background: rgba(255,255,255,0.1); padding: 2px 8px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.1); vertical-align: middle; }
|
||||
|
||||
.badge.vip { font-size: 11px; font-weight: 900; padding: 4px 10px; border-radius: 6px; text-transform: uppercase; background: rgba(255,0,122,0.1); color: #ff007a; border: 1px solid rgba(255,0,122,0.2); }
|
||||
.badges { display: flex; gap: 8px; }
|
||||
.badge { font-size: 11px; font-weight: 900; padding: 4px 10px; border-radius: 6px; text-transform: uppercase; border: 1px solid transparent; letter-spacing: 0.5px; }
|
||||
|
||||
/* Bio Editor */
|
||||
.bio-display {
|
||||
color: #888; font-size: 15px; max-width: 600px; line-height: 1.5; cursor: pointer;
|
||||
padding: 8px 12px; border-radius: 8px; border: 1px dashed transparent; transition: 0.2s;
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.bio-display:hover { border-color: #333; background: #111; color: #aaa; }
|
||||
.edit-icon { width: 14px; height: 14px; opacity: 0.5; }
|
||||
|
||||
.bio-input-wrap { max-width: 600px; background: #111; border: 1px solid #333; border-radius: 12px; padding: 12px; }
|
||||
.bio-input { width: 100%; background: transparent; border: none; color: #fff; font-size: 15px; resize: none; font-family: inherit; }
|
||||
.bio-actions { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; }
|
||||
.char-count { font-size: 11px; color: #555; }
|
||||
.bio-save { background: #ff007a; color: #fff; border: none; padding: 6px 16px; border-radius: 6px; font-size: 12px; font-weight: 700; cursor: pointer; }
|
||||
.bio-save:hover { background: #d40065; }
|
||||
|
||||
/* Public Toggle & Copy */
|
||||
.header-actions { padding-bottom: 20px; display: flex; flex-direction: column; gap: 12px; align-items: flex-end; }
|
||||
.public-toggle {
|
||||
display: flex; align-items: center; gap: 12px; background: #111; padding: 10px 16px;
|
||||
border-radius: 12px; border: 1px solid #222; cursor: pointer; transition: 0.2s;
|
||||
}
|
||||
.public-toggle:hover { border-color: #333; }
|
||||
.toggle-label { font-size: 13px; font-weight: 700; color: #ccc; }
|
||||
.toggle-switch { width: 44px; height: 24px; background: #333; border-radius: 99px; position: relative; transition: 0.3s; }
|
||||
.toggle-switch.active { background: #ff007a; }
|
||||
.toggle-knob {
|
||||
width: 18px; height: 18px; background: #fff; border-radius: 50%; position: absolute;
|
||||
top: 3px; left: 3px; transition: 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.toggle-switch.active .toggle-knob { transform: translateX(20px); }
|
||||
|
||||
.url-copy-box {
|
||||
display: flex; align-items: center; gap: 8px; background: #111; padding: 8px 12px;
|
||||
border-radius: 8px; border: 1px solid #222; cursor: pointer; transition: 0.2s;
|
||||
color: #888; font-size: 12px; font-weight: 700;
|
||||
}
|
||||
.url-copy-box:hover { color: #fff; border-color: #333; }
|
||||
.url-copy-box i { width: 14px; height: 14px; }
|
||||
|
||||
/* Stats Grid (Preview) */
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 20px; }
|
||||
.stat-card { background: #0f0f0f; border: 1px solid #1f1f1f; padding: 20px; border-radius: 16px; display: flex; align-items: center; gap: 16px; }
|
||||
.stat-card.highlight { background: linear-gradient(135deg, rgba(255,0,122,0.05), transparent); border-color: rgba(255,0,122,0.2); }
|
||||
.stat-icon { width: 48px; height: 48px; background: #18181b; border-radius: 12px; display: flex; align-items: center; justify-content: center; color: #666; }
|
||||
.stat-card.highlight .stat-icon { color: #ff007a; background: rgba(255,0,122,0.1); }
|
||||
.stat-label { font-size: 11px; font-weight: 700; color: #666; text-transform: uppercase; margin-bottom: 4px; }
|
||||
.stat-value { font-size: 18px; font-weight: 900; color: #fff; }
|
||||
.text-magenta { color: #ff007a; }
|
||||
|
||||
.preview-hint { text-align: center; color: #444; font-size: 12px; display: flex; align-items: center; justify-content: center; gap: 6px; margin-top: 20px; }
|
||||
.preview-hint i { width: 14px; }
|
||||
|
||||
@keyframes popIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.profile-header { flex-direction: column; align-items: center; text-align: center; margin-top: -60px; }
|
||||
.header-info { padding-bottom: 0; width: 100%; display: flex; flex-direction: column; align-items: center; }
|
||||
.name-row { justify-content: center; }
|
||||
.stats-grid { grid-template-columns: 1fr 1fr; }
|
||||
.banner-editor { height: 200px; }
|
||||
.avatar { width: 120px; height: 120px; }
|
||||
}
|
||||
|
||||
/* Mobile tweaks */
|
||||
@media (max-width: 420px) {
|
||||
.username { font-size: clamp(18px, 7vw, 24px); word-break: break-word; }
|
||||
.stats-grid { grid-template-columns: 1fr 1fr; }
|
||||
.banner-pop { width: min(92vw, 320px); right: 12px; }
|
||||
.avatar-pop { width: min(92vw, 280px); left: 50%; transform: translateX(-50%); }
|
||||
.edit-trigger { padding: 8px 12px; font-size: 12px; top: 12px; right: 12px; }
|
||||
.content-container { margin-top: -50px; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user