changed the code render for the side quests tab

This commit is contained in:
2026-01-12 23:34:12 -06:00
parent b6aa1b0316
commit d31de41ca3
7 changed files with 336 additions and 158 deletions

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import '../styles/components/codeBlock.css';
interface CodeBlockProps {
language?: string;
code: string;
}
const CodeBlock: React.FC<CodeBlockProps> = ({ language, code }) => (
<div className="code-block-container">
<div className="code-block-header">
<div className="window-controls">
<div className="control-dot red"></div>
<div className="control-dot yellow"></div>
<div className="control-dot green"></div>
</div>
<span className="language-label">
{language || 'text'}
</span>
</div>
<SyntaxHighlighter
language={language || 'text'}
style={vscDarkPlus}
customStyle={{
margin: 0,
borderRadius: '0 0 12px 12px',
fontSize: '14px',
background: 'transparent',
padding: '20px'
}}
>
{code}
</SyntaxHighlighter>
</div>
);
export default CodeBlock;

View File

@@ -2,6 +2,11 @@ import { motion, Variants } from "framer-motion";
import "../styles/components/cards.css"; import "../styles/components/cards.css";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
export type DetailSection =
| { type: 'text'; title?: string; content: string }
| { type: 'code'; title?: string; language?: string; code: string }
| { type: 'image'; src: string; alt?: string; caption?: string; title?: string };
export interface Project { export interface Project {
id: number; id: number;
title: string; title: string;
@@ -13,12 +18,7 @@ export interface Project {
repo?: string; repo?: string;
}; };
hasDetails?: boolean; hasDetails?: boolean;
detailContent?: { sections?: DetailSection[];
longDescription: string;
challenges: string;
learnings: string;
codeSnippet?: string;
};
} }
interface ProjectCardProps { interface ProjectCardProps {

View File

@@ -0,0 +1,36 @@
import React from 'react';
import CodeBlock from './CodeBlock';
interface RichTextRendererProps {
content: string;
}
const RichTextRenderer: React.FC<RichTextRendererProps> = ({ content }) => {
if (!content) return null;
const parts = content.split(/```/);
return (
<div className="rich-text-content">
{parts.map((part, index) => {
if (index % 2 === 1) {
// Code block
const firstLineBreak = part.indexOf('\n');
const language = part.slice(0, firstLineBreak).trim();
const code = part.slice(firstLineBreak + 1).trim();
return <CodeBlock key={index} language={language} code={code} />;
}
// Regular text (split by newlines for paragraph handling)
return (
<span key={index} style={{ whiteSpace: 'pre-wrap' }}>
{part}
</span>
);
})}
</div>
);
};
export default RichTextRenderer;

View File

@@ -28,10 +28,17 @@ export const SIDEQUESTS: Project[] = [
repo: "https://github.com/Bayda77/sidequests", // Placeholder link repo: "https://github.com/Bayda77/sidequests", // Placeholder link
}, },
hasDetails: true, hasDetails: true,
detailContent: { sections: [
longDescription: "This project started as a deep dive into the world of fragment shaders. I wanted to understand how to create organic-looking textures without relying on image assets. Using Perlin noise and fractional Brownian motion (fBm), I created a dynamic, shifting terrain that reacts to time and mouse input.", {
codeSnippet: `\`\`\`glsl type: 'text',
float noise(vec2 st) { title: 'Overview',
content: "This project started as a deep dive into the world of fragment shaders. I wanted to understand how to create organic-looking textures without relying on image assets. Using Perlin noise and fractional Brownian motion (fBm), I created a dynamic, shifting terrain that reacts to time and mouse input."
},
{
type: 'code',
language: 'glsl',
title: 'Noise Function',
code: `float noise(vec2 st) {
vec2 i = floor(st); vec2 i = floor(st);
vec2 f = fract(st); vec2 f = fract(st);
float a = random(i); float a = random(i);
@@ -40,11 +47,19 @@ float noise(vec2 st) {
float d = random(i + vec2(1.0, 1.0)); float d = random(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f); vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) + (c - a)* u.y * (1.0 - u.x) + (d - b) * u.x * u.y; return mix(a, b, u.x) + (c - a)* u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}`
},
{
type: 'text',
title: 'Challenges',
content: "One of the biggest challenges was optimizing the GLSL code to run smoothly on lower-end devices. Mathematical operations in the fragment shader can get expensive quickly."
},
{
type: 'text',
title: 'Learnings',
content: "I gained a much deeper understanding of the graphics pipeline, vector math, and how to think about visuals in terms of mathematical functions rather than pixels."
} }
\`\`\``, ]
challenges: "One of the biggest challenges was optimizing the GLSL code to run smoothly on lower-end devices. Mathematical operations in the fragment shader can get expensive quickly.",
learnings: "I gained a much deeper understanding of the graphics pipeline, vector math, and how to think about visuals in terms of mathematical functions rather than pixels."
}
} }
]; ];

View File

@@ -1,84 +1,10 @@
import { useEffect } from "react";
import { useParams, Link } from "react-router-dom"; import { useParams, Link } from "react-router-dom";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { SIDEQUESTS } from "./Projects"; import { SIDEQUESTS } from "./Projects";
import { useEffect } from "react"; import CodeBlock from "../components/CodeBlock";
import RichTextRenderer from "../components/RichTextRenderer";
const RichTextRenderer = ({ content }: { content: string }) => { import "../styles/pages/sidequestDetail.css";
if (!content) return null;
const parts = content.split(/```/);
return (
<div className="rich-text-content">
{parts.map((part, index) => {
if (index % 2 === 1) {
// Code block
const firstLineBreak = part.indexOf('\n');
const language = part.slice(0, firstLineBreak).trim();
const code = part.slice(firstLineBreak + 1).trim();
return (
<div key={index} style={{
margin: '30px 0',
borderRadius: '12px',
overflow: 'hidden',
border: '1px solid rgba(255, 255, 255, 0.1)',
background: '#1e1e1e', // Match VS Code dark background roughly or stay transparent if needed
boxShadow: '0 10px 30px rgba(0,0,0,0.3)'
}}>
{/* Window Header */}
<div style={{
display: 'flex',
alignItems: 'center',
padding: '12px 16px',
background: 'rgba(255, 255, 255, 0.05)',
borderBottom: '1px solid rgba(255, 255, 255, 0.05)'
}}>
<div style={{ display: 'flex', gap: '8px', marginRight: 'auto' }}>
<div style={{ width: '12px', height: '12px', borderRadius: '50%', background: '#ff5f56' }}></div>
<div style={{ width: '12px', height: '12px', borderRadius: '50%', background: '#ffbd2e' }}></div>
<div style={{ width: '12px', height: '12px', borderRadius: '50%', background: '#27c93f' }}></div>
</div>
<span style={{
fontSize: '12px',
color: 'rgba(255, 255, 255, 0.5)',
fontFamily: 'monospace',
textTransform: 'uppercase',
marginTop: '2px'
}}>
{language || 'text'}
</span>
</div>
<SyntaxHighlighter
language={language || 'text'}
style={vscDarkPlus}
customStyle={{
margin: 0,
borderRadius: '0 0 12px 12px', // Only round bottom
fontSize: '14px',
background: 'transparent', // Let container bg show or stick to theme
padding: '20px'
}}
>
{code}
</SyntaxHighlighter>
</div>
);
}
// Regular text (split by newlines for paragraph handling)
return (
<span key={index} style={{ whiteSpace: 'pre-wrap' }}>
{part}
</span>
);
})}
</div>
);
};
export default function SidequestDetail() { export default function SidequestDetail() {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
@@ -90,109 +16,91 @@ export default function SidequestDetail() {
if (!project) { if (!project) {
return ( return (
<div style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column", color: "white" }}> <div className="sidequest-error-container">
<h2>Sidequest not found</h2> <h2>Sidequest not found</h2>
<Link to="/projects" style={{ color: "rgba(255,255,255,0.7)", marginTop: "20px" }}>Back to Projects</Link> <Link to="/projects" className="sidequest-back-link-error">Back to Projects</Link>
</div> </div>
); );
} }
return ( return (
<div className="mainContentBlock" style={{ minWidth: "66vw", display: "flex", justifyContent: "center", paddingTop: "40px" }}> <div className="mainContentBlock sidequest-container">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }} transition={{ duration: 0.6 }}
style={{ width: "100%", maxWidth: "800px", padding: "0 20px" }} className="sidequest-content-wrapper"
> >
<Link to="/projects" style={{ display: "inline-flex", alignItems: "center", color: "rgba(255,255,255,0.6)", textDecoration: "none", marginBottom: "30px" }}> <Link to="/projects" className="sidequest-back-link">
Back to Projects Back to Projects
</Link> </Link>
<div style={{ marginBottom: "40px" }}> <div className="sidequest-header">
<h1 style={{ fontSize: "2.5rem", marginBottom: "10px", color: "white" }}>{project.title}</h1> <h1 className="sidequest-title">{project.title}</h1>
<div style={{ display: "flex", flexWrap: "wrap", gap: "10px", marginBottom: "20px" }}> <div className="sidequest-tech-stack">
{project.techStack.map(tech => ( {project.techStack.map(tech => (
<span key={tech} style={{ <span key={tech} className="sidequest-tech-tag">
background: "rgba(255,255,255,0.1)",
padding: "4px 12px",
borderRadius: "15px",
fontSize: "0.85rem",
color: "rgba(255,255,255,0.9)"
}}>
{tech} {tech}
</span> </span>
))} ))}
</div> </div>
</div> </div>
<div style={{ <div className="sidequest-hero-image-container">
width: "100%",
height: "400px",
borderRadius: "16px",
overflow: "hidden",
marginBottom: "40px",
boxShadow: "0 10px 30px rgba(0,0,0,0.3)"
}}>
<img <img
src={project.image} src={project.image}
alt={project.title} alt={project.title}
style={{ width: "100%", height: "100%", objectFit: "cover" }} className="sidequest-hero-image"
/> />
</div> </div>
<div style={{ <div className="sidequest-details-container">
background: "rgba(255, 255, 255, 0.05)", {/* Dynamic Sections Rendering */}
backdropFilter: "blur(10px)", {project.sections?.map((section, index) => {
borderRadius: "16px", switch (section.type) {
padding: "40px", case 'text':
border: "1px solid rgba(255, 255, 255, 0.1)" return (
}}> <section key={index} className="sidequest-section">
<section style={{ marginBottom: "30px" }}> {section.title && <h2 className="sidequest-section-title">{section.title}</h2>}
<h2 style={{ color: "white", fontSize: "1.5rem", marginBottom: "15px" }}>Overview</h2> <div className="sidequest-section-content">
<div style={{ color: "rgba(255,255,255,0.8)", lineHeight: "1.7" }}> <RichTextRenderer content={section.content} />
<RichTextRenderer content={project.detailContent?.longDescription || project.description} />
</div> </div>
</section> </section>
);
{project.detailContent?.codeSnippet && ( case 'code':
<div style={{ marginBottom: "30px" }}> return (
<RichTextRenderer content={project.detailContent.codeSnippet} /> <section key={index} className="sidequest-section">
</div> {section.title && <h2 className="sidequest-section-title">{section.title}</h2>}
)} <CodeBlock language={section.language} code={section.code} />
{project.detailContent?.challenges && (
<section style={{ marginBottom: "30px" }}>
<h2 style={{ color: "white", fontSize: "1.5rem", marginBottom: "15px" }}>Challenges</h2>
<div style={{ color: "rgba(255,255,255,0.8)", lineHeight: "1.7" }}>
<RichTextRenderer content={project.detailContent.challenges} />
</div>
</section> </section>
)} );
case 'image':
{project.detailContent?.learnings && ( return (
<section> <section key={index} className="sidequest-section">
<h2 style={{ color: "white", fontSize: "1.5rem", marginBottom: "15px" }}>Learnings</h2> {section.title && <h2 className="sidequest-section-title">{section.title}</h2>}
<div style={{ color: "rgba(255,255,255,0.8)", lineHeight: "1.7" }}> <img src={section.src} alt={section.alt || ''} className="sidequest-section-image" />
<RichTextRenderer content={project.detailContent.learnings} /> {section.caption && <p className="sidequest-section-caption">{section.caption}</p>}
</div>
</section> </section>
)} );
default:
return null;
}
})}
<div style={{ marginTop: "40px", paddingTop: "20px", borderTop: "1px solid rgba(255,255,255,0.1)", display: "flex", gap: "20px" }}> <div className="sidequest-footer-links">
{project.links.demo && ( {project.links.demo && (
<a href={project.links.demo} target="_blank" rel="noopener noreferrer" style={{ color: "white", textDecoration: "underline" }}> <a href={project.links.demo} target="_blank" rel="noopener noreferrer" className="sidequest-footer-link">
Live Demo Live Demo
</a> </a>
)} )}
{project.links.repo && ( {project.links.repo && (
<a href={project.links.repo} target="_blank" rel="noopener noreferrer" style={{ color: "white", textDecoration: "underline" }}> <a href={project.links.repo} target="_blank" rel="noopener noreferrer" className="sidequest-footer-link">
View Code View Code
</a> </a>
)} )}
</div> </div>
</div> </div>
<div style={{ height: "100px" }}></div> <div className="sidequest-footer-spacer"></div>
</motion.div> </motion.div>
</div> </div>
); );

View File

@@ -0,0 +1,48 @@
.code-block-container {
margin: 30px 0;
border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
background: #1e1e1e;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.code-block-header {
display: flex;
align-items: center;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.window-controls {
display: flex;
gap: 8px;
margin-right: auto;
}
.control-dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.control-dot.red {
background: #ff5f56;
}
.control-dot.yellow {
background: #ffbd2e;
}
.control-dot.green {
background: #27c93f;
}
.language-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
font-family: monospace;
text-transform: uppercase;
margin-top: 2px;
}

View File

@@ -0,0 +1,131 @@
.sidequest-error-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
color: white;
}
.sidequest-back-link-error {
color: rgba(255, 255, 255, 0.7);
margin-top: 20px;
}
.sidequest-container {
min-width: 66vw;
display: flex;
justify-content: center;
padding-top: 40px;
}
.sidequest-content-wrapper {
width: 100%;
max-width: 800px;
padding: 0 20px;
}
.sidequest-back-link {
display: inline-flex;
align-items: center;
color: rgba(255, 255, 255, 0.6);
text-decoration: none;
margin-bottom: 30px;
transition: color 0.2s ease;
}
.sidequest-back-link:hover {
color: white;
}
.sidequest-header {
margin-bottom: 40px;
}
.sidequest-title {
font-size: 2.5rem;
margin-bottom: 10px;
color: white;
}
.sidequest-tech-stack {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
}
.sidequest-tech-tag {
background: rgba(255, 255, 255, 0.1);
padding: 4px 12px;
border-radius: 15px;
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.9);
}
.sidequest-hero-image-container {
width: 100%;
height: 400px;
border-radius: 16px;
overflow: hidden;
margin-bottom: 40px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.sidequest-hero-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.sidequest-details-container {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 40px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.sidequest-section {
margin-bottom: 30px;
}
.sidequest-section-title {
color: white;
font-size: 1.5rem;
margin-bottom: 15px;
}
.sidequest-section-content {
color: rgba(255, 255, 255, 0.8);
line-height: 1.7;
}
.sidequest-section-image {
width: 100%;
border-radius: 12px;
}
.sidequest-section-caption {
color: rgba(255, 255, 255, 0.5);
font-size: 0.9rem;
margin-top: 10px;
text-align: center;
}
.sidequest-footer-links {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
gap: 20px;
}
.sidequest-footer-link {
color: white;
text-decoration: underline;
}
.sidequest-footer-spacer {
height: 100px;
}