Compare commits

131 Commits

Author SHA1 Message Date
2c5d3c04d0 CHORE: WIP unit test
All checks were successful
Run Unit and Integration Tests / test (push) Successful in 59s
2025-06-07 18:26:15 +01:00
2a1cbb8be3 CHORE: #12 Corrected format of unit test
All checks were successful
Run Unit and Integration Tests / test (push) Successful in 1m36s
2025-05-31 18:32:09 +01:00
24720b782e FEAT: #12 Updated test workflow and removed unwanted branch from container build 2025-05-31 18:28:14 +01:00
0b299b9410 FEAT: #12 Added software tests
Some checks failed
Build and Push Development Docker Image / build-and-push (push) Failing after 3m3s
2025-05-31 18:26:21 +01:00
03b95a6c8c CHORE: Added additional skills and ehanced the way that links are displayed in raw html containers 2025-05-30 18:26:06 +01:00
63c84e1430 CHORE: Updated latest experience for the site 2025-05-30 16:43:00 +01:00
9028175ae4 FEAT: Added icons back into the skills section
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m19s
2025-05-27 22:00:53 +01:00
280c8e15ad CHORE: corrected indentation
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 38s
2025-05-27 00:41:59 +01:00
e081a0cb3e HOTFIX: Corrected display of timeline description
All checks were successful
Build and Push Latest Docker Image / build-and-push (push) Successful in 1m24s
Build and Push Development Docker Image / build-and-push (push) Successful in 26s
2025-05-27 00:35:18 +01:00
41c6964679 Merge pull request 'development' (#43) from development into main
All checks were successful
Build and Push Latest Docker Image / build-and-push (push) Successful in 25s
Reviewed-on: #43
2025-05-26 23:05:11 +00:00
f25c6ddd68 Merge pull request 'FEAT: #36 Added in Collapsible.svelte re-usable component.' (#42) from feature/collapsible-elements into development
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m19s
Reviewed-on: #42
2025-05-26 23:03:13 +00:00
9b49710556 FEAT: #36 Added in Collapsible.svelte re-usable component. 2025-05-27 00:01:54 +01:00
a0c3b27aab Merge pull request 'HOTFIX: Corrected Contact form card' (#41) from development into main
All checks were successful
Build and Push Latest Docker Image / build-and-push (push) Successful in 26s
Reviewed-on: #41
2025-05-26 20:14:48 +00:00
005dfb6929 HOTFIX: Corrected Contact form card
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m20s
2025-05-26 21:12:32 +01:00
b96c6d2caf Merge pull request 'development' (#40) from development into main
All checks were successful
Build and Push Latest Docker Image / build-and-push (push) Successful in 27s
Reviewed-on: #40
2025-05-24 21:45:00 +00:00
ef37e45281 Merge pull request 'chore/tailwind-conversion' (#38) from chore/tailwind-conversion into development
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m21s
Reviewed-on: #38
2025-05-24 21:36:15 +00:00
b55538345f Merge branch 'chore/tailwind-conversion' of ssh://git.luke-else.co.uk:222/luke-else/luke-else.co.uk into chore/tailwind-conversion 2025-05-24 22:34:10 +01:00
b586385d6d Merge pull request 'CHORE: Fixed loading icon and added it into REPOS page.' (#37) from chore/tailwind-conversion into development
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m24s
Reviewed-on: #37
2025-05-24 13:07:55 +00:00
2d3046da48 CHORE: Fixed loading icon and added it into REPOS page. 2025-05-24 14:02:52 +01:00
67f9844534 CHORE: Updated Gallery to use grid. Made card component mode modular. Added colour to the skills section of the page. 2025-05-24 11:46:33 +01:00
50b8845e6c Merge pull request 'development' (#33) from development into main
All checks were successful
Build and Push Latest Docker Image / build-and-push (push) Successful in 27s
Reviewed-on: #33
2025-05-23 21:37:35 +00:00
79f6e8e90b Merge pull request 'chore/tailwind-conversion' (#32) from chore/tailwind-conversion into development
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m36s
Reviewed-on: #32
2025-05-23 21:35:36 +00:00
25f3db52ec CHORE: Cleaning up code before merge 2025-05-23 22:32:58 +01:00
a46ac458dc FEAT: Added in git repos page 2025-05-23 22:26:42 +01:00
206c5665a2 FEAT: Added contact page 2025-05-23 20:41:32 +01:00
fd3c620cb9 FEAT: Added in timeline element 2025-05-23 20:10:37 +01:00
c52d185f76 FEAT: Started fleshing out content on the webpage 2025-05-23 18:13:58 +01:00
538d9593c2 Made components more re-usable. Added section component 2025-05-23 16:04:35 +01:00
24a7ebf02a CHORE: Starting to get to work with changing some of the old formatting over to tailwind config 2025-05-21 22:45:18 +01:00
fc642a4ecd Imported tailwind libraries, started to re-write main page 2025-05-12 00:43:28 +01:00
d9e8b4b56c Updated packages and fixed warnings pertaining to exported types
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m34s
2025-05-11 23:50:26 +01:00
bd689bdb44 Merge pull request 'development' (#27) from development into main
All checks were successful
Build and Push Latest Docker Image / build-and-push (push) Successful in 28s
Reviewed-on: #27
2025-04-27 21:21:09 +00:00
051cd42fdb CHORE: Cleaning up codebase
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m5s
Moved component content to be above inline style blocks
2025-04-27 22:02:33 +01:00
da5f47a841 #26 Re-Made contact form. More work required.]
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m24s
2025-04-27 19:33:46 +01:00
5fe7b83c47 #26 Updated key to support v2 of captcha
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m2s
2025-04-06 23:12:27 +01:00
931e4d2abe Merge branch 'development' of ssh://git.luke-else.co.uk:222/luke-else/luke-else.co.uk into development
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m25s
2025-04-06 23:00:53 +01:00
33ddd2add0 #26 Added re-captcha to the site 2025-04-06 23:00:42 +01:00
8cd763b9d0 Merge pull request 'development' (#25) from development into main
All checks were successful
Build and Push Latest Docker Image / build-and-push (push) Successful in 29s
Reviewed-on: #25
2025-03-12 15:15:39 +00:00
ce38e88885 #10 Added sliding card element for image stored in /assets/images/main.png
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m4s
2025-03-12 15:05:00 +00:00
e1160b3462 #23 Updated the colour of the rotating loading button
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m4s
2025-03-07 22:18:07 +00:00
7042b2d500 Merge pull request 'development' (#24) from development into main
All checks were successful
Build and Push Latest Docker Image / build-and-push (push) Successful in 27s
Reviewed-on: #24
2025-03-07 22:10:07 +00:00
5f1a1d4959 #23 Updated the colour of the send button
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m6s
2025-03-07 21:58:40 +00:00
da2f2bc380 #4 Added new vscode colour scheme option.
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m3s
2025-03-06 16:00:35 +00:00
c3055a6882 Added a series of nicely curated themes
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m4s
2025-03-06 15:58:14 +00:00
3557a6a6ad Added a nice new light pastel blue theme
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m4s
2025-03-06 15:41:06 +00:00
f34761d094 Updated pipeline to create a development build
All checks were successful
Build and Push Development Docker Image / build-and-push (push) Successful in 1m8s
2025-03-06 14:45:39 +00:00
f7e3acf384 #4 Changed light colour theme in favour of a cold blue theme 2025-03-06 14:24:24 +00:00
ccbaa41cab Merge pull request 'Merge Ackee Tracker Changes' (#22) from development into main
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 27s
Reviewed-on: #22
2025-03-05 21:11:56 +00:00
f33456bae3 Merge pull request '#20 Removed ignore own visits as this is the default. Added in detailed tracking for the site.' (#21) from chore/ackee-tracker-enhance into development
All checks were successful
Build and Push Docker Image / build-and-push (pull_request) Successful in 1m36s
Reviewed-on: #21
2025-03-05 21:11:09 +00:00
4cab417bdd #20 Removed ignore own visits as this is the default. Added in detailed tracking for the site. 2025-03-05 21:09:51 +00:00
cb1304aaeb Merge pull request 'Added Loading Spinner' (#19) from development into main
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 27s
Reviewed-on: #19
2025-02-15 14:48:54 +00:00
27488fe860 Added Loading Spinner
All checks were successful
Build and Push Docker Image / build-and-push (pull_request) Successful in 1m24s
2025-02-15 14:47:59 +00:00
b9c4ec540a Merge pull request 'development' (#18) from development into main
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 27s
Reviewed-on: #18
2025-02-09 20:06:19 +00:00
c3f0be36a3 Merge pull request 'feature/repos' (#17) from feature/repos into development
All checks were successful
Build and Push Docker Image / build-and-push (pull_request) Successful in 1m4s
Reviewed-on: #17
2025-02-09 20:05:36 +00:00
71dc20c0ca #5 Styled cards, changed content in me.json 2025-02-09 20:03:48 +00:00
9da13b76d3 #5 Repos are now displayed on the webpage - Additional stlying required. 2025-02-09 17:35:54 +00:00
712d7857db #5 Started on creating page to handle API requests to https://git.luke-else.co.uk 2025-02-06 10:55:26 +00:00
8ab101727e Removed unwanted action
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m6s
2025-02-06 09:44:51 +00:00
5fb67af755 Merge pull request '#7 Updated readme and added photos to the repo.' (#15) from development into main
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m4s
Reviewed-on: #15
2025-02-06 09:39:24 +00:00
15f30c09b1 #13 Reduced file size of profile image.
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m5s
2025-02-06 09:34:42 +00:00
e5c4243a1f #14 Changed font-size of contact page textarea
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m11s
2025-02-06 09:24:19 +00:00
47a43f2b0e #7 Updated readme and added photos to the repo.
All checks were successful
Build and Push Docker Image / build-and-push (pull_request) Successful in 1m3s
2025-02-05 15:03:09 +00:00
1a6c5194e5 Merge pull request 'Merge Development into main.' (#8) from development into main
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 27s
Reviewed-on: #8
2025-02-05 14:31:22 +00:00
aaab8f2c98 Added Sliding Card which has content slide over the top of the main information
All checks were successful
Build and Push Docker Image / build-and-push (pull_request) Successful in 1m21s
2025-02-05 14:23:22 +00:00
2170344c9b #6 Updated profile picture 2025-02-05 11:08:15 +00:00
45208d0ff9 Merge branch 'main' into development 2025-02-05 11:01:54 +00:00
099bf34c19 Updated Colour theme, reduced skills content
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m5s
2025-02-03 13:49:21 +00:00
93ca58f487 Added option to ignore own visits to ackee
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m6s
2025-02-03 11:06:32 +00:00
1087f95bc4 Corrected invalid JS code
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m5s
2025-02-03 10:11:47 +00:00
94b1e80358 Moved tracking script to correct location
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m5s
2025-02-03 09:57:16 +00:00
753e091a85 Added tracking to the site
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 41s
2025-02-03 09:51:26 +00:00
b5f6b3adf8 Added registry to build command
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m16s
2025-02-01 13:15:36 +00:00
7cefc86f08 Added container registry to push command
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 58s
2025-02-01 13:11:50 +00:00
dfd47c0d98 Corrected names in build and push commands
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 1m0s
2025-02-01 13:10:02 +00:00
e4da310ef2 Removed registry from build command
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
2025-02-01 13:08:46 +00:00
196e338e66 Added container registry and username to push and build commands
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 26s
2025-02-01 13:06:39 +00:00
8ccc8e2129 Specify container registry
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 1m17s
2025-02-01 13:02:59 +00:00
83361c4199 Updated name of workflows file
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
2025-02-01 13:01:21 +00:00
112e0f1a5c Created workflow to publish dockerfile 2025-02-01 12:54:06 +00:00
e843870afd Increased font-weight to make links more readable 2023-10-27 09:51:31 +01:00
74dc1196cd Adapted feedback regarding initial messages not showing for long enough. 2023-10-27 09:25:00 +01:00
306396cce1 Changed min width before content starts dissapearing on smalled devices 2023-10-26 22:02:42 +01:00
a2acc03f7c Updated dependencies to resolve vulnerability 2023-10-26 21:49:13 +01:00
6f0f5f1cf7 Added ability to switch between light and dark mode 2023-10-26 21:41:18 +01:00
278fc640ce Added pulsing effect to timeline elements that are still in progress 2023-10-26 13:19:46 +01:00
2e07de0e71 Adapted themings to allow for light and dark 2023-10-26 10:24:30 +01:00
e0ea53a9d3 Re-ordered experience in json file 2023-10-25 14:18:37 +01:00
632eed6810 Updated timeline data colouring to add contrast 2023-10-25 13:51:18 +01:00
dd4dfdea78 Added author and descriptiom to webpage 2023-10-25 13:04:38 +01:00
377f87ced3 Added meta description tag to website 2023-10-25 11:54:16 +01:00
d8d40ddc30 CHORE: Cleaned up components to make main.svelte easier to maintain. 2023-10-25 11:28:22 +01:00
ee2098e6e6 Added timeline component to website 2023-10-25 10:54:39 +01:00
d2066087ae Added actual content to be displayed on cards. May need corrections to be made. 2023-10-13 14:38:04 +01:00
d187ec70c3 #1 Added 'X' to popup cards, adjusted skills container and added heading 2023-10-04 22:07:53 +01:00
4a734b66f9 Updated flex basis on card to make sure that card don't end up super thin 2023-10-04 21:01:40 +01:00
cbdc81c4ce Completed contact form and improved styling on cards 2023-09-29 14:41:37 +01:00
7bd03be127 Created basis for contact page and updated CV 2023-09-28 21:30:41 +01:00
8d5319ac4a Moved cards out into seperate component to enable re-usability 2023-09-28 19:22:52 +01:00
26357e531d External links now open in a new window. 2023-09-28 07:15:13 +01:00
eac1534497 Updated Favicon 2023-09-27 20:11:16 +01:00
94de27084c Updated docker file to use production environment instead of dev 2023-09-27 19:25:07 +01:00
0a6ede8125 Created dockerfile 2023-09-26 23:26:35 +01:00
ebf9a21478 Created base template for contact and repo pages.
Fixed styling issues on toasts on small screens.
2023-09-26 23:15:42 +01:00
86652a4f09 Updated content, created modal and changed skills card to style nicer on smaller screens 2023-09-26 20:09:17 +01:00
3f171dea3c Removed mastery data, don't think it is a necessary item to include 2023-09-25 22:14:24 +01:00
6b0af2fb3e Created skill cards 2023-09-25 21:38:42 +01:00
1b33bd398d Moved Toast logic into own directory 2023-09-19 14:22:00 +01:00
f31f180687 Fixed toasts not working. 2023-09-18 13:02:55 +01:00
8b9a2ac8d5 Uploading toast code... incomplete 2023-09-18 10:17:02 +01:00
212103ab71 Updated personal content and prepared async fetch to be more expandable and include more content rather than a single div. 2023-09-06 20:42:07 +01:00
7d7012eec6 Made more reactive styling on main page and template 2023-09-05 22:53:56 +01:00
b2b56480b8 Created JS function to fetch static JSON. Now loads data Asynchronously :) 2023-09-05 19:03:59 +01:00
79795bc060 Added addional styling to smaller devices to reduce the padding of main content paragraphs 2023-09-04 22:58:24 +01:00
6961d9c34d Created underline on heading element 2023-09-04 22:51:27 +01:00
0797fd9eff Added json content display and updated styling on main card item. 2023-09-04 22:47:39 +01:00
d69078ff26 Created new main card and created a container for inner element of main page. 2023-08-31 22:27:21 +01:00
ba437de706 Created Navbar and added styling to links 2023-08-30 23:37:44 +01:00
065a6a2d17 Created SvelteKit Application as Base 2023-08-29 20:48:14 +01:00
3eb3b6845b Updated line numbers on flip card 2023-05-19 14:51:25 +01:00
71227d7d78 Updated flip card to use rust syntax 2023-05-19 14:45:28 +01:00
52c1abb0b7 Corrected Typo in metadata 2022-05-09 21:31:13 +01:00
de234ddf8c Added metadata to flipcard site 2022-05-09 21:24:04 +01:00
3769f3edc3 Updated flipcard content 2022-04-28 18:21:38 +01:00
87b2e808f7 Added age.js to allow for automatic age update 2022-04-28 18:18:49 +01:00
189ca5034b Updated main page to follow C# syntax 2022-04-28 18:13:31 +01:00
71405fd5a2 Added CV to assets for later reference 2022-04-25 22:35:55 +02:00
357f0b06f3 Allow card to flip both ways 2022-04-22 22:39:25 +01:00
30a5d53bc0 Tap to flip card - hopefully 2022-04-22 22:22:28 +01:00
bd183bf832 Added Mobile Hover to card 2022-04-22 21:24:37 +01:00
69cfbf1b44 Created Business Card Site 2022-04-22 20:32:38 +01:00
a75c919929 Clear Repo 2022-04-22 20:00:18 +01:00
64 changed files with 2684 additions and 3389 deletions

View File

@ -6,6 +6,7 @@ node_modules
.git
.gitattributes
.gitea
.eslintignore
.eslintrc.cjs

30
.gitea/workflows/dev.yaml Normal file
View 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

View File

@ -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

View 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

View File

@ -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" } }]
}

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
assets/images/main.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
assets/images/repos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

BIN
assets/images/skills.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

2088
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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

File diff suppressed because it is too large Load Diff

1
src/app.css Normal file
View File

@ -0,0 +1 @@
@import "tailwindcss"

View File

@ -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
View 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;
}
}

View File

@ -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>

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View 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>

View 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>

View File

@ -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>

View 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">&diams;</div>
{:else}
<div class="absolute top-0 left-[8px] text-green-400 w-4 h-4">&diam;</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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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 };

View File

@ -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
View 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());
}

View File

@ -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
View File

@ -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
View 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;
};
}

View File

@ -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}

View File

@ -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>

View File

@ -2,4 +2,6 @@
import Main from '../main.svelte';
</script>
<Main></Main>
<div>
<Main></Main>
</div>

View File

@ -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>

View File

@ -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}

View File

@ -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}

View File

@ -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>

View File

@ -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 UKs 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 />"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 425 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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
View 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
View 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();
}
});

View File

@ -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
//

View File

@ -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()],
});