dockerization.

This commit is contained in:
Joshua Ashton
2025-01-27 14:26:40 -07:00
parent a9b1fa419e
commit 3e69802567
34 changed files with 1003 additions and 3470 deletions
+23
View File
@@ -0,0 +1,23 @@
# Use a base image with Node.js and npm
FROM node:18-alpine
# Set the working directory inside the container
WORKDIR /app
# Copy package.json and package-lock.json
COPY package.json /app
# Install dependencies
RUN npm install
# Copy the rest of the React app
COPY src/. /app
# Build the React app for production
RUN npm run build
# Expose the port your React app will run on (usually 3000)
EXPOSE 3000
# Start the production server (serve the built files)
CMD ["npx", "serve", "dist"]
+25
View File
@@ -0,0 +1,25 @@
<!doctype html>
<html lang="en">
<head>
<meta
charset="UTF-8"
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/icon?family=Material+Icons"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
/>
<title>Josh Ashton</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+24
View File
@@ -0,0 +1,24 @@
{
"name": "portfolio",
"version": "0.0.1",
"scripts": {
"build": "vite build",
"dev": "vite --host",
"start": "vite --host"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-pdf": "^9.2.1",
"react-router": "^7.1.1",
"swiper": "^10.0.0"
},
"devDependencies": {
"vite": "^4.0.4",
"vite-plugin-terminal": "^1.2.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 732 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.
Binary file not shown.
+31
View File
@@ -0,0 +1,31 @@
import React from "react";
import VerticalSlider from "./components/VerticalSlider";
import Home from "./Home";
import Projects from "./Projects";
import Contact from "./Contact";
import "./main.css";
/*
* Primary portfolio application. Utilizes swiper.js (via /components/VerticalSlider.jsx) to
* have a scrolling section to section effect.
*
* Managed by /main.jsx.
* Data by /main.jsx.
*
* @param projectData JSON[], data necessary to create project cards.
* @param skillData JSON, data necessary to create skill cards.
*/
const App = ({ projectData, skillData }) => {
const slides = [
<Home
image="/images/profile.jpg"
skills={skillData}
/>,
<Projects cardData={projectData} />,
<Contact />,
];
return <VerticalSlider slides={slides} />;
}
export default App;
+98
View File
@@ -0,0 +1,98 @@
import React, { useState } from "react";
/*
* Contact page or "slide". The contact form functionality is enabled by a NodeJS server,
* see /server.js. A basic form for getting contact information. Emails are sent to me@joshashton.dev
* from contact@joshashton.dev.
*
* TODO: Handle blank and spam inputs.
*
* Managed by /App.jsx.
* Interacts with /server.js.
*/
const Contact = () => {
const [formData, setFormData] = useState({
name: "",
emailAddress: "",
message: "",
});
const [isSubmitted, setIsSubmitted] = useState(false);
// Update form data to always be up to date with the entries on the form.
const handleChange = (event) => {
setFormData({
...formData,
[event.target.name]: event.target.value,
});
};
// Transmit data to the server's API for sending email.
const handleSubmit = async (event) => {
event.preventDefault(); // Prevent default form submission behavior
setIsSubmitted(true);
try {
const response = await fetch("http://localhost:3001/api/send-email", {
method: "POST", // Use POST for sending data
headers: { "Content-Type": "application/json" }, // Set the content type
body: JSON.stringify(formData), // Convert the JS object to JSON string
});
const json = await response.json();
setData(json);
} catch (error) {
console.error("Error fetching data:", error);
}
};
return (
<div id="contact-section">
<h1>Contact Me</h1>
<div id="form-container"></div>
{isSubmitted ? (
<p>Thanks for reaching out! I'll get in touch shortly.</p>
) : (
<form onSubmit={handleSubmit}>
<div className="row">
<input
className="shadow"
placeholder="Name"
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
/>
<input
className="shadow"
placeholder="example@email.com"
type="email"
id="emailAddress"
name="emailAddress"
value={formData.emailAddress}
onChange={handleChange}
/>
</div>
<div className="row">
<textarea
className="shadow"
placeholder="Message"
id="message"
name="message"
value={formData.message}
onChange={handleChange}
/>
</div>
<div className="row">
<button type="submit" className="button shadow">
Submit
</button>
</div>
</form>
)}
</div>
);
}
export default Contact;
+26
View File
@@ -0,0 +1,26 @@
import React from "react";
/*
* An error page component, typically used for 404 errors, but available for use for any other errors.
*
* TODO: Stylize the error page.
*
* @param errorCode String
* @param errorMessage String
*/
const ErrorPage = ({ errorCode, errorMessage }) => {
return (
<>
<div className="error-page">
<h1>{errorCode}</h1>
<p>{errorMessage}</p>
<a href="/" className="button shadow">
Return to joshashton.dev
</a>
</div>
</>
);
}
export default ErrorPage;
+67
View File
@@ -0,0 +1,67 @@
import React, { useState } from "react";
import SkillCard from "./components/SkillCard";
/**
* "Home" page/slide of my portfolio.
*
* Managed by /App.jsx.
* Data by /main.jsx.
*
* @param image String
* @param skills json, containing JSON values of String arrays.
*/
const Home = ({ image, skills }) => {
// Manage active card state, defaulting to languages card being active or open. Mobile only.
const [activeCardIndex, setActiveCardIndex] = useState(0);
return (
<>
<div id="primary">
<div className="header">
<div className="header-image center-row">
<img id="profile" src={image} />
</div>
<div className="center-column">
<div className="center-column">
<h1>Hi, I'm Josh Ashton.</h1>
<h3>Junior full-stack developer.</h3>
<a className="button" href="/resume" target="_blank">
<div className="button-grid">Access my resume</div>
</a>
</div>
</div>
</div>
<div id="skill-cards">
<div className="center-row" id="skill-cards-colrow">
<SkillCard
header="Languages"
symbol="code"
list={skills.languages}
isActive={activeCardIndex === 0}
onToggle={() => setActiveCardIndex(0)}
/>
<SkillCard
header="Tools"
symbol="terminal"
list={skills.tools}
isActive={activeCardIndex === 1}
onToggle={() => setActiveCardIndex(1)}
/>
<SkillCard
header="Other"
symbol="hub"
list={skills.other}
isActive={activeCardIndex === 2}
onToggle={() => setActiveCardIndex(2)}
/>
</div>
</div>
<span className="material-icons down-indicator">
keyboard_double_arrow_down
</span>
</div>
</>
);
}
export default Home;
+86
View File
@@ -0,0 +1,86 @@
import React, { useRef } from "react";
import ProjectCard from "./components/ProjectCard";
import { Swiper, SwiperSlide } from "swiper/react";
import { Scrollbar } from "swiper/modules";
import "swiper/css";
import "swiper/css/effect-cards";
import "swiper/css/scrollbar";
/*
* The Projects page or "slide". Implements swiperjs (see documentation at https://swiperjs.com/) to create a nested
* slider/swiper inside of the root VerticalSlider. This nested slider is horizontal, and displays Project Cards
* (see /components/ProjectCard.jsx), generated for each element of the parameter cardData.
*
* Managed by /App.jsx.
* Data by /main.jsx.
*
* @param cardData json[], each object contains the following: title, descriptionA, featureList, image, url
*
* TODO: Cursor icon when hover on the slide buttons should be a pointer, not highlighter.
*/
const Projects = ({ cardData }) => {
// Reference the nested slider instead of the root slider.
const projectSwiperRef = useRef(null);
const prevSlide = () => {
if (projectSwiperRef.current) {
projectSwiperRef.current.swiper.slidePrev();
}
};
const nextSlide = () => {
if (projectSwiperRef.current) {
projectSwiperRef.current.swiper.slideNext();
}
};
return (
<>
<div id="project-section">
<div id="project-grid-container">
<div id="projects-header-row">
<h1>Projects</h1>
<div id="projects-nav-buttons">
<div className="slider-nav-button tooltip" onClick={prevSlide}>
<span className="material-icons project-nav-icon">
chevron_left
</span>
<span className="tooltip-text">Previous Project</span>
</div>
<div className="slider-nav-button tooltip" onClick={nextSlide}>
<span className="material-icons project-nav-icon">
chevron_right
</span>
<span className="tooltip-text">Next Project</span>
</div>
</div>
</div>
<Swiper
modules={[Scrollbar]}
slidesPerView={1}
loop={false}
id="project-cards-section"
className="mySwiper"
scrollbar={{ hide: false, el: ".swiper-slide" }}
ref={projectSwiperRef}
>
{cardData.map((c, index) => (
<SwiperSlide key={index}>
<ProjectCard
title={c.title}
description={c.description}
featureList={c.featureList}
image={c.image}
url={c.url}
/>
</SwiperSlide>
))}
</Swiper>
</div>
</div >
</>
);
}
export default Projects;
+96
View File
@@ -0,0 +1,96 @@
import React from "react";
/*
* Resume page component.
*/
const Resume = ({ jobData, eduData, skillData }) => {
return (
<>
<div id="resume-section">
<h1>Josh Ashton</h1>
<h3>Junior Full-Stack Developer</h3>
<h3>Work Experience</h3>
<div className="jobs">
{jobData.map((job, index) => (
<Job key={index} company={job.company} title={job.title} start={job.start} end={job.end} location={job.location} responsibilities={job.responsibilities} />
))}
</div>
<h3>Education</h3>
<div className="education-section">
{eduData.map((edu, index) => (
<Education key={index} institution={edu.institution} degree={edu.degree} gpa={edu.gpa} start={edu.start} end={edu.end} />
))}
</div>
<h3>Skills</h3>
<Skill languages={skillData.languages} tools={skillData.tools} other={skillData.other} />
</div>
</>
);
}
function Skill({ languages, tools, other }) {
return (
<>
<div className="resume-skills">
<div className="resume-skills-skill">
<h4>Languages:</h4>
<p>{languages.join(", ")}</p>
</div>
<div className="resume-skills-skill">
<h4>Tools:</h4>
<p>{tools.join(", ")}</p>
</div>
<div className="resume-skills-skill">
<h4>Other:</h4>
<p>{other.join(", ")}</p>
</div>
</div>
</>
);
}
function Education({ institution, degree, gpa, start, end }) {
return (
<>
<div className="edu">
<div className="edu-header">
<div className="edu-header-row">
<div className="edu-and-degree">
<h4>{institution}</h4>
<h6>- {degree}</h6>
</div>
<h6 className="edu-time">{start} - {end}</h6>
</div>
<h5>{gpa}</h5>
</div>
</div>
</>
);
}
function Job({ company, title, start, end, location, responsibilities }) {
return (
<>
<div className="job">
<div className="job-header">
<div className="job-header-row">
<div className="company-and-location">
<h4>{company} </h4>
<h6>- {location}</h6>
</div>
<h6 className="job-time">{start} - {end}</h6>
</div>
<h5>{title}</h5>
</div>
<ul className="job-list">
{responsibilities.map((r, index) => (
<li key={index} className="job-list-item">{r}</li>
))}
</ul>
</div>
</>
)
}
export default Resume;
+50
View File
@@ -0,0 +1,50 @@
import React from "react";
/*
* An individual Project Card component, generated from project data in /App.jsx. Displays an image/gif
* of the project to the left on desktop, and above on mobile, a card. This card is simple, just a header,
* some description text, a list of implemented features, and a button to navigate to the GitHub for the
* project.
*
* Managed by /Projects.jsx.
* Data by /main.jsx > /App.jsx > /Projects.jsx.
*
* @param title String
* @param description String
* @param featureList String[]
* @param image String
* @param url String
*/
const ProjectCard = ({
title,
description,
featureList,
image,
url,
}) => {
return (
<div className="project-card">
<div className="img-container">
<img src={image} className="shadow" />
</div>
<div className="project-text-column shadow">
<div className="project-text">
<h3>{title}</h3>
<p>{description}</p>
<ul className="feature-list">
{featureList.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
<div className="row">
<a className="button" href={url} target="_blank">
<div className="button-grid">View on GitHub</div>
</a>
</div>
</div>
</div>
</div>
);
}
export default ProjectCard;
+51
View File
@@ -0,0 +1,51 @@
import React, { useState, useEffect } from "react";
/**
* Skill Card component.
*
* On mobile, only one card at a time may be active. The currently active card is
* managed by the parent component, Home.jsx, via the isActive and onToggle props.
*
* Managed by /Home.jsx.
* Data by /main.jsx > /App.jsx > /Home.jsx
*
* @param header String, the skill card's header
* @param symbol String, a Material Icon from https://fonts.google.com/icons?icon.size=24&icon.color=%23e8eaed
* @param list String[], a list of skills
* @param isActive boolean, the parent's state tracking if this instance of a skill card is the active (open) one. Only applicable to mobile.
* @param onToggle function(), the action the parent component state takes when a skill card header is pressed. Only applicable to mobile.
*/
const SkillCard = ({ header, symbol, list, isActive, onToggle }) => {
const [isMobile, setIsMobile] = useState(window.innerWidth <= 900);
useEffect(() => {
const handleResize = () => {
const mediaQuery = window.matchMedia("(max-width: 900px)");
setIsMobile(mediaQuery.matches);
};
handleResize(); // Initial check on mount
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
// Display if on desktop (!isMobile) or if Home.jsx determines this to be the currently active component.
return (
<div className="card shadow">
<div className="skill-card-header" onClick={() => onToggle()}>
<span className="material-icons skill-symbol">{symbol}</span>
<h4>{header}</h4>
</div>
{(isActive || !isMobile) && (
<ul>
{list.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
)}
</div>
);
};
export default SkillCard;
+44
View File
@@ -0,0 +1,44 @@
import React from "react";
import { Swiper, SwiperSlide } from "swiper/react";
import { Mousewheel, Pagination } from "swiper/modules";
import "swiper/css";
import "swiper/css/pagination";
/*
* For my portfolio, this is the wrapper in which all other content is displayed. Every component is
* a child of the VerticalSlider, excluding the Resume page (for now?).
*
* Utilizes swiperjs, see https://swiperjs.com/ for documentation.
*
* @param slides ReactComponent[], an array of pages or "slides" to display in a vertical slider.
*/
const VerticalSlider = ({ slides }) => {
// Mousewheel is for scrolling effect. Pagination is for "bullets" to indicate active and inactive slides. Pagination
// clickable allows for going to the corresponding page or "slide".
return (
<>
<Swiper
modules={[Mousewheel, Pagination]}
direction={"vertical"}
speed={800}
loop={false}
pagination={{
clickable: true,
}}
mousewheel={{
thresholdDelta: 50,
sensitivity: 1,
}}
>
{slides.map((slide, index) => (
<SwiperSlide key={index}>
{<div className="slide-container">{slide}</div>}
</SwiperSlide>
))}
</Swiper>
</>
);
}
export default VerticalSlider;
+975
View File
@@ -0,0 +1,975 @@
@import url("https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap");
/**GLOBAL**/
:root {
--width: 480px;
--dark-0: #091A19;
--dark-1: #10211E;
--dark-2: #162722;
--dark-3: #1C2E26;
--primary-1: #304334;
--primary: #6B825C;
--accent-0: #90D889;
--accent-1: #8FC973;
--accent-2: #B8DAA6;
--text: #E0EAD9;
}
*,
*::before,
*::after {
padding: 0;
margin: 0;
box-sizing: border-box;
font-family: "Lato", sans-serif;
}
body {
margin: 0;
position: relative;
height: 100%;
background: linear-gradient(135deg,
var(--dark-3) 0%,
var(--dark-2) 39%,
var(--dark-1) 61%,
var(--dark-0) 100%);
background-size:
300% 100%,
cover;
animation: background-fade 5s linear infinite;
animation-direction: alternate;
background-attachment: fixed;
}
h1,
h2,
h3,
h4,
h5,
h6,
p,
li {
color: var(--text);
}
h1 {
font-size: clamp(4rem, 5vw, 10rem);
font-weight: 600;
letter-spacing: 2px;
width: max-content;
}
h2,
h3 {
font-weight: 500;
letter-spacing: 2px;
width: max-content;
}
h2 {
font-size: clamp(3rem, 4vw, 7rem);
}
h3 {
font-size: clamp(2rem, 3rem, 6rem);
}
h4 {
font-weight: 400;
line-height: 1.5;
}
h5,
h6 {
font-weight: 300;
}
h4 {
font-size: clamp(1rem, 1.5vw, 4rem);
}
h5 {
font-size: clamp(1rem, 1.25vw, 3.5rem);
}
h6 {
font-size: clamp(1rem, 1.125vw, 2rem);
}
p,
li {
font-weight: 300;
font-size: clamp(1rem, 1.1vw, 1.5rem);
color: var(--text);
}
.material-icons {
font-variation-settings:
"FILL" 0,
"wght" 200,
"GRAD" 0,
"opsz" 24;
font-size: 5vw;
color: var(--text);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.shadow {
box-shadow:
0 13px 27px -5px var(--dark-0),
0 8px 16px -8px hsla(0, 0%, 0%, 0.3),
0 -6px 16px -6px hsla(0, 0%, 0%, 0.03);
}
.center-column {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.center-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
text-align: center;
}
.text-section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 80%;
}
/**SWIPER PAGES**/
.swiper {
width: 100vw;
height: 100vh;
overflow: hidden;
}
.slide-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: row;
justify-content: center;
}
.mySwiper {
width: 100%;
height: 100%;
}
/**ACTIVE PAGE ANIMATIONS**/
.header,
#skill-cards,
#project-section,
#project-cards-section,
#contact-section {
opacity: 0;
}
.swiper-slide-active .header,
#slcc-splash .header,
.swiper-slide-active #skill-cards,
.swiper-slide-active #project-section,
.swiper-slide-active #project-cards-section,
.swiper-slide-active #contact-section {
opacity: 0;
}
.swiper-slide-active .header,
#slcc-splash .header,
.swiper-slide-active #skill-cards,
.swiper-slide-active #project-section,
.swiper-slide-active #project-cards-section,
.swiper-slide-active #contact-section,
.down-indicator {
animation: moveDown 0.8s ease-in forwards;
}
.swiper-slide-active .header,
#slcc-splash .header,
.swiper-slide-active #project-section,
.down-indicator {
animation-delay: 0.2s;
}
.swiper-slide-active #skills-cards,
.swiper-slide-active #project-cards-section {
animation-delay: 0.8s;
}
/**SWIPER BULLETS**/
.swiper-pagination-bullet {
width: 18px;
height: 18px;
background-color: var(--accent-0);
border-radius: 50%;
transition: all 0.6s ease-in-out;
}
.swiper-pagination-bullet-active {
height: 36px;
background-color: var(--accent-2);
border-radius: 14px;
}
/**PRIMARY PAGE**/
#primary {
width: 100%;
margin-top: 4vh;
display: grid;
height: 100vh;
grid-template-columns: 100%;
grid-template-rows: 40% 40% 10%;
column-gap: 5%;
}
.header {
display: grid;
grid-template-columns: 30% 70%;
grid-template-rows: 100%;
}
.header-text {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.header-image {
height: 40vh;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
}
.header-image img {
width: calc(var(--width) / 2);
filter: drop-shadow(5px 6px 5px rgba(36, 36, 42, 0.5));
user-select: none;
z-index: 1;
animation: imgMoveDown 0.8s ease-in forwards 0.2s;
border-radius: 50%;
}
#skill-cards {
width: 100%;
}
/**SKILL CARDS**/
.card {
text-align: center;
background-color: var(--text);
border-radius: 20px;
min-width: 20vw;
max-width: 50vw;
margin: 1vw;
transition-duration: 500ms;
}
.skill-card-header {
display: grid;
grid-template-columns: 20% 60%;
grid-template-rows: 100%;
column-gap: 10%;
align-items: center;
justify-content: center;
text-align: center;
padding: 1vw 2vw 1vw 2vw;
margin: 0;
background-color: var(--primary-1);
border-radius: 18px 18px 0 0;
}
.skill-card-header h4 {
font-weight: 700;
text-align: right;
}
.skill-symbol {
font-variation-settings:
"FILL" 0,
"wght" 200,
"GRAD" 0,
"opsz" 24;
font-size: 5vw;
text-align: left;
}
.card ul {
padding: 1vw;
}
.card ul li {
list-style: none;
color: var(--dark-0);
}
/**PROJECTS PAGE**/
#project-section {
height: 90vh;
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
margin: 5vw;
}
#project-grid-container {
display: grid;
grid-template-columns: 100%;
grid-template-rows: 20% 80%;
}
#projects-header-row {
display: flex;
align-items: top;
}
#projects-nav-buttons {
margin-top: 1.5vw;
display: flex;
flex-direction: row;
align-items: bottom;
justify-content: left;
width: 100%;
}
.slider-nav-button {
margin-left: 2vw;
margin-right: 2vw;
transition-duration: 500ms;
}
#projects-header-row div span {
color: var(--accent-2);
}
#project-grid-container h1 {
margin-left: 5vw;
text-align: left;
}
#project-cards-section {
display: flex;
flex-direction: column;
justify-content: center;
}
/* Tooltip container */
.tooltip .tooltip-text {
visibility: hidden;
width: 120px;
background-color: var(--dark-0);
color: var(--text);
text-align: center;
padding: 5px 0;
border-radius: 6px;
/* Position the tooltip text - see examples below! */
position: absolute;
z-index: 1;
}
/* Show the tooltip text when you mouse over the tooltip container */
.tooltip:hover .tooltip-text {
visibility: visible;
}
/**PROJECT CARDS**/
.project-card {
display: grid;
grid-template-columns: 35% 65%;
grid-template-rows: 100%;
column-gap: 5%;
overflow: hidden;
height: 90%;
width: 100%;
transition-duration: 1s;
}
.project-card .img-container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-left: 1vw;
}
.project-card img {
max-width: 100%;
max-height: 100%;
border-radius: 18px;
}
.project-text-column {
width: 90%;
display: flex;
flex-direction: column;
justify-content: center;
padding: 2vw;
background-color: var(--primary-1);
border-radius: 18px;
}
.project-text {
display: grid;
width: 100%;
height: 100%;
grid-template-columns: 100%;
grid-template-rows: 10% 35% 40% 15%;
}
.project-text h3 {
font-weight: 600;
}
.project-text p {
font-weight: 400;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.row {
margin: 0;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.feature-list {
display: flex;
flex-direction: column;
color: var(--text);
height: 100%;
padding-left: 4vw;
}
.feature-list li {
color: var(--text);
}
.button {
width: 80%;
color: var(--dark-0);
background-color: var(--text);
border: 3px solid var(--text);
outline: none;
border-radius: 18px;
font-size: clamp(1rem, 1.5vw, 2rem);
text-decoration: none;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 1vw;
transition-duration: 0.5s;
}
.button:hover {
background-color: var(--primary);
color: var(--text);
}
/**Contact Section**/
#contact-section {
height: 90vh;
width: 100%;
margin: 0 5vw 5vw 5vw;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#contact-section form {
width: 80vw;
height: 80%;
}
#contact-section input,
#contact-section textarea {
width: 100%;
padding: 2vw;
margin: 2vw;
border-radius: 18px;
background-color: var(--text);
color: var(--primary);
outline: none;
border: none;
font-size: clamp(0.75rem, 1.1rem, 2rem);
}
#contact-section textarea {
min-height: 30vh;
}
#contact-section input:valid,
#contact-section textarea:valid {
background-color: var(--text);
}
#contact-section input:focus,
#contact-section textarea:focus {
outline: none;
border: none;
}
#contact-section .button {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
#resume-section {
margin: 2vw;
display: flex;
flex-direction: column;
align-items: center;
}
/**PDF Viewer**/
.pdf-container {
margin: 0 auto;
display: grid;
grid-template-columns: 100%;
grid-template-rows: 90% 10%;
}
.pdf-document {
overflow: hidden;
object-fit: scale-down;
}
.pdf-buttons {
width: 100%;
display: grid;
grid-template-rows: 100%;
grid-template-columns: 50% 50%;
}
.pdf-button {
width: 100%;
background-color: var(--text);
outline: none;
border: none;
padding: 1vw;
transition-duration: 500ms;
}
.pdf-button span {
height: 100%;
width: 100%;
color: var(--primary);
font-size: clamp(1rem, 2rem, 3rem);
transition-duration: 500ms;
}
.pdf-button:hover,
.pdf-button:hover span {
background-color: var(--primary);
color: var(--text);
}
/**SLCC Navigation Bar**/
.nav {
width: 90%;
margin-top: 3vw;
}
.nav-list {
display: grid;
grid-template-rows: 100%;
grid-template-columns: 25% 25% 25% 25%;
}
.nav-item {
border-left: 2px solid var(--primary);
border-right: 2px solid var(--primary);
font-size: clamp(1rem, 1.5vw, 1.25rem);
list-style: none;
padding: 2vw;
color: var(--primary);
background-color: var(--text);
transition-duration: 500ms;
}
.nav-item:hover,
.nav-item-active {
background-color: var(--primary);
color: var(--text);
}
/**SLCC Course View**/
#course-view-container {
height: 70vh;
width: 90%;
display: grid;
grid-template-columns: 10% 90%;
grid-template-rows: 100%;
margin: 50px auto;
border: 1px solid white;
}
#course-view-selector {
display: grid;
grid-template-rows: repeat(13, 7.15%);
}
.course-nav-selector {
border-bottom: 1px solid var(--primary);
background-color: var(--text);
color: var(--primary);
height: 100%;
transition-duration: 500ms;
}
.course-nav-selector:hover,
.course-nav-selector-active {
color: var(--text);
background-color: var(--primary);
}
#course-view {
height: max-content;
width: 100%;
border: 1px solid white;
}
.course-header {
margin-left: 2vw;
}
.course-section {
margin: 0 auto;
width: 90%;
display: grid;
grid-template-columns: 47.5% 47.5%;
column-gap: 5%;
border: 1px solid white;
}
.course-section-text {
border: 1px solid white;
width: 100%;
}
.course-page-slider {
width: 100%;
height: max-content;
}
.swiper-button-prev,
.swiper-rtl .swiper-button-next {
color: var(--text) !important;
}
.swiper-button-next,
.swiper-rtl .swiper-button-prev {
color: var(--text) !important;
}
.swiper-button-prev.swiper-button-disabled {
color: var(--text) !important;
}
.swiper-button-next.swiper-button-disabled {
color: var(--text) !important;
}
.error-page {
margin-top: 5vw;
margin-left: 5vw;
}
.error-page a {
width: 50%;
}
.jobs,
.education-section {
border-top: 1px solid var(--accent-0);
width: 100%;
}
.job,
.edu {
width: 100%;
padding: 0vw 1vw 1vw 1vw;
}
.job-header-row,
.edu-header-row {
display: grid;
grid-template-columns: 70% 30%;
align-items: center;
}
.job-time,
.edu-time {
justify-self: right;
}
.company-and-location,
.edu-and-degree {
display: flex;
align-items: center;
}
.company-and-location h6,
.edu-and-degree h6 {
margin-left: .25vw;
}
.job-list {
margin: 0 auto;
width: 90%;
}
.job-list-item {
font-weight: 300;
}
.resume-skills {
border-top: 1px solid var(--accent-0);
width: 100%;
}
.resume-skills-skill {
display: flex;
align-items: center;
}
.resume-skills-skill p {
margin-left: .5vw;
padding: 1vw;
}
/**Animations**/
@keyframes background-fade {
from {
background-position: right, center;
}
}
@keyframes moveDown {
0% {
transform: translateY(-20px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes moveCardsDown {
0% {
transform: translateY(-40px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes imgMoveDown {
0% {
transform: translateY(calc(var(--width) / -5));
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
/**Responsiveness**/
@media (max-width: 1250px) {
#header-text {
padding-left: 90px;
}
}
@media (max-width: 1075px) {
:root {
--width: 400px;
}
.swiper-slide .slide-container {
column-gap: 20px;
}
#header-text {
padding-left: 60px;
}
}
@media (max-width: 990px) {
:root {
--width: 360px;
}
#header-text {
padding-left: 60px;
}
}
@media (max-width: 900px) {
#primary,
#slcc-splash {
width: 100%;
display: grid;
grid-template-rows: 40% 40% 5%;
grid-template-columns: 100%;
}
.header {
height: 100%;
width: 100%;
display: grid;
grid-template-columns: 100%;
grid-template-rows: 35% 65%;
}
.header .header-image {
max-height: 15vh;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.header .header-text {
width: 100%;
}
h1 {
font-size: clamp(2.5rem, 2.75vw, 3rem);
margin-bottom: 16px;
width: unset;
line-height: 1.1;
}
h2 {
padding-bottom: 0;
}
h3 {
font-size: clamp(1.5rem, 2vw, 2.5rem);
}
.project-text p,
.project-text ul li {
font-size: clamp(0.5rem, 0.8rem, 1rem);
}
.skill-card-header {
border-radius: 18px 18px 18px 18px;
}
.card {
min-width: 40vw;
margin: 1vw;
}
.swiper-pagination-bullet {
width: 14px;
height: 14px;
}
.swiper-pagination-bullet-active {
height: 28px;
}
.material-symbols-outlined {
font-size: 10vw;
}
#project-grid-container {
height: 90vh;
grid-template-rows: 10% 80%;
}
.project-card {
display: grid;
grid-template-columns: 100%;
grid-template-rows: 35% 55%;
row-gap: 10%;
overflow: hidden;
height: 100%;
width: 100%;
padding: 2vw;
}
.project-text {
padding: 2vw;
}
.project-text-column {
margin-left: 5vw;
}
#skill-cards-colrow {
flex-direction: column;
}
.button {
border-radius: 10px;
}
#projects-header-row h1 {
margin-top: .2vw;
}
#projects-nav-buttons {
margin-top: 0;
}
.slider-nav-button {
height: max-content;
}
.project-nav-icon {
font-size: clamp(2rem, 3rem, 4rem);
}
}
@media (max-width: 485px) {
:root {
--width: 280px;
}
}
+169
View File
@@ -0,0 +1,169 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter as Router, Routes, Route } from "react-router";
import App from "./App.jsx";
import Resume from "./Resume.jsx";
import ErrorPage from "./ErrorPage.jsx";
/*
* Root React container and renderer.
*
* Contains all data and data processing, as well as routing to different pages.
* TODO: Read data in from plain text files.
*/
const container = document.getElementById("app");
const root = createRoot(container);
const projectData = [
{
title: "One AI",
description:
"An end-to-end self-hosted Artificial Intelligence solution, using Meta's Ollama platform to run Large Language Models. Go was used for the backend as a wrapper API and the frontend is Flutter. Here are some of the implemented features:",
featureList: [
"AES encryption of messages between client and server",
"SHA-256 hashing for message validation",
"Base64 encoding for file uploads to the server",
"Docker container for isolation and security",
],
image: "/images/one-ai-cropped.png",
imageGallery: null,
url: "https://github.com/quaxlyqueen/one-ai",
},
{
title: "Snake",
description:
"A Snake clone in the command line, written in C#. Utilized a Queue data structure to keep track of the snake's body. Implemented features include:",
featureList: [
"basic pseudo-randomized obstacle generation",
"difficulty selection by adjusting the frequency of obstacle generation",
"keyboard input to move the snake",
"ASCII art for the menu and game over state",
],
image: "/images/snake.gif",
imageGallery: null,
url: "https://github.com/quaxlyqueen/2410-Snake",
},
{
title: "Battery Monitor",
description:
"A battery monitor, written in C with a GUI written in Python. Utilizes the acpi Unix command to gather battery information. Implemented features incldue:",
featureList: [
"embed Unix commands in C to get the date, time, battery percentage, and battery status",
"basic file I/O to store historical battery data",
"daemonize to run as a background process",
"script to install as a SystemD service",
"basic Python GUI in TKinter to provide graph of data",
],
image: "/images/battery-mon.png",
imageGallery: null,
url: "https://github.com/quaxlyqueen/battery-mon",
},
{
title: "D&D Character Builder",
description:
"A Dungeons and Dragons character builder, following the 5th edition rules and creation process. A collaborative project creating a GUI in Java with Swing. Features include:",
featureList: [
"serialization of character data into custom filetype",
"global styling and custom font import",
"button hover effects",
],
image: "/images/dnd.png",
imageGallery: null,
url: "https://github.com/quaxlyqueen/dnd",
},
{
title: "Calculator",
description:
"A simple and themable calculator written in Python. This uses CustomTKinter for the GUI and the eva CLI package for the actual calculator functionality. Supports MacOS and Linux. Implemented features include:",
featureList: [
"custom theming read in from a global .theme file",
"button hover effects",
"keyboard input",
],
image: "/images/calculator.gif",
imageGallery: null,
url: "https://github.com/quaxlyqueen/calculator",
},
];
const skillData = {
languages: [
"C & C#",
"HTML & CSS",
"Go",
"Java",
"JavaScript & Dart",
"Python",
],
tools: [
"Git & GitHub",
"Docker",
"Flutter & React",
"Linux & NeoVim",
"Cloudflare",
"Microsoft Intune",
],
other: [
"Written Communication",
"Conflict Resolution",
"Project Management",
"Event Planning",
"Electronics Repair",
"Soldering",
],
};
const jobData = [
{
company: "SLC Public Library",
title: "Technology & Digital Equity Assistant",
start: "Aug '23",
end: "Present",
location: "Salt Lake City, UT",
responsibilities: [
"Assist library patrons with using technology, on devices such as publicly available PCs, laptops, mobile devices, and more.",
"Teach fundamental computer usage skills.",
"Expose patrons to the importance of digital security, password management, and two-factor authentication.",
"Developing workshop curriculum for Raspberry Pi’s."
],
},
{
company: "Trust Lands Admin.",
title: "IT Intern",
start: "Aug '23",
end: "Aug '24",
location: "Salt Lake City, UT",
responsibilities: [
"Orchestrated Microsoft Intune setup and deployment to multiple platforms.",
"Streamlined IT equipment inventory process",
"Provided training to optimize the usage of hardware and software.",
"Troubleshoot and repair hardware and software on a variety of devices.",
"Developed automation scripts for use in various parts of the business.",
],
},
];
const eduData = [
{
institution: "Salt Lake Comm. College",
degree: "AS Comp Sci",
gpa: "3.48 (CS) || 2.87 (Cumulative)",
start: "Jan '21",
end: "Present",
}
];
root.render(
<Router>
<Routes>
<Route exact path="/" element={<App projectData={projectData} skillData={skillData} />} />
<Route exact path="/resume" element={<Resume jobData={jobData} eduData={eduData} skillData={skillData} />} />
<Route
path="*"
element={
<ErrorPage errorCode="404" errorMessage="This page does not exist." />
}
/>
</Routes>
</Router>,
);