<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
<title>You're Offline</title>
<style>
:root {
--primary-color: #55adc5;
--secondary-color: #55adc5;
--text-color: #55adc5;
--light-gray: #2d2d2d;
--border-color: #3d3d3d;
--bg-color: #1a1a1a;
--card-bg: #2d2d2d;
--text-muted: #e0e0e0;
--hero-bg: #1a1a1a;
--footer-bg: #2d2d2d;
--input-bg: #2d2d2d;
--input-text: #ffffff;
--input-border: #3d3d3d;
--gradient-start: #55adc5;
--gradient-mid: #55adc5;
--gradient-end: #55adc5;
--accent-color: #90dfeb;
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.15);
--shadow-md: 0 8px 24px rgba(0, 0, 0, 0.2);
--shadow-lg: 0 16px 32px rgba(0, 0, 0, 0.25);
--shadow-xl: 0 24px 48px rgba(0, 0, 0, 0.3);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--bg-color);
color: var(--text-muted);
height: 100%;
overflow: hidden;
position: relative;
}
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
padding: 2rem;
position: relative;
z-index: 2;
}
.offline-card {
background-color: var(--card-bg);
border-radius: 16px;
padding: 2.5rem;
width: 100%;
max-width: 400px;
text-align: center;
box-shadow: var(--shadow-lg);
position: relative;
overflow: hidden;
border: 1px solid var(--border-color);
}
.offline-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(
90deg,
var(--gradient-start),
var(--gradient-mid),
var(--gradient-end)
);
}
.icon-container {
margin-bottom: 1.5rem;
position: relative;
}
.icon-pulse {
width: 120px;
height: 120px;
border-radius: 50%;
background-color: rgba(241, 196, 15, 0.1);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
position: relative;
}
.icon-pulse::before,
.icon-pulse::after {
content: "";
position: absolute;
border: 2px solid var(--primary-color);
width: 100%;
height: 100%;
border-radius: 50%;
animation: pulse 3s ease-out infinite;
opacity: 0;
}
.icon-pulse::after {
animation-delay: 1.5s;
}
@keyframes pulse {
0% {
transform: scale(0.5);
opacity: 0;
}
50% {
opacity: 0.3;
}
100% {
transform: scale(1.5);
opacity: 0;
}
}
.offline-icon {
width: 60px;
height: 60px;
fill: var(--primary-color);
}
.title {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: var(--primary-color);
}
.message {
font-size: 1.1rem;
line-height: 1.5;
margin-bottom: 2rem;
color: var(--text-muted);
}
.retry-button {
background-color: var(--primary-color);
color: var(--bg-color);
border: none;
border-radius: 8px;
padding: 0.8rem 2rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.retry-button:hover {
background-color: var(--accent-color);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.retry-button:active {
transform: translateY(0);
box-shadow: var(--shadow-sm);
}
.retry-button.loading {
pointer-events: none;
background-color: var(--light-gray);
color: var(--text-muted);
}
.retry-button.loading .spinner {
display: inline-block;
margin-right: 0.5rem;
}
.spinner {
width: 18px;
height: 18px;
border: 2px solid transparent;
border-top-color: var(--bg-color);
border-radius: 50%;
animation: spin 0.8s linear infinite;
display: none;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.status-indicator {
position: absolute;
bottom: 1rem;
right: 1rem;
display: flex;
align-items: center;
font-size: 0.8rem;
opacity: 0.7;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 0.5rem;
background-color: #e74c3c;
}
.status-dot.online {
background-color: #2ecc71;
}
.particles-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
z-index: 1;
}
.particle {
position: absolute;
border-radius: 50%;
background-color: var(--primary-color);
opacity: 0.1;
}
@media (max-width: 480px) {
.offline-card {
padding: 2rem 1.5rem;
}
.title {
font-size: 1.6rem;
}
.message {
font-size: 1rem;
}
.icon-pulse {
width: 100px;
height: 100px;
}
.offline-icon {
width: 50px;
height: 50px;
}
}
</style>
</head>
<body>
<div class="particles-container" id="particles"></div>
<div class="container">
<div class="offline-card">
<div class="icon-container">
<div class="icon-pulse">
<svg
class="offline-icon"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 4C7.31 4 3.07 5.9 0 8.98L12 21L24 8.98C20.93 5.9 16.69 4 12 4zM2.92 9.07C5.51 7.08 8.67 6 12 6s6.49 1.08 9.08 3.07l-2.85 2.86C16.46 10.71 14.31 10 12 10c-2.31 0-4.46.71-6.23 1.93L2.92 9.07z"
/>
<path
d="M12 10c-3.03 0-5.78 1.23-7.78 3.23l1.41 1.41C7.2 12.05 9.48 11 12 11c2.52 0 4.8 1.05 6.37 2.64l1.41-1.41C17.78 11.23 15.03 10 12 10z"
/>
<path
d="M12 12c-2.04 0-3.9.78-5.3 2.18l1.41 1.41c.92-.92 2.19-1.5 3.59-1.59L12 14l.3 0c1.4.09 2.68.67 3.59 1.59l1.41-1.41C15.9 12.78 14.04 12 12 12z"
/>
<path
d="M12 16c-1.11 0-2 .89-2 2s.89 2 2 2 2-.89 2-2-.89-2-2-2z"
/>
</svg>
</div>
</div>
<h1 class="title" id="title">You're Offline</h1>
<p class="message" id="message">
Check your connection and try again when you're back online.
</p>
<button id="retry-button" class="retry-button">
<span class="spinner"></span>
<span id="button-text">Retry</span>
</button>
<div class="status-indicator">
<span class="status-dot" id="status-dot"></span>
<span id="status-text">Offline</span>
</div>
</div>
</div>
<script>
// DOM Elements
const retryButton = document.getElementById("retry-button");
const buttonText = document.getElementById("button-text");
const spinner = document.querySelector(".spinner");
const statusDot = document.getElementById("status-dot");
const statusText = document.getElementById("status-text");
const title = document.getElementById("title");
const message = document.getElementById("message");
const particlesContainer = document.getElementById("particles");
// Language support
const translations = {
en: {
title: "You're Offline",
message:
"Check your connection and try again when you're back online.",
retry: "Retry",
checking: "Checking...",
online: "Online",
offline: "Offline",
},
ko: {
title: "오프라인 상태입니다",
message: "인터넷 연결을 확인하고 온라인 상태가 되면 다시 시도하세요.",
retry: "다시 시도",
checking: "확인 중...",
online: "온라인",
offline: "오프라인",
},
};
// Set language
function setLanguage() {
const userLang = navigator.language.toLowerCase().substring(0, 2);
const lang = ["ko"].includes(userLang) ? userLang : "en";
title.textContent = translations[lang].title;
message.textContent = translations[lang].message;
buttonText.textContent = translations[lang].retry;
statusText.textContent = translations[lang].offline;
}
// Check connection status
function updateOnlineStatus() {
if (navigator.onLine) {
statusDot.classList.add("online");
statusText.textContent =
navigator.language.toLowerCase().substring(0, 2) === "ko"
? translations["ko"].online
: translations["en"].online;
// Auto-reload after short delay when coming back online
setTimeout(() => {
location.reload();
}, 1500);
} else {
statusDot.classList.remove("online");
statusText.textContent =
navigator.language.toLowerCase().substring(0, 2) === "ko"
? translations["ko"].offline
: translations["en"].offline;
}
}
// Handle retry button click
function handleRetry() {
retryButton.classList.add("loading");
spinner.style.display = "inline-block";
buttonText.textContent =
navigator.language.toLowerCase().substring(0, 2) === "ko"
? translations["ko"].checking
: translations["en"].checking;
// Simulate checking connection
setTimeout(() => {
if (navigator.onLine) {
location.reload();
} else {
retryButton.classList.remove("loading");
spinner.style.display = "none";
buttonText.textContent =
navigator.language.toLowerCase().substring(0, 2) === "ko"
? translations["ko"].retry
: translations["en"].retry;
// Shake animation effect on failure
retryButton.animate(
[
{ transform: "translateX(-5px)" },
{ transform: "translateX(5px)" },
{ transform: "translateX(-5px)" },
{ transform: "translateX(5px)" },
{ transform: "translateX(0)" },
],
{
duration: 400,
easing: "ease-in-out",
}
);
}
}, 1500);
}
// Create background particles
function createParticles() {
const particleCount = Math.floor(window.innerWidth / 20);
for (let i = 0; i < particleCount; i++) {
const particle = document.createElement("div");
particle.classList.add("particle");
// Random size between 2-6px
const size = Math.random() * 4 + 2;
particle.style.width = `${size}px`;
particle.style.height = `${size}px`;
// Random position
particle.style.left = `${Math.random() * 100}%`;
particle.style.top = `${Math.random() * 100}%`;
// Random opacity
particle.style.opacity = Math.random() * 0.15 + 0.05;
particlesContainer.appendChild(particle);
// Animate each particle
animateParticle(particle);
}
}
function animateParticle(particle) {
const speed = Math.random() * 40 + 20; // 20-60 seconds for full animation
// Start position
const startX = parseFloat(particle.style.left);
const startY = parseFloat(particle.style.top);
// Random movement range
const rangeX = Math.random() * 20 - 10; // -10% to +10%
const rangeY = Math.random() * 20 - 10; // -10% to +10%
let progress = 0;
let direction = 1;
function moveParticle() {
if (progress >= 1) {
direction = -1;
} else if (progress <= 0) {
direction = 1;
}
progress += 0.002 * direction;
const newX = startX + rangeX * Math.sin(progress * Math.PI);
const newY = startY + rangeY * Math.sin(progress * Math.PI);
particle.style.left = `${newX}%`;
particle.style.top = `${newY}%`;
requestAnimationFrame(moveParticle);
}
moveParticle();
}
// Initialize
function init() {
setLanguage();
updateOnlineStatus();
createParticles();
// Event listeners
window.addEventListener("online", updateOnlineStatus);
window.addEventListener("offline", updateOnlineStatus);
retryButton.addEventListener("click", handleRetry);
// Check connection status periodically
setInterval(updateOnlineStatus, 5000);
}
// Start everything when the page loads
window.addEventListener("load", init);
</script>
</body>
</html>
Comments
Post a Comment