Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion frontend/.env.local.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# set a custom messageboard wallet (should be a sub-wallet with only make invoice and list transactions permissions)
#VITE_LIGHTNING_MESSAGEBOARD_NWC_URL="nostr+walletconnect://5f8e7c098137ccca853327be44a9b2e956cf79a8e2336e27a4f27b3fb55325b6?relay=wss://relay.getalby.com&relay=wss://relay2.getalby.com&secret=ace5c4b9e08138a2ef91b4ccf1379952c77c651866b29f5872b5165134417894"
#VITE_LIGHTNING_MESSAGEBOARD_NWC_URL="nostr+walletconnect://5f8e7c098137ccca853327be44a9b2e956cf79a8e2336e27a4f27b3fb55325b6?relay=wss://relay.getalby.com&relay=wss://relay2.getalby.com&secret=ace5c4b9e08138a2ef91b4ccf1379952c77c651866b29f5872b5165134417894"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need of env variables once we shift api fetching to backend. we have alby internal api endpoints already defined in backend


# optional blog endpoint used by Home Alby Blog widget
#VITE_ALBY_BLOG_ENDPOINT=https://getalby.com/api/hub/blog/latest
177 changes: 177 additions & 0 deletions frontend/src/components/home/widgets/AlbyBlogWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { SquareArrowOutUpRightIcon } from "lucide-react";
import React from "react";
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "src/components/ui/card";
import { ExternalLinkButton } from "src/components/ui/custom/external-link-button";
import { cn } from "src/lib/utils";

type BlogPost = {
id: string;
title: string;
description: string;
url: string;
imageUrl?: string;
publishedAt?: string;
};

const ALBY_BLOG_ENDPOINT =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we shouldn't do fetching in the view itself. its not consistent. we use swr for api requests and api requests shall be done on go backend and repsonse should be passed on frontend

import.meta.env.VITE_ALBY_BLOG_ENDPOINT ||
"https://getalby.com/api/hub/blog/latest";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it uses this single api endpoint. then where this api/hub/blog/feed is used. i see it added in other pr


const fallbackThemes = [
"from-emerald-200 via-cyan-200 to-yellow-200",
"from-orange-200 via-amber-100 to-pink-100",
"from-slate-200 via-zinc-100 to-lime-100",
"from-sky-200 via-indigo-100 to-violet-100",
];

function toStringValue(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}

function normalizePost(input: unknown): BlogPost | null {
if (!input || typeof input !== "object") {
return null;
}
const item = input as Record<string, unknown>;
const id = toStringValue(item.id) || toStringValue(item.slug) || "latest";
const title = toStringValue(item.title);
const description =
toStringValue(item.lead) ||
toStringValue(item.description) ||
toStringValue(item.excerpt);
const url = toStringValue(item.url) || toStringValue(item.link);
const imageUrlValue =
toStringValue(item.imageUrl) ||
toStringValue(item.image_url) ||
toStringValue(item.coverImage) ||
toStringValue(item.cover_image);
const imageUrl = imageUrlValue?.replace(/&amp;/g, "&");
const publishedAt =
toStringValue(item.publishedAt) || toStringValue(item.published_at);

if (!title || !url || !description) {
return null;
}

return {
id,
title,
description,
url,
imageUrl,
publishedAt,
};
}

function pickLatestPost(posts: BlogPost[]): BlogPost {
const dated = posts.filter((p) => p.publishedAt);
if (dated.length > 0) {
return [...dated].sort(
(a, b) =>
new Date(b.publishedAt || "").getTime() -
new Date(a.publishedAt || "").getTime()
)[0];
}
return posts[0];
}

async function fetchBlogPosts(): Promise<BlogPost[]> {
const response = await fetch(ALBY_BLOG_ENDPOINT);
if (!response.ok) {
throw new Error(`Failed to fetch blog posts: ${response.status}`);
}

const payload = (await response.json()) as unknown;
const candidates = Array.isArray(payload)
? payload
: Array.isArray((payload as { posts?: unknown[] })?.posts)
? (payload as { posts: unknown[] }).posts
: [payload];

return candidates
.map(normalizePost)
.filter((post): post is BlogPost => !!post);
}

export function AlbyBlogWidget() {
const [post, setPost] = React.useState<BlogPost | null>(null);
const [themeClassName, setThemeClassName] = React.useState(fallbackThemes[0]);

React.useEffect(() => {
const loadPost = async () => {
try {
const posts = await fetchBlogPosts();
if (!posts.length) {
setPost(null);
return;
}
const latest = pickLatestPost(posts);
setPost(latest);
const themeIndex = Math.abs(
[...latest.id].reduce((sum, ch) => sum + ch.charCodeAt(0), 0)
);
setThemeClassName(fallbackThemes[themeIndex % fallbackThemes.length]);
} catch {
setPost(null);
}
};

void loadPost();
}, []);

if (!post) {
return null;
}

return (
<Card className="overflow-hidden rounded-[14px] shadow-none">
<CardHeader className="px-6 pb-0">
<CardTitle className="text-base font-semibold">Alby Blog</CardTitle>
</CardHeader>
<CardContent className="px-6 pt-0">
<div className="relative h-[247px] overflow-hidden rounded-xl border">
{post.imageUrl ? (
<img
src={post.imageUrl}
alt={post.title}
className="absolute inset-0 size-full object-cover"
/>
) : (
<div
className={cn(
"absolute inset-0 bg-gradient-to-br",
themeClassName
)}
>
<div className="absolute -left-10 top-6 size-36 rounded-full bg-white/35 blur-3xl" />
<div className="absolute -right-8 bottom-2 size-40 rounded-full bg-white/20 blur-3xl" />
<div className="absolute inset-0 bg-white/10" />
</div>
)}
</div>
</CardContent>
<CardFooter className="flex flex-col items-start gap-4 px-6 pb-6 pt-0">
<div className="space-y-1">
<p className="text-xl font-semibold leading-7 text-foreground">
{post.title}
</p>
<p className="text-base leading-6 text-muted-foreground">
{post.description}
</p>
</div>
<div className="flex w-full justify-end">
<ExternalLinkButton to={post.url} variant="outline">
Read on Alby Blog
<SquareArrowOutUpRightIcon />
</ExternalLinkButton>
</div>
</CardFooter>
</Card>
);
}
3 changes: 3 additions & 0 deletions frontend/src/screens/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import React from "react";
import albyGo from "src/assets/suggested-apps/alby-go.png";
import zapplanner from "src/assets/suggested-apps/zapplanner.png";
import { AppOfTheDayWidget } from "src/components/home/widgets/AppOfTheDayWidget";
import { AlbyBlogWidget } from "src/components/home/widgets/AlbyBlogWidget";
import { BlockHeightWidget } from "src/components/home/widgets/BlockHeightWidget";
import { ForwardsWidget } from "src/components/home/widgets/ForwardsWidget";
import { LatestUsedAppsWidget } from "src/components/home/widgets/LatestUsedAppsWidget";
Expand Down Expand Up @@ -193,6 +194,8 @@ function Home() {
</Card>
</Link>

<AlbyBlogWidget />

<Card>
<CardHeader>
<div className="flex justify-between items-center">
Expand Down
2 changes: 1 addition & 1 deletion frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ const insertDevCSPPlugin: Plugin = {
"<head>",
`<head>
<!-- DEV-ONLY CSP - when making changes here, also update the CSP header in http_service.go (without the nonce!) -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self' ${DEVELOPMENT_NONCE}; img-src 'self' https://uploads.getalby-assets.com https://getalby.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://embed.bitrefill.com" />`
<meta http-equiv="Content-Security-Policy" content="default-src 'self' ${DEVELOPMENT_NONCE}; img-src 'self' https://uploads.getalby-assets.com https://getalby.com https://framerusercontent.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://embed.bitrefill.com" />`
);
},
},
Expand Down
2 changes: 1 addition & 1 deletion http/http_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
ContentTypeNosniff: "nosniff",
XFrameOptions: "DENY",
ContentSecurityPolicy: "default-src 'self'; img-src 'self' https://uploads.getalby-assets.com https://getalby.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://embed.bitrefill.com",
ContentSecurityPolicy: "default-src 'self'; img-src 'self' https://uploads.getalby-assets.com https://getalby.com https://framerusercontent.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://embed.bitrefill.com",
ReferrerPolicy: "no-referrer",
}))
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
Expand Down
Loading