Redesign site and modernize tooling
My Gatsby Deployment pipeline / build-and-deploy (push) Successful in 4m4s
Replace the HTML5-UP "Strata" theme with a Swiss-minimal single-page design (plain CSS, Space Grotesk / Hanken Grotesk / Space Mono). Rewrite copy for current positioning: Staff Data, Analytics & AI Engineer at Swisscom, open to new work. Experience shown as a pipeline timeline; contact simplified to email + LinkedIn (drop the broken form). Prune unused deps (lightbox, FontAwesome, sass/gatsby-plugin-sass) and the old SCSS, fonts and gallery assets. Fix package.json metadata. Harden the Gitea deploy: merge build+deploy into one job (removes the deprecated artifact upload/download handoff), bump Node 18 -> 20, use npx gatsby build. Rename .nvrmc -> .nvmrc and refresh the README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@@ -4,37 +4,25 @@ run-name: ${{ gitea.actor }} is deploying this page to production.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Repository auschecken
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Node.js einrichten
|
||||
uses: actions/setup-node@v3
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- name: Clear node_modules and package-lock
|
||||
run: |
|
||||
rm -rf node_modules
|
||||
rm -f package-lock.json
|
||||
- name: Install dependencies
|
||||
run: npm install --legacy-peer-deps --force
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
npm install --legacy-peer-deps --force
|
||||
npm install -g gatsby-cli
|
||||
|
||||
- name: Clean Gatsby cache
|
||||
run: |
|
||||
gatsby clean
|
||||
|
||||
- name: Build Gatsby Site
|
||||
run: |
|
||||
gatsby build
|
||||
- name: Build Gatsby site
|
||||
run: npx gatsby build
|
||||
env:
|
||||
NODE_ENV: production
|
||||
CI: true
|
||||
@@ -46,22 +34,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: gatsby-build
|
||||
path: public/
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: gatsby-build
|
||||
path: public
|
||||
|
||||
- name: Install SSH key
|
||||
uses: shimataro/ssh-key-action@v2
|
||||
with:
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
# gatsby-starter-strata
|
||||
# thiessen.io
|
||||
|
||||
**This is a starter for Gatsby.js V2.**
|
||||
Personal site of **Dennis Thiessen** — Staff Data, Analytics & AI Engineer (Bern, Switzerland).
|
||||
|
||||
**The older V1 version of this starter can be found on the v1 branch**
|
||||
Built with [Gatsby](https://www.gatsbyjs.com/) 5 and React 18. Single-page, Swiss-minimal design; styles are plain CSS in `src/assets/style.css` (no preprocessor).
|
||||
|
||||
Gatsby starter based on the Strata site template, designed by [HTML5 UP](https://html5up.net/strata). Check out https://codebushi.com/gatsby-starters/ for more Gatsby starters and templates.
|
||||
## Develop
|
||||
|
||||
## Preview
|
||||
```bash
|
||||
npm install --legacy-peer-deps
|
||||
npx gatsby develop # http://localhost:8000
|
||||
```
|
||||
|
||||
http://gatsby-strata.surge.sh/
|
||||
## Build
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
npx gatsby build # output in ./public
|
||||
```
|
||||
|
||||
Install this starter (assuming Gatsby is installed) by running from your CLI:
|
||||
`gatsby new gatsby-starter-strata https://github.com/ChangoMan/gatsby-starter-strata`
|
||||
Use Node 20 (see `.nvmrc`).
|
||||
|
||||
Run `gatsby develop` in the terminal to start.
|
||||
## Deploy
|
||||
|
||||
Pushing to `master` triggers the Gitea Actions workflow in
|
||||
`.gitea/workflows/workflow.yaml`, which builds the site and rsyncs `public/`
|
||||
to the web server over SSH. It relies on these repository secrets:
|
||||
`SSH_PRIVATE_KEY`, `SSH_KNOWN_HOSTS`, `DEPLOY_USER`, `DEPLOY_HOST`, `DEPLOY_PATH`.
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
module.exports = {
|
||||
siteMetadata: {
|
||||
title: "Dennis Thiessen - IT Software Engineering",
|
||||
title: "Dennis Thiessen — Data, Analytics & AI Engineer",
|
||||
author: "Dennis Thiessen",
|
||||
description: "Portfolio of Dennis Thiessen - IT Software Engineer Freelancer"
|
||||
description:
|
||||
"Dennis Thiessen is a Staff Data, Analytics & AI Engineer in Bern, Switzerland, building the data pipelines and ML infrastructure that production systems run on."
|
||||
},
|
||||
plugins: [
|
||||
'gatsby-plugin-react-helmet',
|
||||
{
|
||||
resolve: `gatsby-plugin-manifest`,
|
||||
options: {
|
||||
name: 'gatsby-starter-default',
|
||||
short_name: 'Dennis Thiessen',
|
||||
name: 'Dennis Thiessen',
|
||||
short_name: 'D. Thiessen',
|
||||
start_url: '/',
|
||||
background_color: '#663399',
|
||||
theme_color: '#663399',
|
||||
background_color: '#f4f5f7',
|
||||
theme_color: '#15171c',
|
||||
display: 'minimal-ui',
|
||||
icon: 'src/assets/images/website-icon-dt.png',
|
||||
},
|
||||
},
|
||||
'gatsby-plugin-sass',
|
||||
'gatsby-plugin-offline'
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
{
|
||||
"name": "gatsby-starter-default",
|
||||
"description": "Gatsby default starter",
|
||||
"name": "thiessen-io",
|
||||
"description": "Personal site of Dennis Thiessen — thiessen.io",
|
||||
"version": "1.0.0",
|
||||
"author": "Hunter Chang",
|
||||
"author": "Dennis Thiessen",
|
||||
"dependencies": {
|
||||
"gatsby": "^5.12.12",
|
||||
"gatsby-plugin-manifest": "^5.12.3",
|
||||
"gatsby-plugin-offline": "^6.12.3",
|
||||
"gatsby-plugin-react-helmet": "^6.12.0",
|
||||
"gatsby-plugin-sass": "^6.12.3",
|
||||
"sass": "^1.69.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"yet-another-react-lightbox": "^3.15.6",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"ajv": "^8.12.0",
|
||||
"ajv-keywords": "^5.1.0",
|
||||
"schema-utils": "^4.2.0"
|
||||
|
||||
|
Before Width: | Height: | Size: 434 KiB |
|
Before Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 261 KiB |
|
Before Width: | Height: | Size: 343 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 21 KiB |
@@ -1,87 +0,0 @@
|
||||
@import 'libs/vars';
|
||||
@import 'libs/functions';
|
||||
@import 'libs/mixins';
|
||||
@import 'libs/skel';
|
||||
|
||||
/*
|
||||
Strata by HTML5 UP
|
||||
html5up.net | @ajlkn
|
||||
Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
|
||||
*/
|
||||
|
||||
/* Button */
|
||||
|
||||
input[type="submit"],
|
||||
input[type="reset"],
|
||||
input[type="button"],
|
||||
.button {
|
||||
position: relative;
|
||||
-ms-behavior: url('assets/js/ie/PIE.htc');
|
||||
}
|
||||
|
||||
/* Form */
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="email"],
|
||||
select,
|
||||
textarea {
|
||||
position: relative;
|
||||
-ms-behavior: url('assets/js/ie/PIE.htc');
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="email"],
|
||||
select {
|
||||
height: _size(element-height);
|
||||
line-height: _size(element-height);
|
||||
}
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
& + label {
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Image */
|
||||
|
||||
.image {
|
||||
position: relative;
|
||||
-ms-behavior: url('assets/js/ie/PIE.htc');
|
||||
|
||||
&:before, &:after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
-ms-behavior: url('assets/js/ie/PIE.htc');
|
||||
}
|
||||
}
|
||||
|
||||
/* Header */
|
||||
|
||||
#header {
|
||||
background-image: url('../../images/bg.jpg');
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
-ms-behavior: url('assets/js/ie/backgroundsize.min.htc');
|
||||
|
||||
h1 {
|
||||
color: _palette(accent2, fg-bold);
|
||||
}
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
|
||||
#footer {
|
||||
.icons {
|
||||
a {
|
||||
color: _palette(accent2, fg-bold);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 24 KiB |
@@ -1,34 +0,0 @@
|
||||
/// Gets a duration value.
|
||||
/// @param {string} $keys Key(s).
|
||||
/// @return {string} Value.
|
||||
@function _duration($keys...) {
|
||||
@return val($duration, $keys...);
|
||||
}
|
||||
|
||||
/// Gets a font value.
|
||||
/// @param {string} $keys Key(s).
|
||||
/// @return {string} Value.
|
||||
@function _font($keys...) {
|
||||
@return val($font, $keys...);
|
||||
}
|
||||
|
||||
/// Gets a misc value.
|
||||
/// @param {string} $keys Key(s).
|
||||
/// @return {string} Value.
|
||||
@function _misc($keys...) {
|
||||
@return val($misc, $keys...);
|
||||
}
|
||||
|
||||
/// Gets a palette value.
|
||||
/// @param {string} $keys Key(s).
|
||||
/// @return {string} Value.
|
||||
@function _palette($keys...) {
|
||||
@return val($palette, $keys...);
|
||||
}
|
||||
|
||||
/// Gets a size value.
|
||||
/// @param {string} $keys Key(s).
|
||||
/// @return {string} Value.
|
||||
@function _size($keys...) {
|
||||
@return val($size, $keys...);
|
||||
}
|
||||
@@ -1,398 +0,0 @@
|
||||
/// Makes an element's :before pseudoelement a FontAwesome icon.
|
||||
/// @param {string} $content Optional content value to use.
|
||||
/// @param {string} $where Optional pseudoelement to target (before or after).
|
||||
@mixin icon($content: false, $where: before) {
|
||||
|
||||
text-decoration: none;
|
||||
|
||||
&:#{$where} {
|
||||
|
||||
@if $content {
|
||||
content: $content;
|
||||
}
|
||||
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-family: FontAwesome;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
text-transform: none !important;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Applies padding to an element, taking the current element-margin value into account.
|
||||
/// @param {mixed} $tb Top/bottom padding.
|
||||
/// @param {mixed} $lr Left/right padding.
|
||||
/// @param {list} $pad Optional extra padding (in the following order top, right, bottom, left)
|
||||
/// @param {bool} $important If true, adds !important.
|
||||
@mixin padding($tb, $lr, $pad: (0,0,0,0), $important: null) {
|
||||
|
||||
@if $important {
|
||||
$important: '!important';
|
||||
}
|
||||
|
||||
$x: 0.1em;
|
||||
|
||||
@if unit(_size(element-margin)) == 'rem' {
|
||||
$x: 0.1rem;
|
||||
}
|
||||
|
||||
padding: ($tb + nth($pad,1)) ($lr + nth($pad,2)) max($x, $tb - _size(element-margin) + nth($pad,3)) ($lr + nth($pad,4)) #{$important};
|
||||
|
||||
}
|
||||
|
||||
/// Encodes a SVG data URL so IE doesn't choke (via codepen.io/jakob-e/pen/YXXBrp).
|
||||
/// @param {string} $svg SVG data URL.
|
||||
/// @return {string} Encoded SVG data URL.
|
||||
@function svg-url($svg) {
|
||||
|
||||
$svg: str-replace($svg, '"', '\'');
|
||||
$svg: str-replace($svg, '%', '%25');
|
||||
$svg: str-replace($svg, '<', '%3C');
|
||||
$svg: str-replace($svg, '>', '%3E');
|
||||
$svg: str-replace($svg, '&', '%26');
|
||||
$svg: str-replace($svg, '#', '%23');
|
||||
$svg: str-replace($svg, '{', '%7B');
|
||||
$svg: str-replace($svg, '}', '%7D');
|
||||
$svg: str-replace($svg, ';', '%3B');
|
||||
|
||||
@return url("data:image/svg+xml;charset=utf8,#{$svg}");
|
||||
|
||||
}
|
||||
|
||||
/// Initializes base flexgrid classes.
|
||||
/// @param {string} $vertical-align Vertical alignment of cells.
|
||||
/// @param {string} $horizontal-align Horizontal alignment of cells.
|
||||
@mixin flexgrid-base($vertical-align: null, $horizontal-align: null) {
|
||||
|
||||
// Grid.
|
||||
@include vendor('display', 'flex');
|
||||
@include vendor('flex-wrap', 'wrap');
|
||||
|
||||
// Vertical alignment.
|
||||
@if ($vertical-align == top) {
|
||||
@include vendor('align-items', 'flex-start');
|
||||
}
|
||||
@else if ($vertical-align == bottom) {
|
||||
@include vendor('align-items', 'flex-end');
|
||||
}
|
||||
@else if ($vertical-align == center) {
|
||||
@include vendor('align-items', 'center');
|
||||
}
|
||||
@else {
|
||||
@include vendor('align-items', 'stretch');
|
||||
}
|
||||
|
||||
// Horizontal alignment.
|
||||
@if ($horizontal-align != null) {
|
||||
text-align: $horizontal-align;
|
||||
}
|
||||
|
||||
// Cells.
|
||||
> * {
|
||||
@include vendor('flex-shrink', '1');
|
||||
@include vendor('flex-grow', '0');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Sets up flexgrid columns.
|
||||
/// @param {integer} $columns Columns.
|
||||
@mixin flexgrid-columns($columns) {
|
||||
|
||||
> * {
|
||||
$cell-width: 100% / $columns;
|
||||
width: #{$cell-width};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Sets up flexgrid gutters.
|
||||
/// @param {integer} $columns Columns.
|
||||
/// @param {number} $gutters Gutters.
|
||||
@mixin flexgrid-gutters($columns, $gutters) {
|
||||
|
||||
// Apply padding.
|
||||
> * {
|
||||
$cell-width: 100% / $columns;
|
||||
|
||||
padding: ($gutters * 0.5);
|
||||
width: $cell-width;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Sets up flexgrid gutters (flush).
|
||||
/// @param {integer} $columns Columns.
|
||||
/// @param {number} $gutters Gutters.
|
||||
@mixin flexgrid-gutters-flush($columns, $gutters) {
|
||||
|
||||
// Apply padding.
|
||||
> * {
|
||||
$cell-width: 100% / $columns;
|
||||
$cell-width-pad: $gutters / $columns;
|
||||
|
||||
padding: ($gutters * 0.5);
|
||||
width: calc(#{$cell-width} + #{$cell-width-pad});
|
||||
}
|
||||
|
||||
// Clear top/bottom gutters.
|
||||
> :nth-child(-n + #{$columns}) {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
> :nth-last-child(-n + #{$columns}) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
// Clear left/right gutters.
|
||||
> :nth-child(#{$columns}n + 1) {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
> :nth-child(#{$columns}n) {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
// Adjust widths of leftmost and rightmost cells.
|
||||
> :nth-child(#{$columns}n + 1),
|
||||
> :nth-child(#{$columns}n) {
|
||||
$cell-width: 100% / $columns;
|
||||
$cell-width-pad: ($gutters / $columns) - ($gutters / 2);
|
||||
|
||||
width: calc(#{$cell-width} + #{$cell-width-pad});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Reset flexgrid gutters (flush only).
|
||||
/// Used to override a previous set of flexgrid gutter classes.
|
||||
/// @param {integer} $columns Columns.
|
||||
/// @param {number} $gutters Gutters.
|
||||
/// @param {integer} $prev-columns Previous columns.
|
||||
@mixin flexgrid-gutters-flush-reset($columns, $gutters, $prev-columns) {
|
||||
|
||||
// Apply padding.
|
||||
> * {
|
||||
$cell-width: 100% / $prev-columns;
|
||||
$cell-width-pad: $gutters / $prev-columns;
|
||||
|
||||
padding: ($gutters * 0.5);
|
||||
width: calc(#{$cell-width} + #{$cell-width-pad});
|
||||
}
|
||||
|
||||
// Clear top/bottom gutters.
|
||||
> :nth-child(-n + #{$prev-columns}) {
|
||||
padding-top: ($gutters * 0.5);
|
||||
}
|
||||
|
||||
> :nth-last-child(-n + #{$prev-columns}) {
|
||||
padding-bottom: ($gutters * 0.5);
|
||||
}
|
||||
|
||||
// Clear left/right gutters.
|
||||
> :nth-child(#{$prev-columns}n + 1) {
|
||||
padding-left: ($gutters * 0.5);
|
||||
}
|
||||
|
||||
> :nth-child(#{$prev-columns}n) {
|
||||
padding-right: ($gutters * 0.5);
|
||||
}
|
||||
|
||||
// Adjust widths of leftmost and rightmost cells.
|
||||
> :nth-child(#{$prev-columns}n + 1),
|
||||
> :nth-child(#{$prev-columns}n) {
|
||||
$cell-width: 100% / $columns;
|
||||
$cell-width-pad: $gutters / $columns;
|
||||
|
||||
padding: ($gutters * 0.5);
|
||||
width: calc(#{$cell-width} + #{$cell-width-pad});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Adds debug styles to current flexgrid element.
|
||||
@mixin flexgrid-debug() {
|
||||
|
||||
box-shadow: 0 0 0 1px red;
|
||||
|
||||
> * {
|
||||
box-shadow: inset 0 0 0 1px blue;
|
||||
position: relative;
|
||||
|
||||
> * {
|
||||
position: relative;
|
||||
box-shadow: inset 0 0 0 1px green;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Initializes the current element as a flexgrid.
|
||||
/// @param {integer} $columns Columns (optional).
|
||||
/// @param {number} $gutters Gutters (optional).
|
||||
/// @param {bool} $flush If true, clears padding around the very edge of the grid.
|
||||
@mixin flexgrid($settings: ()) {
|
||||
|
||||
// Settings.
|
||||
|
||||
// Debug.
|
||||
$debug: false;
|
||||
|
||||
@if (map-has-key($settings, 'debug')) {
|
||||
$debug: map-get($settings, 'debug');
|
||||
}
|
||||
|
||||
// Vertical align.
|
||||
$vertical-align: null;
|
||||
|
||||
@if (map-has-key($settings, 'vertical-align')) {
|
||||
$vertical-align: map-get($settings, 'vertical-align');
|
||||
}
|
||||
|
||||
// Horizontal align.
|
||||
$horizontal-align: null;
|
||||
|
||||
@if (map-has-key($settings, 'horizontal-align')) {
|
||||
$horizontal-align: map-get($settings, 'horizontal-align');
|
||||
}
|
||||
|
||||
// Columns.
|
||||
$columns: null;
|
||||
|
||||
@if (map-has-key($settings, 'columns')) {
|
||||
$columns: map-get($settings, 'columns');
|
||||
}
|
||||
|
||||
// Gutters.
|
||||
$gutters: 0;
|
||||
|
||||
@if (map-has-key($settings, 'gutters')) {
|
||||
$gutters: map-get($settings, 'gutters');
|
||||
}
|
||||
|
||||
// Flush.
|
||||
$flush: true;
|
||||
|
||||
@if (map-has-key($settings, 'flush')) {
|
||||
$flush: map-get($settings, 'flush');
|
||||
}
|
||||
|
||||
// Initialize base grid.
|
||||
@include flexgrid-base($vertical-align, $horizontal-align);
|
||||
|
||||
// Debug?
|
||||
@if ($debug) {
|
||||
@include flexgrid-debug;
|
||||
}
|
||||
|
||||
// Columns specified?
|
||||
@if ($columns != null) {
|
||||
|
||||
// Initialize columns.
|
||||
@include flexgrid-columns($columns);
|
||||
|
||||
// Gutters specified?
|
||||
@if ($gutters > 0) {
|
||||
|
||||
// Flush gutters?
|
||||
@if ($flush) {
|
||||
|
||||
// Initialize gutters (flush).
|
||||
@include flexgrid-gutters-flush($columns, $gutters);
|
||||
|
||||
}
|
||||
|
||||
// Otherwise ...
|
||||
@else {
|
||||
|
||||
// Initialize gutters.
|
||||
@include flexgrid-gutters($columns, $gutters);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Resizes a previously-initialized grid.
|
||||
/// @param {integer} $columns Columns.
|
||||
/// @param {number} $gutters Gutters (optional).
|
||||
/// @param {list} $reset A list of previously-initialized grid columns (only if $flush is true).
|
||||
/// @param {bool} $flush If true, clears padding around the very edge of the grid.
|
||||
@mixin flexgrid-resize($settings: ()) {
|
||||
|
||||
// Settings.
|
||||
|
||||
// Columns.
|
||||
$columns: 1;
|
||||
|
||||
@if (map-has-key($settings, 'columns')) {
|
||||
$columns: map-get($settings, 'columns');
|
||||
}
|
||||
|
||||
// Gutters.
|
||||
$gutters: 0;
|
||||
|
||||
@if (map-has-key($settings, 'gutters')) {
|
||||
$gutters: map-get($settings, 'gutters');
|
||||
}
|
||||
|
||||
// Previous columns.
|
||||
$prev-columns: false;
|
||||
|
||||
@if (map-has-key($settings, 'prev-columns')) {
|
||||
$prev-columns: map-get($settings, 'prev-columns');
|
||||
}
|
||||
|
||||
// Flush.
|
||||
$flush: true;
|
||||
|
||||
@if (map-has-key($settings, 'flush')) {
|
||||
$flush: map-get($settings, 'flush');
|
||||
}
|
||||
|
||||
// Resize columns.
|
||||
@include flexgrid-columns($columns);
|
||||
|
||||
// Gutters specified?
|
||||
@if ($gutters > 0) {
|
||||
|
||||
// Flush gutters?
|
||||
@if ($flush) {
|
||||
|
||||
// Previous columns specified?
|
||||
@if ($prev-columns) {
|
||||
|
||||
// Convert to list if it isn't one already.
|
||||
@if (type-of($prev-columns) != list) {
|
||||
$prev-columns: ($prev-columns);
|
||||
}
|
||||
|
||||
// Step through list of previous columns and reset them.
|
||||
@each $x in $prev-columns {
|
||||
@include flexgrid-gutters-flush-reset($columns, $gutters, $x);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Resize gutters (flush).
|
||||
@include flexgrid-gutters-flush($columns, $gutters);
|
||||
|
||||
}
|
||||
|
||||
// Otherwise ...
|
||||
@else {
|
||||
|
||||
// Resize gutters.
|
||||
@include flexgrid-gutters($columns, $gutters);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,587 +0,0 @@
|
||||
// skel.scss v3.0.2-dev | (c) skel.io | MIT licensed */
|
||||
|
||||
// Vars.
|
||||
|
||||
/// Breakpoints.
|
||||
/// @var {list}
|
||||
$breakpoints: () !global;
|
||||
|
||||
/// Vendor prefixes.
|
||||
/// @var {list}
|
||||
$vendor-prefixes: (
|
||||
'-moz-',
|
||||
'-webkit-',
|
||||
'-ms-',
|
||||
''
|
||||
);
|
||||
|
||||
/// Properties that should be vendorized.
|
||||
/// @var {list}
|
||||
$vendor-properties: (
|
||||
'align-content',
|
||||
'align-items',
|
||||
'align-self',
|
||||
'animation',
|
||||
'animation-delay',
|
||||
'animation-direction',
|
||||
'animation-duration',
|
||||
'animation-fill-mode',
|
||||
'animation-iteration-count',
|
||||
'animation-name',
|
||||
'animation-play-state',
|
||||
'animation-timing-function',
|
||||
'appearance',
|
||||
'backface-visibility',
|
||||
'box-sizing',
|
||||
'filter',
|
||||
'flex',
|
||||
'flex-basis',
|
||||
'flex-direction',
|
||||
'flex-flow',
|
||||
'flex-grow',
|
||||
'flex-shrink',
|
||||
'flex-wrap',
|
||||
'justify-content',
|
||||
'object-fit',
|
||||
'object-position',
|
||||
'order',
|
||||
'perspective',
|
||||
'pointer-events',
|
||||
'transform',
|
||||
'transform-origin',
|
||||
'transform-style',
|
||||
'transition',
|
||||
'transition-delay',
|
||||
'transition-duration',
|
||||
'transition-property',
|
||||
'transition-timing-function',
|
||||
'user-select'
|
||||
);
|
||||
|
||||
/// Values that should be vendorized.
|
||||
/// @var {list}
|
||||
$vendor-values: (
|
||||
'filter',
|
||||
'flex',
|
||||
'linear-gradient',
|
||||
'radial-gradient',
|
||||
'transform'
|
||||
);
|
||||
|
||||
// Functions.
|
||||
|
||||
/// Removes a specific item from a list.
|
||||
/// @author Hugo Giraudel
|
||||
/// @param {list} $list List.
|
||||
/// @param {integer} $index Index.
|
||||
/// @return {list} Updated list.
|
||||
@function remove-nth($list, $index) {
|
||||
|
||||
$result: null;
|
||||
|
||||
@if type-of($index) != number {
|
||||
@warn "$index: #{quote($index)} is not a number for `remove-nth`.";
|
||||
}
|
||||
@else if $index == 0 {
|
||||
@warn "List index 0 must be a non-zero integer for `remove-nth`.";
|
||||
}
|
||||
@else if abs($index) > length($list) {
|
||||
@warn "List index is #{$index} but list is only #{length($list)} item long for `remove-nth`.";
|
||||
}
|
||||
@else {
|
||||
|
||||
$result: ();
|
||||
$index: if($index < 0, length($list) + $index + 1, $index);
|
||||
|
||||
@for $i from 1 through length($list) {
|
||||
|
||||
@if $i != $index {
|
||||
$result: append($result, nth($list, $i));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@return $result;
|
||||
|
||||
}
|
||||
|
||||
/// Replaces a substring within another string.
|
||||
/// @author Hugo Giraudel
|
||||
/// @param {string} $string String.
|
||||
/// @param {string} $search Substring.
|
||||
/// @param {string} $replace Replacement.
|
||||
/// @return {string} Updated string.
|
||||
@function str-replace($string, $search, $replace: '') {
|
||||
|
||||
$index: str-index($string, $search);
|
||||
|
||||
@if $index {
|
||||
@return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
|
||||
}
|
||||
|
||||
@return $string;
|
||||
|
||||
}
|
||||
|
||||
/// Replaces a substring within each string in a list.
|
||||
/// @param {list} $strings List of strings.
|
||||
/// @param {string} $search Substring.
|
||||
/// @param {string} $replace Replacement.
|
||||
/// @return {list} Updated list of strings.
|
||||
@function str-replace-all($strings, $search, $replace: '') {
|
||||
|
||||
@each $string in $strings {
|
||||
$strings: set-nth($strings, index($strings, $string), str-replace($string, $search, $replace));
|
||||
}
|
||||
|
||||
@return $strings;
|
||||
|
||||
}
|
||||
|
||||
/// Gets a value from a map.
|
||||
/// @author Hugo Giraudel
|
||||
/// @param {map} $map Map.
|
||||
/// @param {string} $keys Key(s).
|
||||
/// @return {string} Value.
|
||||
@function val($map, $keys...) {
|
||||
|
||||
@if nth($keys, 1) == null {
|
||||
$keys: remove-nth($keys, 1);
|
||||
}
|
||||
|
||||
@each $key in $keys {
|
||||
$map: map-get($map, $key);
|
||||
}
|
||||
|
||||
@return $map;
|
||||
|
||||
}
|
||||
|
||||
// Mixins.
|
||||
|
||||
/// Sets the global box model.
|
||||
/// @param {string} $model Model (default is content).
|
||||
@mixin boxModel($model: 'content') {
|
||||
|
||||
$x: $model + '-box';
|
||||
|
||||
*, *:before, *:after {
|
||||
-moz-box-sizing: #{$x};
|
||||
-webkit-box-sizing: #{$x};
|
||||
box-sizing: #{$x};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Wraps @content in a @media block using a given breakpoint.
|
||||
/// @param {string} $breakpoint Breakpoint.
|
||||
/// @param {map} $queries Additional queries.
|
||||
@mixin breakpoint($breakpoint: null, $queries: null) {
|
||||
|
||||
$query: 'screen';
|
||||
|
||||
// Breakpoint.
|
||||
@if $breakpoint and map-has-key($breakpoints, $breakpoint) {
|
||||
$query: $query + ' and ' + map-get($breakpoints, $breakpoint);
|
||||
}
|
||||
|
||||
// Queries.
|
||||
@if $queries {
|
||||
@each $k, $v in $queries {
|
||||
$query: $query + ' and (' + $k + ':' + $v + ')';
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$query} {
|
||||
@content;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Wraps @content in a @media block targeting a specific orientation.
|
||||
/// @param {string} $orientation Orientation.
|
||||
@mixin orientation($orientation) {
|
||||
@media screen and (orientation: #{$orientation}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility mixin for containers.
|
||||
/// @param {mixed} $width Width.
|
||||
@mixin containers($width) {
|
||||
|
||||
// Locked?
|
||||
$lock: false;
|
||||
|
||||
@if length($width) == 2 {
|
||||
$width: nth($width, 1);
|
||||
$lock: true;
|
||||
}
|
||||
|
||||
// Modifiers.
|
||||
.container.\31 25\25 { width: 100%; max-width: $width * 1.25; min-width: $width; }
|
||||
.container.\37 5\25 { width: $width * 0.75; }
|
||||
.container.\35 0\25 { width: $width * 0.5; }
|
||||
.container.\32 5\25 { width: $width * 0.25; }
|
||||
|
||||
// Main class.
|
||||
.container {
|
||||
@if $lock {
|
||||
width: $width !important;
|
||||
}
|
||||
@else {
|
||||
width: $width;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Utility mixin for grid.
|
||||
/// @param {list} $gutters Column and row gutters (default is 40px).
|
||||
/// @param {string} $breakpointName Optional breakpoint name.
|
||||
@mixin grid($gutters: 40px, $breakpointName: null) {
|
||||
|
||||
// Gutters.
|
||||
@include grid-gutters($gutters);
|
||||
@include grid-gutters($gutters, \32 00\25, 2);
|
||||
@include grid-gutters($gutters, \31 50\25, 1.5);
|
||||
@include grid-gutters($gutters, \35 0\25, 0.5);
|
||||
@include grid-gutters($gutters, \32 5\25, 0.25);
|
||||
|
||||
// Cells.
|
||||
$x: '';
|
||||
|
||||
@if $breakpointName {
|
||||
$x: '\\28' + $breakpointName + '\\29';
|
||||
}
|
||||
|
||||
.\31 2u#{$x}, .\31 2u\24#{$x} { width: 100%; clear: none; margin-left: 0; }
|
||||
.\31 1u#{$x}, .\31 1u\24#{$x} { width: 91.6666666667%; clear: none; margin-left: 0; }
|
||||
.\31 0u#{$x}, .\31 0u\24#{$x} { width: 83.3333333333%; clear: none; margin-left: 0; }
|
||||
.\39 u#{$x}, .\39 u\24#{$x} { width: 75%; clear: none; margin-left: 0; }
|
||||
.\38 u#{$x}, .\38 u\24#{$x} { width: 66.6666666667%; clear: none; margin-left: 0; }
|
||||
.\37 u#{$x}, .\37 u\24#{$x} { width: 58.3333333333%; clear: none; margin-left: 0; }
|
||||
.\36 u#{$x}, .\36 u\24#{$x} { width: 50%; clear: none; margin-left: 0; }
|
||||
.\35 u#{$x}, .\35 u\24#{$x} { width: 41.6666666667%; clear: none; margin-left: 0; }
|
||||
.\34 u#{$x}, .\34 u\24#{$x} { width: 33.3333333333%; clear: none; margin-left: 0; }
|
||||
.\33 u#{$x}, .\33 u\24#{$x} { width: 25%; clear: none; margin-left: 0; }
|
||||
.\32 u#{$x}, .\32 u\24#{$x} { width: 16.6666666667%; clear: none; margin-left: 0; }
|
||||
.\31 u#{$x}, .\31 u\24#{$x} { width: 8.3333333333%; clear: none; margin-left: 0; }
|
||||
|
||||
.\31 2u\24#{$x} + *,
|
||||
.\31 1u\24#{$x} + *,
|
||||
.\31 0u\24#{$x} + *,
|
||||
.\39 u\24#{$x} + *,
|
||||
.\38 u\24#{$x} + *,
|
||||
.\37 u\24#{$x} + *,
|
||||
.\36 u\24#{$x} + *,
|
||||
.\35 u\24#{$x} + *,
|
||||
.\34 u\24#{$x} + *,
|
||||
.\33 u\24#{$x} + *,
|
||||
.\32 u\24#{$x} + *,
|
||||
.\31 u\24#{$x} + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
.\-11u#{$x} { margin-left: 91.6666666667% }
|
||||
.\-10u#{$x} { margin-left: 83.3333333333% }
|
||||
.\-9u#{$x} { margin-left: 75% }
|
||||
.\-8u#{$x} { margin-left: 66.6666666667% }
|
||||
.\-7u#{$x} { margin-left: 58.3333333333% }
|
||||
.\-6u#{$x} { margin-left: 50% }
|
||||
.\-5u#{$x} { margin-left: 41.6666666667% }
|
||||
.\-4u#{$x} { margin-left: 33.3333333333% }
|
||||
.\-3u#{$x} { margin-left: 25% }
|
||||
.\-2u#{$x} { margin-left: 16.6666666667% }
|
||||
.\-1u#{$x} { margin-left: 8.3333333333% }
|
||||
|
||||
}
|
||||
|
||||
/// Utility mixin for grid.
|
||||
/// @param {list} $gutters Gutters.
|
||||
/// @param {string} $class Optional class name.
|
||||
/// @param {integer} $multiplier Multiplier (default is 1).
|
||||
@mixin grid-gutters($gutters, $class: null, $multiplier: 1) {
|
||||
|
||||
// Expand gutters if it's not a list.
|
||||
@if length($gutters) == 1 {
|
||||
$gutters: ($gutters, 0);
|
||||
}
|
||||
|
||||
// Get column and row gutter values.
|
||||
$c: nth($gutters, 1);
|
||||
$r: nth($gutters, 2);
|
||||
|
||||
// Get class (if provided).
|
||||
$x: '';
|
||||
|
||||
@if $class {
|
||||
$x: '.' + $class;
|
||||
}
|
||||
|
||||
// Default.
|
||||
.row#{$x} > * { padding: ($r * $multiplier) 0 0 ($c * $multiplier); }
|
||||
.row#{$x} { margin: ($r * $multiplier * -1) 0 -1px ($c * $multiplier * -1); }
|
||||
|
||||
// Uniform.
|
||||
.row.uniform#{$x} > * { padding: ($c * $multiplier) 0 0 ($c * $multiplier); }
|
||||
.row.uniform#{$x} { margin: ($c * $multiplier * -1) 0 -1px ($c * $multiplier * -1); }
|
||||
|
||||
}
|
||||
|
||||
/// Wraps @content in vendorized keyframe blocks.
|
||||
/// @param {string} $name Name.
|
||||
@mixin keyframes($name) {
|
||||
|
||||
@-moz-keyframes #{$name} { @content; }
|
||||
@-webkit-keyframes #{$name} { @content; }
|
||||
@-ms-keyframes #{$name} { @content; }
|
||||
@keyframes #{$name} { @content; }
|
||||
|
||||
}
|
||||
|
||||
///
|
||||
/// Sets breakpoints.
|
||||
/// @param {map} $x Breakpoints.
|
||||
///
|
||||
@mixin skel-breakpoints($x: ()) {
|
||||
$breakpoints: $x !global;
|
||||
}
|
||||
|
||||
///
|
||||
/// Initializes layout module.
|
||||
/// @param {map} config Config.
|
||||
///
|
||||
@mixin skel-layout($config: ()) {
|
||||
|
||||
// Config.
|
||||
$configPerBreakpoint: ();
|
||||
|
||||
$z: map-get($config, 'breakpoints');
|
||||
|
||||
@if $z {
|
||||
$configPerBreakpoint: $z;
|
||||
}
|
||||
|
||||
// Reset.
|
||||
$x: map-get($config, 'reset');
|
||||
|
||||
@if $x {
|
||||
|
||||
/* Reset */
|
||||
|
||||
@include reset($x);
|
||||
|
||||
}
|
||||
|
||||
// Box model.
|
||||
$x: map-get($config, 'boxModel');
|
||||
|
||||
@if $x {
|
||||
|
||||
/* Box Model */
|
||||
|
||||
@include boxModel($x);
|
||||
|
||||
}
|
||||
|
||||
// Containers.
|
||||
$containers: map-get($config, 'containers');
|
||||
|
||||
@if $containers {
|
||||
|
||||
/* Containers */
|
||||
|
||||
.container {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
// Use default is $containers is just "true".
|
||||
@if $containers == true {
|
||||
$containers: 960px;
|
||||
}
|
||||
|
||||
// Apply base.
|
||||
@include containers($containers);
|
||||
|
||||
// Apply per-breakpoint.
|
||||
@each $name in map-keys($breakpoints) {
|
||||
|
||||
// Get/use breakpoint setting if it exists.
|
||||
$x: map-get($configPerBreakpoint, $name);
|
||||
|
||||
// Per-breakpoint config exists?
|
||||
@if $x {
|
||||
$y: map-get($x, 'containers');
|
||||
|
||||
// Setting exists? Use it.
|
||||
@if $y {
|
||||
$containers: $y;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Create @media block.
|
||||
@media screen and #{map-get($breakpoints, $name)} {
|
||||
@include containers($containers);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Grid.
|
||||
$grid: map-get($config, 'grid');
|
||||
|
||||
@if $grid {
|
||||
|
||||
/* Grid */
|
||||
|
||||
// Use defaults if $grid is just "true".
|
||||
@if $grid == true {
|
||||
$grid: ();
|
||||
}
|
||||
|
||||
// Sub-setting: Gutters.
|
||||
$grid-gutters: 40px;
|
||||
$x: map-get($grid, 'gutters');
|
||||
|
||||
@if $x {
|
||||
$grid-gutters: $x;
|
||||
}
|
||||
|
||||
// Rows.
|
||||
.row {
|
||||
border-bottom: solid 1px transparent;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.row > * {
|
||||
float: left;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.row:after, .row:before {
|
||||
content: '';
|
||||
display: block;
|
||||
clear: both;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.row.uniform > * > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.row.uniform > * > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// Gutters (0%).
|
||||
@include grid-gutters($grid-gutters, \30 \25, 0);
|
||||
|
||||
// Apply base.
|
||||
@include grid($grid-gutters);
|
||||
|
||||
// Apply per-breakpoint.
|
||||
@each $name in map-keys($breakpoints) {
|
||||
|
||||
// Get/use breakpoint setting if it exists.
|
||||
$x: map-get($configPerBreakpoint, $name);
|
||||
|
||||
// Per-breakpoint config exists?
|
||||
@if $x {
|
||||
$y: map-get($x, 'grid');
|
||||
|
||||
// Setting exists?
|
||||
@if $y {
|
||||
|
||||
// Sub-setting: Gutters.
|
||||
$x: map-get($y, 'gutters');
|
||||
|
||||
@if $x {
|
||||
$grid-gutters: $x;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Create @media block.
|
||||
@media screen and #{map-get($breakpoints, $name)} {
|
||||
@include grid($grid-gutters, $name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Resets browser styles.
|
||||
/// @param {string} $mode Mode (default is 'normalize').
|
||||
@mixin reset($mode: 'normalize') {
|
||||
|
||||
@if $mode == 'normalize' {
|
||||
|
||||
// normalize.css v3.0.2 | MIT License | git.io/normalize
|
||||
html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}
|
||||
|
||||
}
|
||||
@else if $mode == 'full' {
|
||||
|
||||
// meyerweb.com/eric/tools/css/reset v2.0 | 20110126 | License: none (public domain)
|
||||
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline;}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block;}body{line-height:1;}ol,ul{list-style:none;}blockquote,q{quotes:none;}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;}table{border-collapse:collapse;border-spacing:0;}body{-webkit-text-size-adjust:none}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Vendorizes a declaration's property and/or value(s).
|
||||
/// @param {string} $property Property.
|
||||
/// @param {mixed} $value String/list of value(s).
|
||||
@mixin vendor($property, $value) {
|
||||
|
||||
// Determine if property should expand.
|
||||
$expandProperty: index($vendor-properties, $property);
|
||||
|
||||
// Determine if value should expand (and if so, add '-prefix-' placeholder).
|
||||
$expandValue: false;
|
||||
|
||||
@each $x in $value {
|
||||
@each $y in $vendor-values {
|
||||
@if $y == str-slice($x, 1, str-length($y)) {
|
||||
|
||||
$value: set-nth($value, index($value, $x), '-prefix-' + $x);
|
||||
$expandValue: true;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expand property?
|
||||
@if $expandProperty {
|
||||
@each $vendor in $vendor-prefixes {
|
||||
#{$vendor}#{$property}: #{str-replace-all($value, '-prefix-', $vendor)};
|
||||
}
|
||||
}
|
||||
|
||||
// Expand just the value?
|
||||
@elseif $expandValue {
|
||||
@each $vendor in $vendor-prefixes {
|
||||
#{$property}: #{str-replace-all($value, '-prefix-', $vendor)};
|
||||
}
|
||||
}
|
||||
|
||||
// Neither? Treat them as a normal declaration.
|
||||
@else {
|
||||
#{$property}: #{$value};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// Misc.
|
||||
$misc: (
|
||||
z-index-base: 10000
|
||||
);
|
||||
|
||||
// Duration.
|
||||
$duration: (
|
||||
transition: 0.2s
|
||||
);
|
||||
|
||||
// Size.
|
||||
$size: (
|
||||
border-radius: 0.35em,
|
||||
element-height: 2.75em,
|
||||
element-margin: 2em
|
||||
);
|
||||
|
||||
// Font.
|
||||
$font: (
|
||||
family: ('Source Sans Pro', Helvetica, sans-serif),
|
||||
family-fixed: ('Courier New', monospace),
|
||||
weight: 400,
|
||||
weight-bold: 400
|
||||
);
|
||||
|
||||
// Palette.
|
||||
$palette: (
|
||||
bg: #fff,
|
||||
fg: #a2a2a2,
|
||||
fg-bold: #787878,
|
||||
fg-light: #b2b2b2,
|
||||
border: #efefef,
|
||||
border-bg: #f7f7f7,
|
||||
border2: #dfdfdf,
|
||||
border2-bg: #e7e7e7,
|
||||
|
||||
accent1: (
|
||||
bg: #49bf9d,
|
||||
fg: mix(#49bf9d, #ffffff, 25%),
|
||||
fg-bold: #ffffff,
|
||||
fg-light: mix(#49bf9d, #ffffff, 40%),
|
||||
border: rgba(255,255,255,0.25),
|
||||
border-bg: rgba(255,255,255,0.075),
|
||||
border2: rgba(255,255,255,0.5),
|
||||
border2-bg: rgba(255,255,255,0.2)
|
||||
),
|
||||
|
||||
accent2: (
|
||||
bg: #1f1815,
|
||||
fg: rgba(255,255,255,0.5),
|
||||
fg-bold: #ffffff,
|
||||
fg-light: rgba(255,255,255,0.4),
|
||||
border: rgba(255,255,255,0.25),
|
||||
border-bg: rgba(255,255,255,0.075),
|
||||
border2: rgba(255,255,255,0.5),
|
||||
border2-bg: rgba(255,255,255,0.2)
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,450 @@
|
||||
/*
|
||||
thiessen.io — personal site
|
||||
Swiss-minimal direction. Display: Space Grotesk · Body: Hanken Grotesk · Mono: Space Mono.
|
||||
One accent (signal cobalt), used only for links and the live timeline node.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--paper: #f4f5f7;
|
||||
--paper-2: #ffffff;
|
||||
--ink: #15171c;
|
||||
--graphite: #565d66;
|
||||
--graphite-2: #8a919b;
|
||||
--mist: #e4e7ec;
|
||||
--signal: #1f44f0;
|
||||
--signal-weak: rgba(31, 68, 240, 0.10);
|
||||
|
||||
--font-display: 'Space Grotesk', 'Hanken Grotesk', system-ui, sans-serif;
|
||||
--font-body: 'Hanken Grotesk', system-ui, -apple-system, sans-serif;
|
||||
--font-mono: 'Space Mono', ui-monospace, 'SFMono-Regular', monospace;
|
||||
|
||||
--wrap: 1100px;
|
||||
}
|
||||
|
||||
/* ---------- reset ---------- */
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html { -webkit-text-size-adjust: 100%; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: var(--font-body);
|
||||
font-size: 1.0625rem;
|
||||
line-height: 1.65;
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
img { display: block; max-width: 100%; }
|
||||
|
||||
a { color: inherit; }
|
||||
|
||||
::selection { background: var(--signal); color: #fff; }
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--signal);
|
||||
outline-offset: 3px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* ---------- layout ---------- */
|
||||
|
||||
.wrap {
|
||||
width: 100%;
|
||||
max-width: var(--wrap);
|
||||
margin: 0 auto;
|
||||
padding: 0 clamp(1.25rem, 5vw, 3.5rem);
|
||||
}
|
||||
|
||||
.section {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 11rem) minmax(0, 1fr);
|
||||
gap: 2.75rem;
|
||||
padding: clamp(3.5rem, 7vw, 6rem) 0;
|
||||
border-top: 1px solid var(--mist);
|
||||
}
|
||||
|
||||
.section__label {
|
||||
position: sticky;
|
||||
top: 2rem;
|
||||
align-self: start;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--graphite-2);
|
||||
}
|
||||
|
||||
/* ---------- top bar ---------- */
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1.75rem 0;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.74rem;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.topbar__name { color: var(--ink); text-transform: uppercase; }
|
||||
|
||||
.status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.16em;
|
||||
color: var(--graphite);
|
||||
}
|
||||
|
||||
.status__dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--signal);
|
||||
box-shadow: 0 0 0 4px var(--signal-weak);
|
||||
animation: pulse 2.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 4px var(--signal-weak); }
|
||||
50% { box-shadow: 0 0 0 7px rgba(31, 68, 240, 0.03); }
|
||||
}
|
||||
|
||||
/* ---------- hero ---------- */
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.55fr) minmax(0, 0.7fr);
|
||||
gap: clamp(2rem, 5vw, 4rem);
|
||||
align-items: center;
|
||||
padding: clamp(2rem, 5vw, 3.5rem) 0 clamp(3.5rem, 7vw, 5.5rem);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.74rem;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--graphite-2);
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.hero__name {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
font-size: clamp(3rem, 9.5vw, 7rem);
|
||||
line-height: 0.92;
|
||||
letter-spacing: -0.035em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero__lead {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 400;
|
||||
font-size: clamp(1.35rem, 2.7vw, 2rem);
|
||||
line-height: 1.18;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ink);
|
||||
max-width: 30ch;
|
||||
margin: 1.75rem 0 0;
|
||||
}
|
||||
|
||||
.hero__meta {
|
||||
margin: 1.5rem 0 0;
|
||||
color: var(--graphite);
|
||||
max-width: 46ch;
|
||||
}
|
||||
|
||||
.hero__meta strong { color: var(--ink); font-weight: 600; }
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.25rem 2rem;
|
||||
margin: 2.25rem 0 0;
|
||||
}
|
||||
|
||||
.actions a {
|
||||
font-weight: 500;
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
border-bottom: 1.5px solid var(--ink);
|
||||
padding-bottom: 2px;
|
||||
transition: color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.actions a:hover { color: var(--signal); border-color: var(--signal); }
|
||||
|
||||
.portrait {
|
||||
justify-self: end;
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.portrait img {
|
||||
width: 100%;
|
||||
aspect-ratio: 4 / 5;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--mist);
|
||||
filter: grayscale(100%) contrast(1.02);
|
||||
transition: filter 0.4s ease;
|
||||
}
|
||||
|
||||
.portrait img:hover { filter: grayscale(0%); }
|
||||
|
||||
/* ---------- profile ---------- */
|
||||
|
||||
.prose p {
|
||||
margin: 0 0 1.25rem;
|
||||
color: var(--graphite);
|
||||
max-width: 62ch;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.prose p:last-child { margin-bottom: 0; }
|
||||
|
||||
.prose strong { color: var(--ink); font-weight: 600; }
|
||||
|
||||
/* ---------- experience timeline (the signature) ---------- */
|
||||
|
||||
.roles { list-style: none; margin: 0; padding: 0; }
|
||||
|
||||
.role {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5rem minmax(0, 1fr);
|
||||
column-gap: 1.5rem;
|
||||
}
|
||||
|
||||
.role__rail { position: relative; }
|
||||
|
||||
.role__rail::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: var(--mist);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.role:first-child .role__rail::before { top: 0.62rem; }
|
||||
.role:last-child .role__rail::before { bottom: auto; height: 0.62rem; }
|
||||
|
||||
.role__node {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0.62rem;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: var(--paper);
|
||||
border: 1.5px solid var(--ink);
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.role--live .role__node {
|
||||
background: var(--signal);
|
||||
border-color: var(--signal);
|
||||
box-shadow: 0 0 0 4px var(--signal-weak);
|
||||
animation: pulse 2.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.role__body { padding-bottom: 2.75rem; }
|
||||
.role:last-child .role__body { padding-bottom: 0; }
|
||||
|
||||
.role__year {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--graphite-2);
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.role--live .role__year { color: var(--signal); }
|
||||
|
||||
.role__head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 0.4rem 0.85rem;
|
||||
}
|
||||
|
||||
.role__co {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
font-size: 1.4rem;
|
||||
letter-spacing: -0.015em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.role__loc {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--graphite-2);
|
||||
}
|
||||
|
||||
.role__title {
|
||||
margin: 0.35rem 0 0;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.role__desc {
|
||||
margin: 0.55rem 0 0;
|
||||
color: var(--graphite);
|
||||
max-width: 58ch;
|
||||
}
|
||||
|
||||
/* ---------- stack ---------- */
|
||||
|
||||
.stack { display: flex; flex-direction: column; gap: 1.6rem; }
|
||||
|
||||
.group {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 10rem) minmax(0, 1fr);
|
||||
gap: 0.5rem 1.5rem;
|
||||
align-items: baseline;
|
||||
padding-bottom: 1.6rem;
|
||||
border-bottom: 1px solid var(--mist);
|
||||
}
|
||||
|
||||
.stack .group:last-child { border-bottom: 0; padding-bottom: 0; }
|
||||
|
||||
.group__label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--graphite-2);
|
||||
}
|
||||
|
||||
.group__tags {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.95;
|
||||
color: var(--graphite-2);
|
||||
}
|
||||
|
||||
.group__tags .t { color: var(--ink); }
|
||||
|
||||
/* ---------- contact ---------- */
|
||||
|
||||
.contact__intro {
|
||||
color: var(--graphite);
|
||||
max-width: 50ch;
|
||||
font-size: 1.1rem;
|
||||
margin: 0 0 1.75rem;
|
||||
}
|
||||
|
||||
.contact__email {
|
||||
display: inline-block;
|
||||
font-family: var(--font-display);
|
||||
font-weight: 500;
|
||||
font-size: clamp(1.7rem, 4.5vw, 3rem);
|
||||
letter-spacing: -0.025em;
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.contact__email:hover { color: var(--signal); }
|
||||
|
||||
.contact__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.25rem 2rem;
|
||||
margin: 2rem 0 0;
|
||||
}
|
||||
|
||||
.contact__links a {
|
||||
font-weight: 500;
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
border-bottom: 1.5px solid var(--ink);
|
||||
padding-bottom: 2px;
|
||||
transition: color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.contact__links a:hover { color: var(--signal); border-color: var(--signal); }
|
||||
|
||||
/* ---------- colophon ---------- */
|
||||
|
||||
.colophon {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem 1.5rem;
|
||||
padding: 2.25rem 0 3.5rem;
|
||||
border-top: 1px solid var(--mist);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--graphite-2);
|
||||
}
|
||||
|
||||
/* ---------- 404 ---------- */
|
||||
|
||||
.notfound { padding: clamp(4rem, 12vw, 9rem) 0; }
|
||||
|
||||
.notfound h1 {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
font-size: clamp(2.5rem, 8vw, 5rem);
|
||||
letter-spacing: -0.03em;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.notfound p { color: var(--graphite); max-width: 40ch; }
|
||||
|
||||
.notfound a {
|
||||
color: var(--ink);
|
||||
border-bottom: 1.5px solid var(--ink);
|
||||
text-decoration: none;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.notfound a:hover { color: var(--signal); border-color: var(--signal); }
|
||||
|
||||
/* ---------- responsive ---------- */
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.section { grid-template-columns: 1fr; gap: 1.5rem; }
|
||||
.section__label { position: static; top: auto; }
|
||||
.group { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.hero {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2.5rem;
|
||||
}
|
||||
.portrait { justify-self: start; order: -1; max-width: 150px; }
|
||||
.portrait img { aspect-ratio: 1 / 1; }
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.topbar { flex-wrap: wrap; gap: 0.4rem 1rem; }
|
||||
}
|
||||
|
||||
@media (max-width: 460px) {
|
||||
.topbar { font-size: 0.68rem; letter-spacing: 0.05em; }
|
||||
.status { letter-spacing: 0.12em; }
|
||||
}
|
||||
|
||||
/* ---------- motion ---------- */
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { animation: none !important; transition: none !important; }
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
class Footer extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div id="footer">
|
||||
<div className="inner">
|
||||
<ul className="icons">
|
||||
<li><a href="#" className="icon fa-gitlab"><span className="label">Github</span></a></li>
|
||||
<li><a href="https://www.linkedin.com/in/dennis-thiessen" className="icon fa-linkedin"><span className="label">Linkedin</span></a></li>
|
||||
<li><a href="mailto:dennis@thiessen.io" className="icon fa-envelope-o"><span className="label">Email</span></a></li>
|
||||
</ul>
|
||||
<ul className="copyright">
|
||||
<li>© Gatsby Starter Strata</li><li>Design: <a href="http://html5up.net">HTML5 UP</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Footer
|
||||
@@ -1,50 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
import Lightbox from "yet-another-react-lightbox"
|
||||
import "yet-another-react-lightbox/styles.css"
|
||||
|
||||
const Gallery = ({ images = [] }) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [photoIndex, setPhotoIndex] = useState(0)
|
||||
|
||||
const openLightbox = (index) => {
|
||||
setPhotoIndex(index)
|
||||
setIsOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && (
|
||||
<Lightbox
|
||||
open={isOpen}
|
||||
close={() => setIsOpen(false)}
|
||||
index={photoIndex}
|
||||
slides={images.map(img => ({
|
||||
src: img.src,
|
||||
title: img.caption,
|
||||
description: img.description
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
<div className="row">
|
||||
{images.map((obj, i) => (
|
||||
<article className="6u 12u$(xsmall) work-item" key={i}>
|
||||
<a
|
||||
className="image fit thumb"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
openLightbox(i)
|
||||
}}
|
||||
href={obj.src}
|
||||
>
|
||||
<img src={obj.thumbnail} alt={obj.caption} />
|
||||
</a>
|
||||
<h3>{obj.caption}</h3>
|
||||
<p>{obj.description}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Gallery
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import Footer from './Footer'
|
||||
import avatar from '../assets/images/avatar.jpg'
|
||||
|
||||
class Header extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<header id="header">
|
||||
<div className="inner">
|
||||
<a href="#" className="image avatar"><img src={avatar} alt="Dennis Thießen" /></a>
|
||||
<h1><strong>I'm Dennis Thiessen</strong>, a swiss-based <br />
|
||||
freelance software, data & ai engineer.</h1>
|
||||
</div>
|
||||
<Footer />
|
||||
</header>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Header
|
||||
@@ -1,19 +1,6 @@
|
||||
import React from 'react'
|
||||
import '../assets/scss/main.scss'
|
||||
import '../assets/style.css'
|
||||
|
||||
import Header from './Header'
|
||||
const Layout = ({ children }) => <>{children}</>
|
||||
|
||||
class Template extends React.Component {
|
||||
render() {
|
||||
const { children } = this.props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Template
|
||||
export default Layout
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
|
||||
const NotFoundPage = () => (
|
||||
<Layout>
|
||||
<h1>NOT FOUND</h1>
|
||||
<p>You just hit a route that doesn't exist... the sadness.</p>
|
||||
<Helmet>
|
||||
<html lang="en" />
|
||||
<title>Not found — Dennis Thiessen</title>
|
||||
</Helmet>
|
||||
<div className="wrap notfound">
|
||||
<p className="eyebrow">Error 404</p>
|
||||
<h1>This page took a wrong turn.</h1>
|
||||
<p>
|
||||
The page you were after isn't here. <a href="/">Head back home</a>.
|
||||
</p>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
|
||||
@@ -2,294 +2,201 @@ import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
// import Lightbox from 'react-images'
|
||||
import Gallery from '../components/Gallery'
|
||||
import avatar from '../assets/images/avatar.jpg'
|
||||
|
||||
import thumb01 from '../assets/images/thumbs/01.jpg'
|
||||
import thumb02 from '../assets/images/thumbs/02.jpg'
|
||||
import thumb03 from '../assets/images/thumbs/03.jpg'
|
||||
import thumb04 from '../assets/images/thumbs/04.jpg'
|
||||
import thumb05 from '../assets/images/thumbs/05.jpg'
|
||||
import thumb06 from '../assets/images/thumbs/06.jpg'
|
||||
import thumb07 from '../assets/images/thumbs/07.jpg'
|
||||
import thumb08 from '../assets/images/thumbs/08.jpg'
|
||||
import thumb09 from '../assets/images/thumbs/09.jpg'
|
||||
const SITE_TITLE = 'Dennis Thiessen — Data, Analytics & AI Engineer'
|
||||
const SITE_DESCRIPTION =
|
||||
'Dennis Thiessen is a Staff Data, Analytics & AI Engineer in Bern, Switzerland, building the data pipelines and ML infrastructure that production systems run on.'
|
||||
|
||||
import full01 from '../assets/images/fulls/01.jpg'
|
||||
import full02 from '../assets/images/fulls/02.jpg'
|
||||
import full03 from '../assets/images/fulls/03.jpg'
|
||||
import full04 from '../assets/images/fulls/04.jpg'
|
||||
import full05 from '../assets/images/fulls/05.jpg'
|
||||
import full06 from '../assets/images/fulls/06.jpg'
|
||||
import full07 from '../assets/images/fulls/07.jpg'
|
||||
import full08 from '../assets/images/fulls/08.jpg'
|
||||
import full09 from '../assets/images/fulls/09.jpg'
|
||||
const EMAIL = 'dennis@thiessen.io'
|
||||
const LINKEDIN = 'https://www.linkedin.com/in/dennis-thiessen'
|
||||
|
||||
const DEFAULT_IMAGES = [
|
||||
{ id: '9', src: full09, thumbnail: thumb09, caption: 'Swisscom (Switzerland) AG, Bern, Switzerland', description: 'Working as a (Senior) Data, Analytics and AI Engineer.'},
|
||||
{ id: '8', src: full08, thumbnail: thumb08, caption: 'Robert Bosch GmbH, Dresden, Germany', description: 'Working as a (Senior) Data Engineer for 300mm Semiconductor Lab Startup.'},
|
||||
{ id: '6', src: full06, thumbnail: thumb06, caption: 'Fraunhofer CML, Hamburg, Germany', description: 'Worked as Software Engineer and Research Associate with development tasks in C#, .NET and Python for workforce scheduling and machine learning.'},
|
||||
{ id: '5', src: full05, thumbnail: thumb05, caption: 'Vizrt, Bergen, Norway', description: 'Worked as DevOps-Engineer with development tasks in Python and C++ for a backend transcoding engine. Video & Audio Output Test Automation as well as CI & CD with Jenkins.'},
|
||||
{ id: '4', src: full04, thumbnail: thumb04, caption: 'Generali, Hamburg, Germany', description: 'Worked as Software-Developer and IT Consultant for a web-based workflow application written in Java, using OracleDB while migrating from BPM Process Engine to Camunda BPM. Moreover doing a (successful) PoC with Behaviour Driven Development.'},
|
||||
{ id: '3', src: full03, thumbnail: thumb03, caption: 'Capgemini, Hamburg, Germany', description: 'Worked as Software-Developer in a multi-national distributed team for a international logistics client. Development of a package track & trace application in Java and with great focus on Test Automation.'},
|
||||
{ id: '1', src: full01, thumbnail: thumb01, caption: 'Freelance - RiskAhead.net', description: 'Side-project where I developed a native android app in Java using REST services in PHP with access to a MySQL database. Huge focus on performance and scalability.'},
|
||||
{ id: '2', src: full02, thumbnail: thumb02, caption: 'Freelance - MyIdealAd.com', description: 'Freelance side-project with process automation in ZAPIER, Python scripting, development and hosting of a Wordpress Web-Site.'}
|
||||
];
|
||||
// Career as a pipeline — most recent first. Each node is a role.
|
||||
const ROLES = [
|
||||
{
|
||||
year: '2023 — PRESENT',
|
||||
company: 'Swisscom',
|
||||
location: 'Bern, CH',
|
||||
title: 'Staff Data, Analytics & AI Engineer',
|
||||
description:
|
||||
'ETL and streaming pipelines in Python, Kafka and SAP BODS feeding a Teradata warehouse — and migrating the legacy stack onto AWS (Glue, Redshift, Lambda, Step Functions, Airflow).',
|
||||
live: true,
|
||||
},
|
||||
{
|
||||
year: '2020 — 2023',
|
||||
company: 'Bosch Semiconductor',
|
||||
location: 'Dresden, DE',
|
||||
title: 'Senior Data Engineer',
|
||||
description:
|
||||
'Containerised ML inference (Docker, Kubernetes, Ansible) into 24/7 semiconductor production lines, and built Python, Java and C# data services over Oracle and Hadoop/Impala.',
|
||||
},
|
||||
{
|
||||
year: '2018 — 2020',
|
||||
company: 'Fraunhofer CML',
|
||||
location: 'Hamburg, DE',
|
||||
title: 'Research Software Engineer',
|
||||
description:
|
||||
'Optimisation-based crew-scheduling decision support in C#/.NET, plus research microservices and a Jenkins CI/CD pipeline with quality gates.',
|
||||
},
|
||||
{
|
||||
year: '2017 — 2018',
|
||||
company: 'Vizrt',
|
||||
location: 'Bergen, NO',
|
||||
title: 'DevOps Engineer',
|
||||
description:
|
||||
'A distributed video-transcoding backend in Python and C++, with automated audio/video integration testing for reliable releases.',
|
||||
},
|
||||
{
|
||||
year: '2015 — 2017',
|
||||
company: 'Generali',
|
||||
location: 'Hamburg, DE',
|
||||
title: 'Software Engineer & IT Consultant',
|
||||
description:
|
||||
'A Java/J2EE workflow portal on Oracle, and introduced behaviour-driven development and RPA automation (UiPath, Camunda).',
|
||||
},
|
||||
]
|
||||
|
||||
class HomeIndex extends React.Component {
|
||||
const STACK = [
|
||||
{ label: 'Languages', items: ['Python', 'SQL', 'Java', 'C#', 'TypeScript', 'C++'] },
|
||||
{ label: 'Data & Pipelines', items: ['ETL / ELT', 'Kafka', 'Airflow', 'SAP BODS', 'Teradata', 'Hadoop / Impala'] },
|
||||
{ label: 'Cloud & Infra', items: ['AWS', 'Docker', 'Kubernetes', 'Ansible', 'CI/CD'] },
|
||||
{ label: 'ML & Observability', items: ['PyTorch', 'scikit-learn', 'ELK', 'Grafana', 'Prometheus'] },
|
||||
{ label: 'Credentials', items: ['M.Eng., UniBw München', 'AWS Solutions Architect – Associate', 'iSAQB CPSA-F'] },
|
||||
]
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const Tags = ({ items }) =>
|
||||
items.map((item, i) => (
|
||||
<React.Fragment key={item}>
|
||||
{i > 0 && ' · '}
|
||||
<span className="t">{item}</span>
|
||||
</React.Fragment>
|
||||
))
|
||||
|
||||
this.state = {
|
||||
invalid: false,
|
||||
displayErrors: false,
|
||||
res: null
|
||||
}
|
||||
this.handleSubmit = this.handleSubmit.bind(this)
|
||||
this.closeLightbox = this.closeLightbox.bind(this);
|
||||
this.gotoNext = this.gotoNext.bind(this);
|
||||
this.gotoPrevious = this.gotoPrevious.bind(this);
|
||||
this.openLightbox = this.openLightbox.bind(this);
|
||||
this.handleClickImage = this.handleClickImage.bind(this);
|
||||
}
|
||||
const IndexPage = () => (
|
||||
<Layout>
|
||||
<Helmet>
|
||||
<html lang="en" />
|
||||
<meta charSet="utf-8" />
|
||||
<title>{SITE_TITLE}</title>
|
||||
<meta name="description" content={SITE_DESCRIPTION} />
|
||||
<meta name="theme-color" content="#15171c" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap"
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
if (!event.target.checkValidity()) {
|
||||
this.setState({
|
||||
invalid: true,
|
||||
displayErrors: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const form = event.target;
|
||||
const data = new FormData(form);
|
||||
<div className="wrap">
|
||||
<header className="topbar">
|
||||
<span className="topbar__name">Dennis Thiessen</span>
|
||||
<span className="status">
|
||||
<span className="status__dot" aria-hidden="true" />
|
||||
Open to new work
|
||||
</span>
|
||||
</header>
|
||||
|
||||
for (let name of data.keys()) {
|
||||
const input = form.elements[name];
|
||||
const parserName = input.dataset.parse;
|
||||
console.log('parser name is', parserName);
|
||||
if (parserName) {
|
||||
const parsedValue = inputParsers[parserName](data.get(name))
|
||||
data.set(name, parsedValue);
|
||||
}
|
||||
}
|
||||
<section className="hero">
|
||||
<div className="hero__text">
|
||||
<p className="eyebrow">Data · Analytics · AI Engineering</p>
|
||||
<h1 className="hero__name">Dennis Thiessen</h1>
|
||||
<p className="hero__lead">
|
||||
I build the pipelines and platforms that move data and run models in production.
|
||||
</p>
|
||||
<p className="hero__meta">
|
||||
<strong>Staff Data, Analytics & AI Engineer at Swisscom</strong> — based in Bern,
|
||||
Switzerland, with 8+ years across data engineering, ML infrastructure and DevOps.
|
||||
</p>
|
||||
<nav className="actions">
|
||||
<a href={`mailto:${EMAIL}`}>{EMAIL}</a>
|
||||
<a href={LINKEDIN} target="_blank" rel="noopener noreferrer">
|
||||
LinkedIn ↗
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="portrait">
|
||||
<img src={avatar} alt="Dennis Thiessen" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
data.append('token', 'gdfgdfashdf');
|
||||
data.append('receiver', 'dennis@thiessen.io');
|
||||
data.append('subject', 'Contact from www.thiessen.io');
|
||||
|
||||
this.setState({
|
||||
res: stringifyFormData(data),
|
||||
invalid: false,
|
||||
displayErrors: false,
|
||||
});
|
||||
|
||||
fetch('https://www.riskahead.de/api/v1/web/sendcustommail', {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
});
|
||||
}
|
||||
|
||||
openLightbox (index, event) {
|
||||
event.preventDefault();
|
||||
this.setState({
|
||||
currentImage: index,
|
||||
lightboxIsOpen: true,
|
||||
});
|
||||
}
|
||||
closeLightbox () {
|
||||
this.setState({
|
||||
currentImage: 0,
|
||||
lightboxIsOpen: false,
|
||||
});
|
||||
}
|
||||
gotoPrevious () {
|
||||
this.setState({
|
||||
currentImage: this.state.currentImage - 1,
|
||||
});
|
||||
}
|
||||
gotoNext () {
|
||||
this.setState({
|
||||
currentImage: this.state.currentImage + 1,
|
||||
});
|
||||
}
|
||||
handleClickImage () {
|
||||
if (this.state.currentImage === this.props.images.length - 1) return;
|
||||
|
||||
this.gotoNext();
|
||||
}
|
||||
|
||||
render() {
|
||||
const siteTitle = "Dennis Thiessen - IT Software Engineering"
|
||||
const siteDescription = "Portfolio of Dennis Thiessen - IT Software Engineering"
|
||||
const { res, invalid, displayErrors } = this.state;
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Helmet>
|
||||
<title>{siteTitle}</title>
|
||||
<meta name="description" content={siteDescription}/>
|
||||
<html lang="en" />
|
||||
<meta charset="utf-8"/>
|
||||
</Helmet>
|
||||
|
||||
<div id="main">
|
||||
|
||||
<section id="one">
|
||||
<header className="major">
|
||||
<h3>Are you looking for a freelance Full-Stack Developer / Consultant? <br />Look no more!</h3>
|
||||
</header>
|
||||
<p>I'm a <b>Software, Data & AI Engineer</b> and <b>Consultant</b> who has work experience in <b>Java</b>, <b>C#</b> and <b>Python</b>. I did multiple projects for various companies and below you can find a short resume and project portfolio.
|
||||
If you are looking for a passionate <b>IT Freelancer</b>, don't hesitate to contact me. I mostly work in the D-A-CH area but I am also available for international remote work.</p>
|
||||
|
||||
<p>Here is a short enumeration of my skills & tools. Further below you can find the project & companies I have worked with. For a full CV, or if you need further information, just contact me.</p>
|
||||
|
||||
<p><strong>Programming & Script & Markup Languages: </strong></p>
|
||||
<ul>
|
||||
<li>Python</li>
|
||||
<li>Java, J2EE</li>
|
||||
<li>JavaScript</li>
|
||||
<li>C# & .NET</li>
|
||||
<li>PHP</li>
|
||||
<li>HTML & CSS</li>
|
||||
<li>XML, JSON & YAML</li>
|
||||
</ul>
|
||||
<p><strong>Databases & DWH:</strong></p>
|
||||
<ul>
|
||||
<li>MySQL</li>
|
||||
<li>PostgreSQL</li>
|
||||
<li>Oracle</li>
|
||||
<li>Teradata DWH</li>
|
||||
</ul>
|
||||
<p><strong>Tools & Frameworks:</strong></p>
|
||||
<ul>
|
||||
<li>IDEs: IntelliJ, Android Studio, VS Code</li>
|
||||
<li>Versioning: git (Gitlab, Gitea, Github)</li>
|
||||
<li>Project Management: JIRA, Confluence</li>
|
||||
<li>RPA: UIPath</li>
|
||||
<li>Test Automation: JUnit, Selenium, Serenity BDD</li>
|
||||
<li>Frameworks: Pandas, Numpy, Torch, .NET, Entity, Spring, PyMath</li>
|
||||
<li>Build & Virtualization: Jenkins, Docker, Kubernetes, Gitlab CI/CD</li>
|
||||
<li>Web: Nginx, RESTless Web-Services</li>
|
||||
<li>OS: Win, MacOs, Linux (Debian, RedHat, Ubuntu)</li>
|
||||
</ul>
|
||||
<p><strong>Certifications:</strong></p>
|
||||
<ul>
|
||||
<li>AWS Certified Solutions Architect – Associate (AWS)</li>
|
||||
<li>Certified Professional for Software Architecture, Foundation Level (ISAQB)</li>
|
||||
<li>Robotic Process Automation / UIPath Developer Training (UIPath)</li>
|
||||
<li>Camunda BPM Process Engine Basic & Advanced Training (Camunda Services GmbH)</li>
|
||||
<li>ITIL v3 Foundation Certificate in IT Service Management (Serview GmbH)</li>
|
||||
<li>IT Project Management (Integrata AG)</li>
|
||||
</ul>
|
||||
<p><strong>Spoken Languages:</strong></p>
|
||||
<ul>
|
||||
<li>German (mother tongue)</li>
|
||||
<li>English (Fluent)</li>
|
||||
<li>Russian (Basic)</li>
|
||||
<li>Norwegian (Basic)</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="two">
|
||||
<h2>Recent Work</h2>
|
||||
|
||||
<Gallery images={DEFAULT_IMAGES.map(({ id, src, thumbnail, caption, description }) => ({
|
||||
src,
|
||||
thumbnail,
|
||||
caption,
|
||||
description
|
||||
}))} />
|
||||
|
||||
</section>
|
||||
|
||||
<section id="three">
|
||||
<h2>Send request</h2>
|
||||
<p>You can contact me via this contact form. <br />I will get in touch with you <b>ASAP</b>.</p>
|
||||
<div className="row">
|
||||
<div className="8u 12u$(small)">
|
||||
<form onSubmit={this.handleSubmit} noValidate className={displayErrors ? 'displayErrors' : ''}>
|
||||
<div className="row uniform 50%">
|
||||
<div className="6u 12u$(xsmall)"><input type="text" name="name" id="name" placeholder="Name" required/></div>
|
||||
<div className="6u 12u$(xsmall)"><input type="email" name="email" id="email" placeholder="Email" required/></div>
|
||||
<div className="12u"><textarea name="text" id="text" placeholder="Message" rows="4" required></textarea></div>
|
||||
</div>
|
||||
<ul className="actions">
|
||||
<br />
|
||||
<li><input type="submit" value="Send Message" /></li>
|
||||
</ul>
|
||||
</form>
|
||||
|
||||
<div className="res-block">
|
||||
{invalid && (
|
||||
<ShakingError text="Form is not valid, please check your input." />
|
||||
)}
|
||||
{!invalid && res && (
|
||||
<ShakingError text="Message sent successfully." />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div className="4u 12u$(small)">
|
||||
<ul className="labeled-icons">
|
||||
<li>
|
||||
<h3 className="icon fa-home"><span className="label">Address</span></h3>
|
||||
Region Bern / Zurich<br />
|
||||
Switzerland
|
||||
</li>
|
||||
<li>
|
||||
<h3 className="icon fa-mobile"><span className="label">Phone</span></h3>
|
||||
(on request)
|
||||
</li>
|
||||
<li>
|
||||
<h3 className="icon fa-envelope-o"><span className="label">Email</span></h3>
|
||||
<a href="mailto:dennis@thiessen.io">dennis@thiessen.io</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section className="section">
|
||||
<h2 className="section__label">Profile</h2>
|
||||
<div className="prose">
|
||||
<p>
|
||||
I work where <strong>software engineering meets operations</strong> — designing ETL/ELT
|
||||
and streaming pipelines, containerising ML inference, and turning monolithic data
|
||||
processes into event-driven, serverless systems.
|
||||
</p>
|
||||
<p>
|
||||
Right now I'm at Swisscom in Bern, moving a legacy Teradata and Oracle stack onto
|
||||
AWS. What I care about most is <strong>automation and reliability</strong>: systems that
|
||||
run themselves, recover on their own, and don't page anyone at 3 a.m.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="section">
|
||||
<h2 className="section__label">Experience</h2>
|
||||
<ol className="roles">
|
||||
{ROLES.map((role) => (
|
||||
<li className={`role${role.live ? ' role--live' : ''}`} key={role.company}>
|
||||
<div className="role__rail" aria-hidden="true">
|
||||
<span className="role__node" />
|
||||
</div>
|
||||
<div className="role__body">
|
||||
<p className="role__year">{role.year}</p>
|
||||
<div className="role__head">
|
||||
<h3 className="role__co">{role.company}</h3>
|
||||
<span className="role__loc">{role.location}</span>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
}
|
||||
<p className="role__title">{role.title}</p>
|
||||
<p className="role__desc">{role.description}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
const inputParsers = {
|
||||
uppercase(input) {
|
||||
return input.toUpperCase();
|
||||
},
|
||||
number(input) {
|
||||
return parseFloat(input);
|
||||
},
|
||||
};
|
||||
<section className="section">
|
||||
<h2 className="section__label">Stack</h2>
|
||||
<div className="stack">
|
||||
{STACK.map((group) => (
|
||||
<div className="group" key={group.label}>
|
||||
<p className="group__label">{group.label}</p>
|
||||
<p className="group__tags">
|
||||
<Tags items={group.items} />
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
class ShakingError extends React.Component {
|
||||
constructor() { super(); this.state = { key: 0 }; }
|
||||
<section className="section">
|
||||
<h2 className="section__label">Contact</h2>
|
||||
<div>
|
||||
<p className="contact__intro">
|
||||
Open to interesting problems in data, ML and platform engineering — in Switzerland or
|
||||
remote. Email is the fastest way to reach me.
|
||||
</p>
|
||||
<a className="contact__email" href={`mailto:${EMAIL}`}>
|
||||
{EMAIL}
|
||||
</a>
|
||||
<div className="contact__links">
|
||||
<a href={LINKEDIN} target="_blank" rel="noopener noreferrer">
|
||||
LinkedIn ↗
|
||||
</a>
|
||||
{/* Add a GitHub profile URL here when you have one you want public. */}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
componentWillReceiveProps() {
|
||||
this.setState({ key: ++this.state.key });
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div key={this.state.key} className="bounce">{this.props.text}</div>;
|
||||
}
|
||||
}
|
||||
<footer className="colophon">
|
||||
<span>© {new Date().getFullYear()} Dennis Thiessen</span>
|
||||
<span>Bern, Switzerland</span>
|
||||
</footer>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
function stringifyFormData(fd) {
|
||||
const data = {};
|
||||
for (let key of fd.keys()) {
|
||||
data[key] = fd.get(key);
|
||||
}
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
export default HomeIndex
|
||||
export default IndexPage
|
||||
|
||||