|
<html> |
|
<head> |
|
<!-- WRITE A NEW URL, DON'T USE WEBSIM.AI --> |
|
<base href="https://websim.ai" /> |
|
<title>websim.ai</title> |
|
<script> |
|
if ( |
|
document.cookie |
|
.split("; ") |
|
.find((row) => row.startsWith("nosleep=")) |
|
?.split("=")[1] === "true" |
|
) { |
|
document.documentElement.classList.add("dark"); |
|
} |
|
</script> |
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> |
|
<style> |
|
*, |
|
*::before, |
|
*::after { |
|
box-sizing: border-box; |
|
} |
|
a { |
|
color: inherit; |
|
text-decoration: none; |
|
} |
|
body { |
|
margin: 0; |
|
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, |
|
segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, |
|
arial, sans-serif; |
|
} |
|
body::-webkit-scrollbar { |
|
display: none; |
|
} |
|
.dark body { |
|
color: #f3f4f6; |
|
background: #171717; |
|
} |
|
.logo { |
|
font-family: "Comic Sans MS", cursive; |
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); |
|
letter-spacing: 0.1em; |
|
font-weight: bold; |
|
} |
|
.logo span { |
|
display: inline-block; |
|
} |
|
.logo span:nth-child(1) { |
|
color: #4285f4; |
|
transform: rotate(-10deg); |
|
} |
|
.logo span:nth-child(2) { |
|
color: #ea4335; |
|
transform: rotate(5deg) translateY(-0.2rem); |
|
} |
|
.logo span:nth-child(3) { |
|
color: #fbbc05; |
|
transform: rotate(-5deg) translateY(0.1rem); |
|
} |
|
.logo span:nth-child(4) { |
|
color: #4285f4; |
|
transform: rotate(10deg) translateY(-0.2rem); |
|
} |
|
.logo span:nth-child(5) { |
|
color: #34a853; |
|
transform: rotate(-10deg) translateY(0.2rem); |
|
} |
|
.logo span:nth-child(6) { |
|
color: #ea4335; |
|
transform: rotate(5deg) translateY(-0.2rem); |
|
} |
|
.dark .logo span:nth-child(1) { |
|
color: rgb(254, 240, 138); |
|
} |
|
.dark .logo span:nth-child(2) { |
|
color: rgb(153, 246, 228); |
|
} |
|
.dark .logo span:nth-child(3) { |
|
color: rgb(56, 189, 248); |
|
} |
|
.dark .logo span:nth-child(4) { |
|
color: rgb(254, 240, 138); |
|
} |
|
.dark .logo span:nth-child(5) { |
|
color: rgb(249, 168, 212); |
|
} |
|
.dark .logo span:nth-child(6) { |
|
color: rgb(153, 246, 228); |
|
} |
|
.login { |
|
display: block; |
|
padding: 4px 8px; |
|
border-radius: 5px; |
|
font-size: 0.875rem; |
|
background-color: #e5e7eb; |
|
color: #374151; |
|
} |
|
.app { |
|
display: flex; |
|
flex-direction: column; |
|
justify-content: flex-start; |
|
min-height: 100vh; |
|
} |
|
.bookmarks-bar { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
flex-wrap: wrap; |
|
gap: 1rem; |
|
padding: 0.5rem; |
|
} |
|
.social-links { |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 1rem; |
|
} |
|
.social-link:any-link { |
|
display: flex; |
|
align-items: center; |
|
column-gap: 0.25rem; |
|
color: inherit; |
|
text-decoration: none; |
|
} |
|
.social-link:hover { |
|
color: #4d90fe; |
|
} |
|
.social-icon { |
|
width: 1rem; |
|
height: 1rem; |
|
} |
|
.footer-logo { |
|
position: fixed; |
|
bottom: 1rem; |
|
right: 1rem; |
|
z-index: 10; |
|
font-size: 1.5rem; |
|
} |
|
.container { |
|
max-width: 1280px; |
|
width: 100%; |
|
margin: 0 auto; |
|
padding: 0.5rem; |
|
} |
|
.section { |
|
border-radius: 0.375rem; |
|
margin: 1rem 0; |
|
} |
|
.section-header { |
|
display: flex; |
|
flex-wrap: wrap; |
|
align-items: center; |
|
gap: 0.5rem; |
|
} |
|
.section-title { |
|
flex: 1; |
|
white-space: nowrap; |
|
font-size: 1.25rem; |
|
font-weight: 600; |
|
color: #171717; |
|
margin-top: 0.5rem; |
|
margin-bottom: 1rem; |
|
} |
|
.dark .section-title { |
|
color: #f3f4f6; |
|
} |
|
.grid-container { |
|
display: grid; |
|
grid-template-columns: repeat(1, minmax(0, 1fr)); |
|
gap: 1rem; |
|
} |
|
@media (min-width: 640px) { |
|
.grid-container { |
|
grid-template-columns: repeat(2, minmax(0, 1fr)); |
|
} |
|
} |
|
@media (min-width: 768px) { |
|
.grid-container { |
|
grid-template-columns: repeat(3, minmax(0, 1fr)); |
|
} |
|
} |
|
@media (min-width: 1024px) { |
|
.grid-container { |
|
grid-template-columns: repeat(4, minmax(0, 1fr)); |
|
} |
|
} |
|
.loading-text { |
|
color: #6b7280; |
|
margin-top: 0.5rem; |
|
} |
|
.pill-buttons { |
|
display: flex; |
|
column-gap: 0.5rem; |
|
margin-left: 0.5rem; |
|
} |
|
.pill-button { |
|
appearance: none; |
|
border: none; |
|
padding: 0.5rem 0.75rem; |
|
border-radius: 9999px; |
|
font-size: 0.875rem; |
|
cursor: pointer; |
|
} |
|
.pill-button.active { |
|
background-color: #4d90fe; |
|
color: white; |
|
} |
|
.pill-button.inactive { |
|
background-color: #e5e7eb; |
|
color: #374151; |
|
} |
|
.pill-button-group { |
|
display: flex; |
|
} |
|
.pill-button-group .pill-button:not(:last-child) { |
|
border-top-right-radius: 0; |
|
border-bottom-right-radius: 0; |
|
} |
|
.pill-button-group .pill-button:not(:first-child) { |
|
border-top-left-radius: 0; |
|
border-bottom-left-radius: 0; |
|
} |
|
.feed-item { |
|
display: flex; |
|
flex-direction: column; |
|
background-color: #f3f4f6; |
|
border-radius: 0.375rem; |
|
width: 100%; |
|
border: 1px solid #d1d5db; |
|
transition: background-color 0.2s, border-color 0.2s; |
|
} |
|
.feed-item:hover { |
|
background-color: #e5e7eb; |
|
border-color: #6b7280; |
|
} |
|
.feed-image { |
|
display: flex; |
|
width: 100%; |
|
} |
|
.feed-image img { |
|
display: block; |
|
width: 100%; |
|
height: auto; |
|
border-top-left-radius: 0.375rem; |
|
border-top-right-radius: 0.375rem; |
|
} |
|
.feed-content { |
|
padding: 0.5rem; |
|
text-align: left; |
|
overflow: hidden; |
|
width: 100%; |
|
box-sizing: border-box; |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
.feed-title { |
|
font-size: 1.125rem; |
|
font-weight: 500; |
|
color: #1f2937; |
|
white-space: nowrap; |
|
text-overflow: ellipsis; |
|
overflow: hidden; |
|
margin-bottom: 0.25rem; |
|
} |
|
.feed-url { |
|
white-space: nowrap; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
max-width: 100%; |
|
color: #2563eb; |
|
text-decoration: none; |
|
} |
|
.feed-url:hover { |
|
text-decoration: underline; |
|
} |
|
.feed-footer { |
|
display: flex; |
|
align-items: center; |
|
justify-content: space-between; |
|
font-size: 0.875rem; |
|
width: 100%; |
|
margin-top: 0.5rem; |
|
} |
|
.feed-avatar { |
|
color: #171717; |
|
display: flex; |
|
align-items: center; |
|
overflow: hidden; |
|
} |
|
.feed-avatar img { |
|
width: 1.25rem; |
|
height: 1.25rem; |
|
border-radius: 9999px; |
|
margin-right: 0.25rem; |
|
} |
|
.feed-stats { |
|
color: #6b7280; |
|
font-size: 0.75rem; |
|
white-space: nowrap; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div id="app" class="app"> |
|
<div id="bookmarks-bar" class="bookmarks-bar"> |
|
<div class="social-links"> |
|
<a href="https://twitter.com/websim_ai" class="social-link"> |
|
<img |
|
src="https://abs.twimg.com/favicons/twitter.ico" |
|
alt="Twitter favicon" |
|
class="social-icon" |
|
/> |
|
<span>@websim_ai</span> |
|
</a> |
|
<a href="https://discord.gg/websim" class="social-link"> |
|
<img |
|
src="https://assets-global.website-files.com/6257adef93867e50d84d30e2/636e0a6a49cf127bf92de1e2_icon_clyde_blurple_RGB.png" |
|
alt="Discord favicon" |
|
class="social-icon" |
|
/> |
|
<span>discord.gg/websim</span> |
|
</a> |
|
<a href="https://www.reddit.com/r/WebSim" class="social-link"> |
|
<img |
|
src="https://www.redditstatic.com/desktop2x/img/favicon/android-icon-192x192.png" |
|
alt="Reddit favicon" |
|
class="social-icon" |
|
/> |
|
<span>r/WebSim</span> |
|
</a> |
|
</div> |
|
</div> |
|
<div class="logo footer-logo"> |
|
<span>w</span><span>e</span><span>b</span><span>s</span><span>i</span |
|
><span>m</span> |
|
</div> |
|
<div class="container"> |
|
<div class="section"> |
|
<h1 class="section-title">Last Creation</h1> |
|
<div class="grid-container"> |
|
<div class="loading-text" v-if="loadingLastPage">Loading...</div> |
|
<feed-item |
|
v-for="item in lastPage" |
|
:key="item.site_id" |
|
:feed-item="item" |
|
></feed-item> |
|
</div> |
|
</div> |
|
<div class="section"> |
|
<div class="section-header"> |
|
<h1 class="section-title">{{ feedTitle }}</h1> |
|
<div class="pill-buttons"> |
|
<button |
|
class="pill-button" |
|
:class="{ active: activeTab === 'likes', inactive: activeTab !== 'likes' }" |
|
@click="changeTab('likes')" |
|
> |
|
Liked |
|
</button> |
|
<button |
|
class="pill-button" |
|
:class="{ active: activeTab === 'new_bookmarks', inactive: activeTab !== 'new_bookmarks' }" |
|
@click="changeTab('new_bookmarks')" |
|
> |
|
New |
|
</button> |
|
<span class="pill-button-group"> |
|
<button |
|
class="pill-button" |
|
:class="{ active: activeTab === 'top_day', inactive: activeTab !== 'top_day' }" |
|
@click="changeTab('top_day')" |
|
> |
|
Today |
|
</button> |
|
<button |
|
class="pill-button" |
|
:class="{ active: activeTab === 'top_week', inactive: activeTab !== 'top_week' }" |
|
@click="changeTab('top_week')" |
|
> |
|
Week |
|
</button> |
|
<button |
|
class="pill-button" |
|
:class="{ active: activeTab === 'top_month', inactive: activeTab !== 'top_month' }" |
|
@click="changeTab('top_month')" |
|
> |
|
Month |
|
</button> |
|
</span> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="grid-container"> |
|
<div class="loading-text" v-if="loading">Loading...</div> |
|
<feed-item |
|
v-for="item in currentFeed" |
|
:key="item.site_id" |
|
:feed-item="item" |
|
></feed-item> |
|
</div> |
|
</div> |
|
</div> |
|
<script> |
|
const { createApp } = Vue; |
|
const app = createApp({ |
|
data() { |
|
return { |
|
activeTab: localStorage.getItem("activeTab") || "top_week", |
|
loading: true, |
|
loadingLastPage: true, |
|
lastPage: [], |
|
feedData: { |
|
likes: [], |
|
new_bookmarks: [], |
|
top_day: [], |
|
top_week: [], |
|
top_month: [], |
|
}, |
|
}; |
|
}, |
|
computed: { |
|
feedTitle() { |
|
return { |
|
likes: "My Likes", |
|
new_bookmarks: "New", |
|
top_day: "Top Today", |
|
top_week: "Top Week", |
|
top_month: "Top Month", |
|
}[this.activeTab]; |
|
}, |
|
currentFeed() { |
|
return this.feedData[this.activeTab]; |
|
}, |
|
}, |
|
methods: { |
|
async fetchLastPage() { |
|
try { |
|
const response = await fetch("/api/last_site", { |
|
method: "GET", |
|
}); |
|
const data = await response.json(); |
|
this.lastPage = data.map((site) => ({ |
|
site_id: site.id, |
|
title: site.title, |
|
simulated_url: site.url, |
|
username: site.username, |
|
avatar_url: site.avatar_url, |
|
})); |
|
} catch (error) { |
|
console.error("Error fetching last page:", error); |
|
} |
|
this.loadingLastPage = false; |
|
}, |
|
async fetchLikes() { |
|
try { |
|
const userid = window.params?.profile_user_id; |
|
const response = await fetch("/api/likes", { |
|
method: "POST", |
|
body: JSON.stringify({ userid }), |
|
headers: { |
|
"Content-Type": "application/json", |
|
}, |
|
}); |
|
const data = await response.json(); |
|
this.feedData["likes"] = data.data.sort( |
|
(a, b) => new Date(b.liked_at) - new Date(a.liked_at) |
|
); |
|
} catch (error) { |
|
console.error("Error fetching likes:", error); |
|
} |
|
}, |
|
async fetchBookmarks() { |
|
try { |
|
const response = await fetch("/api/bookmarks", { |
|
headers: { |
|
"Content-Type": "application/json", |
|
}, |
|
}); |
|
const { data } = await response.json(); |
|
this.feedData["new_bookmarks"] = data; |
|
} catch (error) { |
|
console.error("Error fetching likes:", error); |
|
} |
|
}, |
|
|
|
async fetchTrending(tab) { |
|
const maxAgeHours = { |
|
top_day: 24, |
|
top_week: 168, |
|
top_month: 720, |
|
top_year: 8760, |
|
}[tab]; |
|
|
|
const response = await fetch( |
|
`/api/trending?${new URLSearchParams({ |
|
max_age_hours: maxAgeHours, |
|
})}` |
|
); |
|
const data = await response.json(); |
|
this.feedData[tab] = data.data; |
|
}, |
|
async fetchFeedData(tab) { |
|
try { |
|
if (tab === "likes") { |
|
await this.fetchLikes(); |
|
this.loading = false; |
|
return; |
|
} else if (tab.includes("top")) { |
|
await this.fetchTrending(tab); |
|
this.loading = false; |
|
return; |
|
} else if (tab === "new_bookmarks") { |
|
await this.fetchBookmarks(); |
|
this.loading = false; |
|
return; |
|
} |
|
} catch (error) { |
|
console.error(error); |
|
} |
|
}, |
|
async changeTab(tab) { |
|
this.activeTab = tab; |
|
|
|
if (this.feedData[tab].length === 0) { |
|
this.loading = true; |
|
await this.fetchFeedData(tab); |
|
this.loading = false; |
|
} |
|
localStorage.setItem("activeTab", tab); |
|
}, |
|
}, |
|
created() { |
|
this.fetchFeedData(this.activeTab); |
|
this.fetchLastPage(); |
|
}, |
|
}); |
|
app.component("feed-item", { |
|
props: ["feedItem"], |
|
template: ` |
|
<a class="feed-item" :href="'https://websim.ai/c/' + feedItem.site_id" target="_parent" tabindex="-1"> |
|
<div class="feed-image"> |
|
<img :src="'https://images.websim.ai/v1/site/' + feedItem.site_id + '/600'" |
|
@error="handleImageError" width="600" height="315" /> |
|
</div> |
|
<div class="feed-content"> |
|
<div> |
|
<h3 class="feed-title">{{ feedItem.title || 'Untitled' }}</h3> |
|
<a class="feed-url" |
|
:href="'https://websim.ai/c/' + feedItem.site_id" target="_parent" |
|
v-html="formatUrl(feedItem.simulated_url)"></a> |
|
</div> |
|
<div class="feed-footer"> |
|
<a v-if="feedItem.avatar_url" :href="'https://websim.ai/@' + feedItem.username" class="feed-avatar"> |
|
<img :src="feedItem.avatar_url" alt="Avatar" /> |
|
<span>{{ feedItem.username }}</span> |
|
</a> |
|
<span class="feed-stats"> |
|
<span v-if="feedItem.likes"> ♡{{ feedItem.likes }} </span> |
|
<span v-if="feedItem.views" class="ml-1"> |
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="inline-block"> |
|
<line x1="6" y1="8" x2="6" y2="20" /> |
|
<line x1="10" y1="1" x2="10" y2="20" /> |
|
<line x1="14" y1="11" x2="14" y2="20"/> |
|
<line x1="18" y1="6" x2="18" y2="20"/> |
|
</svg>{{ feedItem.views }} </span> |
|
</span> |
|
</div> |
|
</div> |
|
</a> |
|
`, |
|
methods: { |
|
handleImageError(event) { |
|
event.target.style.display = "none"; |
|
}, |
|
formatUrl(url) { |
|
if (!url) return ""; |
|
return url |
|
.replace(/&/g, "&") |
|
.replace(/</g, "<") |
|
.replace(/>/g, ">") |
|
.replace(/"/g, """) |
|
.replace(/'/g, "'"); |
|
}, |
|
}, |
|
}); |
|
|
|
app.mount("#app"); |
|
</script> |
|
</body> |
|
</html> |
Thank you very much. ❤️ for providing a way to get prompt.