-
Notifications
You must be signed in to change notification settings - Fork 12
Update home page buttons to be dynamic #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| // app/api/home/route.ts | ||
| import { NextResponse } from "next/server"; | ||
|
|
||
| export const runtime = "edge"; | ||
|
|
||
| export interface HomeAction { | ||
| key: string; | ||
| label: string; | ||
| url: string; | ||
| isVisible: boolean; | ||
| } | ||
|
|
||
| export interface GalleryItem { | ||
| imageUrl: string; | ||
| caption: string; | ||
| altText: string; | ||
| } | ||
|
|
||
| export interface HomeResponse { | ||
| success: boolean; | ||
| data: { | ||
| actions: HomeAction[]; | ||
| hero: { | ||
| imageUrl: string; | ||
| caption: string; | ||
| altText: string; | ||
| }; | ||
| gallery: GalleryItem[]; | ||
| }; | ||
| } | ||
|
|
||
| export async function GET() { | ||
| const apiUrl = process.env.API_BASE_URL; | ||
|
|
||
| if (!apiUrl) { | ||
| return NextResponse.json( | ||
| { success: false, message: "API base URL is not defined", data: { actions: [] } }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
|
|
||
| try { | ||
| const res = await fetch(`${apiUrl}/api/v1/site-content`, { | ||
| cache: "no-store", | ||
| signal: AbortSignal.timeout(30000), // 30 second timeout | ||
| }); | ||
|
|
||
| if (!res.ok) { | ||
| console.error(`Upstream API returned status ${res.status}`); | ||
| return NextResponse.json( | ||
| { success: false, message: `Upstream API error: ${res.status}`, data: { actions: [] } }, | ||
| { status: 502 } | ||
| ); | ||
| } | ||
|
|
||
| const text = await res.text(); | ||
| let data; | ||
| try { | ||
| data = JSON.parse(text); | ||
| } catch (err: unknown) { | ||
| if (err instanceof Error) { | ||
| console.error("Invalid JSON from upstream API:", text, err.message); | ||
| } else { | ||
| console.error("Invalid JSON from upstream API:", text); | ||
| } | ||
|
|
||
| return NextResponse.json( | ||
| { success: false, message: "Upstream API returned invalid JSON", data: { actions: [] } }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
|
|
||
| if (!data || !data.success || !data.data || !Array.isArray(data.data.actions)) { | ||
| console.error("Unexpected API response structure", data); | ||
| return NextResponse.json( | ||
| { success: false, message: "Invalid response from upstream API", data: { actions: [] } }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
|
|
||
| return NextResponse.json({ | ||
| success: true, | ||
| data: data.data, | ||
| }); | ||
| } catch (error: unknown) { | ||
| if (error instanceof Error) { | ||
| console.error("Error in /api/home route:", error.message); | ||
| } else { | ||
| console.error("Error in /api/home route:", error); | ||
| } | ||
|
|
||
| return NextResponse.json( | ||
| { success: false, message: "Internal error", data: { actions: [] } }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,32 @@ import Ferris from "./ui/ferris-eyes"; | |||||
| import { Button } from 'pixel-retroui'; | ||||||
| import localFont from "next/font/local"; | ||||||
|
|
||||||
| interface HomeAction { | ||||||
| key: string; | ||||||
| label: string; | ||||||
| url: string; | ||||||
| isVisible: boolean; | ||||||
| } | ||||||
|
|
||||||
| interface GalleryItem { | ||||||
| imageUrl: string; | ||||||
| caption: string; | ||||||
| altText: string; | ||||||
| } | ||||||
|
|
||||||
| interface HomeResponse { | ||||||
| success: boolean; | ||||||
| data: { | ||||||
| actions: HomeAction[]; | ||||||
| hero: { | ||||||
| imageUrl: string; | ||||||
| caption: string; | ||||||
| altText: string; | ||||||
| }; | ||||||
| gallery: GalleryItem[]; | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| const pressStart2P = localFont({ | ||||||
| src: "../app/fonts/PressStart2P-Regular.ttf", | ||||||
| display: "swap", | ||||||
|
|
@@ -18,7 +44,24 @@ const pressStart2P = localFont({ | |||||
| export default function HeroSection() { | ||||||
| const [color, setColor] = useState("#000000"); | ||||||
| const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); | ||||||
| const [showButton ] = useState(false); | ||||||
| const [actions, setActions] = useState<HomeAction[]>([]); | ||||||
|
|
||||||
| useEffect(() => { | ||||||
| async function fetchHomeData() { | ||||||
| try { | ||||||
| const res = await fetch("/api/home"); | ||||||
| if (res.ok) { | ||||||
| const json: HomeResponse = await res.json(); | ||||||
| if (json.success && json.data && Array.isArray(json.data.actions)) { | ||||||
| setActions(json.data.actions); | ||||||
| } | ||||||
| } | ||||||
| } catch (err) { | ||||||
| console.error("Failed to fetch home page data:", err); | ||||||
| } | ||||||
| } | ||||||
| fetchHomeData(); | ||||||
| }, []); | ||||||
|
|
||||||
| useEffect(() => { | ||||||
| const updateColor = () => { | ||||||
|
|
@@ -68,7 +111,7 @@ export default function HeroSection() { | |||||
| shadow-[6px_6px_0px_rgba(0,0,0,1)] dark:shadow-[6px_6px_0px_rgba(255,255,255,1)] | ||||||
| hover:shadow-[8px_8px_0px_rgba(0,0,0,1)] dark:hover:shadow-[8px_8px_0px_rgba(255,255,255,1)] | ||||||
| active:translate-x-0.5 active:translate-y-0.5 | ||||||
| transition-all duration-200 mb-32 | ||||||
| transition-all duration-200 | ||||||
| `.replace(/\s+/g, ' ').trim(); | ||||||
|
|
||||||
|
|
||||||
|
|
@@ -183,17 +226,24 @@ export default function HeroSection() { | |||||
| </motion.svg> | ||||||
|
|
||||||
|
|
||||||
| <Button | ||||||
| onClick={() => { | ||||||
| window.open("https://forms.gle/qME3Qh1Skj7JvsBr9", "_blank"); | ||||||
| }} | ||||||
| className={buttonStyles} | ||||||
| aria-label="Register for Call of Code event" | ||||||
| style={{display : showButton ? "inline-block" : "none"}} | ||||||
| > | ||||||
| Register Now! | ||||||
| </Button> | ||||||
|
|
||||||
| {actions.filter(action => action.isVisible).length > 0 && ( | ||||||
| <div className="flex flex-wrap justify-center items-center gap-6 mb-32 z-20"> | ||||||
| {actions.filter(action => action.isVisible).map((action) => ( | ||||||
| <a | ||||||
| key={action.key} | ||||||
| href={action.url} | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Validate The URL from the API response is used directly in 🛡️ Proposed fix: validate URL protocol before rendering <a
key={action.key}
- href={action.url}
+ href={action.url.startsWith("http://") || action.url.startsWith("https://") ? action.url : "#"}
target="_blank"
rel="noopener noreferrer"
className="inline-block"
>Alternatively, perform this validation in the API route when filtering actions. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| target="_blank" | ||||||
| rel="noopener noreferrer" | ||||||
| className="inline-block" | ||||||
| > | ||||||
| <Button className={buttonStyles} aria-label={action.label}> | ||||||
| {action.label} | ||||||
| </Button> | ||||||
| </a> | ||||||
| ))} | ||||||
| </div> | ||||||
| )} | ||||||
|
|
||||||
|
|
||||||
| <h2 className="bg-clip-text text-transparent text-center bg-gradient-to-r from-red-600 via-purple-600 to-blue-500 dark:from-red-600 dark:via-purple-600 dark:to-blue-500 text-2xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl font-sans relative z-20 font-bold tracking-tight leading-tight mb-8 px-4"> | ||||||
| <> CALL OF CODE </> | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Validate individual action objects before passing through to the client.
The route checks that
data.data.actionsis an array but does not validate the shape of each element. If the upstream returns actions with missing or malformed fields (e.g.,urlas a non-string,isVisibleas the string"false"instead of booleanfalse), the client will render broken UI or show buttons that should be hidden.🛡️ Proposed fix: add per-action validation
if (!data || !data.success || !data.data || !Array.isArray(data.data.actions)) { console.error("Unexpected API response structure", data); return NextResponse.json( { success: false, message: "Invalid response from upstream API", data: { actions: [] } }, { status: 500 } ); } + const validActions = data.data.actions.filter( + (a): a is HomeAction => + a != null && + typeof a === "object" && + typeof a.key === "string" && + typeof a.label === "string" && + typeof a.url === "string" && + typeof a.isVisible === "boolean" + ); + + if (validActions.length !== data.data.actions.length) { + console.error("Some actions failed validation"); + } - return NextResponse.json({ + return NextResponse.json({ success: true, - data: data.data, + data: { ...data.data, actions: validActions }, });📝 Committable suggestion
🤖 Prompt for AI Agents