Compare commits
	
		
			11 Commits
		
	
	
		
			bd689bdb44
			...
			50b8845e6c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 50b8845e6c | |||
| 79f6e8e90b | |||
| 
						
						
							
						
						25f3db52ec
	
				 | 
					
					
						|||
| 
						
						
							
						
						a46ac458dc
	
				 | 
					
					
						|||
| 
						
						
							
						
						206c5665a2
	
				 | 
					
					
						|||
| 
						
						
							
						
						fd3c620cb9
	
				 | 
					
					
						|||
| 
						
						
							
						
						c52d185f76
	
				 | 
					
					
						|||
| 
						
						
							
						
						538d9593c2
	
				 | 
					
					
						|||
| 
						
						
							
						
						24a7ebf02a
	
				 | 
					
					
						|||
| 
						
						
							
						
						fc642a4ecd
	
				 | 
					
					
						|||
| 
						
						
							
						
						d9e8b4b56c
	
				 | 
					
					
						
@@ -1,5 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
	"useTabs": true,
 | 
			
		||||
	"useTabs": false,
 | 
			
		||||
	"tabWidth": 4,
 | 
			
		||||
	"singleQuote": true,
 | 
			
		||||
	"trailingComma": "none",
 | 
			
		||||
	"printWidth": 100,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1885
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1885
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										28
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								package.json
									
									
									
									
									
								
							@@ -14,16 +14,22 @@
 | 
			
		||||
	},
 | 
			
		||||
	"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"
 | 
			
		||||
	},
 | 
			
		||||
	"type": "module"
 | 
			
		||||
	"type": "module",
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"@tailwindcss/vite": "^4.1.6",
 | 
			
		||||
		"svelte-toasts": "^1.1.2",
 | 
			
		||||
		"tailwindcss": "^4.1.6"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1532
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1532
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1
									
								
								src/app.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/app.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
@import "tailwindcss"
 | 
			
		||||
							
								
								
									
										138
									
								
								src/app.html
									
									
									
									
									
								
							
							
						
						
									
										138
									
								
								src/app.html
									
									
									
									
									
								
							@@ -1,132 +1,30 @@
 | 
			
		||||
<!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." />
 | 
			
		||||
        <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">
 | 
			
		||||
        <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>
 | 
			
		||||
        <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;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			hr {
 | 
			
		||||
				border: .12em solid var(--accent);
 | 
			
		||||
  				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);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			.container {
 | 
			
		||||
				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;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@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>
 | 
			
		||||
        <style></style>
 | 
			
		||||
    </head>
 | 
			
		||||
 | 
			
		||||
    <body data-sveltekit-preload-data="hover">
 | 
			
		||||
        <div style="display: contents">%sveltekit.body%</div>
 | 
			
		||||
    </body>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,69 +1,31 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import { createEventDispatcher } from 'svelte';
 | 
			
		||||
    export let headerLeft: string = "";
 | 
			
		||||
    export let headerRight: string = "";
 | 
			
		||||
    export let headerColour: string = "text-red-500";
 | 
			
		||||
    export let footer: string = "";
 | 
			
		||||
 | 
			
		||||
    const dispatch = createEventDispatcher();
 | 
			
		||||
 | 
			
		||||
    function onClick() {
 | 
			
		||||
        dispatch('click');
 | 
			
		||||
    }
 | 
			
		||||
    // Allows additional styling to be applied to the Card component's outer wrapping
 | 
			
		||||
    export let containerStyle: string = "";
 | 
			
		||||
</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;
 | 
			
		||||
        box-shadow: .25em .25em .5em var(--hover);
 | 
			
		||||
    }
 | 
			
		||||
<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="{headerColour} flex flex-row justify-between items-center mb-4">
 | 
			
		||||
            <p class="text-2xl md:text-3xl font-bold truncate">{headerLeft}</p>
 | 
			
		||||
            
 | 
			
		||||
    .card:hover, .card:focus-within {
 | 
			
		||||
        box-shadow: .5em .5em .5em var(--hover);
 | 
			
		||||
        transform: scale(1.02);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .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: center;
 | 
			
		||||
        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>
 | 
			
		||||
            {#if headerRight}
 | 
			
		||||
                <p class="max-md:hidden text-xl md:text-2xl truncate">{@html headerRight}</p>
 | 
			
		||||
            {/if}
 | 
			
		||||
        </div>
 | 
			
		||||
    <hr />
 | 
			
		||||
    <div class="card-content">
 | 
			
		||||
        <slot name="content"></slot>
 | 
			
		||||
        <hr class="mb-4 border-1" />
 | 
			
		||||
        <div class="flex-1 flex flex-col justify-center p-5">
 | 
			
		||||
            <slot />
 | 
			
		||||
        </div>
 | 
			
		||||
    <hr class="not-required"/>
 | 
			
		||||
    <div class="card-footer">
 | 
			
		||||
        <slot name="footer"></slot>
 | 
			
		||||
        {#if footer}
 | 
			
		||||
            <hr class="my-4 border-1" />
 | 
			
		||||
            <div class="mt-2 text-base opacity-90">
 | 
			
		||||
                {@html footer}
 | 
			
		||||
            </div>
 | 
			
		||||
        {/if}
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,102 +0,0 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import { createEventDispatcher } from 'svelte';
 | 
			
		||||
 | 
			
		||||
    const dispatch = createEventDispatcher();
 | 
			
		||||
 | 
			
		||||
    function onClick() {
 | 
			
		||||
        dispatch('click');
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
    .sliding-card {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        justify-content: space-between;
 | 
			
		||||
        flex-wrap: wrap;
 | 
			
		||||
        flex: 2 1 15em;
 | 
			
		||||
        padding: 0.5em 2.5em 2em 2.5em;
 | 
			
		||||
        background: var(--bg-secondary);
 | 
			
		||||
        border-radius: 0.5em;
 | 
			
		||||
        scroll-snap-align: start;
 | 
			
		||||
        transition: all 0.3s ease-in-out;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
        position: relative;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .sliding-card:hover {
 | 
			
		||||
        box-shadow: .5em .5em .5em var(--hover);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .sliding-card .sliding-card-header :global(div) {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        justify-content: space-between;
 | 
			
		||||
        margin-bottom: 0em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .content-wrapper {
 | 
			
		||||
        position: relative;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        overflow: hidden; /* Ensure smooth sliding */
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .sliding-card-content {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        justify-content: center;
 | 
			
		||||
        max-width: 100%;
 | 
			
		||||
        flex-grow: 1;
 | 
			
		||||
        z-index: 1; /* Keep it below the sliding content */
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .sliding-content {
 | 
			
		||||
        position: absolute; /* Now it sits on top */
 | 
			
		||||
        top: 0;
 | 
			
		||||
        left: 0;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        height: 100%; /* Cover entire content */
 | 
			
		||||
        background: var(--bg-secondary);
 | 
			
		||||
        transform: translateY(100%); /* Start hidden */
 | 
			
		||||
        transition: transform 0.3s ease-in-out;
 | 
			
		||||
        z-index: 2; /* Now above main content */
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .sliding-card:hover .sliding-content {
 | 
			
		||||
        transform: translateY(0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .sliding-card .sliding-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="sliding-card" on:click={onClick}>
 | 
			
		||||
    <div class="sliding-card-header">
 | 
			
		||||
        <slot name="header"></slot>
 | 
			
		||||
    </div>
 | 
			
		||||
    <hr />
 | 
			
		||||
    
 | 
			
		||||
    <!-- Wrapper to stack sliding-card-content and sliding-content -->
 | 
			
		||||
    <div class="content-wrapper">
 | 
			
		||||
        <div class="sliding-card-content">
 | 
			
		||||
            <slot name="content"></slot>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="sliding-content">
 | 
			
		||||
            <slot name="sliding-content"></slot>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <hr class="not-required"/>
 | 
			
		||||
    <div class="sliding-card-footer">
 | 
			
		||||
        <slot name="footer"></slot>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										7
									
								
								src/lib/components/FlexGallery.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/lib/components/FlexGallery.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<!-- FlexGallery.svelte -->
 | 
			
		||||
<div class="flex flex-wrap gap-10 w-full">
 | 
			
		||||
    <slot />
 | 
			
		||||
</div>
 | 
			
		||||
@@ -67,7 +67,7 @@
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<div class="loader">
 | 
			
		||||
    <div class="inner one" />
 | 
			
		||||
    <div class="inner two" />
 | 
			
		||||
    <div class="inner three" />
 | 
			
		||||
    <div class="inner one"></div>
 | 
			
		||||
    <div class="inner two"></div>
 | 
			
		||||
    <div class="inner three"></div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,82 +0,0 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	export let showModal: boolean;
 | 
			
		||||
 | 
			
		||||
	let dialog: HTMLDialogElement;
 | 
			
		||||
 | 
			
		||||
	$: if (dialog && showModal) dialog.showModal();
 | 
			
		||||
 | 
			
		||||
	import CloseIcon from "./Toasts/CloseIcon.svelte";
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
 | 
			
		||||
<dialog
 | 
			
		||||
	bind:this={dialog}
 | 
			
		||||
	on:close={() => (showModal = false)}
 | 
			
		||||
	on:click|self={() => dialog.close()}
 | 
			
		||||
>
 | 
			
		||||
	<!-- svelte-ignore a11y-no-static-element-interactions -->
 | 
			
		||||
	<div on:click|stopPropagation>
 | 
			
		||||
		<slot name="header" />
 | 
			
		||||
		<hr />
 | 
			
		||||
		<slot />
 | 
			
		||||
		<hr />
 | 
			
		||||
	</div>
 | 
			
		||||
	<button class="close" on:click={() => dialog.close()}>
 | 
			
		||||
		<CloseIcon width="0.8em" />
 | 
			
		||||
	</button>
 | 
			
		||||
</dialog>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	dialog {
 | 
			
		||||
		max-width: 70%;
 | 
			
		||||
		border-radius: 0.2em;
 | 
			
		||||
		border: none;
 | 
			
		||||
		padding: 0em 2em 2em 2em;
 | 
			
		||||
        border-left: 2em;
 | 
			
		||||
        border-right: 2em;
 | 
			
		||||
        background: var(--bg);
 | 
			
		||||
        color: var(--fg);
 | 
			
		||||
        box-shadow: .5em .5em .5em var(--header);
 | 
			
		||||
	}
 | 
			
		||||
	dialog::backdrop {
 | 
			
		||||
		background: rgba(0, 0, 0, 0.7);
 | 
			
		||||
	}
 | 
			
		||||
	dialog > div {
 | 
			
		||||
		padding: 1em;
 | 
			
		||||
	}
 | 
			
		||||
	dialog[open] {
 | 
			
		||||
		animation: zoom 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
 | 
			
		||||
	}
 | 
			
		||||
	@keyframes zoom {
 | 
			
		||||
		from {
 | 
			
		||||
			transform: scale(0.95);
 | 
			
		||||
		}
 | 
			
		||||
		to {
 | 
			
		||||
			transform: scale(1);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	dialog[open]::backdrop {
 | 
			
		||||
		animation: fade 0.3s ease-out;
 | 
			
		||||
	}
 | 
			
		||||
	@keyframes fade {
 | 
			
		||||
		from {
 | 
			
		||||
			opacity: 0;
 | 
			
		||||
		}
 | 
			
		||||
		to {
 | 
			
		||||
			opacity: 1;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.close {
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		top: 1em;
 | 
			
		||||
		right: 1.5em;
 | 
			
		||||
		color: var(--fg);
 | 
			
		||||
		background: transparent;
 | 
			
		||||
		border: 0 none;
 | 
			
		||||
		padding: 0;
 | 
			
		||||
		margin: 0 0 0 auto;
 | 
			
		||||
		line-height: 1;
 | 
			
		||||
		font-size: 1.2em;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										26
									
								
								src/lib/components/Section.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/lib/components/Section.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    export let label: string = "";
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div id={label} class="relative flex flex-row w-full min-h-[300px] mt-5 mb-25">
 | 
			
		||||
    <!-- Sticky/Sliding Label -->
 | 
			
		||||
    <div class="hidden md:flex flex-col items-center mr-6">
 | 
			
		||||
        <div class="sticky top-24 left-0 z-10">
 | 
			
		||||
            <span class="text-2xl font-bold text-blue-400 tracking-widest"
 | 
			
		||||
                  style="writing-mode: vertical-rl; text-orientation: mixed;">
 | 
			
		||||
                {label}
 | 
			
		||||
            </span>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- Main Content -->
 | 
			
		||||
    <div class="flex-1 flex flex-col">
 | 
			
		||||
        <!-- Label for mobile -->
 | 
			
		||||
        <div class="md:hidden mb-2">
 | 
			
		||||
            <span class="text-2xl font-bold text-blue-400">{label}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <hr class="border-blue-400 mb-6" />
 | 
			
		||||
        <div>
 | 
			
		||||
            <slot />
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										16
									
								
								src/lib/components/SkillProgress.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/lib/components/SkillProgress.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    export let value: number = 0; // 0 to 100
 | 
			
		||||
</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="bg-orange-400 h-5 rounded-full transition-all duration-500"
 | 
			
		||||
            style="width: {value}%"
 | 
			
		||||
        ></div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,95 +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(--accent);
 | 
			
		||||
        top: 0;
 | 
			
		||||
        left: 0;
 | 
			
		||||
        right: 0;
 | 
			
		||||
        bottom: 0;
 | 
			
		||||
        background-color: var(--accent);
 | 
			
		||||
        -webkit-transition: .4s;
 | 
			
		||||
        transition: .4s;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .slider:before {
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        content: "";
 | 
			
		||||
        height: 80%;
 | 
			
		||||
        width: 45%;
 | 
			
		||||
        left: 4px;
 | 
			
		||||
        bottom: 4px;
 | 
			
		||||
        background-color: var(--bg);
 | 
			
		||||
        -webkit-transition: .4s;
 | 
			
		||||
        transition: .4s;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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>
 | 
			
		||||
							
								
								
									
										41
									
								
								src/lib/components/Timeline.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/lib/components/Timeline.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    export let timelineData: Array<{
 | 
			
		||||
        title: string;
 | 
			
		||||
        description: string;
 | 
			
		||||
        duration: string;
 | 
			
		||||
    }>;
 | 
			
		||||
 | 
			
		||||
    // Track open/closed state for each entry
 | 
			
		||||
    let openStates = timelineData.map(() => false);
 | 
			
		||||
 | 
			
		||||
    function toggle(index: number) {
 | 
			
		||||
        openStates[index] = !openStates[index];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toggle(0); // Open the first entry by default
 | 
			
		||||
</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 openStates[i]}
 | 
			
		||||
        <div class="absolute top-0 left-[8px] text-green-400 w-4 h-4">♦</div>
 | 
			
		||||
        {:else}
 | 
			
		||||
          <div class="absolute top-0 left-[8px] text-green-400 w-4 h-4">⋄</div>
 | 
			
		||||
        {/if}
 | 
			
		||||
        <p class="text-sm opacity-70">{entry.duration}</p>
 | 
			
		||||
        <button
 | 
			
		||||
          class="text-2lg font-semibold text-red-400 mt-1 focus:outline-none hover:underline transition"
 | 
			
		||||
          on:click={() => toggle(i)}
 | 
			
		||||
          aria-expanded={openStates[i]}
 | 
			
		||||
        >
 | 
			
		||||
        <h3 class="text-2lg font-semibold text-red-400 mt-1">{entry.title}</h3>
 | 
			
		||||
        </button>
 | 
			
		||||
        {#if openStates[i]}
 | 
			
		||||
          <p class="mt-2 whitespace-pre-line transition-all duration-300">{@html entry.description}</p>
 | 
			
		||||
        {/if}
 | 
			
		||||
      </div>
 | 
			
		||||
    {/each}
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
<script>
 | 
			
		||||
	export let width = "1em"
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<svg
 | 
			
		||||
  width={width}
 | 
			
		||||
	style="text-align: center; display: inline-block;"
 | 
			
		||||
  aria-hidden="true"
 | 
			
		||||
  focusable="false"
 | 
			
		||||
  role="img"
 | 
			
		||||
  xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
  viewBox="0 0 352 512"
 | 
			
		||||
>
 | 
			
		||||
  <path
 | 
			
		||||
    fill="currentColor"
 | 
			
		||||
    d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"
 | 
			
		||||
  />
 | 
			
		||||
</svg>
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
<script>
 | 
			
		||||
	export let width = "1em"
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<svg
 | 
			
		||||
  width={width}
 | 
			
		||||
	style="text-align: center; display: inline-block;"
 | 
			
		||||
  aria-hidden="true"
 | 
			
		||||
  focusable="false"
 | 
			
		||||
  role="img"
 | 
			
		||||
  xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
  viewBox="0 0 512 512"
 | 
			
		||||
>
 | 
			
		||||
  <path
 | 
			
		||||
    fill="currentColor"
 | 
			
		||||
    d="M256 40c118.621 0 216 96.075 216 216 0 119.291-96.61 216-216 216-119.244 0-216-96.562-216-216 0-119.203 96.602-216 216-216m0-32C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm-11.49 120h22.979c6.823 0 12.274 5.682 11.99 12.5l-7 168c-.268 6.428-5.556 11.5-11.99 11.5h-8.979c-6.433 0-11.722-5.073-11.99-11.5l-7-168c-.283-6.818 5.167-12.5 11.99-12.5zM256 340c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28z"
 | 
			
		||||
    class=""
 | 
			
		||||
  ></path>
 | 
			
		||||
</svg>
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
<script>
 | 
			
		||||
	export let width = "1em"
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<svg
 | 
			
		||||
  width={width}
 | 
			
		||||
	style="text-align: center; display: inline-block;"
 | 
			
		||||
  aria-hidden="true"
 | 
			
		||||
  focusable="false"
 | 
			
		||||
  role="img"
 | 
			
		||||
  xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
  viewBox="0 0 512 512"
 | 
			
		||||
>
 | 
			
		||||
  <path
 | 
			
		||||
    fill="currentColor"
 | 
			
		||||
    d="M256 40c118.621 0 216 96.075 216 216 0 119.291-96.61 216-216 216-119.244 0-216-96.562-216-216 0-119.203 96.602-216 216-216m0-32C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm-36 344h12V232h-12c-6.627 0-12-5.373-12-12v-8c0-6.627 5.373-12 12-12h48c6.627 0 12 5.373 12 12v140h12c6.627 0 12 5.373 12 12v8c0 6.627-5.373 12-12 12h-72c-6.627 0-12-5.373-12-12v-8c0-6.627 5.373-12 12-12zm36-240c-17.673 0-32 14.327-32 32s14.327 32 32 32 32-14.327 32-32-14.327-32-32-32z"
 | 
			
		||||
  />
 | 
			
		||||
</svg>
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
<script>
 | 
			
		||||
	export let width = "1em"
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<svg
 | 
			
		||||
  width={width}
 | 
			
		||||
	style="text-align: center; display: inline-block;"
 | 
			
		||||
  aria-hidden="true"
 | 
			
		||||
  focusable="false"
 | 
			
		||||
  role="img"
 | 
			
		||||
  xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
  viewBox="0 0 512 512"
 | 
			
		||||
>
 | 
			
		||||
  <path
 | 
			
		||||
    fill="currentColor"
 | 
			
		||||
    d="M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 464c-118.664 0-216-96.055-216-216 0-118.663 96.055-216 216-216 118.664 0 216 96.055 216 216 0 118.663-96.055 216-216 216zm141.63-274.961L217.15 376.071c-4.705 4.667-12.303 4.637-16.97-.068l-85.878-86.572c-4.667-4.705-4.637-12.303.068-16.97l8.52-8.451c4.705-4.667 12.303-4.637 16.97.068l68.976 69.533 163.441-162.13c4.705-4.667 12.303-4.637 16.97.068l8.451 8.52c4.668 4.705 4.637 12.303-.068 16.97z"
 | 
			
		||||
  />
 | 
			
		||||
</svg>
 | 
			
		||||
@@ -1,67 +0,0 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import { createEventDispatcher } from "svelte";
 | 
			
		||||
  import { fade } from "svelte/transition";
 | 
			
		||||
  import { Toast, ToastType } from "$lib/toast";
 | 
			
		||||
  import SuccessIcon from "./SuccessIcon.svelte";
 | 
			
		||||
  import ErrorIcon from "./ErrorIcon.svelte";
 | 
			
		||||
  import InfoIcon from "./InfoIcon.svelte";
 | 
			
		||||
  import CloseIcon from "./CloseIcon.svelte";
 | 
			
		||||
 | 
			
		||||
  const dispatch = createEventDispatcher();
 | 
			
		||||
 | 
			
		||||
  export let toastData: Toast;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<article class={toastData.type.toString().toLowerCase()} role="alert" transition:fade>
 | 
			
		||||
  {#if toastData.type === ToastType.Success}
 | 
			
		||||
    <SuccessIcon width="1.1em" />
 | 
			
		||||
  {:else if toastData.type === ToastType.Error}
 | 
			
		||||
    <ErrorIcon width="1.1em" />
 | 
			
		||||
  {:else}
 | 
			
		||||
    <InfoIcon width="1.1em" />
 | 
			
		||||
  {/if}
 | 
			
		||||
 | 
			
		||||
  <div class="text">
 | 
			
		||||
    {toastData.text}
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  {#if toastData.dismissable}
 | 
			
		||||
    <button class="close" on:click={() => dispatch("dismiss")}>
 | 
			
		||||
      <CloseIcon width="0.8em" />
 | 
			
		||||
    </button>
 | 
			
		||||
  {/if}
 | 
			
		||||
</article>
 | 
			
		||||
 | 
			
		||||
<style lang="postcss">
 | 
			
		||||
  article {
 | 
			
		||||
    color: white;
 | 
			
		||||
    padding: 0.75rem 1.5rem;
 | 
			
		||||
    border-radius: 0.2rem;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    margin: 0 auto 0.5rem auto;
 | 
			
		||||
    width: 20rem;
 | 
			
		||||
    max-width: 80%;
 | 
			
		||||
  }
 | 
			
		||||
  .error {
 | 
			
		||||
    background: var(--red);
 | 
			
		||||
  }
 | 
			
		||||
  .success {
 | 
			
		||||
    background: var(--green);
 | 
			
		||||
  }
 | 
			
		||||
  .info {
 | 
			
		||||
    background: var(--blue);
 | 
			
		||||
  }
 | 
			
		||||
  .text {
 | 
			
		||||
    margin-left: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
  .close {
 | 
			
		||||
    color: white;
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    border: 0 none;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    margin: 0 0 0 auto;
 | 
			
		||||
    line-height: 1;
 | 
			
		||||
    font-size: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import Toast from "./Toast.svelte";
 | 
			
		||||
 | 
			
		||||
  import { dismissToast, toasts } from "$lib/stores";
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if $toasts}
 | 
			
		||||
  <section>
 | 
			
		||||
    {#each $toasts as toast (toast.id)}
 | 
			
		||||
      <Toast toastData = {toast} on:dismiss={() => dismissToast(toast.id)} />
 | 
			
		||||
    {/each}
 | 
			
		||||
  </section>
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
<style lang="postcss">
 | 
			
		||||
  section {
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    margin-top: 1rem;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    z-index: 10;
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,21 +0,0 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { setContext } from 'svelte';
 | 
			
		||||
	import type { TimelinePosition, TimelineConfig } from '../types';
 | 
			
		||||
	export let position: TimelinePosition = 'right';
 | 
			
		||||
	export let style: string = null;
 | 
			
		||||
 | 
			
		||||
	setContext<TimelineConfig>('TimelineConfig', { rootPosition: position });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<ul class="timeline" {style}>
 | 
			
		||||
	<slot />
 | 
			
		||||
</ul>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.timeline {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		padding: 6px 16px;
 | 
			
		||||
		flex-grow: 1;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	export let style: string = null;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<span class="timeline-connector" {style} />
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.timeline-connector {
 | 
			
		||||
		width: 2px;
 | 
			
		||||
		background-color: #bdbdbd;
 | 
			
		||||
		flex-grow: 1;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,30 +0,0 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { getContext } from 'svelte';
 | 
			
		||||
	import type { TimelineConfig, TimelinePosition } from '../types';
 | 
			
		||||
	export let style: string = null;
 | 
			
		||||
 | 
			
		||||
	const config = getContext<TimelineConfig>('TimelineConfig');
 | 
			
		||||
	const parentPosition = getContext<TimelinePosition>('ParentPosition');
 | 
			
		||||
 | 
			
		||||
	const itemPosition = parentPosition ? parentPosition : config.rootPosition;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class={`timeline-content ${itemPosition}`} {style}>
 | 
			
		||||
	<slot />
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.timeline-content {
 | 
			
		||||
		margin: 0;
 | 
			
		||||
		flex: 1;
 | 
			
		||||
		margin: 6px 16px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.left {
 | 
			
		||||
		text-align: right;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.right {
 | 
			
		||||
		text-align: left;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	export let style: string = null;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<span class="timeline-dot" {style}>
 | 
			
		||||
	<slot />
 | 
			
		||||
</span>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.timeline-dot {
 | 
			
		||||
		background-color: #121212;
 | 
			
		||||
		border: solid 2px #121212;
 | 
			
		||||
		display: flex;
 | 
			
		||||
		align-self: baseline;
 | 
			
		||||
		padding: 4px;
 | 
			
		||||
		border-radius: 50%;
 | 
			
		||||
		margin: 11.5px 0;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { getContext, setContext } from 'svelte';
 | 
			
		||||
	import type { TimelinePosition, ParentPosition, TimelineConfig } from '../types';
 | 
			
		||||
 | 
			
		||||
	export let position: ParentPosition | null = null;
 | 
			
		||||
	export let style: string = null;
 | 
			
		||||
 | 
			
		||||
	const config = getContext<TimelineConfig>('TimelineConfig');
 | 
			
		||||
	const itemPosition = position ? position : config.rootPosition;
 | 
			
		||||
	setContext<TimelinePosition>('ParentPosition', itemPosition);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<li class={`timeline-item ${itemPosition}`} {style}>
 | 
			
		||||
	{#if !$$slots['opposite-content']}
 | 
			
		||||
		<div class="opposite-block" />
 | 
			
		||||
	{:else}
 | 
			
		||||
		<slot name="opposite-content" />
 | 
			
		||||
	{/if}
 | 
			
		||||
	<slot />
 | 
			
		||||
</li>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	:global(.alternate:nth-of-type(even) > .timeline-content) {
 | 
			
		||||
		text-align: right;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	:global(.alternate:nth-of-type(odd) > .timeline-opposite-content) {
 | 
			
		||||
		text-align: right;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.opposite-block {
 | 
			
		||||
		flex: 1;
 | 
			
		||||
		margin: 6px 16px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.timeline-item {
 | 
			
		||||
		list-style: none;
 | 
			
		||||
		display: flex;
 | 
			
		||||
		position: relative;
 | 
			
		||||
		min-height: 70px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.left {
 | 
			
		||||
		flex-direction: row-reverse;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.right {
 | 
			
		||||
		flex-direction: row;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.alternate:nth-of-type(even) {
 | 
			
		||||
		flex-direction: row-reverse;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.alternate:nth-of-type(odd) {
 | 
			
		||||
		flex-direction: row;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,31 +0,0 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { getContext } from 'svelte';
 | 
			
		||||
	import type { TimelineConfig, TimelinePosition } from '../types';
 | 
			
		||||
	export let style: string = null;
 | 
			
		||||
 | 
			
		||||
	const config = getContext<TimelineConfig>('TimelineConfig');
 | 
			
		||||
	const parentPosition = getContext<TimelinePosition>('ParentPosition');
 | 
			
		||||
 | 
			
		||||
	const itemPosition = parentPosition ? parentPosition : config.rootPosition;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class={`timeline-opposite-content ${itemPosition}`} {style}>
 | 
			
		||||
	<slot />
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.timeline-opposite-content {
 | 
			
		||||
		margin: 0;
 | 
			
		||||
		flex: 1;
 | 
			
		||||
		margin-right: auto;
 | 
			
		||||
		margin: 6px 16px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.left {
 | 
			
		||||
		text-align: left;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.right {
 | 
			
		||||
		text-align: right;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,16 +0,0 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	export let style: string = null;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="timeline-separator" {style}>
 | 
			
		||||
	<slot />
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.timeline-separator {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		flex: 0;
 | 
			
		||||
		align-items: center;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,41 +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 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/Cards/Card.svelte';
 | 
			
		||||
import SlidingCard from '$lib/components/Cards/SlidingCard.svelte';
 | 
			
		||||
import Modal from '$lib/components/Modal.svelte';
 | 
			
		||||
import FlexGallery from './components/FlexGallery.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';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
	Timeline,
 | 
			
		||||
	TimelineItem,
 | 
			
		||||
	TimelineSeparator,
 | 
			
		||||
	TimelineDot,
 | 
			
		||||
	TimelineConnector,
 | 
			
		||||
	TimelineContent,
 | 
			
		||||
	TimelineOppositeContent,
 | 
			
		||||
 | 
			
		||||
    Toasts,
 | 
			
		||||
    Toast,
 | 
			
		||||
    CloseIcon,
 | 
			
		||||
    InfoIcon,
 | 
			
		||||
    SuccessIcon,
 | 
			
		||||
    ErrorIcon,
 | 
			
		||||
 | 
			
		||||
    Card,
 | 
			
		||||
    SlidingCard,
 | 
			
		||||
    Modal
 | 
			
		||||
};
 | 
			
		||||
export { Card, FlexGallery, Loading, Section, SkillProgress, Timeline };
 | 
			
		||||
 
 | 
			
		||||
@@ -1,38 +1,7 @@
 | 
			
		||||
import { ToastType, type Toast } from "$lib/toast";
 | 
			
		||||
import { writable, type Writable } from "svelte/store";
 | 
			
		||||
import { writable } from "svelte/store";
 | 
			
		||||
import type { GitRepo } from "./types";
 | 
			
		||||
import { fetchRepos } from "./api/git";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
////////////////////////////////////////
 | 
			
		||||
// Toast Stores
 | 
			
		||||
////////////////////////////////////////
 | 
			
		||||
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));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
////////////////////////////////////////
 | 
			
		||||
// Git Repo Stores
 | 
			
		||||
////////////////////////////////////////
 | 
			
		||||
 
 | 
			
		||||
@@ -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
									
									
								
							
							
						
						
									
										9
									
								
								src/lib/types.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -1,9 +0,0 @@
 | 
			
		||||
type TimelinePosition = 'right' | 'left' | 'alternate';
 | 
			
		||||
 | 
			
		||||
type ParentPosition = 'right' | 'left';
 | 
			
		||||
 | 
			
		||||
type TimelineConfig = {
 | 
			
		||||
	rootPosition: TimelinePosition;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { TimelinePosition, ParentPosition, TimelineConfig };
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,11 @@
 | 
			
		||||
export type TimelinePosition = 'right' | 'left' | 'alternate';
 | 
			
		||||
 | 
			
		||||
export type ParentPosition = 'right' | 'left';
 | 
			
		||||
 | 
			
		||||
export type TimelineConfig = {
 | 
			
		||||
    rootPosition: TimelinePosition;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface GitRepo {
 | 
			
		||||
    name: string;
 | 
			
		||||
    description: string;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										143
									
								
								src/main.svelte
									
									
									
									
									
								
							
							
						
						
									
										143
									
								
								src/main.svelte
									
									
									
									
									
								
							@@ -1,107 +1,66 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import { getJson } from "$lib/data";
 | 
			
		||||
	import { Toast, ToastType } from "$lib/toast";
 | 
			
		||||
    import { addToast } from "$lib/stores";
 | 
			
		||||
    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 Loading from '$lib/components/Loading.svelte';
 | 
			
		||||
    import Section from '$lib/components/Section.svelte';
 | 
			
		||||
    import Card from '$lib/components/Cards/Card.svelte';
 | 
			
		||||
    import FlexGallery from "$lib/components/FlexGallery.svelte";
 | 
			
		||||
    import SkillProgress from "$lib/components/SkillProgress.svelte";
 | 
			
		||||
    import Timeline from '$lib/components/Timeline.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#await getJson('/json/me.json')}
 | 
			
		||||
    <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>
 | 
			
		||||
    <!-- Main Card -->
 | 
			
		||||
    <Section label="[Experience]">
 | 
			
		||||
        <Card headerLeft={info.name} headerRight={info.job_title} footer={info.location}>
 | 
			
		||||
            <div 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>{@html info.about}</p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </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]">
 | 
			
		||||
        <FlexGallery>
 | 
			
		||||
            {#each info.skills as skill}
 | 
			
		||||
                <Card headerLeft={skill.name} footer={skill.link} containerStyle="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">
 | 
			
		||||
                    {skill.about}
 | 
			
		||||
 | 
			
		||||
    <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>
 | 
			
		||||
                    <SkillProgress value={skill.competency} />
 | 
			
		||||
                </Card>
 | 
			
		||||
            {/each}
 | 
			
		||||
        </FlexGallery>
 | 
			
		||||
    </Section>
 | 
			
		||||
 | 
			
		||||
    <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 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>
 | 
			
		||||
    <div style="display: none;">{addToast(new Toast("Unable to load me.json", ToastType.Error, true, 3000))}</div>
 | 
			
		||||
{/await}
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
    .main-card {
 | 
			
		||||
        background-color: var(--bg-secondary);
 | 
			
		||||
        border-radius: 1em;
 | 
			
		||||
        padding: .2em 2em 2em 2em;
 | 
			
		||||
        box-shadow: .5em .5em .5em var(--glow);
 | 
			
		||||
        margin-bottom: 4em;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .flex-container {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .profile {
 | 
			
		||||
        border-radius: 100%;
 | 
			
		||||
        height: 8em;
 | 
			
		||||
        width: 8em;
 | 
			
		||||
        padding: 1em 1em 1em 1em;
 | 
			
		||||
        border: .25em solid var(--accent);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
<nav>
 | 
			
		||||
    <a href = "/">//Profile</a>
 | 
			
		||||
    <a href = "/repos">//Repos</a>
 | 
			
		||||
    <a href = "/contact">//Contact</a>
 | 
			
		||||
    <ThemeSwitcher />
 | 
			
		||||
</nav>
 | 
			
		||||
<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>
 | 
			
		||||
 | 
			
		||||
<div class="main-container fade">
 | 
			
		||||
    <Toasts />
 | 
			
		||||
    <div class="container mx-auto justify-center items-center flex flex-col">
 | 
			
		||||
        <slot />
 | 
			
		||||
        <ToastContainer let:data>
 | 
			
		||||
            <FlatToast {data} />
 | 
			
		||||
        </ToastContainer>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<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>
 | 
			
		||||
@@ -2,4 +2,6 @@
 | 
			
		||||
    import Main from '../main.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Main></Main>
 | 
			
		||||
<div>
 | 
			
		||||
    <Main></Main>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,149 +1,95 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import { toasts } from 'svelte-toasts';
 | 
			
		||||
    import Card from '$lib/components/Cards/Card.svelte';
 | 
			
		||||
    import { Toast, ToastType } from '$lib/toast';
 | 
			
		||||
    import { addToast } from '$lib/stores';
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
        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
 | 
			
		||||
    )
 | 
			
		||||
        );
 | 
			
		||||
        toasts.add({
 | 
			
		||||
            title: 'Message not sent!',
 | 
			
		||||
            description: 'Please try again later.',
 | 
			
		||||
            type: 'error',
 | 
			
		||||
            duration: 4000,
 | 
			
		||||
            placement: 'bottom-center'
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Card>
 | 
			
		||||
    <div slot="header">
 | 
			
		||||
        <h2>Contact</h2>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div slot="content">
 | 
			
		||||
        <form action="https://api.staticforms.xyz/submit" method="post" class="contact-form">
 | 
			
		||||
<Card headerLeft="Contact Me">
 | 
			
		||||
    <!-- Contact Form -->
 | 
			
		||||
     <form 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">
 | 
			
		||||
 | 
			
		||||
            <!-- Form Items-->
 | 
			
		||||
            <div class="input-group">
 | 
			
		||||
                <input type="text" id="name" name="name" required placeholder="Your Name" />
 | 
			
		||||
                <input type="email" id="email" name="email" required placeholder="Your Email" />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="input-group">
 | 
			
		||||
                <input type="text" name="subject" placeholder="Subject" required>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="input-group">
 | 
			
		||||
                <textarea id="message" name="message" rows="4" required placeholder="Your Message" />
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
            <!-- Hidden Attributes-->
 | 
			
		||||
            <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">
 | 
			
		||||
            
 | 
			
		||||
        </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="input-group">
 | 
			
		||||
        <div class="">
 | 
			
		||||
            <div class="g-recaptcha" data-sitekey="6LfjQAwrAAAAAIF57u8Wt4w5L5vBEWi5DfXXBuGy"></div>
 | 
			
		||||
            <script src="https://www.google.com/recaptcha/api.js" async defer></script>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
            <div class="input-group">
 | 
			
		||||
                <button type="submit" class="submit-button">Send Message</button>
 | 
			
		||||
            </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>
 | 
			
		||||
    </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>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
 | 
			
		||||
    /* Contact form styling */
 | 
			
		||||
    .contact-form {
 | 
			
		||||
        background: none;
 | 
			
		||||
        padding: 1rem;
 | 
			
		||||
        width: 80%;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        gap: 1rem;
 | 
			
		||||
        transition: all 0.3s ease;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Input groups */
 | 
			
		||||
    .input-group {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: row;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        justify-content: center;
 | 
			
		||||
        gap: 1rem;
 | 
			
		||||
        flex-wrap: wrap;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Input fields and textarea */
 | 
			
		||||
    .contact-form input,
 | 
			
		||||
    .contact-form textarea {
 | 
			
		||||
        padding: 0.8rem 1rem;
 | 
			
		||||
        border: 1px solid var(--fg);
 | 
			
		||||
        border-radius: 0.5rem;
 | 
			
		||||
        background: var(--input);
 | 
			
		||||
        color: var(--fg);
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
        transition: border-color 0.3s ease, background 0.3s ease;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .contact-form button {
 | 
			
		||||
        border: 1px solid var(--fg);
 | 
			
		||||
        width: 60%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .contact-form textarea {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        min-width: none;
 | 
			
		||||
        resize: vertical;
 | 
			
		||||
        min-height: fit-content;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .contact-form input:focus,
 | 
			
		||||
    .contact-form textarea:focus {
 | 
			
		||||
        border-color: var(--glow);
 | 
			
		||||
        outline: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Submit button */
 | 
			
		||||
    .submit-button {
 | 
			
		||||
        padding: 0.8rem 1rem;
 | 
			
		||||
        background: var(--accent);
 | 
			
		||||
        color: var(--fg);
 | 
			
		||||
        border: none;
 | 
			
		||||
        border-radius: 0.5rem;
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
        transition: background 0.3s ease;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .submit-button:hover {
 | 
			
		||||
        background: var(--link);
 | 
			
		||||
        color: var(--input);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .g-recaptcha {
 | 
			
		||||
        width: fit-content;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,64 +1,46 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import { onMount } from "svelte";
 | 
			
		||||
    import { Toast, ToastType } from "$lib/toast";
 | 
			
		||||
    import { repos, loadRepos, addToast } from "$lib/stores";
 | 
			
		||||
    import { timeSince, checkImage, IMAGE_URL_SUFFIX } from "$lib/api/git";
 | 
			
		||||
    import Card from "$lib/components/Cards/Card.svelte";
 | 
			
		||||
    import SlidingCard from "$lib/components/Cards/SlidingCard.svelte";
 | 
			
		||||
    import Loading from "$lib/components/Loading.svelte";
 | 
			
		||||
    import { loadRepos, repos } from '$lib/stores';    import { onMount } from 'svelte';
 | 
			
		||||
    import { timeSince, checkImage, IMAGE_URL_SUFFIX } from '$lib/api/git';
 | 
			
		||||
 | 
			
		||||
    import FlexGallery from '$lib/components/FlexGallery.svelte';
 | 
			
		||||
    import Card from '$lib/components/Cards/Card.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;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onMount(loadRepos);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<h1>My Projects</h1>
 | 
			
		||||
<p>This here is a list of my most recently worked on projects. Note this does not show any private repositories. For more in depth information <a href="https://git.luke-else.co.uk">Click Here</a>.</p>
 | 
			
		||||
 | 
			
		||||
<div class="container">
 | 
			
		||||
    {#if $repos.length > 0}
 | 
			
		||||
        <div style="display: none;">{addToast(new Toast("See a snapshot of my latest work.", ToastType.Info, true, 8_000))}</div>
 | 
			
		||||
        <div class="cards">
 | 
			
		||||
<FlexGallery>
 | 
			
		||||
    {#each $repos as repo}
 | 
			
		||||
                {#await checkImage(repo)}
 | 
			
		||||
                    <Loading />
 | 
			
		||||
                {:then hasImage}
 | 
			
		||||
                    {#if hasImage}
 | 
			
		||||
                        <SlidingCard>
 | 
			
		||||
                            <div slot="header">
 | 
			
		||||
                                <h2>{repo.name}</h2>
 | 
			
		||||
                                {repo.language}
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div slot="content">
 | 
			
		||||
                                <p class="not-required">{@html repo.description}</p>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div slot="sliding-content">
 | 
			
		||||
                                <img width="100%" src="{repo.html_url}{IMAGE_URL_SUFFIX}" alt="{repo.name}" />
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div slot="footer">
 | 
			
		||||
                                <!-- svelte-ignore a11y-invalid-attribute -->
 | 
			
		||||
                                <a href="{repo.html_url}">{repo.name}</a>
 | 
			
		||||
                                {timeSince(repo.updated_at)}
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </SlidingCard>
 | 
			
		||||
                    {:else}
 | 
			
		||||
                        <Card>
 | 
			
		||||
                            <div slot="header">
 | 
			
		||||
                                <h2>{repo.name}</h2>
 | 
			
		||||
                                {repo.language}
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div slot="content">
 | 
			
		||||
                                <p class="not-required">{@html repo.description}</p>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div slot="footer">
 | 
			
		||||
                                <!-- svelte-ignore a11y-invalid-attribute -->
 | 
			
		||||
                                <a href="{repo.html_url}">{repo.name}</a>
 | 
			
		||||
                                {timeSince(repo.updated_at)}
 | 
			
		||||
        <Card
 | 
			
		||||
            headerLeft={repo.name}
 | 
			
		||||
            headerRight={repo.language}
 | 
			
		||||
            footer={timeSince(repo.updated_at)}
 | 
			
		||||
            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"
 | 
			
		||||
        >
 | 
			
		||||
            <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}
 | 
			
		||||
        </Card>
 | 
			
		||||
                    {/if}
 | 
			
		||||
                {/await}
 | 
			
		||||
    {/each}
 | 
			
		||||
        </div>
 | 
			
		||||
    {:else}
 | 
			
		||||
        <Loading />
 | 
			
		||||
    {/if}
 | 
			
		||||
</div>
 | 
			
		||||
</FlexGallery>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,59 +0,0 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    export let skills: any;
 | 
			
		||||
 | 
			
		||||
    import Card from '$lib/components/Cards/Card.svelte';
 | 
			
		||||
    import Modal from '$lib/components/Modal.svelte';
 | 
			
		||||
 | 
			
		||||
    let showModal: boolean = false;
 | 
			
		||||
    let activeModal: any = null;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#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}
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
    .card-footer {
 | 
			
		||||
        margin-bottom: 1em;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        gap: 1.5em;
 | 
			
		||||
        justify-content: space-between;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .logo {
 | 
			
		||||
        color: var(--fg);
 | 
			
		||||
        font-size: 3em;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -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(--accent);`} />
 | 
			
		||||
					</div>
 | 
			
		||||
				{:else}
 | 
			
		||||
					<TimelineDot style={`background-color: var(--link); border-color: var(--accent);`} />
 | 
			
		||||
				{/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(--accent);
 | 
			
		||||
	}
 | 
			
		||||
	.content-title {
 | 
			
		||||
		margin: 0;
 | 
			
		||||
		padding: 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.content-description {
 | 
			
		||||
		margin: 0;
 | 
			
		||||
		padding: 0;
 | 
			
		||||
		margin-top: 1rem;
 | 
			
		||||
		color: var(--fg);
 | 
			
		||||
		font-weight: lighter;
 | 
			
		||||
		padding: 0.5rem 0;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,42 +1,48 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "Luke Else",
 | 
			
		||||
  "job_title": "Software Engineer",
 | 
			
		||||
  "location": "Crawley, Sussex <br /> UK",
 | 
			
		||||
  "profile_photo": "/profile.jpg",
 | 
			
		||||
  "skills": [
 | 
			
		||||
    {
 | 
			
		||||
      "skill": "Rust",
 | 
			
		||||
      "name": "Rust",
 | 
			
		||||
      "logo": "devicon-rust-plain",
 | 
			
		||||
      "link": "https://rust-lang.org",
 | 
			
		||||
      "usage": "Rust is a memory-safe language with zero-cost abstractions, making it ideal for embedded systems. I used Rust to build a <a href='https://git.luke-else.co.uk/luke-else/esp32_gps_display'>GPS-based speedometer</a> for my car and a <a href='https://git.luke-else.co.uk/luke-else/subnet_calculator'>Subnet Calculator</a> for university studies.",
 | 
			
		||||
      "about": "Rust combines safety, efficiency, and clean code, making it a powerful choice for reliable software development."
 | 
			
		||||
      "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",
 | 
			
		||||
      "link": "https://cplusplus.com/",
 | 
			
		||||
      "usage": "Since joining Thales in 2022, I’ve worked on a distributed simulation system using C++, primarily with <a href='https://www.qt.io'>Qt</a> and <a href='https://github.com/ocornut/imgui'>ImGui</a> to develop customer-facing applications.",
 | 
			
		||||
      "about": "C++ offers high-level abstractions with low-level control, making it essential for performance-critical applications."
 | 
			
		||||
      "about": "C++ offers high-level abstractions with low-level control, making it essential for performance-critical applications.",
 | 
			
		||||
      "competency": 80
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "skill": "Git",
 | 
			
		||||
      "name": "Git",
 | 
			
		||||
      "logo": "devicon-git-plain",
 | 
			
		||||
      "link": "https://git-scm.com",
 | 
			
		||||
      "usage": "I have extensive experience with Git, including advanced features like <a href='https://www.atlassian.com/git/tutorials/advanced-overview'>branching, merging and hooks</a>. I've also set up self-hosted <a href='https://git.luke-else.co.uk/luke-else/'>Git services</a> with CI/CD automation.",
 | 
			
		||||
      "about": "Git is an essential tool for version control, enabling efficient collaboration and streamlined code management."
 | 
			
		||||
      "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",
 | 
			
		||||
      "link": "https://docker.com",
 | 
			
		||||
      "usage": "I use Docker and Docker Compose for containerized deployments, including hosting <a href='https://git.luke-else.co.uk/luke-else/server'>home-lab services</a> such as this <a href='https://git.luke-else.co.uk/luke-else/luke-else.co.uk'>website</a> and remote Git repositories.",
 | 
			
		||||
      "about": "Docker simplifies deployment by packaging applications in lightweight containers, ensuring consistency across environments."
 | 
			
		||||
      "about": "Docker simplifies deployment by packaging applications in lightweight containers, ensuring consistency across environments.",
 | 
			
		||||
      "competency": 100
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "skill": "Svelte",
 | 
			
		||||
      "name": "Svelte",
 | 
			
		||||
      "logo": "devicon-svelte-plain",
 | 
			
		||||
      "link": "https://svelte.dev",
 | 
			
		||||
      "usage": "I built <a href='https://git.luke-else.co.uk/luke-else/luke-else.co.uk'>this website</a> using Svelte and plan to explore <a href='https://github.com/tauri-apps/tauri'>Tauri</a> for building desktop apps.",
 | 
			
		||||
      "about": "Svelte compiles to optimized JavaScript, offering a fast, efficient, and maintainable front-end development experience."
 | 
			
		||||
      "about": "Svelte compiles to optimized JavaScript, offering a fast, efficient, and maintainable front-end development experience.",
 | 
			
		||||
      "competency": 40
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "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>.",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
:root {
 | 
			
		||||
    --bg: #f2f6f7; /* Soft blue-tinted white */
 | 
			
		||||
    --bg-secondary: #d7e1e4; /* Cool grey-blue */
 | 
			
		||||
    --accent: #92a9b0; /* Subtle blue-green */
 | 
			
		||||
 | 
			
		||||
    --header: #5d7075; /* Deep slate blue-green */
 | 
			
		||||
    --fg: #3c4649; /* Rich dark grey */
 | 
			
		||||
    
 | 
			
		||||
    --input: #e0e6e8; /* Light desaturated blue-grey */
 | 
			
		||||
    
 | 
			
		||||
    --link: #678d97; /* Muted sea blue */
 | 
			
		||||
    --glow: #b2c4c8; /* Gentle cool glow */
 | 
			
		||||
    --hover: #8fa7af; /* Soft grey-blue hover */
 | 
			
		||||
 | 
			
		||||
    --green: #78a890; /* Balanced green */
 | 
			
		||||
    --red: #e08c96; /* Soft dusty red */
 | 
			
		||||
    --blue: #729da5; /* Medium desaturated blue */
 | 
			
		||||
}
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
:root {
 | 
			
		||||
    --bg: #282c34;
 | 
			
		||||
    --bg-secondary: #3e434b;
 | 
			
		||||
    --accent: #59616d;
 | 
			
		||||
 | 
			
		||||
    --header: #E06C75;
 | 
			
		||||
    --fg: #9eaac0;
 | 
			
		||||
    
 | 
			
		||||
    --input: #2b3136;
 | 
			
		||||
    
 | 
			
		||||
    --link: #98C379;
 | 
			
		||||
    --glow: #C678DD;
 | 
			
		||||
    --hover: #56B6C2;
 | 
			
		||||
 | 
			
		||||
    --green: #98C379;
 | 
			
		||||
    --red: #E06C75;
 | 
			
		||||
    --blue: #79aec3;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
:root {
 | 
			
		||||
    --bg: #1e1f2a;
 | 
			
		||||
    --bg-secondary: #3a3f4b;
 | 
			
		||||
    --accent: #777f8d;
 | 
			
		||||
 | 
			
		||||
    --header: #cad1da;
 | 
			
		||||
    --fg: #e4e1db;
 | 
			
		||||
    
 | 
			
		||||
    --input: #2e3438;
 | 
			
		||||
    
 | 
			
		||||
    --link: #95add8;
 | 
			
		||||
    --glow: #bcc3ca;
 | 
			
		||||
    --hover: #cdd8e2;
 | 
			
		||||
 | 
			
		||||
    --green: #98C379;
 | 
			
		||||
    --red: #E06C75;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
:root {
 | 
			
		||||
    --bg: #2b2b2b; /* Dark but not too harsh */
 | 
			
		||||
    --bg-secondary: #3c3f41; /* Deep warm grey */
 | 
			
		||||
    --accent: #6897bb; /* Muted but clear blue */
 | 
			
		||||
 | 
			
		||||
    --header: #a9b7c6; /* Softer contrast */
 | 
			
		||||
    --fg: #bbbbbb; /* Light but not pure white */
 | 
			
		||||
    
 | 
			
		||||
    --input: #414141; /* Dark grey input */
 | 
			
		||||
    
 | 
			
		||||
    --link: #519aba; /* Soft coding blue */
 | 
			
		||||
    --glow: #4e5d68; /* Subtle bluish glow */
 | 
			
		||||
    --hover: #8c9da8; /* Brighter on hover */
 | 
			
		||||
 | 
			
		||||
    --green: #6a8759; /* Classic Darcula green */
 | 
			
		||||
    --red: #cc6666; /* Softer, warm red */
 | 
			
		||||
    --blue: #6897bb; /* Standard coding blue */
 | 
			
		||||
}
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
:root {
 | 
			
		||||
    --bg: #f5f5f5; /* Slightly deeper light grey for subtle contrast */
 | 
			
		||||
    --bg-secondary: #d9dddf; /* More defined soft grey */
 | 
			
		||||
    --accent: #8ea29b; /* Stronger muted sage green */
 | 
			
		||||
 | 
			
		||||
    --header: #4a5a56; /* Darker desaturated green-grey for better contrast */
 | 
			
		||||
    --fg: #2f3739; /* Richer dark grey for improved readability */
 | 
			
		||||
    
 | 
			
		||||
    --input: #e4e7e8; /* Slightly deeper soft grey input background */
 | 
			
		||||
    
 | 
			
		||||
    --link: #5f8480; /* Darker muted teal for contrast */
 | 
			
		||||
    --glow: #b0bdb9; /* More noticeable but soft glow */
 | 
			
		||||
    --hover: #85a29c; /* Stronger pastel hover effect */
 | 
			
		||||
 | 
			
		||||
    --green: #6fa984; /* More vibrant pastel green */
 | 
			
		||||
    --red: #e8858f; /* Slightly deeper pastel red for contrast */
 | 
			
		||||
    --blue: #6fa9a4; /* Same as accent */
 | 
			
		||||
}
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
:root {
 | 
			
		||||
    --bg: #272822; /* Classic Monokai dark */
 | 
			
		||||
    --bg-secondary: #3e3d32; /* Darker olive grey */
 | 
			
		||||
    --accent: #f92672; /* Monokai’s signature pink-red */
 | 
			
		||||
 | 
			
		||||
    --header: #a6e22e; /* Neon green */
 | 
			
		||||
    --fg: #f8f8f2; /* Soft off-white */
 | 
			
		||||
    
 | 
			
		||||
    --input: #373831; /* Slightly lighter grey-green */
 | 
			
		||||
    
 | 
			
		||||
    --link: #66d9ef; /* Monokai cyan */
 | 
			
		||||
    --glow: #49483e; /* Muted background glow */
 | 
			
		||||
    --hover: #fd7c95; /* Lighter pink hover */
 | 
			
		||||
 | 
			
		||||
    --green: #a6e22e; /* Bright Monokai green */
 | 
			
		||||
    --red: #f92672; /* Soft pinkish-red */
 | 
			
		||||
    --blue: #66d9ef; /* Bright cyan */
 | 
			
		||||
}
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
:root {
 | 
			
		||||
    --bg: #1e1e1e; /* Dark neutral background */
 | 
			
		||||
    --bg-secondary: #252526; /* Slightly lighter for separation */
 | 
			
		||||
    --accent: #569cd6; /* Signature VS Code blue */
 | 
			
		||||
 | 
			
		||||
    --header: #9cdcfe; /* Brighter cyan for contrast */
 | 
			
		||||
    --fg: #d4d4d4; /* Light grey for readability */
 | 
			
		||||
    
 | 
			
		||||
    --input: #333; /* Dark but still visible */
 | 
			
		||||
    
 | 
			
		||||
    --link: #4fc1ff; /* Brighter blue for hyperlinks */
 | 
			
		||||
    --glow: #2a3f5f; /* Soft deep blue glow */
 | 
			
		||||
    --hover: #7bb8e8; /* Brighter accent on hover */
 | 
			
		||||
 | 
			
		||||
    --green: #4ec9b0; /* Soft green */
 | 
			
		||||
    --red: #d16969; /* Coding red */
 | 
			
		||||
    --blue: #9cdcfe; /* Soft bright cyan */
 | 
			
		||||
}
 | 
			
		||||
@@ -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
									
								
							
							
						
						
									
										19
									
								
								tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  theme: {
 | 
			
		||||
    extend: {
 | 
			
		||||
      colors: {
 | 
			
		||||
        'one-bg':      '#282c34',
 | 
			
		||||
        'one-bg-light': '#3a3f4b',
 | 
			
		||||
        'one-fg':      '#abb2bf',
 | 
			
		||||
        'one-accent':  '#61afef',
 | 
			
		||||
        'one-green':   '#98c379',
 | 
			
		||||
        'one-orange':  '#d19a66',
 | 
			
		||||
        'one-red':     '#e06c75',
 | 
			
		||||
        'one-yellow':  '#e5c07b',
 | 
			
		||||
        'one-purple':  '#c678dd',
 | 
			
		||||
        'one-cyan':    '#56b6c2',
 | 
			
		||||
        'one-comment': '#5c6370',
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -8,7 +8,8 @@
 | 
			
		||||
		"resolveJsonModule": true,
 | 
			
		||||
		"skipLibCheck": true,
 | 
			
		||||
		"sourceMap": true,
 | 
			
		||||
		"strict": true
 | 
			
		||||
		"strict": true,
 | 
			
		||||
		"moduleResolution": "bundler"
 | 
			
		||||
	}
 | 
			
		||||
	// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
 | 
			
		||||
	//
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,10 @@
 | 
			
		||||
import { sveltekit } from '@sveltejs/kit/vite';
 | 
			
		||||
import { defineConfig } from 'vite';
 | 
			
		||||
import tailwindcss from '@tailwindcss/vite';
 | 
			
		||||
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
	plugins: [sveltekit()]
 | 
			
		||||
	plugins: [
 | 
			
		||||
		tailwindcss(),
 | 
			
		||||
		sveltekit(),
 | 
			
		||||
	]
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user