Compare commits
131 Commits
1eab47ee41
...
chore/soft
Author | SHA1 | Date | |
---|---|---|---|
2c5d3c04d0 | |||
2a1cbb8be3 | |||
24720b782e | |||
0b299b9410 | |||
03b95a6c8c | |||
63c84e1430 | |||
9028175ae4
|
|||
280c8e15ad
|
|||
e081a0cb3e
|
|||
41c6964679 | |||
f25c6ddd68 | |||
9b49710556
|
|||
a0c3b27aab | |||
005dfb6929
|
|||
b96c6d2caf | |||
ef37e45281 | |||
b55538345f
|
|||
b586385d6d | |||
2d3046da48 | |||
67f9844534
|
|||
50b8845e6c | |||
79f6e8e90b | |||
25f3db52ec
|
|||
a46ac458dc
|
|||
206c5665a2
|
|||
fd3c620cb9
|
|||
c52d185f76
|
|||
538d9593c2
|
|||
24a7ebf02a
|
|||
fc642a4ecd
|
|||
d9e8b4b56c
|
|||
bd689bdb44 | |||
051cd42fdb
|
|||
da5f47a841
|
|||
5fe7b83c47 | |||
931e4d2abe | |||
33ddd2add0 | |||
8cd763b9d0 | |||
ce38e88885 | |||
e1160b3462 | |||
7042b2d500 | |||
5f1a1d4959 | |||
da2f2bc380 | |||
c3055a6882 | |||
3557a6a6ad | |||
f34761d094 | |||
f7e3acf384 | |||
ccbaa41cab | |||
f33456bae3 | |||
4cab417bdd | |||
cb1304aaeb | |||
27488fe860 | |||
b9c4ec540a | |||
c3f0be36a3 | |||
71dc20c0ca | |||
9da13b76d3 | |||
712d7857db | |||
8ab101727e | |||
5fb67af755 | |||
15f30c09b1 | |||
e5c4243a1f | |||
47a43f2b0e | |||
1a6c5194e5 | |||
aaab8f2c98 | |||
2170344c9b | |||
45208d0ff9 | |||
099bf34c19 | |||
93ca58f487 | |||
1087f95bc4 | |||
94b1e80358 | |||
753e091a85 | |||
b5f6b3adf8 | |||
7cefc86f08 | |||
dfd47c0d98 | |||
e4da310ef2 | |||
196e338e66 | |||
8ccc8e2129 | |||
83361c4199 | |||
112e0f1a5c | |||
e843870afd | |||
74dc1196cd | |||
306396cce1 | |||
a2acc03f7c | |||
6f0f5f1cf7 | |||
278fc640ce | |||
2e07de0e71 | |||
e0ea53a9d3 | |||
632eed6810 | |||
dd4dfdea78 | |||
377f87ced3 | |||
d8d40ddc30 | |||
ee2098e6e6 | |||
d2066087ae | |||
d187ec70c3 | |||
4a734b66f9 | |||
cbdc81c4ce | |||
7bd03be127 | |||
8d5319ac4a | |||
26357e531d | |||
eac1534497 | |||
94de27084c | |||
0a6ede8125 | |||
ebf9a21478 | |||
86652a4f09 | |||
3f171dea3c | |||
6b0af2fb3e | |||
1b33bd398d | |||
f31f180687 | |||
8b9a2ac8d5 | |||
212103ab71 | |||
7d7012eec6 | |||
b2b56480b8 | |||
79795bc060 | |||
6961d9c34d | |||
0797fd9eff | |||
d69078ff26 | |||
ba437de706 | |||
065a6a2d17 | |||
3eb3b6845b | |||
71227d7d78 | |||
52c1abb0b7 | |||
de234ddf8c | |||
3769f3edc3 | |||
87b2e808f7 | |||
189ca5034b | |||
71405fd5a2 | |||
357f0b06f3 | |||
30a5d53bc0 | |||
bd183bf832 | |||
69cfbf1b44 | |||
a75c919929 |
@ -6,6 +6,7 @@ node_modules
|
||||
|
||||
.git
|
||||
.gitattributes
|
||||
.gitea
|
||||
|
||||
.eslintignore
|
||||
.eslintrc.cjs
|
||||
|
30
.gitea/workflows/dev.yaml
Normal file
@ -0,0 +1,30 @@
|
||||
name: Build and Push Development Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ development ]
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ secrets.CONTAINER_REGISTRY }}
|
||||
username: ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.CONTAINER_REGISTRY_PASSKEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and Tag Docker Image
|
||||
run: |
|
||||
docker build -t ${{ secrets.CONTAINER_REGISTRY }}/${{ secrets.CONTAINER_REGISTRY_USERNAME }}/luke-else.co.uk:dev .
|
||||
|
||||
- name: Push Docker Image
|
||||
run: |
|
||||
docker push ${{ secrets.CONTAINER_REGISTRY }}/${{ secrets.CONTAINER_REGISTRY_USERNAME }}/luke-else.co.uk:dev
|
@ -1,10 +1,8 @@
|
||||
name: Build and Push Docker Image
|
||||
name: Build and Push Latest Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
@ -16,6 +14,7 @@ jobs:
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ secrets.CONTAINER_REGISTRY }}
|
||||
username: ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.CONTAINER_REGISTRY_PASSKEY }}
|
||||
|
||||
@ -24,8 +23,8 @@ jobs:
|
||||
|
||||
- name: Build and Tag Docker Image
|
||||
run: |
|
||||
docker build -t ${{ secrets.CONTAINER_REGISTRY_USERNAME }}/luke-else.co.uk:latest .
|
||||
docker build -t ${{ secrets.CONTAINER_REGISTRY }}/${{ secrets.CONTAINER_REGISTRY_USERNAME }}/luke-else.co.uk:latest .
|
||||
|
||||
- name: Push Docker Image
|
||||
run: |
|
||||
docker push ${{ secrets.CONTAINER_REGISTRY_USERNAME }}/luke-else.co.uk:latest
|
||||
docker push ${{ secrets.CONTAINER_REGISTRY }}/${{ secrets.CONTAINER_REGISTRY_USERNAME }}/luke-else.co.uk:latest
|
25
.gitea/workflows/test.yaml
Normal file
@ -0,0 +1,25 @@
|
||||
name: Run Unit and Integration Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "development"
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Run Tests
|
||||
run: npm test
|
15
.prettierrc
@ -1,9 +1,10 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
"useTabs": false,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
|
42
README.md
@ -1,38 +1,56 @@
|
||||
# create-svelte
|
||||
## Welcome
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||
This site contains information relating to my personal situation, however, you are able to clone this project and change the `me.json` file to update this as required.
|
||||
|
||||
## Creating a project
|
||||
## Screenshots
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
<p align="center">
|
||||
<img src="assets/images/main.png" width="40%">
|
||||
<img src="assets/images/light_mode.png" width="40%">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/images/skills.png" width="30%">
|
||||
<img src="assets/images/repos.png" width="30%">
|
||||
<img src="assets/images/contact.png" width="30%">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/images/experience.png" width="30%">
|
||||
</p>
|
||||
|
||||
## Getting Started
|
||||
|
||||
Get starting but installing all of the dependencies of the project.
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
npm install
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
```
|
||||
|
||||
```bash
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
To create a production version of the app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
|
||||
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. In this case, vite is used.
|
||||
|
BIN
assets/images/contact.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
assets/images/experience.png
Normal file
After Width: | Height: | Size: 74 KiB |
BIN
assets/images/light_mode.png
Normal file
After Width: | Height: | Size: 181 KiB |
BIN
assets/images/main.png
Normal file
After Width: | Height: | Size: 178 KiB |
BIN
assets/images/repos.png
Normal file
After Width: | Height: | Size: 298 KiB |
BIN
assets/images/skills.png
Normal file
After Width: | Height: | Size: 85 KiB |
2088
package-lock.json
generated
31
package.json
@ -5,6 +5,8 @@
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"start": "export PORT=3000 && node ./build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
@ -14,16 +16,23 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-json": "^6.0.0",
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"@sveltejs/kit": "^1.20.4",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-check": "^3.4.3",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2"
|
||||
"@sveltejs/adapter-auto": "6.0.0",
|
||||
"@sveltejs/adapter-node": "5.2.12",
|
||||
"@sveltejs/kit": "2.20.8",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"prettier": "3.5.3",
|
||||
"prettier-plugin-svelte": "3.3.3",
|
||||
"svelte": "5.28.2",
|
||||
"svelte-check": "4.1.7",
|
||||
"tslib": "2.8.1",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "6.3.5",
|
||||
"vitest": "^3.1.4"
|
||||
},
|
||||
"type": "module"
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.6",
|
||||
"svelte-toasts": "^1.1.2",
|
||||
"tailwindcss": "^4.1.6"
|
||||
}
|
||||
}
|
||||
|
1818
pnpm-lock.yaml
generated
Normal file
1
src/app.css
Normal file
@ -0,0 +1 @@
|
||||
@import "tailwindcss"
|
150
src/app.html
@ -1,129 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="description" content="Luke Else - Software Developer at Thales UK. I specialise in developing distributed systems in C++ using highly scalable internal frameworks. I also develop backend and system applications in my spare time using both Svelte, Rust and C++. Feel free to check my work out at https://git.luke-else.co.uk." />
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Luke Else - Software Developer at Thales UK. I specialise in developing distributed systems in C++ using highly scalable internal frameworks. I also develop backend and system applications in my spare time using both Svelte, Rust and C++. Feel free to check my work out at https://git.luke-else.co.uk."
|
||||
/>
|
||||
<meta name="author" content="Luke Else (mail@luke-else.co.uk)" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/devicons/devicon@v2.15.1/devicon.min.css">
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/gh/devicons/devicon@v2.15.1/devicon.min.css"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<script
|
||||
async
|
||||
src="https://tracking.luke-else.co.uk/tracker.js"
|
||||
data-ackee-server="https://tracking.luke-else.co.uk"
|
||||
data-ackee-domain-id="6c59ab88-dc6d-4d53-9831-0d6bff919dcd"
|
||||
data-ackee-opts='{ "detailed": true }'
|
||||
></script>
|
||||
%sveltekit.head%
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--font: Consolas, 'Cascadia Code', Monaco, 'SF Mono', 'DejaVu Sans Mono', 'Roboto Mono';
|
||||
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: var(--font);
|
||||
font-size: 110%;
|
||||
margin: 2rem;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
color: var(--header);
|
||||
border: 0;
|
||||
}
|
||||
<style></style>
|
||||
</head>
|
||||
|
||||
hr {
|
||||
border: .2em solid var(--bg-grad-3);
|
||||
border-radius: 5em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar,
|
||||
*::-webkit-scrollbar-thumb {
|
||||
width: 26px;
|
||||
border-radius: 13px;
|
||||
background-clip: padding-box;
|
||||
border: 10px solid transparent;
|
||||
color: var(--fg);
|
||||
}
|
||||
*::-webkit-scrollbar-thumb:hover{
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
box-shadow: inset 0 0 0 10px;
|
||||
}
|
||||
|
||||
@media (max-width:600px) {
|
||||
.not-required {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
color: var(--link);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
a:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0%;
|
||||
border-bottom: 2px solid var(--fg);
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
a:hover:after {
|
||||
width: 100%;
|
||||
color: var(--glow);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--glow);
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: var(--header);
|
||||
}
|
||||
|
||||
.button {
|
||||
color: var(--fg);
|
||||
background-color: var(--bg-grad-1);
|
||||
box-shadow: .1em .1em .1em var(--link);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
box-shadow: .3em .3em .3em var(--header);
|
||||
}
|
||||
|
||||
@keyframes animationName {
|
||||
0% { opacity:0; }
|
||||
50% { opacity:1; }
|
||||
100% { opacity:0; }
|
||||
}
|
||||
@-o-keyframes animationName{
|
||||
0% { opacity:0; }
|
||||
50% { opacity:1; }
|
||||
100% { opacity:0; }
|
||||
}
|
||||
@-moz-keyframes animationName{
|
||||
0% { opacity:0; }
|
||||
50% { opacity:1; }
|
||||
100% { opacity:0; }
|
||||
}
|
||||
@-webkit-keyframes animationName{
|
||||
0% { opacity:0; }
|
||||
50% { opacity:1; }
|
||||
100% { opacity:0; }
|
||||
}
|
||||
.elementToFadeInAndOut {
|
||||
-webkit-animation: animationName 1.5s infinite;
|
||||
-moz-animation: animationName 1.5s infinite;
|
||||
-o-animation: animationName 1.5s infinite;
|
||||
animation: animationName 1.5s infinite;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
61
src/lib/api/git.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import type { GitRepo } from "../types";
|
||||
|
||||
const API_BASE_URL = "https://git.luke-else.co.uk/api/v1";
|
||||
export const IMAGE_URL_SUFFIX = "/raw/branch/main/assets/images/main.png";
|
||||
|
||||
|
||||
export async function fetchRepos(): Promise<GitRepo[]> {
|
||||
try {
|
||||
console.log("Fetching repos...");
|
||||
const response = await fetch(`${API_BASE_URL}/repos/search?sort=updated&order=desc&limit=12`, {
|
||||
headers: {
|
||||
// "Authorization": `token ${ACCESS_TOKEN}`,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data: { data: GitRepo[] } = await response.json();
|
||||
return data.data; // Extract the list of repositories
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch repos:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function timeSince(inputDate: Date | string): string {
|
||||
const date = new Date(inputDate); // Ensure it's a Date object
|
||||
if (isNaN(date.getTime())) {
|
||||
throw new Error("Invalid date provided");
|
||||
}
|
||||
|
||||
const now: Date = new Date();
|
||||
const diffInMs: number = now.getTime() - date.getTime();
|
||||
const diffInSeconds: number = Math.floor(diffInMs / 1000);
|
||||
const diffInMinutes: number = Math.floor(diffInSeconds / 60);
|
||||
const diffInHours: number = Math.floor(diffInMinutes / 60);
|
||||
const diffInDays: number = Math.floor(diffInHours / 24);
|
||||
const diffInMonths: number = Math.floor(diffInDays / 30); // Approximate
|
||||
const diffInYears: number = Math.floor(diffInDays / 365); // Approximate
|
||||
|
||||
if (diffInDays === 0) return "Today";
|
||||
if (diffInDays === 1) return "Yesterday";
|
||||
if (diffInDays < 7) return `${diffInDays} days ago`;
|
||||
if (diffInDays < 30) return `${Math.floor(diffInDays / 7)} week${diffInDays >= 14 ? 's' : ''} ago`;
|
||||
if (diffInMonths < 12) return `${diffInMonths} month${diffInMonths > 1 ? 's' : ''} ago`;
|
||||
return `${diffInYears} year${diffInYears > 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
export async function checkImage(repo: GitRepo): Promise<boolean> {
|
||||
try {
|
||||
const URL = repo.html_url + IMAGE_URL_SUFFIX;
|
||||
console.log("Checking image:", URL);
|
||||
const response = await fetch(URL);
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function onClick() {
|
||||
dispatch('click');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
flex: 2 1 15em;
|
||||
padding: .5em 2.5em 2em 2.5em;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: .5em;
|
||||
scroll-snap-align: start;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.card:hover {
|
||||
box-shadow: .5em .5em .5em var(--hover);
|
||||
}
|
||||
|
||||
.card .card-header :global(div) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card .card-content :global(div) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.card .card-footer :global(div){
|
||||
margin-bottom: 1em;
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
max-width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="card" on:click={onClick}>
|
||||
<div class="card-header">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="card-content">
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
<hr class="not-required"/>
|
||||
<div class="card-footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
23
src/lib/components/Cards/Card.svelte
Normal file
@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
// Allows additional styling to be applied to the Card component's outer wrapping
|
||||
export let containerStyle: string = "";
|
||||
</script>
|
||||
|
||||
<div class={containerStyle}>
|
||||
<div class="bg-slate-100/10 dark:bg-slate-100/10 rounded-2xl shadow-2xl p-6 flex flex-col h-full w-full">
|
||||
<div class="text-red-400 font-bold flex flex-row justify-between items-center mb-4">
|
||||
<slot name="headerLeft" class="text-2xl md:text-3xl truncate"></slot>
|
||||
<slot name="headerRight" class="max-md:hidden text-xl md:text-2xl truncate"></slot>
|
||||
</div>
|
||||
<hr class="border-1" />
|
||||
<div class="flex-1 flex flex-col justify-center mt-4 mb-8">
|
||||
<slot name="content"/>
|
||||
</div>
|
||||
|
||||
<hr class="border-1" />
|
||||
<div class="flex flex-row justify-between items-center mt-4 text-base opacity-90">
|
||||
<slot name="footerLeft"/>
|
||||
<slot name="footerRight"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
33
src/lib/components/Collapsible.svelte
Normal file
@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
export let open = false;
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-between w-full px-2 py-2 text-left rounded hover:font-bold transition group"
|
||||
on:click={() => open = !open}
|
||||
aria-expanded={open}
|
||||
>
|
||||
<span><slot name="label"/></span>
|
||||
<svg
|
||||
class="w-5 h-5 ml-2 transition-transform duration-200"
|
||||
style="transform: rotate({open ? 90 : 0}deg)"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
class="overflow-hidden transition-all duration-300"
|
||||
style="max-height: {open ? '1000px' : '0'}"
|
||||
aria-hidden={!open}
|
||||
>
|
||||
<div class="pt-2">
|
||||
<slot name="content"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
7
src/lib/components/GridGallery.svelte
Normal file
@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<!-- GridGallery.svelte -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 w-full">
|
||||
<slot />
|
||||
</div>
|
73
src/lib/components/Loading.svelte
Normal file
@ -0,0 +1,73 @@
|
||||
<style>
|
||||
.loader {
|
||||
position: absolute;
|
||||
top: calc(50% - 32px);
|
||||
left: calc(50% - 32px);
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
perspective: 800px;
|
||||
}
|
||||
|
||||
.inner {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.inner.one {
|
||||
left: 0%;
|
||||
top: 0%;
|
||||
animation: rotate-one 1s linear infinite;
|
||||
border-bottom: 3px solid gray;
|
||||
}
|
||||
|
||||
.inner.two {
|
||||
right: 0%;
|
||||
top: 0%;
|
||||
animation: rotate-two 1s linear infinite;
|
||||
border-right: 3px solid gray;
|
||||
}
|
||||
|
||||
.inner.three {
|
||||
right: 0%;
|
||||
bottom: 0%;
|
||||
animation: rotate-three 1s linear infinite;
|
||||
border-top: 3px solid gray;
|
||||
}
|
||||
|
||||
@keyframes rotate-one {
|
||||
0% {
|
||||
transform: rotateX(35deg) rotateY(-45deg) rotateZ(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotateX(35deg) rotateY(-45deg) rotateZ(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate-two {
|
||||
0% {
|
||||
transform: rotateX(50deg) rotateY(10deg) rotateZ(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotateX(50deg) rotateY(10deg) rotateZ(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate-three {
|
||||
0% {
|
||||
transform: rotateX(35deg) rotateY(55deg) rotateZ(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotateX(35deg) rotateY(55deg) rotateZ(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="loader">
|
||||
<div class="inner one"></div>
|
||||
<div class="inner two"></div>
|
||||
<div class="inner three"></div>
|
||||
</div>
|
@ -1,82 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let showModal: boolean;
|
||||
|
||||
let dialog: HTMLDialogElement;
|
||||
|
||||
$: if (dialog && showModal) dialog.showModal();
|
||||
|
||||
import CloseIcon from "./Toasts/CloseIcon.svelte";
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
|
||||
<dialog
|
||||
bind:this={dialog}
|
||||
on:close={() => (showModal = false)}
|
||||
on:click|self={() => dialog.close()}
|
||||
>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div on:click|stopPropagation>
|
||||
<slot name="header" />
|
||||
<hr />
|
||||
<slot />
|
||||
<hr />
|
||||
</div>
|
||||
<button class="close" on:click={() => dialog.close()}>
|
||||
<CloseIcon width="0.8em" />
|
||||
</button>
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
dialog {
|
||||
max-width: 70%;
|
||||
border-radius: 0.2em;
|
||||
border: none;
|
||||
padding: 0em 2em 2em 2em;
|
||||
border-left: 2em;
|
||||
border-right: 2em;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
box-shadow: .5em .5em .5em var(--header);
|
||||
}
|
||||
dialog::backdrop {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
dialog > div {
|
||||
padding: 1em;
|
||||
}
|
||||
dialog[open] {
|
||||
animation: zoom 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
@keyframes zoom {
|
||||
from {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
dialog[open]::backdrop {
|
||||
animation: fade 0.3s ease-out;
|
||||
}
|
||||
@keyframes fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 1.5em;
|
||||
color: var(--fg);
|
||||
background: transparent;
|
||||
border: 0 none;
|
||||
padding: 0;
|
||||
margin: 0 0 0 auto;
|
||||
line-height: 1;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
</style>
|
26
src/lib/components/Section.svelte
Normal file
@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
export let label: string = "";
|
||||
</script>
|
||||
|
||||
<div id={label} class="relative flex flex-row w-full min-h-[300px] mt-5 mb-25">
|
||||
<!-- Sticky/Sliding Label -->
|
||||
<div class="hidden md:flex flex-col items-center mr-6">
|
||||
<div class="sticky top-24 left-0 z-10">
|
||||
<span class="text-2xl font-bold text-blue-400 tracking-widest"
|
||||
style="writing-mode: vertical-rl; text-orientation: mixed;">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 flex flex-col">
|
||||
<!-- Label for mobile -->
|
||||
<div class="md:hidden mb-2">
|
||||
<span class="text-2xl font-bold text-blue-400">{label}</span>
|
||||
</div>
|
||||
<hr class="border-blue-400 mb-6" />
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
17
src/lib/components/SkillProgress.svelte
Normal file
@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
export let value: number = 0; // 0 to 100
|
||||
export let skillColour: string = 'bg-orange-400'; // Default color
|
||||
</script>
|
||||
|
||||
<div class="w-full mt-3">
|
||||
<div class="flex justify-between mb-1">
|
||||
<span class="text-sm font-medium">Competency Level</span>
|
||||
<span class="text-sm font-medium">{value}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-800 rounded-full h-5">
|
||||
<div
|
||||
class="{skillColour} h-5 rounded-full transition-all duration-500"
|
||||
style="width: {value}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
@ -1,102 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export let darkMode: boolean = true;
|
||||
|
||||
function onThemeSwitch() {
|
||||
darkMode = !darkMode;
|
||||
|
||||
localStorage.setItem('theme', darkMode ? 'dark' : 'light');
|
||||
|
||||
darkMode
|
||||
? document.documentElement.classList.add('dark')
|
||||
: document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
|
||||
if (browser) {
|
||||
if (
|
||||
localStorage.theme === 'dark' ||
|
||||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
) {
|
||||
document.documentElement.classList.add('dark');
|
||||
darkMode = true;
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
darkMode = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.switch {
|
||||
position: absolute;
|
||||
top: 0em;
|
||||
right: 0em;
|
||||
display: inline-block;
|
||||
width: 3.75em;
|
||||
height: 2.125em;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
background-color: var(--bg-grad-4);
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 80%;
|
||||
width: 45%;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
input:checked + .slider {
|
||||
background-color: var(--bg-grad-1);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(1.625em);
|
||||
-ms-transform: translateX(1.625em);
|
||||
transform: translateX(1.625em);
|
||||
}
|
||||
|
||||
.slider.round {
|
||||
border-radius: 2.125em;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="stylesheet" href={`/themes/${darkMode ? 'dark' : 'light'}.css`} />
|
||||
</svelte:head>
|
||||
|
||||
|
||||
<div class="toggle-wrapper not-required">
|
||||
<label class="switch">
|
||||
<input type="checkbox" checked={darkMode} on:click={onThemeSwitch}>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
</div>
|
29
src/lib/components/Timeline.svelte
Normal file
@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import Collapsible from "./Collapsible.svelte";
|
||||
|
||||
export let timelineData: Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
duration: string;
|
||||
}>;
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="max-w-4xl w-full">
|
||||
{#each timelineData as entry, i}
|
||||
<div class="relative border-l border-gray-700 pl-8 pb-12">
|
||||
{#if i == 0}
|
||||
<div class="absolute top-0 left-[8px] text-green-400 w-4 h-4">♦</div>
|
||||
{:else}
|
||||
<div class="absolute top-0 left-[8px] text-green-400 w-4 h-4">⋄</div>
|
||||
{/if}
|
||||
<p class="text-sm opacity-70">{entry.duration}</p>
|
||||
|
||||
<Collapsible open={i==0}>
|
||||
<span slot="label" class="text-2lg font-semibold text-red-400 mt-1 focus:outline-none hover:underline transition">{entry.title}</span>
|
||||
<span slot="content">{@html entry.description}</span>
|
||||
</Collapsible>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
@ -1,18 +0,0 @@
|
||||
<script>
|
||||
export let width = "1em"
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width={width}
|
||||
style="text-align: center; display: inline-block;"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 352 512"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"
|
||||
/>
|
||||
</svg>
|
@ -1,19 +0,0 @@
|
||||
<script>
|
||||
export let width = "1em"
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width={width}
|
||||
style="text-align: center; display: inline-block;"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M256 40c118.621 0 216 96.075 216 216 0 119.291-96.61 216-216 216-119.244 0-216-96.562-216-216 0-119.203 96.602-216 216-216m0-32C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm-11.49 120h22.979c6.823 0 12.274 5.682 11.99 12.5l-7 168c-.268 6.428-5.556 11.5-11.99 11.5h-8.979c-6.433 0-11.722-5.073-11.99-11.5l-7-168c-.283-6.818 5.167-12.5 11.99-12.5zM256 340c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28z"
|
||||
class=""
|
||||
></path>
|
||||
</svg>
|
@ -1,18 +0,0 @@
|
||||
<script>
|
||||
export let width = "1em"
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width={width}
|
||||
style="text-align: center; display: inline-block;"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M256 40c118.621 0 216 96.075 216 216 0 119.291-96.61 216-216 216-119.244 0-216-96.562-216-216 0-119.203 96.602-216 216-216m0-32C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm-36 344h12V232h-12c-6.627 0-12-5.373-12-12v-8c0-6.627 5.373-12 12-12h48c6.627 0 12 5.373 12 12v140h12c6.627 0 12 5.373 12 12v8c0 6.627-5.373 12-12 12h-72c-6.627 0-12-5.373-12-12v-8c0-6.627 5.373-12 12-12zm36-240c-17.673 0-32 14.327-32 32s14.327 32 32 32 32-14.327 32-32-14.327-32-32-32z"
|
||||
/>
|
||||
</svg>
|
@ -1,18 +0,0 @@
|
||||
<script>
|
||||
export let width = "1em"
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width={width}
|
||||
style="text-align: center; display: inline-block;"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 464c-118.664 0-216-96.055-216-216 0-118.663 96.055-216 216-216 118.664 0 216 96.055 216 216 0 118.663-96.055 216-216 216zm141.63-274.961L217.15 376.071c-4.705 4.667-12.303 4.637-16.97-.068l-85.878-86.572c-4.667-4.705-4.637-12.303.068-16.97l8.52-8.451c4.705-4.667 12.303-4.637 16.97.068l68.976 69.533 163.441-162.13c4.705-4.667 12.303-4.637 16.97.068l8.451 8.52c4.668 4.705 4.637 12.303-.068 16.97z"
|
||||
/>
|
||||
</svg>
|
@ -1,67 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
import { Toast, ToastType } from "$lib/toast";
|
||||
import SuccessIcon from "./SuccessIcon.svelte";
|
||||
import ErrorIcon from "./ErrorIcon.svelte";
|
||||
import InfoIcon from "./InfoIcon.svelte";
|
||||
import CloseIcon from "./CloseIcon.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let toastData: Toast;
|
||||
</script>
|
||||
|
||||
<article class={toastData.type.toString().toLowerCase()} role="alert" transition:fade>
|
||||
{#if toastData.type === ToastType.Success}
|
||||
<SuccessIcon width="1.1em" />
|
||||
{:else if toastData.type === ToastType.Error}
|
||||
<ErrorIcon width="1.1em" />
|
||||
{:else}
|
||||
<InfoIcon width="1.1em" />
|
||||
{/if}
|
||||
|
||||
<div class="text">
|
||||
{toastData.text}
|
||||
</div>
|
||||
|
||||
{#if toastData.dismissable}
|
||||
<button class="close" on:click={() => dispatch("dismiss")}>
|
||||
<CloseIcon width="0.8em" />
|
||||
</button>
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
<style lang="postcss">
|
||||
article {
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 auto 0.5rem auto;
|
||||
width: 20rem;
|
||||
max-width: 80%;
|
||||
}
|
||||
.error {
|
||||
background: IndianRed;
|
||||
}
|
||||
.success {
|
||||
background: MediumSeaGreen;
|
||||
}
|
||||
.info {
|
||||
background: SkyBlue;
|
||||
}
|
||||
.text {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.close {
|
||||
color: white;
|
||||
background: transparent;
|
||||
border: 0 none;
|
||||
padding: 0;
|
||||
margin: 0 0 0 auto;
|
||||
line-height: 1;
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
@ -1,28 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Toast from "./Toast.svelte";
|
||||
|
||||
import { dismissToast, toasts } from "$lib/store";
|
||||
</script>
|
||||
|
||||
{#if $toasts}
|
||||
<section>
|
||||
{#each $toasts as toast (toast.id)}
|
||||
<Toast toastData = {toast} on:dismiss={() => dismissToast(toast.id)} />
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
section {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
@ -1,21 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { setContext } from 'svelte';
|
||||
import type { TimelinePosition, TimelineConfig } from '../types';
|
||||
export let position: TimelinePosition = 'right';
|
||||
export let style: string = null;
|
||||
|
||||
setContext<TimelineConfig>('TimelineConfig', { rootPosition: position });
|
||||
</script>
|
||||
|
||||
<ul class="timeline" {style}>
|
||||
<slot />
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
.timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 6px 16px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
@ -1,13 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let style: string = null;
|
||||
</script>
|
||||
|
||||
<span class="timeline-connector" {style} />
|
||||
|
||||
<style>
|
||||
.timeline-connector {
|
||||
width: 2px;
|
||||
background-color: #bdbdbd;
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
@ -1,30 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import type { TimelineConfig, TimelinePosition } from '../types';
|
||||
export let style: string = null;
|
||||
|
||||
const config = getContext<TimelineConfig>('TimelineConfig');
|
||||
const parentPosition = getContext<TimelinePosition>('ParentPosition');
|
||||
|
||||
const itemPosition = parentPosition ? parentPosition : config.rootPosition;
|
||||
</script>
|
||||
|
||||
<div class={`timeline-content ${itemPosition}`} {style}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.timeline-content {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
margin: 6px 16px;
|
||||
}
|
||||
|
||||
.left {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
@ -1,19 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let style: string = null;
|
||||
</script>
|
||||
|
||||
<span class="timeline-dot" {style}>
|
||||
<slot />
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.timeline-dot {
|
||||
background-color: #121212;
|
||||
border: solid 2px #121212;
|
||||
display: flex;
|
||||
align-self: baseline;
|
||||
padding: 4px;
|
||||
border-radius: 50%;
|
||||
margin: 11.5px 0;
|
||||
}
|
||||
</style>
|
@ -1,58 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import type { TimelinePosition, ParentPosition, TimelineConfig } from '../types';
|
||||
|
||||
export let position: ParentPosition | null = null;
|
||||
export let style: string = null;
|
||||
|
||||
const config = getContext<TimelineConfig>('TimelineConfig');
|
||||
const itemPosition = position ? position : config.rootPosition;
|
||||
setContext<TimelinePosition>('ParentPosition', itemPosition);
|
||||
</script>
|
||||
|
||||
<li class={`timeline-item ${itemPosition}`} {style}>
|
||||
{#if !$$slots['opposite-content']}
|
||||
<div class="opposite-block" />
|
||||
{:else}
|
||||
<slot name="opposite-content" />
|
||||
{/if}
|
||||
<slot />
|
||||
</li>
|
||||
|
||||
<style>
|
||||
:global(.alternate:nth-of-type(even) > .timeline-content) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:global(.alternate:nth-of-type(odd) > .timeline-opposite-content) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.opposite-block {
|
||||
flex: 1;
|
||||
margin: 6px 16px;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
position: relative;
|
||||
min-height: 70px;
|
||||
}
|
||||
|
||||
.left {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.right {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.alternate:nth-of-type(even) {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.alternate:nth-of-type(odd) {
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
@ -1,31 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import type { TimelineConfig, TimelinePosition } from '../types';
|
||||
export let style: string = null;
|
||||
|
||||
const config = getContext<TimelineConfig>('TimelineConfig');
|
||||
const parentPosition = getContext<TimelinePosition>('ParentPosition');
|
||||
|
||||
const itemPosition = parentPosition ? parentPosition : config.rootPosition;
|
||||
</script>
|
||||
|
||||
<div class={`timeline-opposite-content ${itemPosition}`} {style}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.timeline-opposite-content {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
margin-right: auto;
|
||||
margin: 6px 16px;
|
||||
}
|
||||
|
||||
.left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
@ -1,16 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let style: string = null;
|
||||
</script>
|
||||
|
||||
<div class="timeline-separator" {style}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.timeline-separator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
@ -1,4 +1,6 @@
|
||||
export async function getJson(path: string) {
|
||||
import type {ContentTemplate} from "$lib/types"
|
||||
|
||||
export async function getJson(path: string): Promise<ContentTemplate> {
|
||||
let response = await fetch(path);
|
||||
let users = await response.json();
|
||||
return users;
|
||||
|
@ -1,39 +1,9 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
import Timeline from '$lib/components/timeline/Timeline.svelte';
|
||||
import TimelineItem from '$lib/components/timeline/TimelineItem.svelte';
|
||||
import TimelineSeparator from '$lib/components/timeline/TimelineSeparator.svelte';
|
||||
import TimelineDot from '$lib/components/timeline/TimelineDot.svelte';
|
||||
import TimelineConnector from '$lib/components/timeline/TimelineConnector.svelte';
|
||||
import TimelineContent from '$lib/components/timeline/TimelineContent.svelte';
|
||||
import TimelineOppositeContent from '$lib/components/timeline/TimelineOppositeContent.svelte';
|
||||
import Card from '$lib/components/Cards/Card.svelte';
|
||||
import GridGallery from './components/GridGallery.svelte';
|
||||
import Loading from './components/Loading.svelte';
|
||||
import Section from './components/Section.svelte';
|
||||
import SkillProgress from './components/SkillProgress.svelte';
|
||||
import Timeline from './components/Timeline.svelte';
|
||||
|
||||
import Toasts from '$lib/components/Toasts/Toasts.svelte';
|
||||
import Toast from '$lib/components/Toasts/Toast.svelte';
|
||||
import CloseIcon from '$lib/components/Toasts/CloseIcon.svelte';
|
||||
import InfoIcon from '$lib/components/Toasts/InfoIcon.svelte';
|
||||
import SuccessIcon from '$lib/components/Toasts/SuccessIcon.svelte';
|
||||
import ErrorIcon from '$lib/components/Toasts/ErrorIcon.svelte';
|
||||
|
||||
import Card from '$lib/components/Card.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
|
||||
|
||||
export {
|
||||
Timeline,
|
||||
TimelineItem,
|
||||
TimelineSeparator,
|
||||
TimelineDot,
|
||||
TimelineConnector,
|
||||
TimelineContent,
|
||||
TimelineOppositeContent,
|
||||
|
||||
Toasts,
|
||||
Toast,
|
||||
CloseIcon,
|
||||
InfoIcon,
|
||||
SuccessIcon,
|
||||
ErrorIcon,
|
||||
|
||||
Card,
|
||||
Modal
|
||||
};
|
||||
export { Card, GridGallery, Loading, Section, SkillProgress, Timeline };
|
||||
|
@ -1,28 +0,0 @@
|
||||
import { ToastType, type Toast } from "$lib/toast";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
|
||||
export const toasts: Writable<Toast[]> = writable([]);
|
||||
|
||||
export const addToast = (toast: Toast) => {
|
||||
// Create a unique ID so we can easily find/remove it
|
||||
// if it is dismissible/has a timeout.
|
||||
toast.id = Math.floor(Math.random() * 10000);
|
||||
|
||||
// Setup some sensible defaults for a toast.
|
||||
const defaults = {
|
||||
id: toast.id,
|
||||
type: ToastType.Info,
|
||||
dismissible: true,
|
||||
timeout: 3000,
|
||||
};
|
||||
|
||||
// Push the toast to the top of the list of toasts
|
||||
toasts.update((all) => [{ ...defaults, ...toast }, ...all]);
|
||||
|
||||
// If toast is dismissible, dismiss it after "timeout" amount of time.
|
||||
if (toast.timeout) setTimeout(() => dismissToast(toast.id), toast.timeout);
|
||||
};
|
||||
|
||||
export const dismissToast = (id: number) => {
|
||||
toasts.update((all) => all.filter((t) => t.id !== id));
|
||||
};
|
12
src/lib/stores.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { writable } from "svelte/store";
|
||||
import type { GitRepo } from "./types";
|
||||
import { fetchRepos } from "./api/git";
|
||||
|
||||
////////////////////////////////////////
|
||||
// Git Repo Stores
|
||||
////////////////////////////////////////
|
||||
export const repos = writable<GitRepo[]>([]);
|
||||
|
||||
export async function loadRepos() {
|
||||
repos.set(await fetchRepos());
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
/**
|
||||
* @enum Used to refer to the type of toast being displayed
|
||||
*/
|
||||
export enum ToastType {
|
||||
Info = "info",
|
||||
Success = "success",
|
||||
Error = "error"
|
||||
}
|
||||
|
||||
/**
|
||||
* @class Toast Notification
|
||||
*/
|
||||
export class Toast {
|
||||
constructor(text: String, type: ToastType, dismissable: boolean, timeout: number ) {
|
||||
this.text = text;
|
||||
this.type = type;
|
||||
this.dismissable = dismissable;
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
id: number = 0;
|
||||
text: String;
|
||||
type: ToastType;
|
||||
dismissable: Boolean;
|
||||
timeout: number;
|
||||
}
|
9
src/lib/types.d.ts
vendored
@ -1,9 +0,0 @@
|
||||
type TimelinePosition = 'right' | 'left' | 'alternate';
|
||||
|
||||
type ParentPosition = 'right' | 'left';
|
||||
|
||||
type TimelineConfig = {
|
||||
rootPosition: TimelinePosition;
|
||||
};
|
||||
|
||||
export { TimelinePosition, ParentPosition, TimelineConfig };
|
40
src/lib/types.ts
Normal file
@ -0,0 +1,40 @@
|
||||
export interface ContentTemplate {
|
||||
name: string;
|
||||
job_title: string;
|
||||
location: string;
|
||||
profile_photo: string;
|
||||
about: string;
|
||||
|
||||
skills: Skill[];
|
||||
timeline: TimelineEvent[];
|
||||
}
|
||||
|
||||
export interface Skill {
|
||||
name: string;
|
||||
logo: string;
|
||||
colour: string;
|
||||
link: string;
|
||||
about: string;
|
||||
competency: number;
|
||||
}
|
||||
|
||||
export interface TimelineEvent {
|
||||
duration: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface GitRepo {
|
||||
name: string;
|
||||
description: string;
|
||||
language: string;
|
||||
size: number;
|
||||
updated_at: Date;
|
||||
html_url: string;
|
||||
private: boolean;
|
||||
fork: boolean;
|
||||
owner: {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
};
|
||||
}
|
178
src/main.svelte
@ -1,125 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { getJson } from "$lib/data";
|
||||
import { Toast, ToastType } from "$lib/toast";
|
||||
import { addToast } from "$lib/store";
|
||||
import { getJson } from '$lib/data';
|
||||
import { toasts } from 'svelte-toasts';
|
||||
|
||||
import Skills from './skills.svelte';
|
||||
|
||||
import Timeline from "./timeline.svelte";
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
import Section from '$lib/components/Section.svelte';
|
||||
import Card from '$lib/components/Cards/Card.svelte';
|
||||
import GridGallery from "$lib/components/GridGallery.svelte";
|
||||
import SkillProgress from "$lib/components/SkillProgress.svelte";
|
||||
import Timeline from '$lib/components/Timeline.svelte';
|
||||
import Collapsible from '$lib/components/Collapsible.svelte';
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.main-card {
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: 1em;
|
||||
padding: .2em 2em 2em 2em;
|
||||
box-shadow: .5em .5em .5em var(--glow);
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.profile {
|
||||
border-radius: 100%;
|
||||
height: 8em;
|
||||
width: 8em;
|
||||
padding: 1em 1em 1em 1em;
|
||||
border: .5em solid var(--bg-grad-3);
|
||||
}
|
||||
|
||||
.about {
|
||||
padding: 0em 5% 0em 5%;
|
||||
font-size: 125%;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.flex-container {
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.about {
|
||||
font-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
.card-header h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
/* Skills Cards CSS */
|
||||
.container {
|
||||
padding-top: 3em;
|
||||
max-width: 90%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
gap: 3em 3em;
|
||||
padding: 2em 0em 2em 0em;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
{#await getJson('/json/me.json')}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1>Loading...</h1>
|
||||
</div>
|
||||
</div>
|
||||
<Loading />
|
||||
{:then info}
|
||||
<div class="main-card">
|
||||
<div class="card-header">
|
||||
<h1>{info.name}</h1>
|
||||
<h3 class="not-required">{info.job_title}</h3>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="flex-container">
|
||||
<img class="profile not-required" src={info.profile_photo} alt="{info.name}'s Profile Photo">
|
||||
<p class="about">{@html info.about}</p>
|
||||
</div>
|
||||
<div style="display: none;">
|
||||
{toasts.add({
|
||||
title: 'Welcome',
|
||||
duration: 5000,
|
||||
type: 'success',
|
||||
placement: 'bottom-center',
|
||||
showProgress: true
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h1>Skills</h1>
|
||||
<hr />
|
||||
<div class="cards">
|
||||
<Skills skills="{info.skills}"></Skills>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main Card -->
|
||||
<Section label="[About]">
|
||||
<Card>
|
||||
<h2 slot="headerLeft">{info.name}</h2>
|
||||
<h2 slot="headerRight">{info.job_title}</h2>
|
||||
<div slot="content" class="flex flex-row items-center gap-5">
|
||||
<img
|
||||
src={info.profile_photo}
|
||||
alt="Avatar"
|
||||
class="max-md:hidden rounded-full w-32 h-32 md:w-48 md:h-48 mt-2 mb-2 p-2 border-3"
|
||||
/>
|
||||
<p class="[&>*]:underline [&>*]:decoration-2 [&>*]:decoration-transparent [&>*]:hover:decoration-inherit [&>*]:transition-all [&>*]:duration-300 [&>*]:text-green-600">{@html info.about}</p>
|
||||
</div>
|
||||
<h3 slot="footerLeft">{@html info.location}</h3>
|
||||
</Card>
|
||||
</Section>
|
||||
|
||||
<div class="container">
|
||||
<h1>Experience</h1>
|
||||
<hr />
|
||||
<!-- https://github.com/K-Sato1995/svelte-vertical-timeline -->
|
||||
<Timeline timelineData="{info.timeline}"></Timeline>
|
||||
</div>
|
||||
<!-- SKills -->
|
||||
<Section label="[Skills]">
|
||||
<GridGallery>
|
||||
{#each info.skills as skill}
|
||||
<Card containerStyle="opacity-100 hover:opacity-100 hover:scale-[105%] md:opacity-70 transition-all duration-300">
|
||||
<h2 slot="headerLeft">{skill.name}</h2>
|
||||
<i slot="headerRight" class="text-slate-300 text-5xl {skill.logo}"></i>
|
||||
<div slot="content">
|
||||
<Collapsible>
|
||||
<span slot="label" class="text-lg">About {skill.name}</span>
|
||||
<span slot="content">{skill.about}</span>
|
||||
</Collapsible>
|
||||
<SkillProgress skillColour={skill.colour} value={skill.competency} />
|
||||
</div>
|
||||
<h3 slot="footerLeft"><a href="{skill.link}" target="_blank">{skill.link}</a></h3>
|
||||
</Card>
|
||||
{/each}
|
||||
</GridGallery>
|
||||
</Section>
|
||||
|
||||
<div style="display: none;">{addToast(new Toast("Click on a skill to open a prompt", ToastType.Info, true, 8_000))}</div>
|
||||
<div style="display: none;">{addToast(new Toast("Welcome!", ToastType.Success, true, 7_000))}</div>
|
||||
<Section label="[Experience]">
|
||||
<Timeline timelineData={info.timeline} />
|
||||
</Section>
|
||||
{:catch}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1>Unable to load portfolio overview data</h1>
|
||||
</div>
|
||||
<div style="display: none;">
|
||||
{toasts.add({
|
||||
title: 'Error',
|
||||
description: 'There was an error loading static site data',
|
||||
duration: 0,
|
||||
placement: 'bottom-center',
|
||||
showProgress: true
|
||||
})}
|
||||
</div>
|
||||
<div style="display: none;">{addToast(new Toast("Unable to load me.json", ToastType.Error, true, 3000))}</div>
|
||||
{/await}
|
||||
{/await}
|
||||
|
@ -1,60 +1,23 @@
|
||||
<script lang="ts">
|
||||
import Toasts from "$lib/components/Toasts/Toasts.svelte";
|
||||
import ThemeSwitcher from "$lib/components/ThemeSwitcher.svelte";
|
||||
import { ToastContainer, FlatToast } from 'svelte-toasts';
|
||||
import '../app.css';
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.main-container {
|
||||
margin-left: 10%;
|
||||
margin-right: 10%;
|
||||
padding-top: 2em;
|
||||
}
|
||||
<div
|
||||
class="min-h-screen px-8 py-4 bg-white text-slate-600 dark:bg-slate-900/90 dark:text-slate-200/60 md:text-2xl sm:text-md font-mono flex flex-col gap-5 transition duration-1000 ease-in-out"
|
||||
>
|
||||
<nav
|
||||
class="w-full px-8 py-4 flex gap-10 text-xl justify-center items-center text-green-600 font-semibold"
|
||||
>
|
||||
<a href="/" class="hover:underline">//Profile</a>
|
||||
<a href="/repos" class="hover:underline">//Repos</a>
|
||||
<a href="/contact" class="hover:underline">//Contact</a>
|
||||
</nav>
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.main-container {
|
||||
margin: 0em;
|
||||
padding-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
position: relative;
|
||||
font-weight: bold;
|
||||
font-size: 110%;
|
||||
overflow: visible;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1.5em;
|
||||
padding: 0em 0em 1.5em 0em;
|
||||
z-index: 2;
|
||||
height: 1.5em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.fade {
|
||||
-webkit-animation: fadeinout 1s linear forwards;
|
||||
animation: fadeinout 1s linear forwards;
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadeinout {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeinout {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<nav>
|
||||
<a href = "/">//Profile</a>
|
||||
<a href = "/repos">//Repos</a>
|
||||
<a href = "/contact">//Contact</a>
|
||||
<ThemeSwitcher />
|
||||
</nav>
|
||||
|
||||
<div class="main-container fade">
|
||||
<Toasts />
|
||||
<slot />
|
||||
<div class="container mx-auto justify-center items-center flex flex-col">
|
||||
<slot />
|
||||
<ToastContainer let:data>
|
||||
<FlatToast {data} />
|
||||
</ToastContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,4 +2,6 @@
|
||||
import Main from '../main.svelte';
|
||||
</script>
|
||||
|
||||
<Main></Main>
|
||||
<div>
|
||||
<Main></Main>
|
||||
</div>
|
@ -1,82 +1,98 @@
|
||||
<script lang="ts">
|
||||
import Card from '$lib/components/Card.svelte';
|
||||
import { Toast, ToastType } from "$lib/toast";
|
||||
import { addToast } from "$lib/store";
|
||||
import { toasts } from 'svelte-toasts';
|
||||
import Card from '$lib/components/Cards/Card.svelte';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
const sent = $page.url.searchParams.get('sent');
|
||||
import { page } from '$app/state';
|
||||
const sent = page.url.searchParams.get('sent');
|
||||
|
||||
if (sent == "true") {
|
||||
addToast(new Toast("Thank you! Your E-Mail has been sent. I will reply as soon as possible!", ToastType.Success, true, 5000));
|
||||
if (sent == 'true') {
|
||||
toasts.add({
|
||||
title: 'Message sent!',
|
||||
description: 'Thank you for contacting me.',
|
||||
type: 'success',
|
||||
duration: 4000,
|
||||
placement: 'bottom-center'
|
||||
});
|
||||
}
|
||||
// Can't use else otherwise the warning will display on load
|
||||
if (sent == "false") {
|
||||
addToast(new Toast("Sorry, your E-Mail could not be sent... Please try again later!", ToastType.Error, true, 5000));
|
||||
|
||||
if (sent == 'false') {
|
||||
toasts.add({
|
||||
title: 'Message not sent!',
|
||||
description: 'Please try again later.',
|
||||
type: 'error',
|
||||
duration: 4000,
|
||||
placement: 'bottom-center'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: 2 1;
|
||||
align-items: center;
|
||||
margin: 1em;
|
||||
gap: 1em 3em;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
padding: 1em 0em 1em 1em;
|
||||
background-color: var(--input);
|
||||
color: var(--fg);
|
||||
border: 0;
|
||||
outline: 0;
|
||||
border-radius: 8px;
|
||||
resize: none;
|
||||
min-width: 100%;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:focus, &:hover{
|
||||
box-shadow: .2em .2em .2em var(--green);
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 10em;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex: 2 1 5rem;
|
||||
flex-direction: column;
|
||||
gap: 1em 1em;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<Card>
|
||||
<div slot="header">
|
||||
<h2>Contact</h2>
|
||||
<div slot="headerLeft">
|
||||
Contact Me
|
||||
</div>
|
||||
<div slot="content">
|
||||
<form action="https://api.staticforms.xyz/submit" method="post">
|
||||
<div class="container">
|
||||
<input type="hidden" name="accessKey" value="fbb5ec04-506b-448a-a445-a2e47579a966">
|
||||
<input type="text" name="name" placeholder="Name" required>
|
||||
<input type="text" name="email" placeholder="Email address" required>
|
||||
<input type="hidden" name="replyTo" value="@">
|
||||
<input type="text" name="subject" placeholder="Subject" required>
|
||||
<input type="text" name="honeypot" style="display: none;">
|
||||
</div>
|
||||
<div class="container">
|
||||
<textarea name="message" id="message" placeholder="Message" required></textarea>
|
||||
</div>
|
||||
<input class="button" type="submit" value="Send" />
|
||||
<!-- Contact Form -->
|
||||
<form slot="content" class="w-full max-w-3xl mx-auto flex flex-col gap-4 text-lg" action="https://api.staticforms.xyz/submit" method="post">
|
||||
<div class="hidden">
|
||||
<input type="hidden" name="accessKey" value="fbb5ec04-506b-448a-a445-a2e47579a966">
|
||||
<input type="hidden" name="replyTo" value="@">
|
||||
<input type="text" name="honeypot" style="display: none;">
|
||||
<input type="hidden" name="redirectTo" value="https://luke-else.co.uk/contact?sent=true">
|
||||
</form>
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<a href="/Luke Else - CV.pdf" target="_blank" rel="noopener noreferrer">Curriculum Vitae</a>
|
||||
<a href="mailto:contact@luke-else.co.uk">E-Mail</a>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-3">
|
||||
<div class="flex-1">
|
||||
<label class="block text-xs font-medium mb-1" for="name">Name</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
class="w-full rounded-lg border border-gray-400 px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-600 transition placeholder-gray-400"
|
||||
required
|
||||
placeholder="Your name"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="block text-xs font-medium mb-1" for="email">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
class="w-full rounded-lg border border-gray-400 px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-600 transition placeholder-gray-400"
|
||||
required
|
||||
placeholder="you@email.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="block text-xs font-medium mb-1" for="subject">Subject</label>
|
||||
<input
|
||||
id="subject"
|
||||
name="subject"
|
||||
type="text"
|
||||
class="w-full rounded-lg border border-gray-400 px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-600 transition placeholder-gray-400"
|
||||
required
|
||||
placeholder="Subject"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium mb-1" for="message">Message</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
class="w-full rounded-lg border border-gray-400 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-600 transition min-h-[80px] placeholder-gray-400"
|
||||
required
|
||||
placeholder="Your message"
|
||||
></textarea>
|
||||
</div>
|
||||
<!-- reCAPTCHA integration -->
|
||||
<div class="">
|
||||
<div class="g-recaptcha" data-sitekey="6LfjQAwrAAAAAIF57u8Wt4w5L5vBEWi5DfXXBuGy"></div>
|
||||
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="self-end bg-blue-600 hover:bg-blue-700 text-white font-semibold py-1.5 px-8 rounded-lg transition"
|
||||
>
|
||||
Send Message
|
||||
</button>
|
||||
</form>
|
||||
</Card>
|
||||
|
@ -1,3 +1,71 @@
|
||||
<h1>Repos</h1>
|
||||
<p>Stay tuned! This is still in development.</p>
|
||||
<p>Come back later to find out what projects I'm currently working on!</p>
|
||||
<script lang="ts">
|
||||
import { loadRepos, repos } from '$lib/stores';
|
||||
import { timeSince, checkImage, IMAGE_URL_SUFFIX } from '$lib/api/git';
|
||||
import { toasts } from 'svelte-toasts';
|
||||
|
||||
import GridGallery from '$lib/components/GridGallery.svelte';
|
||||
import Card from '$lib/components/Cards/Card.svelte';
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
|
||||
let repoImages: Record<string, string | null> = {};
|
||||
|
||||
// When repos load, check for images
|
||||
$: if ($repos.length) {
|
||||
(async () => {
|
||||
for (const repo of $repos) {
|
||||
if (repoImages[repo.name] === undefined) {
|
||||
const url = repo.html_url + IMAGE_URL_SUFFIX;
|
||||
repoImages[repo.name] = (await checkImage(repo)) ? url : null;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await loadRepos()}
|
||||
<Loading />
|
||||
{:then _}
|
||||
{#if $repos.length == 0}
|
||||
{console.log('No Repos')}
|
||||
<div style="display: none;">
|
||||
{toasts.add({
|
||||
title: 'Error',
|
||||
description: 'Failed to load repositories',
|
||||
duration: 5000,
|
||||
type: 'error',
|
||||
placement: 'bottom-center',
|
||||
showProgress: true
|
||||
})}
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Repositories loaded successfully -->
|
||||
<GridGallery>
|
||||
{#each $repos as repo}
|
||||
<!-- <Loading /> -->
|
||||
<Card
|
||||
containerStyle="group relative flex-1 min-w-[250px] max-w-full md:min-w-[33%] opacity-100 hover:opacity-100 hover:scale-[105%] md:opacity-70 transition-all duration-300 overflow-hidden"
|
||||
>
|
||||
<h2 slot="headerLeft">{repo.name}</h2>
|
||||
<h2 slot="headerRight" class="text-sm text-gray-500">
|
||||
{repo.language}
|
||||
</h2>
|
||||
<div slot="content">
|
||||
<div class="relative z-0">
|
||||
{repo.description}
|
||||
</div>
|
||||
{#if repoImages[repo.name]}
|
||||
<!-- svelte-ignore a11y_img_redundant_alt -->
|
||||
<img
|
||||
src={repoImages[repo.name]}
|
||||
alt="repo image"
|
||||
class="absolute left-0 bottom-0 h-full w-full object-cover rounded-2xl transition-transform duration-500 translate-y-full group-hover:translate-y-0 z-10 pointer-events-none"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<h3 slot="footerLeft">
|
||||
Last Updated: {timeSince(repo.updated_at)}
|
||||
</h3>
|
||||
</Card>
|
||||
{/each}
|
||||
</GridGallery>
|
||||
{/await}
|
||||
|
@ -1,59 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let skills: any;
|
||||
|
||||
import Card from '$lib/components/Card.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
|
||||
let showModal: boolean = false;
|
||||
let activeModal: any = null;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.card-footer {
|
||||
margin-bottom: 1em;
|
||||
display: flex;
|
||||
gap: 1.5em;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: var(--fg);
|
||||
font-size: 3em;
|
||||
}
|
||||
</style>
|
||||
|
||||
{#each skills as skill}
|
||||
<Card on:click={() => {showModal = true; activeModal = skill}}>
|
||||
<div slot="header">
|
||||
<h2>{skill.skill}</h2>
|
||||
<i class="{skill.logo} logo"></i>
|
||||
</div>
|
||||
<div slot="content">
|
||||
<p class="not-required">{@html skill.usage}</p>
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<!-- svelte-ignore a11y-invalid-attribute -->
|
||||
<a href="#">View More</a>
|
||||
<a href="/repos">Repos</a>
|
||||
</div>
|
||||
</Card>
|
||||
{/each}
|
||||
|
||||
<!--Modal to be displayed on click-->
|
||||
{#if activeModal != null}
|
||||
<Modal bind:showModal>
|
||||
<h2 slot="header" class="card-header">
|
||||
{activeModal.skill}
|
||||
<i class="{activeModal.logo} logo"></i>
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
{activeModal.about}
|
||||
</p>
|
||||
|
||||
<div class="card-footer">
|
||||
<a href="{activeModal.link}" target="_blank" rel="noopener noreferrer">Learn More</a>
|
||||
<a href="/repos">Repos</a>
|
||||
</div>
|
||||
</Modal>
|
||||
{/if}
|
@ -1,63 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let timelineData: any;
|
||||
|
||||
import Timeline from '$lib/components/timeline/Timeline.svelte';
|
||||
import TimelineItem from '$lib/components/timeline/TimelineItem.svelte';
|
||||
import TimelineSeparator from '$lib/components/timeline/TimelineSeparator.svelte';
|
||||
import TimelineDot from '$lib/components/timeline/TimelineDot.svelte';
|
||||
import TimelineConnector from '$lib/components/timeline/TimelineConnector.svelte';
|
||||
import TimelineContent from '$lib/components/timeline/TimelineContent.svelte';
|
||||
import TimelineOppositeContent from '$lib/components/timeline/TimelineOppositeContent.svelte';
|
||||
</script>
|
||||
|
||||
<Timeline
|
||||
position="alternate"
|
||||
style={`
|
||||
border-radius: 3%;
|
||||
padding: 1rem;
|
||||
`}
|
||||
>
|
||||
{#each timelineData as item}
|
||||
<TimelineItem>
|
||||
<TimelineOppositeContent slot="opposite-content">
|
||||
<p class="oposite-content-title">{item.duration}</p>
|
||||
</TimelineOppositeContent>
|
||||
|
||||
<TimelineSeparator>
|
||||
{#if item.duration.includes('Present') || !item.duration.includes('-')}
|
||||
<div class="elementToFadeInAndOut">
|
||||
<TimelineDot style={`background-color: var(--link); border-color: var(--bg-grad-2);`} />
|
||||
</div>
|
||||
{:else}
|
||||
<TimelineDot style={`background-color: var(--link); border-color: var(--bg-grad-2);`} />
|
||||
{/if}
|
||||
<TimelineConnector />
|
||||
</TimelineSeparator>
|
||||
<TimelineContent>
|
||||
<h3 class="content-title">{item.title}</h3>
|
||||
<p class="content-description">{@html item.description}</p>
|
||||
</TimelineContent>
|
||||
</TimelineItem>
|
||||
{/each}
|
||||
</Timeline>
|
||||
|
||||
<style>
|
||||
.oposite-content-title {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--bg-grad-2);
|
||||
}
|
||||
.content-title {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content-description {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-top: 1rem;
|
||||
color: var(--fg);
|
||||
font-weight: lighter;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
</style>
|
@ -1,50 +1,122 @@
|
||||
{
|
||||
"name": "Luke Else",
|
||||
"job_title": "Software Engineer",
|
||||
"location": "Crawley, Sussex <br /> UK",
|
||||
"profile_photo": "/profile.jpg",
|
||||
"skills" : [
|
||||
"about": "Hello! I'm an enthusiastic, dedicated software engineer passionate about backend development, networking, and embedded systems. I am currently employed at <a href='https://www.thalesgroup.com/en'>Thales UK</a> and thrive on architecting robust backend solutions, optimizing data transmission, and crafting efficient embedded software. I love tackling complex challenges, collaborating with fellow professionals, and staying up-to-date with tech trends such as my current venture in learning <a href='https://rust-lang.org'>Rust-Lang</a>.",
|
||||
"skills": [
|
||||
{
|
||||
"skill": "Rust",
|
||||
"name": "Rust",
|
||||
"logo": "devicon-rust-plain",
|
||||
"colour": "bg-orange-400",
|
||||
"link": "https://rust-lang.org",
|
||||
"usage": "Rust is a memory safe programming language that relies on a borrow checker to keep track of who owns memory. This makes it versatile in embedded software becuase of the zero cost abstraction and lack of requirement for a garbage colllector. Becuase of this, I opted to use Rust for one of my own projects in which I created a <a href='https://git@git.luke-else.co.uk/luke-else/esp32_gps_display'>GPS fueled speedometer</a> and position tracker to act as a form of telematics for my car. I have also used Rust when creating some software to assist with university studies. See here for my <a href='https://git@git.luke-else.co.uk/luke-else/subnet_calculator'>Subnet Calculator</a>.",
|
||||
"about": "Rust is a remarkable programming language that combines the best of both high-level and low-level programming. It prioritizes safety and efficiency through strict memory management and concurrent programming features, all while maintaining clean and readable code. The active Rust community makes it a valuable choice for building reliable software."
|
||||
"about": "Rust combines safety, efficiency, and clean code, making it a powerful choice for reliable software development.",
|
||||
"competency": 70
|
||||
},
|
||||
{
|
||||
"skill": "C++",
|
||||
"name": "C++",
|
||||
"logo": "devicon-cplusplus-plain",
|
||||
"colour": "bg-blue-400",
|
||||
"link": "https://cplusplus.com/",
|
||||
"usage": "Opposing Rust, C++, has been my primary language since joining Thales in late 2022. I have predominantly been working on a distributed simulation system that relies upon <a href='https://en.wikipedia.org/wiki/Inter-process_communication'>IPC</a> through the use of internal tools to orchestrate itself and the platform's sessions. I've had a lot of experience using both <a href='https://www.qt.io'>QT</a>, as well as the lesser known <a href='https://github.com/ocornut/imgui'>ImGui</a>, both of which have given me the platform to build readily deployable apps to customers at Thales UK.",
|
||||
"about": "C++, a versatile and powerful programming language, has stood the test of time as a cornerstone of software development. Its combination of high-level abstractions and low-level control allows developers to tackle a wide range of projects efficiently. With a rich standard library and a massive ecosystem of libraries and frameworks; C++ empowers programmers to create efficient and scalable software solutions."
|
||||
"about": "C++ offers high-level abstractions with low-level control, making it essential for performance-critical applications.",
|
||||
"competency": 80
|
||||
},
|
||||
{
|
||||
"skill": "Git",
|
||||
"name" : "Python",
|
||||
"logo": "devicon-python-plain",
|
||||
"colour": "bg-yellow-400",
|
||||
"link": "https://python.org",
|
||||
"about": "Python is a versatile language known for its simplicity and readability, making it ideal for rapid development and data analysis.",
|
||||
"competency": 70
|
||||
},
|
||||
{
|
||||
"name": "Git",
|
||||
"logo": "devicon-git-plain",
|
||||
"colour": "bg-red-400",
|
||||
"link": "https://git-scm.com",
|
||||
"usage": "With git being pretty much the 'Defaqto standard' when it comes to version control systems, I have had a lot of experience with its use, including more advanced features such as <a href='https://www.atlassian.com/git/tutorials/advanced-overview'>'Branching / Merging'</a>, <a href='https://www.atlassian.com/git/tutorials/advanced-overview'>'Hooks'</a>, <a href='https://www.atlassian.com/git/tutorials/advanced-overview'>'Stashing'</a> etc... I've further setup my own <a href='https://git.luke-else.co.uk/luke-else/'>services</a> for hosting remote Git reporitories to allow for complete access and control over my work. Appending to this, I have setup my own form of actions to aid in the actioning of <a href='https://www.redhat.com/en/topics/devops/what-is-ci-cd'>CI/CD</a> on my own projects.",
|
||||
"about": "Git, a fundamental tool in version control, streamlines collaboration and code management. Its simplicity and robustness empower developers to track changes, collaborate seamlessly, and maintain code efficiently. With Git, managing and tracking code changes becomes a breeze, making it an essential tool for software development."
|
||||
"about": "Git is an essential tool for version control, enabling efficient collaboration and streamlined code management.",
|
||||
"competency": 80
|
||||
},
|
||||
{
|
||||
"skill": "Docker",
|
||||
"name": "Docker",
|
||||
"logo": "devicon-docker-plain",
|
||||
"colour": "bg-blue-500",
|
||||
"link": "https://docker.com",
|
||||
"usage": "With docker being so versatile when it comes to deploying software to end users, I decided to pick it up as a skill in order to improve the quality of applications and services I can offer to individuals. I use docker alongside its child, 'docker-compose' to control the <a href='https://git.luke-else.co.uk/luke-else/server'>provisioning of containers</a> on my main home-lab server which delivers content like <a href='https://git.luke-else.co.uk/luke-else/luke-else.co.uk'>this website</a> as well as database and <a href='https://git.luke-else.co.uk/luke-else/'>remote git services</a>.",
|
||||
"about": "Docker, a transformative containerization platform, simplifies application deployment and management. It encapsulates applications and their dependencies in lightweight containers, providing consistency across different environments. Docker's user-friendly approach makes it accessible to developers and DevOps professionals, streamlining the software development and deployment process. Its efficiency, scalability, and support for microservices architecture have made it a go-to tool for creating, deploying, and scaling applications. Docker has revolutionized the way we think about software packaging and distribution, making it an essential component of modern software development workflows."
|
||||
"about": "Docker simplifies deployment by packaging applications in lightweight containers, ensuring consistency across environments.",
|
||||
"competency": 100
|
||||
},
|
||||
{
|
||||
"skill": "Svelte",
|
||||
"name": "Kubernetes",
|
||||
"logo": "devicon-kubernetes-plain",
|
||||
"colour": "bg-blue-600",
|
||||
"link": "https://kubernetes.io",
|
||||
"about": "Kubernetes automates the deployment, scaling, and management of containerized applications, enhancing operational efficiency.",
|
||||
"competency": 40
|
||||
},
|
||||
{
|
||||
"name": "PostgreSQL",
|
||||
"logo": "devicon-postgresql-plain",
|
||||
"colour": "bg-blue-700",
|
||||
"link": "https://postgresql.org",
|
||||
"about": "PostgreSQL is a powerful, open-source relational database known for its robustness and advanced features.",
|
||||
"competency": 70
|
||||
},
|
||||
{
|
||||
"name": "MongoDB",
|
||||
"logo": "devicon-mongodb-plain",
|
||||
"colour": "bg-green-500",
|
||||
"link": "https://mongodb.com",
|
||||
"about": "MongoDB is a NoSQL database that provides flexibility and scalability for modern applications with unstructured data.",
|
||||
"competency": 70
|
||||
},
|
||||
{
|
||||
"name": "Redis",
|
||||
"logo": "devicon-redis-plain",
|
||||
"colour": "bg-red-600",
|
||||
"link": "https://redis.io",
|
||||
"about": "Redis is an in-memory data structure store, used as a database, cache, and message broker for high-performance applications.",
|
||||
"competency": 30
|
||||
},
|
||||
{
|
||||
"name": "JavaScript",
|
||||
"logo": "devicon-javascript-plain",
|
||||
"colour": "bg-yellow-500",
|
||||
"link": "https://javascript.com",
|
||||
"about": "JavaScript is a versatile language that powers dynamic web applications and enhances user interactivity.",
|
||||
"competency": 60
|
||||
},
|
||||
{
|
||||
"name": "Tailwind CSS",
|
||||
"logo": "devicon-tailwindcss-plain",
|
||||
"colour": "bg-blue-800",
|
||||
"link": "https://tailwindcss.com/",
|
||||
"about": "Tailwind CSS is a utility-first CSS framework that enables rapid UI development with a focus on customization and responsiveness.",
|
||||
"competency": 60
|
||||
},
|
||||
{
|
||||
"name": "Svelte",
|
||||
"logo": "devicon-svelte-plain",
|
||||
"colour": "bg-orange-400",
|
||||
"link": "https://svelte.dev",
|
||||
"usage": "Svelte is a front and backend web framework that allows for stylised components to be reused in an efficient manor. This <a href='https://git.luke-else.co.uk/luke-else/luke-else.co.uk'>website</a> was made using svelte and has given me a great opportunity to learn about Svelte's power and usage. I further want to expand this in the future by using <a href='https://github.com/tauri-apps/tauri'>Tauri</a> to create rendered web application in containers that can be deployed as desktop apps.",
|
||||
"about": "Svelte is an impressive web framework that stands out in the world of front-end development. It's known for its unique approach, compiling components to highly optimized vanilla JavaScript, resulting in blazing-fast web applications. Svelte's simplicity and declarative syntax make it easy to learn and use, and it encourages efficient, maintainable code. "
|
||||
"about": "Svelte compiles to optimized JavaScript, offering a fast, efficient, and maintainable front-end development experience.",
|
||||
"competency": 55
|
||||
}
|
||||
],
|
||||
"about": "Hello! I'm an enthusiastic, dedicated software engineer passionate about backend development, networking, and embedded systems. I am currently employed at <a href='https://www.thalesgroup.com/en'>Thales UK</a> and thrive on architecting robust backend solutions, optimizing data transmission, and crafting efficient embedded software. I love tackling complex challenges, collaborating with fellow professionals, and staying up-to-date with tech trends such as my current venture in learning <a href='https://rust-lang.org'>Rust-Lang</a>.",
|
||||
"timeline" : [
|
||||
{
|
||||
"duration" : "April 2025 - Present",
|
||||
"title" : "Thales UK (DDCC) - Software Engineer",
|
||||
"description" : "As a 3rd year apprentice at Thales UK’s Digital Data Competency Centre, I have taken on responsibility for developing microservices that encapsulate Machine Learning models provided by R&D teams, helping to advance product readiness. These services are primarily written in Python and deployed to Kubernetes clusters for use across the business. Our team also designs and maintains CI/CD pipelines to automate the deployment of both these services and their supporting infrastructure."
|
||||
},
|
||||
{
|
||||
"duration" : "September 2022 - April 2025",
|
||||
"title" : "Thales UK (ISR) - Software Engineer",
|
||||
"description" : "As a software engineering apprentice at Thales UK, Intelligence Surveillance and Reconnaissance, I worked within an agile team of six engineers, contributing to the ongoing development of a C++ system for the MOD. My role involved collaborating closely with colleagues, following Scrum methodologies, and leveraging internal frameworks to enhance and maintain the existing platform."
|
||||
},
|
||||
{
|
||||
"duration" : "September 2022 - Present",
|
||||
"title" : "Thales UK - Software Engineer",
|
||||
"description" : "As a software engineering apprentice at Thales UK, I find myself partaking in agile / scrum development methodologies in a strong team of 6 other engineers. The team iterates on a pre-existing system designed for the MOD, written in C++, using internal frameworks to assist. <br /><br /> To extend this, the apprenticeship includes allocated time for studying a Digital and Thechnology Solutions degree with the University of Warwick, including modules relevant to business management, devlopment processes and data integrity etc..."
|
||||
"title" : "University of Warwick - Digital and Technology Solutions",
|
||||
"description" : "The apprenticeship includes allocated time for studying a Digital and Thechnology Solutions degree with the University of Warwick, including modules relevant to business management, devlopment processes and data integrity etc..."
|
||||
},
|
||||
{
|
||||
"duration" : "September 2020 - July 2022",
|
||||
@ -54,7 +126,7 @@
|
||||
{
|
||||
"duration" : "September 2015 - July 2020",
|
||||
"title" : "The Norton Knatchbull School (GCSEs)",
|
||||
"description" : "FSMQ (C) <br /> Maths (8) <br /> Geography (<b>9</b>) <br /> Biology (<b>9</b>) <br /> Chemistry (<b>9</b>) <br /> Physics (<b>9</b>) <br /> Spanish (7) <br /> English (Literature & Language) (7, 7) <br /> Computer Science (<b>9</b>)"
|
||||
"description" : "Computer Science (<b>9</b>) <br /> Physics (<b>9</b>) <br /> Chemistry (<b>9</b>) <br /> Biology (<b>9</b>) <br /> Geography (<b>9</b>) <br /> FSMQ (C) <br /> Maths (8) <br /> Spanish (7) <br /> English (Literature & Language) (7, 7) <br />"
|
||||
}
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 425 KiB After Width: | Height: | Size: 67 KiB |
@ -1,18 +0,0 @@
|
||||
:root {
|
||||
--bg: #282c34;
|
||||
--bg-secondary: #474d57;
|
||||
--bg-grad-1: #484e58;
|
||||
--bg-grad-2: #4e5560;
|
||||
--bg-grad-3: #59616d;
|
||||
--bg-grad-4: #606a7b;
|
||||
--bg-grad-5: #606978;
|
||||
--input: #4e5560;
|
||||
--fg: #ABB2BF;
|
||||
--header: #E06C75;
|
||||
--link: #98C379;
|
||||
--hover: #56B6C2;
|
||||
--glow: #C678DD;
|
||||
|
||||
--green: #98C379;
|
||||
--red: #E06C75;
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
:root {
|
||||
--bg: #fff;
|
||||
--bg-secondary: #ebebeb;
|
||||
--bg-grad-1: #c1c1c1;
|
||||
--bg-grad-2: #a1a1a1;
|
||||
--bg-grad-3: #858585;
|
||||
--bg-grad-4: #616161;
|
||||
--bg-grad-5: #484848;
|
||||
--input: #a9a9a9;
|
||||
--fg: #2f2f2f;
|
||||
--header: #514a4a;
|
||||
--link: #df0000;
|
||||
--hover: #4f4b489b;
|
||||
--glow: #545454;
|
||||
|
||||
--green: #98C379;
|
||||
--red: #E06C75;
|
||||
}
|
@ -1,19 +1,10 @@
|
||||
// import adapter from '@sveltejs/adapter-auto';
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
kit: { adapter: adapter() }
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
19
tailwind.config.js
Normal file
@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'one-bg': '#282c34',
|
||||
'one-bg-light': '#3a3f4b',
|
||||
'one-fg': '#abb2bf',
|
||||
'one-accent': '#61afef',
|
||||
'one-green': '#98c379',
|
||||
'one-orange': '#d19a66',
|
||||
'one-red': '#e06c75',
|
||||
'one-yellow': '#e5c07b',
|
||||
'one-purple': '#c678dd',
|
||||
'one-cyan': '#56b6c2',
|
||||
'one-comment': '#5c6370',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
tests/git.svelte.test.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { fetchRepos, timeSince, checkImage } from '$lib/api/git';
|
||||
|
||||
test('Fetch Repos', async () => {
|
||||
let repos = await fetchRepos();
|
||||
expect(repos.length).toEqual(12);
|
||||
});
|
||||
|
||||
test.each([
|
||||
[new Date((Date.now() - 1000 * 60 * 60 * 24)), 'Yesterday'],
|
||||
[new Date((Date.now() - 1000 * 60 * 60 * 24 * 2)), '2 days ago'],
|
||||
[new Date((Date.now() - 1000 * 60 * 60 * 24 * 7)), '1 week ago'],
|
||||
[new Date((Date.now() - 1000 * 60 * 60 * 24 * 7 * 2)), '2 weeks ago'],
|
||||
[new Date((Date.now() - 1000 * 60 * 60 * 24 * 30)), '1 month ago'],
|
||||
[new Date((Date.now() - 1000 * 60 * 60 * 24 * 365)), '1 year ago'],
|
||||
])('', (time, expected) => {
|
||||
expect(timeSince(time)).toBe(expected)
|
||||
})
|
||||
|
||||
|
||||
test('Check Image', async () => {
|
||||
let repos = await fetchRepos();
|
||||
|
||||
for (let repo of repos) {
|
||||
let image = await checkImage(repo);
|
||||
expect(image).toBeDefined();
|
||||
}
|
||||
});
|
@ -8,7 +8,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
plugins: [tailwindcss(), sveltekit()],
|
||||
});
|
||||
|