-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathview.html
More file actions
146 lines (138 loc) · 11.1 KB
/
view.html
File metadata and controls
146 lines (138 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LinkStack</title>
<meta name="description" id="meta-description" content="Check out this profile on LinkStack.">
<meta property="og:type" content="website">
<meta property="og:title" id="og-title" content="LinkStack Profile">
<meta property="og:description" id="og-description" content="Everything you are in one simple link.">
<meta property="og:image" id="og-image" content="">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" id="twitter-title" content="LinkStack Profile">
<meta name="twitter:description" id="twitter-description" content="Everything you are in one simple link.">
<meta name="twitter:image" id="twitter-image" content="">
<link id="dynamic-favicon" rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236366f1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polygon points='12 2 2 7 12 12 22 7 12 2'/><polyline points='2 17 12 22 22 17'/><polyline points='2 12 12 17 22 12'/></svg>">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>
<script src="https://unpkg.com/feather-icons"></script>
<style>
body { transition: background 0.5s ease, color 0.5s ease; }
.link-card { transition: transform 0.2s, background 0.2s, box-shadow 0.2s; }
.link-card:hover { transform: scale(1.02); }
.social-btn { transition: transform 0.2s, opacity 0.2s; }
.social-btn:hover { transform: translateY(-2px); opacity: 1; }
.badge-tooltip { position: relative; display: inline-block; cursor: help; }
.badge-tooltip .tooltiptext { visibility: hidden; width: 140px; background-color: #0f172a; color: #fff; text-align: center; border-radius: 8px; padding: 8px; position: absolute; z-index: 50; bottom: 130%; left: 50%; transform: translateX(-50%); opacity: 0; transition: opacity 0.3s; font-size: 10px; font-family: sans-serif; font-weight: bold; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3); pointer-events: none; }
.badge-tooltip .tooltiptext::after { content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: #0f172a transparent transparent transparent; }
.badge-tooltip:hover .tooltiptext { visibility: visible; opacity: 1; }
</style>
</head>
<body class="min-h-screen flex flex-col items-center p-8">
<div id="loading" class="fixed inset-0 bg-white z-50 flex items-center justify-center">
<div class="animate-spin text-indigo-600"><i data-feather="loader"></i></div>
</div>
<main id="profile-content" class="w-full max-w-md text-center hidden">
<div class="relative inline-block mb-4">
<img id="avatar" class="w-24 h-24 rounded-full border-4 border-white/20 shadow-2xl object-cover mx-auto">
<div id="badge-container" class="absolute -right-1 bottom-0"></div>
</div>
<h1 id="name" class="text-2xl font-black tracking-tight mb-2"></h1>
<p id="bio" class="text-sm opacity-80 mb-6 px-4"></p>
<div id="socials-container" class="flex flex-wrap justify-center gap-4 mb-8"></div>
<div id="links-container" class="w-full space-y-4"></div>
<footer class="mt-12 opacity-50 text-[10px] font-bold uppercase tracking-widest">
<a href="/" class="flex items-center justify-center gap-2">
Created with <i data-feather="layers" class="w-3 h-3"></i> LinkStack
</a>
</footer>
</main>
<script>
const SUPABASE_URL = 'https://xcicvuqdoztxhidwdmlc.supabase.co';
const SUPABASE_ANON_KEY = 'sb_publishable_DeSw5UHzjV444ZF6eMkB0g_taV-dynR';
const supabaseClient = supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
const goldBadge = `<svg width="24" height="24" viewBox="0 0 512 512"><g fill="none" fill-rule="evenodd"><path d="M256 472.153L176.892 512l-41.725-81.129-86.275-16.654 11.596-91.422L0 256l60.488-66.795-11.596-91.422 86.275-16.654L176.892 0 256 39.847 335.108 0l41.725 81.129 86.275 16.654-11.596 91.422L512 256l-60.488 66.795 11.596 91.422-86.275 16.654L335.108 512z" fill="#EFBF04"></path><path d="M211.824 284.5L171 243.678l-36 36 40.824 40.824-.063.062 36 36 .063-.062.062.062 36-36-.062-.062L376.324 192l-36-36z" fill="#fff"></path></g></svg>`;
const standardBadge = `<svg width="24" height="24" viewBox="0 0 512 512"><g fill="none" fill-rule="evenodd"><path d="M256 472.153L176.892 512l-41.725-81.129-86.275-16.654 11.596-91.422L0 256l60.488-66.795-11.596-91.422 86.275-16.654L176.892 0 256 39.847 335.108 0l41.725 81.129 86.275 16.654-11.596 91.422L512 256l-60.488 66.795 11.596 91.422-86.275 16.654L335.108 512z" fill="#4285f4"/><path d="M211.824 284.5L171 243.678l-36 36 40.824 40.824-.063.062 36 36 .063-.062.062.062 36-36-.062-.062L376.324 192l-36-36z" fill="#fff"/></g></svg>`;
const themeConfig = {
default: { body: 'bg-slate-900 text-white', card: 'bg-white/10 border-white/10 text-white', social: 'border-white/20' },
sunset: { body: 'bg-gradient-to-br from-orange-400 to-pink-600 text-white', card: 'bg-white text-orange-600 border-transparent shadow-lg', social: 'bg-white/20 border-white/30' },
glass: { body: 'bg-[url(https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe)] bg-cover bg-center text-white', card: 'bg-white/10 backdrop-blur-xl border-white/20 text-white', social: 'bg-white/10 border-white/20' },
neo: { body: 'bg-white text-slate-900', card: 'bg-white border-2 border-slate-900 shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] text-slate-900', social: 'border-slate-900' },
cyber: { body: 'bg-black text-[#00ff41] font-mono', card: 'bg-black border border-[#00ff41] text-[#00ff41] shadow-[0_0_10px_rgba(0,255,65,0.3)]', social: 'border-[#00ff41]' },
forest: { body: 'bg-[#1b2e1b] text-[#e8f5e9]', card: 'bg-[#2d4a2d] border-[#3e663e] text-[#e8f5e9]', social: 'bg-[#2d4a2d] border-[#3e663e]' }
};
const socialMap = {
instagram: { icon: 'instagram', url: (v) => `https://instagram.com/${v}` },
youtube: { icon: 'youtube', url: (v) => `https://youtube.com/@${v}` },
twitter: { icon: 'twitter', url: (v) => `https://x.com/${v}` },
tiktok: { icon: 'music', url: (v) => `https://tiktok.com/@${v}` },
linkedin: { icon: 'linkedin', url: (v) => `https://linkedin.com/in/${v}` },
twitch: { icon: 'tv', url: (v) => `https://twitch.tv/${v}` },
discord: { icon: 'hash', url: (v) => `https://discord.gg/${v}` },
spotify: { icon: 'disc', url: (v) => v.includes('http') ? v : `https://open.spotify.com/user/${v}` }
};
async function loadProfile() {
const params = new URLSearchParams(window.location.search);
const handle = params.get('u');
if (!handle) return window.location.href = '/';
const { data: profile } = await supabaseClient.from('profiles').select('*').eq('handle', handle.toLowerCase()).single();
if (!profile) return window.location.href = '/';
const { data: links } = await supabaseClient.from('links').select('*').eq('profile_id', profile.id).order('order_index', { ascending: true });
const pageTitle = `${profile.handle} on LinkStack`;
const pageDesc = profile.bio || `View ${profile.handle}'s profile on LinkStack.`;
const avatarUrl = profile.avatar_url || `https://ui-avatars.com/api/?name=${profile.handle}&background=6366f1&color=fff`;
document.title = pageTitle;
document.getElementById('meta-description').content = pageDesc;
document.getElementById('og-title').content = pageTitle;
document.getElementById('og-description').content = pageDesc;
document.getElementById('og-image').content = avatarUrl;
document.getElementById('twitter-title').content = pageTitle;
document.getElementById('twitter-description').content = pageDesc;
document.getElementById('twitter-image').content = avatarUrl;
const favicon = document.getElementById('dynamic-favicon');
if (profile.avatar_url) {
favicon.href = profile.avatar_url;
favicon.type = "image/x-icon";
}
renderProfile(profile, links || []);
logVisit(profile.id);
}
function renderProfile(p, links) {
const theme = themeConfig[p.theme_id] || themeConfig.default;
document.body.className = `min-h-screen flex flex-col items-center p-8 ${theme.body}`;
document.getElementById('name').innerText = `@${p.handle}`;
document.getElementById('bio').innerText = p.bio || '';
document.getElementById('avatar').src = p.avatar_url || `https://ui-avatars.com/api/?name=${p.handle}&background=6366f1&color=fff`;
if (p.is_verified) {
const badgeHtml = p.handle === 'james' ? `<div class="badge-tooltip">${goldBadge}<span class="tooltiptext">Official account of LinkStack</span></div>` : `<div class="badge-tooltip">${standardBadge}<span class="tooltiptext">Verified Business Account</span></div>`;
document.getElementById('badge-container').innerHTML = badgeHtml;
}
const sd = p.socials_data || {};
const socialsHtml = Object.entries(sd).filter(([key, val]) => val && socialMap[key]).map(([key, val]) => {
const cfg = socialMap[key];
return `<a href="${cfg.url(val)}" target="_blank" class="w-10 h-10 rounded-full flex items-center justify-center border social-btn ${theme.social} opacity-80"><i data-feather="${cfg.icon}" class="w-5 h-5"></i></a>`;
}).join('');
document.getElementById('socials-container').innerHTML = socialsHtml;
document.getElementById('links-container').innerHTML = links.map(l => `<button onclick="handleLinkClick('${p.id}', '${l.id}', '${l.url}')" class="w-full p-4 rounded-2xl font-black text-sm border link-card ${theme.card}">${l.label}</button>`).join('');
document.getElementById('loading').classList.add('hidden');
document.getElementById('profile-content').classList.remove('hidden');
feather.replace();
}
async function logVisit(profileId) {
let country = 'XX';
try {
const res = await fetch('https://ipapi.co/json/');
const json = await res.json();
country = json.country_code;
} catch (e) {}
await supabaseClient.from('visits').insert([{ profile_id: profileId, country_code: country }]);
}
async function handleLinkClick(profileId, linkId, url) {
await supabaseClient.from('clicks').insert([{ profile_id: profileId, link_id: linkId }]);
window.open(url, '_blank');
}
loadProfile();
</script>
</body>
</html>