feat: add daily vani
This commit is contained in:
+3
-1
@@ -4,6 +4,7 @@ import Link from "next/link";
|
|||||||
import { Compass, Loader2, LogIn, Sparkles } from "lucide-react";
|
import { Compass, Loader2, LogIn, Sparkles } from "lucide-react";
|
||||||
import { useAuth } from "@/components/auth/auth-provider";
|
import { useAuth } from "@/components/auth/auth-provider";
|
||||||
import { SadhnaStreakCTA } from "@/components/sadhna/streak-cta";
|
import { SadhnaStreakCTA } from "@/components/sadhna/streak-cta";
|
||||||
|
import { VaniOfTheDayCTA } from "@/components/vani/vani-of-the-day-cta";
|
||||||
import { HomeFeed } from "@/components/feed/home-feed";
|
import { HomeFeed } from "@/components/feed/home-feed";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,8 +33,9 @@ export default function HomePage() {
|
|||||||
// mobile this max-width is wider than the viewport, so it's full-bleed
|
// mobile this max-width is wider than the viewport, so it's full-bleed
|
||||||
// as before.
|
// as before.
|
||||||
<main className="flex-1 max-w-lg w-full mx-auto px-4 sm:px-6 py-6">
|
<main className="flex-1 max-w-lg w-full mx-auto px-4 sm:px-6 py-6">
|
||||||
<div className="mb-6">
|
<div className="mb-6 flex flex-col gap-3">
|
||||||
<SadhnaStreakCTA />
|
<SadhnaStreakCTA />
|
||||||
|
<VaniOfTheDayCTA />
|
||||||
</div>
|
</div>
|
||||||
<HomeFeed />
|
<HomeFeed />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VaniOfTheDayCTA — a home-page card suggesting the day's "Vāṇī of the Day":
|
||||||
|
* one prose chapter, verse, or lecture chosen by the backend (the same for
|
||||||
|
* everyone that day, never repeating within 90 days). Tapping it opens the
|
||||||
|
* item. Renders nothing until/unless the pick loads, so it never leaves an
|
||||||
|
* empty frame. Styled to match SadhnaStreakCTA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { ChevronRight, Quote } from "lucide-react";
|
||||||
|
import { getVaniOfTheDay, type Vani } from "@/lib/vani";
|
||||||
|
|
||||||
|
export function VaniOfTheDayCTA() {
|
||||||
|
const [vani, setVani] = useState<Vani | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
getVaniOfTheDay()
|
||||||
|
.then((v) => {
|
||||||
|
if (!cancelled) setVani(v);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
/* card simply doesn't render */
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!vani) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={vani.href}
|
||||||
|
className="group flex w-full max-w-md items-center gap-3.5 rounded-2xl border border-border bg-card p-4 text-left transition-colors hover:border-primary/40 hover:bg-muted/40"
|
||||||
|
>
|
||||||
|
<span className="inline-flex size-12 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||||
|
<Quote className="size-6" />
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="flex items-center gap-1.5 text-[0.7rem] font-medium uppercase tracking-[0.14em] text-primary">
|
||||||
|
Vāṇī of the day
|
||||||
|
<span className="rounded bg-muted px-1.5 py-0.5 text-[0.65rem] tracking-normal text-muted-foreground">
|
||||||
|
{vani.label}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p className="mt-0.5 truncate text-base font-semibold text-foreground leading-snug">
|
||||||
|
{vani.title}
|
||||||
|
</p>
|
||||||
|
{vani.subtitle ? (
|
||||||
|
<p className="truncate text-sm text-muted-foreground leading-snug">
|
||||||
|
{vani.subtitle}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<ChevronRight className="size-5 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5 group-hover:text-foreground" />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Client for "Vāṇī of the Day" — the home page's daily content pick.
|
||||||
|
*
|
||||||
|
* Public endpoint (no auth): the BFF returns the same pick to everyone for the
|
||||||
|
* current day, chosen from a prose chapter, verse, or transcript and not
|
||||||
|
* repeated within 90 days.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BFF_BASE = process.env.NEXT_PUBLIC_BFF_URL || "http://localhost:8080";
|
||||||
|
|
||||||
|
export type Vani = {
|
||||||
|
kind: "verse" | "prose" | "transcript";
|
||||||
|
label: string; // "Verse" | "Chapter" | "Lecture" | "Conversation"
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
href: string;
|
||||||
|
book_title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Envelope<T> = { success?: boolean; data?: T };
|
||||||
|
|
||||||
|
export async function getVaniOfTheDay(): Promise<Vani | null> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BFF_BASE}/api/v1/vani/today`, {
|
||||||
|
headers: { Accept: "application/json" },
|
||||||
|
});
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const body = (await res.json().catch(() => ({}))) as Envelope<Vani | null>;
|
||||||
|
return body.data ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user