Published your App

Replit-Commit-Author: Deployment
Replit-Commit-Session-Id: 517dfa7b-26ac-463d-a6e1-a58c6df97188
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: a5b0cf00-faad-490d-a860-0918e26dc01b
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/517dfa7b-26ac-463d-a6e1-a58c6df97188/jdAEdU5
Replit-Commit-Deployment-Build-Id: 3212db73-3802-4ff5-b9d5-49fe3af1b5ad
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sebastjanartic 2026-03-04 15:26:35 +00:00
parent ef836d9ddf
commit b7bac69edc
2 changed files with 151 additions and 1 deletions

View File

@ -0,0 +1,102 @@
import { SiFacebook, SiLinkedin, SiX, SiWhatsapp, SiTelegram, SiPinterest } from "react-icons/si";
import { Mail, Link2, Check } from "lucide-react";
import { useState } from "react";
interface ShareButtonsProps {
url: string;
title: string;
image?: string;
}
const SHARE_TARGETS = [
{
name: "Facebook",
icon: SiFacebook,
color: "hover:bg-[#1877F2] hover:text-white",
getUrl: (url: string) => `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
},
{
name: "X",
icon: SiX,
color: "hover:bg-[#000] hover:text-white",
getUrl: (url: string, title: string) => `https://x.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`,
},
{
name: "LinkedIn",
icon: SiLinkedin,
color: "hover:bg-[#0A66C2] hover:text-white",
getUrl: (url: string) => `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`,
},
{
name: "WhatsApp",
icon: SiWhatsapp,
color: "hover:bg-[#25D366] hover:text-white",
getUrl: (url: string, title: string) => `https://wa.me/?text=${encodeURIComponent(title + " " + url)}`,
},
{
name: "Telegram",
icon: SiTelegram,
color: "hover:bg-[#26A5E4] hover:text-white",
getUrl: (url: string, title: string) => `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`,
},
{
name: "Pinterest",
icon: SiPinterest,
color: "hover:bg-[#BD081C] hover:text-white",
getUrl: (url: string, title: string, image?: string) => `https://pinterest.com/pin/create/button/?url=${encodeURIComponent(url)}&description=${encodeURIComponent(title)}${image ? "&media=" + encodeURIComponent(image) : ""}`,
},
{
name: "E-Mail",
icon: Mail,
color: "hover:bg-muted-foreground hover:text-white",
getUrl: (url: string, title: string) => `mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(title + "\n\n" + url)}`,
},
];
export default function ShareButtons({ url, title, image }: ShareButtonsProps) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(url);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
const input = document.createElement("input");
input.value = url;
document.body.appendChild(input);
input.select();
document.execCommand("copy");
document.body.removeChild(input);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return (
<div className="flex flex-wrap items-center gap-2" data-testid="share-buttons">
<span className="text-sm text-muted-foreground mr-1">Teilen:</span>
{SHARE_TARGETS.map((target) => (
<a
key={target.name}
href={target.getUrl(url, title, image)}
target="_blank"
rel="noopener noreferrer"
className={`inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-muted-foreground transition-all duration-200 ${target.color}`}
aria-label={`Auf ${target.name} teilen`}
data-testid={`button-share-${target.name.toLowerCase()}`}
>
<target.icon className="w-4 h-4" />
</a>
))}
<button
onClick={handleCopy}
className={`inline-flex items-center justify-center w-9 h-9 rounded-full border border-border transition-all duration-200 ${copied ? "bg-green-600 text-white border-green-600" : "text-muted-foreground hover:bg-muted-foreground hover:text-white"}`}
aria-label="Link kopieren"
data-testid="button-share-copy"
>
{copied ? <Check className="w-4 h-4" /> : <Link2 className="w-4 h-4" />}
</button>
</div>
);
}

View File

@ -5,9 +5,14 @@ import viteConfig from "../vite.config";
import fs from "fs";
import path from "path";
import { nanoid } from "nanoid";
import { storage } from "./storage";
const viteLogger = createLogger();
function escapeHtml(str: string): string {
return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
export async function setupVite(server: Server, app: Express) {
const serverOptions = {
middlewareMode: true,
@ -42,12 +47,55 @@ export async function setupVite(server: Server, app: Express) {
"index.html",
);
// always reload the index.html file from disk incase it changes
let template = await fs.promises.readFile(clientTemplate, "utf-8");
template = template.replace(
`src="/src/main.tsx"`,
`src="/src/main.tsx?v=${nanoid()}"`,
);
const articleMatch = url.match(/^\/article\/([^?#]+)/);
if (articleMatch) {
try {
const slug = decodeURIComponent(articleMatch[1]);
const article = await storage.getArticleBySlug(slug);
if (article) {
const host = req.get("host") || "folx.tv";
const protocol = req.get("x-forwarded-proto") || "https";
const baseUrl = `${protocol}://${host}`;
const articleUrl = `${baseUrl}/article/${article.slug}`;
const imageUrl = article.coverImage ? (article.coverImage.startsWith("http") ? article.coverImage : `${baseUrl}${article.coverImage}`) : "";
const ogTags = [
`<meta property="og:title" content="${escapeHtml(article.title)}" />`,
`<meta property="og:description" content="${escapeHtml(article.excerpt)}" />`,
`<meta property="og:type" content="article" />`,
`<meta property="og:url" content="${escapeHtml(articleUrl)}" />`,
imageUrl ? `<meta property="og:image" content="${escapeHtml(imageUrl)}" />` : "",
`<meta property="og:site_name" content="Folx Music Television" />`,
`<meta name="twitter:card" content="summary_large_image" />`,
`<meta name="twitter:title" content="${escapeHtml(article.title)}" />`,
`<meta name="twitter:description" content="${escapeHtml(article.excerpt)}" />`,
imageUrl ? `<meta name="twitter:image" content="${escapeHtml(imageUrl)}" />` : "",
`<title>${escapeHtml(article.title)} - Folx Music Television</title>`,
].filter(Boolean).join("\n ");
template = template.replace(
/<title>.*?<\/title>/,
ogTags
);
template = template.replace(
/<meta property="og:title"[^>]*\/>\s*/,
""
);
template = template.replace(
/<meta property="og:description"[^>]*\/>\s*/,
""
);
}
} catch (e) {
}
}
const page = await vite.transformIndexHtml(url, template);
res.status(200).set({ "Content-Type": "text/html" }).end(page);
} catch (e) {