Fix admin login and add subscription popup settings
Some checks failed
docker-images / build-and-push (admin, admin, termi-astro-admin, admin/Dockerfile) (push) Failing after 6s
docker-images / build-and-push (backend, backend, termi-astro-backend, backend/Dockerfile) (push) Failing after 5s
docker-images / build-and-push (frontend, frontend, termi-astro-frontend, frontend/Dockerfile) (push) Failing after 6s
Some checks failed
docker-images / build-and-push (admin, admin, termi-astro-admin, admin/Dockerfile) (push) Failing after 6s
docker-images / build-and-push (backend, backend, termi-astro-backend, backend/Dockerfile) (push) Failing after 5s
docker-images / build-and-push (frontend, frontend, termi-astro-frontend, frontend/Dockerfile) (push) Failing after 6s
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/markdown-remark": "^7.0.1",
|
||||
"@astrojs/node": "^10.0.4",
|
||||
"@astrojs/svelte": "^8.0.3",
|
||||
"@astrojs/tailwind": "^6.0.2",
|
||||
|
||||
3
frontend/pnpm-lock.yaml
generated
3
frontend/pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@astrojs/markdown-remark':
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1
|
||||
'@astrojs/node':
|
||||
specifier: ^10.0.4
|
||||
version: 10.0.4(astro@6.0.8(jiti@1.21.7)(rollup@4.60.0)(typescript@6.0.2)(yaml@2.8.3))
|
||||
|
||||
678
frontend/src/components/SubscriptionPopup.astro
Normal file
678
frontend/src/components/SubscriptionPopup.astro
Normal file
@@ -0,0 +1,678 @@
|
||||
---
|
||||
import { resolvePublicApiBaseUrl } from '../lib/api/client';
|
||||
import type { SiteSettings } from '../lib/types';
|
||||
|
||||
interface Props {
|
||||
requestUrl?: string | URL;
|
||||
siteSettings: SiteSettings;
|
||||
}
|
||||
|
||||
const { requestUrl, siteSettings } = Astro.props as Props;
|
||||
const subscribeApiUrl = `${resolvePublicApiBaseUrl(requestUrl)}/subscriptions`;
|
||||
const popupSettings = siteSettings.subscriptions;
|
||||
---
|
||||
|
||||
{popupSettings.popupEnabled && (
|
||||
<div
|
||||
class="subscription-popup-root"
|
||||
data-subscription-popup-root
|
||||
data-api-url={subscribeApiUrl}
|
||||
data-delay-ms={String(Math.max(popupSettings.popupDelaySeconds, 3) * 1000)}
|
||||
hidden
|
||||
>
|
||||
<section
|
||||
class="subscription-popup-panel terminal-panel"
|
||||
data-subscription-popup-panel
|
||||
role="dialog"
|
||||
aria-labelledby="subscription-popup-title"
|
||||
aria-describedby="subscription-popup-description"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="subscription-popup-close"
|
||||
data-subscription-popup-close
|
||||
aria-label="关闭订阅提示"
|
||||
>
|
||||
<i class="fas fa-xmark"></i>
|
||||
</button>
|
||||
|
||||
<div class="subscription-popup-main">
|
||||
<div class="subscription-popup-copy">
|
||||
<div class="subscription-popup-copy-head">
|
||||
<div class="subscription-popup-copy-mark">
|
||||
<span class="subscription-popup-icon" aria-hidden="true">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</span>
|
||||
<div class="subscription-popup-copy-body">
|
||||
<p class="terminal-kicker">reader updates</p>
|
||||
<h3 id="subscription-popup-title">{popupSettings.popupTitle}</h3>
|
||||
<p id="subscription-popup-description">{popupSettings.popupDescription}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="subscription-popup-badges" aria-hidden="true">
|
||||
<span class="subscription-popup-badge">新文章</span>
|
||||
<span class="subscription-popup-badge">汇总简报</span>
|
||||
<span class="subscription-popup-badge">低频提醒</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="subscription-popup-meta">
|
||||
<span class="terminal-stat-pill">
|
||||
<i class="fas fa-newspaper text-[var(--primary)]"></i>
|
||||
新文章 / 汇总简报
|
||||
</span>
|
||||
<span class="terminal-stat-pill">
|
||||
<i class="fas fa-envelope-open-text text-[var(--primary)]"></i>
|
||||
邮箱确认后生效
|
||||
</span>
|
||||
<span class="terminal-stat-pill">
|
||||
<i class="fas fa-user-shield text-[var(--primary)]"></i>
|
||||
随时可退订
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="subscription-popup-form" data-subscription-popup-form>
|
||||
<label class="subscription-popup-field">
|
||||
<span class="subscription-popup-field-label">邮箱地址</span>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="name@example.com"
|
||||
autocomplete="email"
|
||||
required
|
||||
data-subscription-popup-email
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div class="subscription-popup-actions">
|
||||
<button
|
||||
type="submit"
|
||||
class="terminal-action-button terminal-action-button-primary"
|
||||
>
|
||||
订阅更新
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="terminal-action-button"
|
||||
data-subscription-popup-dismiss
|
||||
>
|
||||
稍后提醒
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p class="subscription-popup-status" data-subscription-popup-status aria-live="polite">
|
||||
只在有新文章或汇总简报时发送提醒,不会把它做成高频打扰。
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const DISMISS_KEY = 'termi:subscription-popup:dismiss-until';
|
||||
const SUBSCRIBED_KEY = 'termi:subscription-popup:subscribed-at';
|
||||
const DISMISS_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
document.querySelectorAll('[data-subscription-popup-root]').forEach((root) => {
|
||||
if (!(root instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const form = root.querySelector('[data-subscription-popup-form]');
|
||||
const status = root.querySelector('[data-subscription-popup-status]');
|
||||
const emailInput = root.querySelector('[data-subscription-popup-email]');
|
||||
const dismissButton = root.querySelector('[data-subscription-popup-dismiss]');
|
||||
const apiUrl = root.getAttribute('data-api-url');
|
||||
const pathname = window.location.pathname || '/';
|
||||
const delayMs = Math.max(3000, Number(root.getAttribute('data-delay-ms') || '18000'));
|
||||
const defaultStatus = status instanceof HTMLElement ? status.textContent?.trim() || '' : '';
|
||||
|
||||
if (
|
||||
!(form instanceof HTMLFormElement) ||
|
||||
!(status instanceof HTMLElement) ||
|
||||
!(emailInput instanceof HTMLInputElement) ||
|
||||
!(dismissButton instanceof HTMLButtonElement) ||
|
||||
!apiUrl ||
|
||||
pathname.startsWith('/subscriptions/')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let opened = false;
|
||||
let autoReady = false;
|
||||
let engaged = false;
|
||||
let autoOpened = false;
|
||||
let hideTimer = 0;
|
||||
let successTimer = 0;
|
||||
const header = document.querySelector('header');
|
||||
|
||||
const shouldFocusEmail = () =>
|
||||
window.matchMedia('(hover: hover) and (pointer: fine)').matches;
|
||||
|
||||
const syncPopupOffset = () => {
|
||||
const headerBottom =
|
||||
header instanceof HTMLElement ? Math.round(header.getBoundingClientRect().bottom) : 0;
|
||||
const offset = Math.max(14, headerBottom + 14);
|
||||
root.style.setProperty('--subscription-popup-offset', `${offset}px`);
|
||||
};
|
||||
|
||||
const hasDismissed = () => {
|
||||
try {
|
||||
return Number(window.localStorage.getItem(DISMISS_KEY) || '0') > Date.now();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const hasSubmitted = () => {
|
||||
try {
|
||||
return Boolean(window.localStorage.getItem(SUBSCRIBED_KEY));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const rememberDismiss = () => {
|
||||
try {
|
||||
window.localStorage.setItem(DISMISS_KEY, String(Date.now() + DISMISS_TTL_MS));
|
||||
} catch {
|
||||
// Ignore storage failures.
|
||||
}
|
||||
};
|
||||
|
||||
const rememberSubmitted = () => {
|
||||
try {
|
||||
window.localStorage.setItem(SUBSCRIBED_KEY, new Date().toISOString());
|
||||
} catch {
|
||||
// Ignore storage failures.
|
||||
}
|
||||
};
|
||||
|
||||
const resetStatus = () => {
|
||||
delete status.dataset.state;
|
||||
status.textContent = defaultStatus;
|
||||
};
|
||||
|
||||
const openPopup = ({ focusEmail = false } = {}) => {
|
||||
if (opened || hasSubmitted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncPopupOffset();
|
||||
window.clearTimeout(hideTimer);
|
||||
if (status.dataset.state !== 'pending') {
|
||||
resetStatus();
|
||||
}
|
||||
|
||||
opened = true;
|
||||
root.hidden = false;
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
root.classList.add('is-visible');
|
||||
if (focusEmail && shouldFocusEmail()) {
|
||||
emailInput.focus({ preventScroll: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closePopup = (remember = true) => {
|
||||
if (!opened) {
|
||||
return;
|
||||
}
|
||||
|
||||
opened = false;
|
||||
window.clearTimeout(successTimer);
|
||||
root.classList.remove('is-visible');
|
||||
|
||||
if (remember) {
|
||||
rememberDismiss();
|
||||
}
|
||||
|
||||
hideTimer = window.setTimeout(() => {
|
||||
if (!opened) {
|
||||
root.hidden = true;
|
||||
}
|
||||
}, 260);
|
||||
};
|
||||
|
||||
const maybeAutoOpen = () => {
|
||||
if (autoOpened || !autoReady || !engaged || hasDismissed() || hasSubmitted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
autoOpened = true;
|
||||
openPopup();
|
||||
};
|
||||
|
||||
const markEngaged = () => {
|
||||
engaged = true;
|
||||
maybeAutoOpen();
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
const doc = document.documentElement;
|
||||
const maxScroll = Math.max(doc.scrollHeight - window.innerHeight, 1);
|
||||
const progress = (window.scrollY / maxScroll) * 100;
|
||||
|
||||
if (progress >= 35) {
|
||||
markEngaged();
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
};
|
||||
|
||||
syncPopupOffset();
|
||||
|
||||
if (header instanceof HTMLElement && typeof ResizeObserver !== 'undefined') {
|
||||
const observer = new ResizeObserver(() => syncPopupOffset());
|
||||
observer.observe(header);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', syncPopupOffset, { passive: true });
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
window.addEventListener('pointerdown', markEngaged, { once: true, passive: true });
|
||||
window.addEventListener('keydown', markEngaged, { once: true });
|
||||
window.setTimeout(() => {
|
||||
autoReady = true;
|
||||
maybeAutoOpen();
|
||||
}, delayMs);
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
const trigger =
|
||||
event.target instanceof Element
|
||||
? event.target.closest('[data-subscription-popup-open]')
|
||||
: null;
|
||||
|
||||
if (!trigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
openPopup({ focusEmail: true });
|
||||
});
|
||||
|
||||
root.querySelectorAll('[data-subscription-popup-close]').forEach((button) => {
|
||||
button.addEventListener('click', () => closePopup(true));
|
||||
});
|
||||
|
||||
dismissButton.addEventListener('click', () => closePopup(true));
|
||||
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape' && opened) {
|
||||
closePopup(true);
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(form);
|
||||
const email = String(formData.get('email') || '').trim();
|
||||
const displayName = String(formData.get('displayName') || '').trim();
|
||||
|
||||
if (!email) {
|
||||
status.dataset.state = 'error';
|
||||
status.textContent = '请输入邮箱地址。';
|
||||
emailInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
status.dataset.state = 'pending';
|
||||
status.textContent = '正在提交订阅申请...';
|
||||
|
||||
try {
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
displayName,
|
||||
source: 'frontend-popup',
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(payload?.message || payload?.description || '订阅失败,请稍后再试。');
|
||||
}
|
||||
|
||||
rememberSubmitted();
|
||||
form.reset();
|
||||
status.dataset.state = 'success';
|
||||
status.textContent =
|
||||
payload?.message || '订阅申请已提交,请前往邮箱确认后生效。';
|
||||
successTimer = window.setTimeout(() => closePopup(false), 2200);
|
||||
} catch (error) {
|
||||
status.dataset.state = 'error';
|
||||
status.textContent = error instanceof Error ? error.message : '订阅失败,请稍后重试。';
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.subscription-popup-root {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 90;
|
||||
display: grid;
|
||||
align-items: start;
|
||||
justify-items: center;
|
||||
padding: 0 max(1rem, env(safe-area-inset-right, 0px)) 0 max(1rem, env(safe-area-inset-left, 0px));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.subscription-popup-root[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.subscription-popup-panel {
|
||||
position: relative;
|
||||
width: min(100%, 62rem);
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
margin-top: var(--subscription-popup-offset, calc(env(safe-area-inset-top, 0px) + 5.25rem));
|
||||
padding: 1rem 1rem 1.05rem;
|
||||
border-radius: 1.45rem;
|
||||
opacity: 0;
|
||||
transform: translateY(-1rem) scale(0.985);
|
||||
transition:
|
||||
opacity 0.24s ease,
|
||||
transform 0.24s ease;
|
||||
pointer-events: auto;
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(16px) saturate(135%);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(var(--primary-rgb), 0.12), transparent 24%),
|
||||
linear-gradient(
|
||||
180deg,
|
||||
color-mix(in oklab, var(--terminal-bg) 98%, transparent),
|
||||
color-mix(in oklab, var(--header-bg) 92%, transparent)
|
||||
);
|
||||
box-shadow:
|
||||
0 24px 64px rgba(var(--text-rgb), 0.14),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.34);
|
||||
}
|
||||
|
||||
.subscription-popup-panel::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0 0 auto;
|
||||
height: 3px;
|
||||
background:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
color-mix(in oklab, var(--primary) 88%, white),
|
||||
color-mix(in oklab, var(--secondary) 80%, white)
|
||||
);
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.subscription-popup-root.is-visible .subscription-popup-panel {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
.subscription-popup-close {
|
||||
position: absolute;
|
||||
top: 0.8rem;
|
||||
right: 0.8rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.15rem;
|
||||
height: 2.15rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid color-mix(in oklab, var(--primary) 12%, var(--border-color));
|
||||
background: color-mix(in oklab, var(--terminal-bg) 94%, transparent);
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
color 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.subscription-popup-close:hover {
|
||||
color: var(--title-color);
|
||||
border-color: color-mix(in oklab, var(--primary) 24%, var(--border-color));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.subscription-popup-copy {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.subscription-popup-main {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.subscription-popup-copy-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.subscription-popup-copy-mark {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.9rem;
|
||||
min-width: 0;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.subscription-popup-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.8rem;
|
||||
height: 2.8rem;
|
||||
flex-shrink: 0;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid color-mix(in oklab, var(--primary) 14%, var(--border-color));
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
color-mix(in oklab, var(--primary) 12%, var(--terminal-bg)),
|
||||
color-mix(in oklab, var(--primary) 6%, var(--header-bg))
|
||||
);
|
||||
color: var(--primary);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.24),
|
||||
0 10px 24px rgba(var(--primary-rgb), 0.12);
|
||||
}
|
||||
|
||||
.subscription-popup-copy-body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.subscription-popup-copy-body .terminal-kicker {
|
||||
margin: 0 0 0.55rem;
|
||||
}
|
||||
|
||||
.subscription-popup-copy h3 {
|
||||
margin: 0;
|
||||
font-size: 1.12rem;
|
||||
line-height: 1.35;
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
.subscription-popup-copy p:last-child {
|
||||
margin: 0.45rem 0 0;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.72;
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
.subscription-popup-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.subscription-popup-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
padding-right: 2.55rem;
|
||||
}
|
||||
|
||||
.subscription-popup-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 999px;
|
||||
border: 1px solid color-mix(in oklab, var(--primary) 12%, var(--border-color));
|
||||
background: color-mix(in oklab, var(--primary) 6%, var(--terminal-bg));
|
||||
color: var(--text-secondary);
|
||||
padding: 0.28rem 0.7rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.subscription-popup-form {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.subscription-popup-field {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.subscription-popup-field-label {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.subscription-popup-form input {
|
||||
width: 100%;
|
||||
min-height: 3rem;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid color-mix(in oklab, var(--primary) 12%, var(--border-color));
|
||||
background: color-mix(in oklab, var(--terminal-bg) 96%, transparent);
|
||||
color: var(--title-color);
|
||||
padding: 0.9rem 1rem;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
background 0.2s ease;
|
||||
}
|
||||
|
||||
.subscription-popup-form input::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.subscription-popup-form input:focus-visible {
|
||||
outline: none;
|
||||
border-color: color-mix(in oklab, var(--primary) 28%, var(--border-color));
|
||||
background: color-mix(in oklab, var(--terminal-bg) 98%, transparent);
|
||||
box-shadow: 0 0 0 4px rgba(var(--primary-rgb), 0.08);
|
||||
}
|
||||
|
||||
.subscription-popup-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.subscription-popup-actions .terminal-action-button {
|
||||
min-width: 8.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.subscription-popup-status {
|
||||
margin: 0;
|
||||
padding: 0.8rem 0.95rem;
|
||||
border-radius: 1rem;
|
||||
border: 1px dashed color-mix(in oklab, var(--primary) 12%, var(--border-color));
|
||||
background: color-mix(in oklab, var(--primary) 4%, var(--terminal-bg));
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.subscription-popup-status[data-state='pending'] {
|
||||
color: var(--primary);
|
||||
border-color: color-mix(in oklab, var(--primary) 26%, var(--border-color));
|
||||
background: color-mix(in oklab, var(--primary) 8%, var(--terminal-bg));
|
||||
}
|
||||
|
||||
.subscription-popup-status[data-state='success'] {
|
||||
color: var(--success-dark);
|
||||
border-color: color-mix(in oklab, var(--success) 24%, var(--border-color));
|
||||
background: color-mix(in oklab, var(--success) 10%, var(--terminal-bg));
|
||||
}
|
||||
|
||||
.subscription-popup-status[data-state='error'] {
|
||||
color: var(--danger);
|
||||
border-color: color-mix(in oklab, var(--danger) 24%, var(--border-color));
|
||||
background: color-mix(in oklab, var(--danger) 8%, var(--terminal-bg));
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.subscription-popup-main {
|
||||
grid-template-columns: minmax(0, 1.18fr) minmax(19rem, 0.88fr);
|
||||
align-items: end;
|
||||
gap: 1rem 1.2rem;
|
||||
}
|
||||
|
||||
.subscription-popup-panel {
|
||||
padding: 1rem 1.1rem 1.1rem;
|
||||
}
|
||||
|
||||
.subscription-popup-form {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
.subscription-popup-status {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.subscription-popup-root {
|
||||
padding-inline: 0.75rem;
|
||||
}
|
||||
|
||||
.subscription-popup-panel {
|
||||
gap: 0.9rem;
|
||||
padding: 0.95rem 0.9rem 0.95rem;
|
||||
}
|
||||
|
||||
.subscription-popup-copy-mark {
|
||||
gap: 0.75rem;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.subscription-popup-icon {
|
||||
width: 2.45rem;
|
||||
height: 2.45rem;
|
||||
border-radius: 0.9rem;
|
||||
}
|
||||
|
||||
.subscription-popup-badges {
|
||||
padding-right: 2.15rem;
|
||||
}
|
||||
|
||||
.subscription-popup-actions .terminal-action-button {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import '../styles/global.css';
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import SubscriptionPopup from '../components/SubscriptionPopup.astro';
|
||||
import BackToTop from '../components/interactive/BackToTop.svelte';
|
||||
import { api, DEFAULT_SITE_SETTINGS } from '../lib/api/client';
|
||||
import { getI18n, LOCALE_COOKIE_NAME, SUPPORTED_LOCALES } from '../lib/i18n';
|
||||
@@ -475,6 +476,7 @@ const i18nPayload = JSON.stringify({ locale, messages });
|
||||
</main>
|
||||
|
||||
<Footer siteSettings={siteSettings} />
|
||||
<SubscriptionPopup siteSettings={siteSettings} requestUrl={Astro.url} />
|
||||
<BackToTop client:load />
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -230,6 +230,10 @@ export interface ApiSiteSettings {
|
||||
}> | null;
|
||||
ai_enabled: boolean;
|
||||
paragraph_comments_enabled: boolean;
|
||||
subscription_popup_enabled: boolean;
|
||||
subscription_popup_title: string | null;
|
||||
subscription_popup_description: string | null;
|
||||
subscription_popup_delay_seconds: number | null;
|
||||
seo_default_og_image: string | null;
|
||||
seo_default_twitter_handle: string | null;
|
||||
}
|
||||
@@ -398,6 +402,12 @@ export const DEFAULT_SITE_SETTINGS: SiteSettings = {
|
||||
comments: {
|
||||
paragraphsEnabled: true,
|
||||
},
|
||||
subscriptions: {
|
||||
popupEnabled: true,
|
||||
popupTitle: '订阅更新',
|
||||
popupDescription: '有新文章或汇总简报时,通过邮件第一时间收到提醒。需要先确认邮箱,可随时退订。',
|
||||
popupDelaySeconds: 18,
|
||||
},
|
||||
seo: {
|
||||
defaultOgImage: undefined,
|
||||
defaultTwitterHandle: undefined,
|
||||
@@ -523,6 +533,18 @@ const normalizeSiteSettings = (settings: ApiSiteSettings): SiteSettings => ({
|
||||
comments: {
|
||||
paragraphsEnabled: settings.paragraph_comments_enabled ?? true,
|
||||
},
|
||||
subscriptions: {
|
||||
popupEnabled:
|
||||
settings.subscription_popup_enabled ?? DEFAULT_SITE_SETTINGS.subscriptions.popupEnabled,
|
||||
popupTitle:
|
||||
settings.subscription_popup_title || DEFAULT_SITE_SETTINGS.subscriptions.popupTitle,
|
||||
popupDescription:
|
||||
settings.subscription_popup_description ||
|
||||
DEFAULT_SITE_SETTINGS.subscriptions.popupDescription,
|
||||
popupDelaySeconds:
|
||||
settings.subscription_popup_delay_seconds ??
|
||||
DEFAULT_SITE_SETTINGS.subscriptions.popupDelaySeconds,
|
||||
},
|
||||
seo: {
|
||||
defaultOgImage: settings.seo_default_og_image ?? undefined,
|
||||
defaultTwitterHandle: settings.seo_default_twitter_handle ?? undefined,
|
||||
|
||||
@@ -77,6 +77,12 @@ export interface SiteSettings {
|
||||
comments: {
|
||||
paragraphsEnabled: boolean;
|
||||
};
|
||||
subscriptions: {
|
||||
popupEnabled: boolean;
|
||||
popupTitle: string;
|
||||
popupDescription: string;
|
||||
popupDelaySeconds: number;
|
||||
};
|
||||
seo: {
|
||||
defaultOgImage?: string;
|
||||
defaultTwitterHandle?: string;
|
||||
|
||||
@@ -8,7 +8,6 @@ import FriendLinkCard from '../components/FriendLinkCard.astro';
|
||||
import ViewMoreLink from '../components/ui/ViewMoreLink.astro';
|
||||
import StatsList from '../components/StatsList.astro';
|
||||
import TechStackList from '../components/TechStackList.astro';
|
||||
import SubscriptionSignup from '../components/SubscriptionSignup.astro';
|
||||
import { terminalConfig } from '../lib/config/terminal';
|
||||
import { api, DEFAULT_SITE_SETTINGS } from '../lib/api/client';
|
||||
import { formatReadTime, getI18n } from '../lib/i18n';
|
||||
@@ -243,6 +242,12 @@ const navLinks = [
|
||||
<i class="fas fa-file-alt text-[10px]"></i>
|
||||
<span id="home-results-count">{t('common.resultsCount', { count: filteredPostsCount })}</span>
|
||||
</span>
|
||||
{siteSettings.subscriptions.popupEnabled && (
|
||||
<button type="button" class="terminal-subtle-link" data-subscription-popup-open>
|
||||
<i class="fas fa-envelope text-[11px]"></i>
|
||||
<span>订阅更新</span>
|
||||
</button>
|
||||
)}
|
||||
{siteSettings.ai.enabled && (
|
||||
<a href="/ask" class="terminal-subtle-link">
|
||||
<i class="fas fa-robot text-[11px]"></i>
|
||||
@@ -260,10 +265,6 @@ const navLinks = [
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="ml-4 mt-5">
|
||||
<SubscriptionSignup requestUrl={Astro.request.url} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{apiError && (
|
||||
|
||||
@@ -11,8 +11,8 @@ const api = createApiClient({ requestUrl: Astro.url });
|
||||
const apiBaseUrl = resolvePublicApiBaseUrl(Astro.url);
|
||||
const EVENT_OPTIONS = [
|
||||
{ value: 'post.published', label: '新文章通知' },
|
||||
{ value: 'digest.weekly', label: '周报 Digest' },
|
||||
{ value: 'digest.monthly', label: '月报 Digest' },
|
||||
{ value: 'digest.weekly', label: '每周简报' },
|
||||
{ value: 'digest.monthly', label: '每月简报' },
|
||||
{ value: 'comment.created', label: '评论通知' },
|
||||
{ value: 'friend_link.created', label: '友链申请通知' },
|
||||
] as const;
|
||||
|
||||
Reference in New Issue
Block a user