├── .eslintrc ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── assets └── Inter.ttf ├── components ├── Blog.js ├── Case.js ├── Contact.js ├── Footer.js ├── Layout.js ├── Navigation.js ├── Portfolio.js ├── SocialLink.js ├── StravaStats.js └── TimelineItem.js ├── hooks ├── useIsMounted.js └── useWindowSize.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── _document.js ├── api │ └── og.jsx ├── cv.js └── index.js ├── postcss.config.js ├── public ├── contact.svg ├── logoaccent.png ├── logoachterderegenboog.png ├── logocarglass.png ├── logocarlier.png ├── logodeckdeckgo.png ├── logokaraton.png ├── logonalo.png ├── logopoa.png ├── logorialto.png ├── me.jpeg ├── myAvatar.ico ├── personal.svg └── robots.txt ├── styles └── index.css ├── tailwind.config.js └── utils └── db └── index.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | .next 39 | .now 40 | 41 | .env.local -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": false, 4 | "endOfLine": "lf", 5 | "htmlWhitespaceSensitivity": "css", 6 | "insertPragma": false, 7 | "jsxBracketSameLine": false, 8 | "jsxSingleQuote": false, 9 | "printWidth": 80, 10 | "proseWrap": "always", 11 | "quoteProps": "as-needed", 12 | "requirePragma": false, 13 | "semi": false, 14 | "singleQuote": true, 15 | "tabWidth": 2, 16 | "trailingComma": "all", 17 | "useTabs": false 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.detectIndentation": true, 5 | "editor.fontFamily": "'Dank Mono', Menlo, Monaco, 'Courier New', monospace", 6 | "editor.fontLigatures": false, 7 | "editor.rulers": [80], 8 | "editor.snippetSuggestions": "top", 9 | "editor.wordBasedSuggestions": false, 10 | "editor.suggest.localityBonus": true, 11 | "editor.acceptSuggestionOnCommitCharacter": false, 12 | "[javascript]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode", 14 | "editor.suggestSelection": "recentlyUsed", 15 | "editor.suggest.showKeywords": false 16 | }, 17 | "editor.renderWhitespace": "boundary", 18 | "files.exclude": { 19 | "USE_GITIGNORE": true 20 | }, 21 | "files.defaultLanguage": "{activeEditorLanguage}", 22 | "javascript.validate.enable": false, 23 | "search.exclude": { 24 | "**/node_modules": true, 25 | "**/bower_components": true, 26 | "**/coverage": true, 27 | "**/dist": true, 28 | "**/build": true, 29 | "**/.build": true, 30 | "**/.gh-pages": true 31 | }, 32 | "editor.codeActionsOnSave": { 33 | "source.fixAll.eslint": false 34 | }, 35 | "eslint.validate": [ 36 | "javascript", 37 | "javascriptreact", 38 | "typescript", 39 | "typescriptreact" 40 | ], 41 | "eslint.options": { 42 | "env": { 43 | "browser": true, 44 | "jest/globals": true, 45 | "es6": true 46 | }, 47 | "parserOptions": { 48 | "ecmaVersion": 2019, 49 | "sourceType": "module", 50 | "ecmaFeatures": { 51 | "jsx": true 52 | } 53 | }, 54 | "rules": { 55 | "no-debugger": "off" 56 | } 57 | }, 58 | "workbench.colorTheme": "Night Owl", 59 | "workbench.iconTheme": "material-icon-theme", 60 | "breadcrumbs.enabled": true, 61 | "grunt.autoDetect": "off", 62 | "gulp.autoDetect": "off", 63 | "npm.runSilent": true, 64 | "explorer.confirmDragAndDrop": false, 65 | "editor.formatOnPaste": false, 66 | "editor.cursorSmoothCaretAnimation": true, 67 | "editor.smoothScrolling": true, 68 | "php.suggest.basic": false 69 | } 70 | -------------------------------------------------------------------------------- /assets/Inter.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasledoux1/website-thomas/958ba6b44cccd170c50cf3d830a338c7d68a3b96/assets/Inter.ttf -------------------------------------------------------------------------------- /components/Blog.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {faEye} from '@fortawesome/free-regular-svg-icons' 3 | import {FontAwesomeIcon} from '@fortawesome/react-fontawesome' 4 | 5 | const Blog = ({blogs}) => { 6 | const blogsToShow = blogs 7 | .sort((a, b) => b.page_views_count - a.page_views_count) 8 | .slice(0, 5) 9 | 10 | return ( 11 | <> 12 | {blogsToShow && 13 | blogsToShow.map((blog, i) => ( 14 | 22 |
27 | <> 28 |
29 |
30 |

31 | {blog.title} 32 |

33 |
34 | 35 | {blog.page_views_count} 36 |
37 |
38 |
39 |

{blog.description}

40 | 50 | 51 |
52 |
53 | ))} 54 | 60 | Read more blogs 61 | 62 | 63 | ) 64 | } 65 | 66 | export default Blog 67 | -------------------------------------------------------------------------------- /components/Case.js: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import React from 'react' 3 | 4 | export default function Case({url, logoWidth, logoAlt, img, tags, children}) { 5 | return ( 6 |
7 |
8 | 14 |
15 | {`Logo 22 |
23 | {children} 24 |
25 |
    26 | {tags.map((tag, i) => ( 27 |
  • 31 | {tag} 32 |
  • 33 | ))} 34 |
35 |
36 |
37 |
38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /components/Contact.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import Image from 'next/image' 3 | 4 | const Contact = () => { 5 | const [formResult, setFormResult] = useState('') 6 | const submitForm = ev => { 7 | ev.preventDefault() 8 | const form = ev.target 9 | const data = new FormData(form) 10 | const xhr = new XMLHttpRequest() 11 | xhr.open(form.method, form.action) 12 | xhr.setRequestHeader('Accept', 'application/json') 13 | xhr.onreadystatechange = () => { 14 | if (xhr.readyState !== XMLHttpRequest.DONE) return 15 | if (xhr.status === 200) { 16 | form.reset() 17 | setFormResult('ok') 18 | } else { 19 | setFormResult('error') 20 | } 21 | } 22 | xhr.send(data) 23 | } 24 | 25 | return ( 26 | <> 27 |
28 | Illustration of man sitting on a block 35 |
36 |
37 |

Drop me a message

38 |
submitForm(e)} 40 | action="https://formspree.io/xzbgjqdq" 41 | method="POST" 42 | > 43 |
44 | 47 | 55 |
56 |
57 | 60 |