changed the code render for the side quests tab
This commit is contained in:
40
src/components/CodeBlock.tsx
Normal file
40
src/components/CodeBlock.tsx
Normal 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;
|
||||
@@ -2,6 +2,11 @@ import { motion, Variants } from "framer-motion";
|
||||
import "../styles/components/cards.css";
|
||||
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 {
|
||||
id: number;
|
||||
title: string;
|
||||
@@ -13,12 +18,7 @@ export interface Project {
|
||||
repo?: string;
|
||||
};
|
||||
hasDetails?: boolean;
|
||||
detailContent?: {
|
||||
longDescription: string;
|
||||
challenges: string;
|
||||
learnings: string;
|
||||
codeSnippet?: string;
|
||||
};
|
||||
sections?: DetailSection[];
|
||||
}
|
||||
|
||||
interface ProjectCardProps {
|
||||
|
||||
36
src/components/RichTextRenderer.tsx
Normal file
36
src/components/RichTextRenderer.tsx
Normal 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;
|
||||
@@ -28,10 +28,17 @@ export const SIDEQUESTS: Project[] = [
|
||||
repo: "https://github.com/Bayda77/sidequests", // Placeholder link
|
||||
},
|
||||
hasDetails: true,
|
||||
detailContent: {
|
||||
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
|
||||
float noise(vec2 st) {
|
||||
sections: [
|
||||
{
|
||||
type: 'text',
|
||||
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 f = fract(st);
|
||||
float a = random(i);
|
||||
@@ -40,11 +47,19 @@ float noise(vec2 st) {
|
||||
float d = random(i + vec2(1.0, 1.0));
|
||||
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;
|
||||
}
|
||||
\`\`\``,
|
||||
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."
|
||||
}`
|
||||
},
|
||||
{
|
||||
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."
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,84 +1,10 @@
|
||||
import { useEffect } from "react";
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
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 { useEffect } from "react";
|
||||
|
||||
const RichTextRenderer = ({ content }: { content: string }) => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
import CodeBlock from "../components/CodeBlock";
|
||||
import RichTextRenderer from "../components/RichTextRenderer";
|
||||
import "../styles/pages/sidequestDetail.css";
|
||||
|
||||
export default function SidequestDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -90,109 +16,91 @@ export default function SidequestDetail() {
|
||||
|
||||
if (!project) {
|
||||
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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mainContentBlock" style={{ minWidth: "66vw", display: "flex", justifyContent: "center", paddingTop: "40px" }}>
|
||||
<div className="mainContentBlock sidequest-container">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
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
|
||||
</Link>
|
||||
|
||||
<div style={{ marginBottom: "40px" }}>
|
||||
<h1 style={{ fontSize: "2.5rem", marginBottom: "10px", color: "white" }}>{project.title}</h1>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "10px", marginBottom: "20px" }}>
|
||||
<div className="sidequest-header">
|
||||
<h1 className="sidequest-title">{project.title}</h1>
|
||||
<div className="sidequest-tech-stack">
|
||||
{project.techStack.map(tech => (
|
||||
<span key={tech} style={{
|
||||
background: "rgba(255,255,255,0.1)",
|
||||
padding: "4px 12px",
|
||||
borderRadius: "15px",
|
||||
fontSize: "0.85rem",
|
||||
color: "rgba(255,255,255,0.9)"
|
||||
}}>
|
||||
<span key={tech} className="sidequest-tech-tag">
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
width: "100%",
|
||||
height: "400px",
|
||||
borderRadius: "16px",
|
||||
overflow: "hidden",
|
||||
marginBottom: "40px",
|
||||
boxShadow: "0 10px 30px rgba(0,0,0,0.3)"
|
||||
}}>
|
||||
<div className="sidequest-hero-image-container">
|
||||
<img
|
||||
src={project.image}
|
||||
alt={project.title}
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
className="sidequest-hero-image"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
background: "rgba(255, 255, 255, 0.05)",
|
||||
backdropFilter: "blur(10px)",
|
||||
borderRadius: "16px",
|
||||
padding: "40px",
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)"
|
||||
}}>
|
||||
<section style={{ marginBottom: "30px" }}>
|
||||
<h2 style={{ color: "white", fontSize: "1.5rem", marginBottom: "15px" }}>Overview</h2>
|
||||
<div style={{ color: "rgba(255,255,255,0.8)", lineHeight: "1.7" }}>
|
||||
<RichTextRenderer content={project.detailContent?.longDescription || project.description} />
|
||||
<div className="sidequest-details-container">
|
||||
{/* Dynamic Sections Rendering */}
|
||||
{project.sections?.map((section, index) => {
|
||||
switch (section.type) {
|
||||
case 'text':
|
||||
return (
|
||||
<section key={index} className="sidequest-section">
|
||||
{section.title && <h2 className="sidequest-section-title">{section.title}</h2>}
|
||||
<div className="sidequest-section-content">
|
||||
<RichTextRenderer content={section.content} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{project.detailContent?.codeSnippet && (
|
||||
<div style={{ marginBottom: "30px" }}>
|
||||
<RichTextRenderer content={project.detailContent.codeSnippet} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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>
|
||||
);
|
||||
case 'code':
|
||||
return (
|
||||
<section key={index} className="sidequest-section">
|
||||
{section.title && <h2 className="sidequest-section-title">{section.title}</h2>}
|
||||
<CodeBlock language={section.language} code={section.code} />
|
||||
</section>
|
||||
)}
|
||||
|
||||
{project.detailContent?.learnings && (
|
||||
<section>
|
||||
<h2 style={{ color: "white", fontSize: "1.5rem", marginBottom: "15px" }}>Learnings</h2>
|
||||
<div style={{ color: "rgba(255,255,255,0.8)", lineHeight: "1.7" }}>
|
||||
<RichTextRenderer content={project.detailContent.learnings} />
|
||||
</div>
|
||||
);
|
||||
case 'image':
|
||||
return (
|
||||
<section key={index} className="sidequest-section">
|
||||
{section.title && <h2 className="sidequest-section-title">{section.title}</h2>}
|
||||
<img src={section.src} alt={section.alt || ''} className="sidequest-section-image" />
|
||||
{section.caption && <p className="sidequest-section-caption">{section.caption}</p>}
|
||||
</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 && (
|
||||
<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 ↗
|
||||
</a>
|
||||
)}
|
||||
{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 ↗
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ height: "100px" }}></div>
|
||||
<div className="sidequest-footer-spacer"></div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
|
||||
48
src/styles/components/codeBlock.css
Normal file
48
src/styles/components/codeBlock.css
Normal 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;
|
||||
}
|
||||
131
src/styles/pages/sidequestDetail.css
Normal file
131
src/styles/pages/sidequestDetail.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user