Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
48 changes: 39 additions & 9 deletions app/(core)/components/Chapter.jsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,59 @@
// app/components/Chapter.jsx
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import { faArrowRight, faAtom } from "@fortawesome/free-solid-svg-icons";
import Tag from "./Tag.jsx";
import Link from "next/link.js";
import Image from "next/image";
import useTranslation from "../hooks/useTranslation.ts";
import { COLORS } from "../data/tags.js";

function Chapter(props) {
const { t, meta } = useTranslation();
const isCompleted = meta?.completed || false;
console.log("Chapter props:", props); // Debug log to check props
const primaryTag = props.tags?.[0];
const secondaryTag = props.tags?.[1];
const primaryColor = COLORS[primaryTag?.color]?.primary || "#00e6e6";
const secondaryColor = COLORS[secondaryTag?.color]?.secondary || "#7dd3fc";

return (
<section
id={props.id}
className={`chapter-card ${isCompleted ? "notranslate" : ""}`}
>
{/* Hero thumbnail */}
<div className="chapter-card-hero">
<div
className="chapter-card-hero"
style={{
"--chapter-accent": primaryColor,
"--chapter-accent-2": secondaryColor,
}}
>
<div className="chapter-card-hero-fallback">
<div className="chapter-card-generated-orbit chapter-card-generated-orbit--outer" />
<div className="chapter-card-generated-orbit chapter-card-generated-orbit--inner" />
<FontAwesomeIcon
icon={faAtom}
className="chapter-card-generated-icon"
/>
<div className="chapter-card-generated-topic">
{t(primaryTag?.name || "Physics")}
</div>
</div>
<div className="chapter-card-hero-glow" />

<Image
src={props.thumbnail}
alt={`${t(props.name)} thumbnail`}
fill={true}
/>
{props.thumbnail ? (
// Cards can use local or arbitrary external article images.
// eslint-disable-next-line @next/next/no-img-element
<img
src={props.thumbnail}
alt={`${t(props.name)} thumbnail`}
className="chapter-card-hero-img"
loading="lazy"
decoding="async"
onError={(event) => {
event.currentTarget.style.display = "none";
}}
/>
) : null}
</div>

<div className="chapter-card-overlay">
Expand Down
162 changes: 117 additions & 45 deletions app/(core)/components/Hero.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import React from "react";
import Link from "next/link";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
import {
faArrowRight,
faAtom,
faBolt,
faPlay,
faWaveSquare,
} from "@fortawesome/free-solid-svg-icons";
import { faGithub } from "@fortawesome/free-brands-svg-icons";
import chaptersData from "../data/chapters.js";
import { motion, useReducedMotion } from "framer-motion";
Expand Down Expand Up @@ -105,58 +111,124 @@ export function Hero() {

return (
<motion.div
className={`ph-hero__container ${isCompleted ? "notranslate" : ""}`}
className={`ph-hero__container ph-hero__container--landing ${
isCompleted ? "notranslate" : ""
}`}
variants={containerVariants(reduceMotion)}
initial="hidden"
animate="show"
style={{ position: "relative", overflow: "hidden" }}
>
{/* Animated H1 with per-word glow on PhysicsHub */}
<motion.h1
className="ph-hero__title"
variants={textContainer(reduceMotion)}
>
{titleWords.map((word, idx) => {
const isHighlight = word.includes("PhysicsHub");
return (
<motion.span
key={idx}
variants={isHighlight ? glowVariant : wordVariant}
style={{ display: "inline-block", marginRight: "0.25ch" }}
<div className="ph-hero__copy">
<motion.p className="ph-hero__eyebrow" variants={fadeUp(reduceMotion)}>
<FontAwesomeIcon icon={faAtom} />
{t("Interactive physics learning")}
</motion.p>

{/* Animated H1 with per-word glow on PhysicsHub */}
<motion.h1
className="ph-hero__title"
variants={textContainer(reduceMotion)}
>
{titleWords.map((word, idx) => {
const isHighlight = word.includes("PhysicsHub");
return (
<motion.span
key={idx}
variants={isHighlight ? glowVariant : wordVariant}
style={{ display: "inline-block", marginRight: "0.25ch" }}
>
{word}
</motion.span>
);
})}
</motion.h1>

{/* Subtitle */}
<motion.p className="ph-hero__subtitle" variants={fadeUp(reduceMotion)}>
{t(
"Experience physics in real time, uncover the concepts behind the formulas, and instantly see how they apply to the real world."
)}
</motion.p>

{/* CTA buttons */}
<motion.div className="ph-hero__ctas" variants={fadeUp(reduceMotion)}>
<motion.div
variants={buttonVariant}
whileHover="hover"
whileTap="tap"
>
<Link
className="ph-btn ph-btn--primary main-btn"
href="/simulations"
>
{word}
</motion.span>
);
})}
</motion.h1>

{/* Subtitle */}
<motion.p className="ph-hero__subtitle" variants={fadeUp(reduceMotion)}>
{t(
"Experience physics in real time, uncover the concepts behind the formulas, and instantly see how they apply to the real world."
)}
</motion.p>

{/* CTA buttons */}
<motion.div className="ph-hero__ctas" variants={fadeUp(reduceMotion)}>
<motion.div variants={buttonVariant} whileHover="hover" whileTap="tap">
<Link className="ph-btn ph-btn--primary main-btn" href="/simulations">
{t("Go to Simulations")}
<FontAwesomeIcon icon={faArrowRight} style={{ marginLeft: 8 }} />
</Link>
</motion.div>
<motion.div variants={buttonVariant} whileHover="hover" whileTap="tap">
<Link className="ph-btn ph-btn--ghost main-btn" href="/contribute">
{t("Contribute")}
<FontAwesomeIcon icon={faGithub} style={{ marginLeft: 8 }} />
</Link>
{t("Go to Simulations")}
<FontAwesomeIcon icon={faArrowRight} style={{ marginLeft: 8 }} />
</Link>
</motion.div>
<motion.div
variants={buttonVariant}
whileHover="hover"
whileTap="tap"
>
<Link className="ph-btn ph-btn--ghost main-btn" href="/contribute">
{t("Contribute")}
<FontAwesomeIcon icon={faGithub} style={{ marginLeft: 8 }} />
</Link>
</motion.div>
</motion.div>
</motion.div>

{/* Info text */}
<motion.p className="ph-hero__info" variants={fadeUp(reduceMotion)}>
{t("Currently")} {chaptersCount} {t("chapters available.")}
</motion.p>
{/* Info text */}
<motion.p className="ph-hero__info" variants={fadeUp(reduceMotion)}>
{t("Currently")} {chaptersCount} {t("chapters available.")}
</motion.p>
</div>

<motion.aside
className="ph-hero-preview"
aria-label={t("Physics simulation preview")}
variants={fadeUp(reduceMotion)}
>
<div className="ph-hero-preview__toolbar">
<span />
<span />
<span />
<strong>{t("Live Simulation")}</strong>
</div>
<div className="ph-hero-preview__stage" aria-hidden="true">
<div className="ph-hero-preview__scan" />
<div className="ph-hero-preview__pulse ph-hero-preview__pulse--one" />
<div className="ph-hero-preview__pulse ph-hero-preview__pulse--two" />
<div className="ph-hero-preview__orbit ph-hero-preview__orbit--outer" />
<div className="ph-hero-preview__orbit ph-hero-preview__orbit--inner" />
<div className="ph-hero-preview__orbit ph-hero-preview__orbit--tilt" />
<div className="ph-hero-preview__mass ph-hero-preview__mass--primary" />
<div className="ph-hero-preview__mass ph-hero-preview__mass--secondary" />
<div className="ph-hero-preview__mass ph-hero-preview__mass--tertiary" />
<div className="ph-hero-preview__vector" />
<FontAwesomeIcon className="ph-hero-preview__atom" icon={faAtom} />
</div>
<div
className="ph-hero-preview__topics"
aria-label={t("Related topics")}
>
<Link href="/simulations/BallGravity">{t("Gravity")}</Link>
<Link href="/simulations/VectorsOperations">{t("Vectors")}</Link>
<Link href="/simulations/SimplePendulum">{t("Oscillations")}</Link>
</div>
<div className="ph-hero-preview__metrics">
<Link href="/simulations/CircularMotion">
<FontAwesomeIcon icon={faWaveSquare} />v = 8.2 m/s
</Link>
<Link href="/simulations/SpringConnection">
<FontAwesomeIcon icon={faBolt} />E = 42 J
</Link>
<Link className="ph-hero-preview__try" href="/simulations">
<FontAwesomeIcon icon={faPlay} />
{t("Try it live")}
</Link>
</div>
</motion.aside>
</motion.div>
);
}
2 changes: 1 addition & 1 deletion app/(core)/components/TopSim.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default function TopSim() {
<div className={`top-nav-sim ${isCompleted ? "notranslate" : ""}`}>
{!isMobile && (
<div className="top-nav-sim-back-to-home-wrapper">
<Back link="/simulations" />
<Back link="/" />
</div>
)}
<div
Expand Down
30 changes: 21 additions & 9 deletions app/(core)/components/theory/elements/TheoryImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import useTranslation from "../../../../../app/(core)/hooks/useTranslation.ts";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCopyright, faUpload } from "@fortawesome/free-solid-svg-icons";
import { EditableProps } from "../types";
import Image from "next/image";

type ImageSize = "xsmall" | "small" | "medium" | "large" | "full";

Expand Down Expand Up @@ -44,6 +43,18 @@ export const TheoryImage: React.FC<TheoryImageProps> = ({
}
};

const fallbackSrc = "https://via.placeholder.com/800x400?text=Image";
const imageSrc = currentSrc || fallbackSrc;

const sourceHostname = (() => {
if (!href) return "";
try {
return new URL(href).hostname;
} catch {
return href;
}
})();

return (
<figure
className={`theory-image-container size-${size} ${isCompleted ? "notranslate" : ""}`}
Expand Down Expand Up @@ -82,21 +93,22 @@ export const TheoryImage: React.FC<TheoryImageProps> = ({
className={`image-wrapper ${!isEditing && href ? "is-link" : ""}`}
onClick={handleImageClick}
>
<Image
src={
currentSrc || "https://via.placeholder.com/800x400?text=No+Image"
}
{/* Blog articles use arbitrary external/redirecting sources, so render
them directly instead of routing through next/image validation. */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={imageSrc}
alt={t(alt ?? "")}
className={isEditing ? "editing-border" : ""}
width={800}
height={450}
style={{ width: "100%", height: "auto" }}
loading="lazy"
decoding="async"
onError={() => setCurrentSrc(fallbackSrc)}
/>

{href && (
<div className="copyright-badge">
<FontAwesomeIcon icon={faCopyright} className="badge-icon" />
<span className="badge-text">{new URL(href).hostname}</span>
<span className="badge-text">{sourceHostname}</span>
</div>
)}
</div>
Expand Down
1 change: 1 addition & 0 deletions app/(core)/components/theory/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface SectionData {
export interface BlogContent {
title: string;
desc: string;
thumbnail?: string;
tags: string[];
sections: SectionData[];
}
Expand Down
1 change: 1 addition & 0 deletions app/(core)/data/initialContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
export const initialContentData = {
title: "Visual Editor Guide",
desc: "",
thumbnail: "",
tags: [],
sections: [
{
Expand Down
Loading
Loading