Compare commits
52 Commits
d729c3fcfb
...
1eab47ee41
Author | SHA1 | Date | |
---|---|---|---|
1eab47ee41 | |||
9938e304c7 | |||
d6716f4f4b | |||
6499044081 | |||
fdae605dcb | |||
05ccfc0997 | |||
7a932eed95 | |||
be1bd54fb9 | |||
3c900a9aef | |||
6dc67adf9f | |||
dd03919de3 | |||
3738f0d215 | |||
f8020a19a2 | |||
083fbe1c20 | |||
dad56090ac | |||
05474689be | |||
97502acbff | |||
93fde2410b | |||
8a8ecc14f3 | |||
823ff4cb33 | |||
75dd8090bb | |||
e567027ec6 | |||
adf372f533 | |||
e9ca6a2697 | |||
e457d909cd | |||
88da650bba | |||
6c0b2c2f67 | |||
e6c45567f2 | |||
abe1ea2055 | |||
cd4bcd544f | |||
9158ca1608 | |||
e6c7d3a0f6 | |||
864152ca9a | |||
dcc6aa000a | |||
4ef0573b55 | |||
5e229ec264 | |||
bb14189833 | |||
94cc74bcdb | |||
0a647357b5 | |||
aebdce25d2 | |||
7d00e51c6b | |||
b2f0a2e6b0 | |||
1173388600 | |||
8207831f78 | |||
a237297383 | |||
cd9676eaf3 | |||
84e67fb35a | |||
a9387cf84c | |||
24659bc269 | |||
68496c1677 | |||
8c82d99b6c | |||
2a98cce593 |
18
.dockerignore
Normal file
18
.dockerignore
Normal file
@ -0,0 +1,18 @@
|
||||
# .dockerignore
|
||||
|
||||
.vscode
|
||||
|
||||
node_modules
|
||||
|
||||
.git
|
||||
.gitattributes
|
||||
|
||||
.eslintignore
|
||||
.eslintrc.cjs
|
||||
|
||||
.prettierrc
|
||||
.pretieriignore
|
||||
|
||||
README.md
|
||||
|
||||
Dockerfile
|
31
.gitea/workflows/build.yaml
Normal file
31
.gitea/workflows/build.yaml
Normal file
@ -0,0 +1,31 @@
|
||||
name: Build and Push Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
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:
|
||||
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_USERNAME }}/luke-else.co.uk:latest .
|
||||
|
||||
- name: Push Docker Image
|
||||
run: |
|
||||
docker push ${{ secrets.CONTAINER_REGISTRY_USERNAME }}/luke-else.co.uk:latest
|
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
.vscode
|
13
.prettierignore
Normal file
13
.prettierignore
Normal file
@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
38
README.md
Normal file
38
README.md
Normal file
@ -0,0 +1,38 @@
|
||||
# create-svelte
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# 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
|
||||
|
||||
# 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:
|
||||
|
||||
```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.
|
20
dockerfile
Normal file
20
dockerfile
Normal file
@ -0,0 +1,20 @@
|
||||
FROM node:lts-slim as build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN rm -rf node_modules
|
||||
RUN rm -rf build
|
||||
COPY . .
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
FROM node:lts-slim as run
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/package.json ./package.json
|
||||
COPY --from=build /app/build ./build
|
||||
RUN npm install --omit=dev
|
||||
|
||||
EXPOSE 3000
|
||||
ENTRYPOINT [ "npm", "run", "start" ]
|
2088
package-lock.json
generated
Normal file
2088
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
Normal file
29
package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "luke-else.co.uk",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"start": "export PORT=3000 && node ./build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --plugin-search-dir . --check .",
|
||||
"format": "prettier --plugin-search-dir . --write ."
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
12
src/app.d.ts
vendored
Normal file
12
src/app.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
129
src/app.html
Normal file
129
src/app.html
Normal file
@ -0,0 +1,129 @@
|
||||
<!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." />
|
||||
<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%
|
||||
|
||||
<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;
|
||||
}
|
||||
|
||||
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>
|
||||
</html>
|
66
src/lib/components/Card.svelte
Normal file
66
src/lib/components/Card.svelte
Normal file
@ -0,0 +1,66 @@
|
||||
<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>
|
82
src/lib/components/Modal.svelte
Normal file
82
src/lib/components/Modal.svelte
Normal file
@ -0,0 +1,82 @@
|
||||
<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>
|
102
src/lib/components/ThemeSwitcher.svelte
Normal file
102
src/lib/components/ThemeSwitcher.svelte
Normal file
@ -0,0 +1,102 @@
|
||||
<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>
|
18
src/lib/components/Toasts/CloseIcon.svelte
Normal file
18
src/lib/components/Toasts/CloseIcon.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<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>
|
19
src/lib/components/Toasts/ErrorIcon.svelte
Normal file
19
src/lib/components/Toasts/ErrorIcon.svelte
Normal file
@ -0,0 +1,19 @@
|
||||
<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>
|
18
src/lib/components/Toasts/InfoIcon.svelte
Normal file
18
src/lib/components/Toasts/InfoIcon.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<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>
|
18
src/lib/components/Toasts/SuccessIcon.svelte
Normal file
18
src/lib/components/Toasts/SuccessIcon.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<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>
|
67
src/lib/components/Toasts/Toast.svelte
Normal file
67
src/lib/components/Toasts/Toast.svelte
Normal file
@ -0,0 +1,67 @@
|
||||
<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>
|
28
src/lib/components/Toasts/Toasts.svelte
Normal file
28
src/lib/components/Toasts/Toasts.svelte
Normal file
@ -0,0 +1,28 @@
|
||||
<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>
|
21
src/lib/components/timeline/Timeline.svelte
Normal file
21
src/lib/components/timeline/Timeline.svelte
Normal file
@ -0,0 +1,21 @@
|
||||
<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>
|
13
src/lib/components/timeline/TimelineConnector.svelte
Normal file
13
src/lib/components/timeline/TimelineConnector.svelte
Normal file
@ -0,0 +1,13 @@
|
||||
<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>
|
30
src/lib/components/timeline/TimelineContent.svelte
Normal file
30
src/lib/components/timeline/TimelineContent.svelte
Normal file
@ -0,0 +1,30 @@
|
||||
<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>
|
19
src/lib/components/timeline/TimelineDot.svelte
Normal file
19
src/lib/components/timeline/TimelineDot.svelte
Normal file
@ -0,0 +1,19 @@
|
||||
<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>
|
58
src/lib/components/timeline/TimelineItem.svelte
Normal file
58
src/lib/components/timeline/TimelineItem.svelte
Normal file
@ -0,0 +1,58 @@
|
||||
<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>
|
31
src/lib/components/timeline/TimelineOppositeContent.svelte
Normal file
31
src/lib/components/timeline/TimelineOppositeContent.svelte
Normal file
@ -0,0 +1,31 @@
|
||||
<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>
|
16
src/lib/components/timeline/TimelineSeparator.svelte
Normal file
16
src/lib/components/timeline/TimelineSeparator.svelte
Normal file
@ -0,0 +1,16 @@
|
||||
<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>
|
5
src/lib/data.ts
Normal file
5
src/lib/data.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export async function getJson(path: string) {
|
||||
let response = await fetch(path);
|
||||
let users = await response.json();
|
||||
return users;
|
||||
}
|
39
src/lib/index.ts
Normal file
39
src/lib/index.ts
Normal file
@ -0,0 +1,39 @@
|
||||
// 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 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
|
||||
};
|
28
src/lib/store.ts
Normal file
28
src/lib/store.ts
Normal file
@ -0,0 +1,28 @@
|
||||
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));
|
||||
};
|
26
src/lib/toast.ts
Normal file
26
src/lib/toast.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @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
Normal file
9
src/lib/types.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
type TimelinePosition = 'right' | 'left' | 'alternate';
|
||||
|
||||
type ParentPosition = 'right' | 'left';
|
||||
|
||||
type TimelineConfig = {
|
||||
rootPosition: TimelinePosition;
|
||||
};
|
||||
|
||||
export { TimelinePosition, ParentPosition, TimelineConfig };
|
125
src/main.svelte
Normal file
125
src/main.svelte
Normal file
@ -0,0 +1,125 @@
|
||||
<script lang="ts">
|
||||
import { getJson } from "$lib/data";
|
||||
import { Toast, ToastType } from "$lib/toast";
|
||||
import { addToast } from "$lib/store";
|
||||
|
||||
import Skills from './skills.svelte';
|
||||
|
||||
import Timeline from "./timeline.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>
|
||||
{: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>
|
||||
|
||||
<div class="container">
|
||||
<h1>Skills</h1>
|
||||
<hr />
|
||||
<div class="cards">
|
||||
<Skills skills="{info.skills}"></Skills>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h1>Experience</h1>
|
||||
<hr />
|
||||
<!-- https://github.com/K-Sato1995/svelte-vertical-timeline -->
|
||||
<Timeline timelineData="{info.timeline}"></Timeline>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{:catch}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1>Unable to load portfolio overview data</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: none;">{addToast(new Toast("Unable to load me.json", ToastType.Error, true, 3000))}</div>
|
||||
{/await}
|
60
src/routes/+layout.svelte
Normal file
60
src/routes/+layout.svelte
Normal file
@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import Toasts from "$lib/components/Toasts/Toasts.svelte";
|
||||
import ThemeSwitcher from "$lib/components/ThemeSwitcher.svelte";
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.main-container {
|
||||
margin-left: 10%;
|
||||
margin-right: 10%;
|
||||
padding-top: 2em;
|
||||
}
|
||||
|
||||
@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>
|
5
src/routes/+page.svelte
Normal file
5
src/routes/+page.svelte
Normal file
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
import Main from '../main.svelte';
|
||||
</script>
|
||||
|
||||
<Main></Main>
|
82
src/routes/contact/+page.svelte
Normal file
82
src/routes/contact/+page.svelte
Normal file
@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import Card from '$lib/components/Card.svelte';
|
||||
import { Toast, ToastType } from "$lib/toast";
|
||||
import { addToast } from "$lib/store";
|
||||
|
||||
import { page } from '$app/stores';
|
||||
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));
|
||||
}
|
||||
// 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));
|
||||
}
|
||||
</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>
|
||||
<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" />
|
||||
<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>
|
3
src/routes/repos/+page.svelte
Normal file
3
src/routes/repos/+page.svelte
Normal file
@ -0,0 +1,3 @@
|
||||
<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>
|
59
src/skills.svelte
Normal file
59
src/skills.svelte
Normal file
@ -0,0 +1,59 @@
|
||||
<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}
|
63
src/timeline.svelte
Normal file
63
src/timeline.svelte
Normal file
@ -0,0 +1,63 @@
|
||||
<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>
|
BIN
static/Luke Else - CV.pdf
Normal file
BIN
static/Luke Else - CV.pdf
Normal file
Binary file not shown.
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 738 B |
60
static/json/me.json
Normal file
60
static/json/me.json
Normal file
@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "Luke Else",
|
||||
"job_title": "Software Engineer",
|
||||
"profile_photo": "/profile.jpg",
|
||||
"skills" : [
|
||||
{
|
||||
"skill": "Rust",
|
||||
"logo": "devicon-rust-plain",
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"skill": "C++",
|
||||
"logo": "devicon-cplusplus-plain",
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"skill": "Git",
|
||||
"logo": "devicon-git-plain",
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"skill": "Docker",
|
||||
"logo": "devicon-docker-plain",
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"skill": "Svelte",
|
||||
"logo": "devicon-svelte-plain",
|
||||
"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": "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" : "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..."
|
||||
},
|
||||
{
|
||||
"duration" : "September 2020 - July 2022",
|
||||
"title" : "The Norton Knatchbull School (A-Levels)",
|
||||
"description" : "Computer Science (<b>A*</b>) <br /> Mathematics (<b>A</b>) <br /> Physics (<b>A</b>)"
|
||||
},
|
||||
{
|
||||
"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>)"
|
||||
}
|
||||
]
|
||||
}
|
BIN
static/profile.jpg
Normal file
BIN
static/profile.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 425 KiB |
18
static/themes/dark.css
Normal file
18
static/themes/dark.css
Normal file
@ -0,0 +1,18 @@
|
||||
: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;
|
||||
}
|
18
static/themes/light.css
Normal file
18
static/themes/light.css
Normal file
@ -0,0 +1,18 @@
|
||||
: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;
|
||||
}
|
19
svelte.config.js
Normal file
19
svelte.config.js
Normal file
@ -0,0 +1,19 @@
|
||||
// import adapter from '@sveltejs/adapter-auto';
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||
|
||||
/** @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()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
6
vite.config.ts
Normal file
6
vite.config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user