dockerization.
@@ -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"]
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 739 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 732 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
);
|
||||