add icons to technologies
All checks were successful
publish.yml / publish (push) Successful in 1m45s

This commit is contained in:
2026-04-16 17:02:27 +02:00
parent 13a6c24cd1
commit eb1dd36195
16 changed files with 218 additions and 20 deletions

View File

@@ -0,0 +1 @@
<svg fill="#FFFFFF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Angular</title><path d="M16.712 17.711H7.288l-1.204 2.916L12 24l5.916-3.373-1.204-2.916ZM14.692 0l7.832 16.855.814-12.856L14.692 0ZM9.308 0 .662 3.999l.814 12.856L9.308 0Zm-.405 13.93h6.198L12 6.396 8.903 13.93Z"/></svg>

After

Width:  |  Height:  |  Size: 313 B

View File

@@ -0,0 +1 @@
<svg fill="#FFFFFF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>sharp</title><path d="M14.2209.0875v5.9613l-3.7433.5012v3.5233l3.7433-.5012v3.5735l3.492-.4672V9.1047L24 8.2634l-.4631-3.4613-5.824.7794V.0875zM6.287 1.145v5.9618L0 7.9483l.4634 3.4613 5.8514-.7834 3.4644-.4637V1.145zm3.5198 9.7185l-3.492.4675v3.578l-6.183.8276.4633 3.4613 5.8239-.7796v5.4942h3.492v-5.962l3.6114-.4834V13.944l-3.7156.4973zm13.73 1.7405l-5.824.779-3.492.4673v9.0179h3.492v-5.9618L24 16.0652Z"/></svg>

After

Width:  |  Height:  |  Size: 510 B

View File

@@ -0,0 +1 @@
<svg fill="#FFFFFF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Docker</title><path d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1 @@
<svg fill="#FFFFFF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>.NET</title><path d="M24 8.77h-2.468v7.565h-1.425V8.77h-2.462V7.53H24zm-6.852 7.565h-4.821V7.53h4.63v1.24h-3.205v2.494h2.953v1.234h-2.953v2.604h3.396zm-6.708 0H8.882L4.78 9.863a2.896 2.896 0 0 1-.258-.51h-.036c.032.189.048.592.048 1.21v5.772H3.157V7.53h1.659l3.965 6.32c.167.261.275.442.323.54h.024c-.04-.233-.06-.629-.06-1.185V7.529h1.372zm-8.703-.693a.868.829 0 0 1-.869.829.868.829 0 0 1-.868-.83.868.829 0 0 1 .868-.828.868.829 0 0 1 .869.829Z"/></svg>

After

Width:  |  Height:  |  Size: 549 B

View File

@@ -0,0 +1 @@
<svg fill="#FFFFFF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Flutter</title><path d="M14.314 0L2.3 12 6 15.7 21.684.013h-7.357zm.014 11.072L7.857 17.53l6.47 6.47H21.7l-6.46-6.468 6.46-6.46h-7.37z"/></svg>

After

Width:  |  Height:  |  Size: 236 B

View File

@@ -0,0 +1 @@
<svg fill="#FFFFFF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Ionic</title><path d="M22.922 7.027l-.103-.23-.169.188c-.408.464-.928.82-1.505 1.036l-.159.061.066.155a9.745 9.745 0 0 1 .75 3.759c0 5.405-4.397 9.806-9.806 9.806-5.409 0-9.802-4.397-9.802-9.802 0-5.405 4.402-9.806 9.806-9.806 1.467 0 2.883.319 4.2.947l.155.075.066-.155a3.767 3.767 0 0 1 1.106-1.453l.197-.159-.225-.117A11.905 11.905 0 0 0 12.001.001c-6.619 0-12 5.381-12 12s5.381 12 12 12 12-5.381 12-12c0-1.73-.361-3.403-1.078-4.973zM12 6.53A5.476 5.476 0 0 0 6.53 12 5.476 5.476 0 0 0 12 17.47 5.476 5.476 0 0 0 17.47 12 5.479 5.479 0 0 0 12 6.53zm10.345-2.007a2.494 2.494 0 1 1-4.988 0 2.494 2.494 0 0 1 4.988 0z"/></svg>

After

Width:  |  Height:  |  Size: 719 B

View File

@@ -0,0 +1 @@
<svg fill="#FFFFFF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>OpenJDK</title><path d="M11.915 0 11.7.215C9.515 2.4 7.47 6.39 6.046 10.483c-1.064 1.024-3.633 2.81-3.711 3.551-.093.87 1.746 2.611 1.55 3.235-.198.625-1.304 1.408-1.014 1.939.1.188.823.011 1.277-.491a13.389 13.389 0 0 0-.017 2.14c.076.906.27 1.668.643 2.232.372.563.956.911 1.667.911.397 0 .727-.114 1.024-.264.298-.149.571-.33.91-.5.68-.34 1.634-.666 3.53-.604 1.903.062 2.872.39 3.559.704.687.314 1.15.664 1.925.664.767 0 1.395-.336 1.807-.9.412-.563.631-1.33.72-2.24.06-.623.055-1.32 0-2.066.454.45 1.117.604 1.213.424.29-.53-.816-1.314-1.013-1.937-.198-.624 1.642-2.366 1.549-3.236-.08-.748-2.707-2.568-3.748-3.586C16.428 6.374 14.308 2.394 12.13.215zm.175 6.038a2.95 2.95 0 0 1 2.943 2.942 2.95 2.95 0 0 1-2.943 2.943A2.95 2.95 0 0 1 9.148 8.98a2.95 2.95 0 0 1 2.942-2.942zM8.685 7.983a3.515 3.515 0 0 0-.145.997c0 1.951 1.6 3.55 3.55 3.55 1.95 0 3.55-1.598 3.55-3.55 0-.329-.046-.648-.132-.951.334.095.64.208.915.336a42.699 42.699 0 0 1 2.042 5.829c.678 2.545 1.01 4.92.846 6.607-.082.844-.29 1.51-.606 1.94-.315.431-.713.651-1.315.651-.593 0-.932-.27-1.673-.61-.741-.338-1.825-.694-3.792-.758-1.974-.064-3.073.293-3.821.669-.375.188-.659.373-.911.5s-.466.2-.752.2c-.53 0-.876-.209-1.16-.64-.285-.43-.474-1.101-.545-1.948-.141-1.693.176-4.069.823-6.614a43.155 43.155 0 0 1 1.934-5.783c.348-.167.749-.31 1.192-.425zm-3.382 4.362a.216.216 0 0 1 .13.031c-.166.56-.323 1.116-.463 1.665a33.849 33.849 0 0 0-.547 2.555 3.9 3.9 0 0 0-.2-.39c-.58-1.012-.914-1.642-1.16-2.08.315-.24 1.679-1.755 2.24-1.781zm13.394.01c.562.027 1.926 1.543 2.24 1.783-.246.438-.58 1.068-1.16 2.08a4.428 4.428 0 0 0-.163.309 32.354 32.354 0 0 0-.562-2.49 40.579 40.579 0 0 0-.482-1.652.216.216 0 0 1 .127-.03z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1 @@
<svg fill="#FFFFFF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>JavaScript</title><path d="M0 0h24v24H0V0zm22.034 18.276c-.175-1.095-.888-2.015-3.003-2.873-.736-.345-1.554-.585-1.797-1.14-.091-.33-.105-.51-.046-.705.15-.646.915-.84 1.515-.66.39.12.75.42.976.9 1.034-.676 1.034-.676 1.755-1.125-.27-.42-.404-.601-.586-.78-.63-.705-1.469-1.065-2.834-1.034l-.705.089c-.676.165-1.32.525-1.71 1.005-1.14 1.291-.811 3.541.569 4.471 1.365 1.02 3.361 1.244 3.616 2.205.24 1.17-.87 1.545-1.966 1.41-.811-.18-1.26-.586-1.755-1.336l-1.83 1.051c.21.48.45.689.81 1.109 1.74 1.756 6.09 1.666 6.871-1.004.029-.09.24-.705.074-1.65l.046.067zm-8.983-7.245h-2.248c0 1.938-.009 3.864-.009 5.805 0 1.232.063 2.363-.138 2.711-.33.689-1.18.601-1.566.48-.396-.196-.597-.466-.83-.855-.063-.105-.11-.196-.127-.196l-1.825 1.125c.305.63.75 1.172 1.324 1.517.855.51 2.004.675 3.207.405.783-.226 1.458-.691 1.811-1.411.51-.93.402-2.07.397-3.346.012-2.054 0-4.109 0-6.179l.004-.056z"/></svg>

After

Width:  |  Height:  |  Size: 989 B

View File

@@ -0,0 +1 @@
<svg fill="#FFFFFF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>React</title><path d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z"/></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1 @@
<svg fill="#FFFFFF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Spring</title><path d="M21.8537 1.4158a10.4504 10.4504 0 0 1-1.284 2.2471A11.9666 11.9666 0 1 0 3.8518 20.7757l.4445.3951a11.9543 11.9543 0 0 0 19.6316-8.2971c.3457-3.0126-.568-6.8649-2.0743-11.458zM5.5805 20.8745a1.0174 1.0174 0 1 1-.1482-1.4323 1.0396 1.0396 0 0 1 .1482 1.4323zm16.1991-3.5806c-2.9385 3.9263-9.2601 2.5928-13.2852 2.7904 0 0-.7161.0494-1.4323.1481 0 0 .2717-.1234.6174-.2469 2.8398-.9877 4.1732-1.1853 5.9018-2.0743 3.2349-1.6545 6.4698-5.2844 7.1118-9.0379-1.2347 3.6053-4.9881 6.7167-8.3959 7.9761-2.3459.8643-6.5685 1.7039-6.5685 1.7039l-.1729-.0988c-2.8645-1.4076-2.9632-7.6304 2.2718-9.6306 2.2966-.889 4.4696-.395 6.9637-.9877 2.6422-.6174 5.7043-2.5929 6.939-5.1857 1.3828 4.1732 3.062 10.643.0493 14.6434z"/></svg>

After

Width:  |  Height:  |  Size: 834 B

View File

@@ -0,0 +1 @@
<svg fill="#FFFFFF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>TypeScript</title><path d="M1.125 0C.502 0 0 .502 0 1.125v21.75C0 23.498.502 24 1.125 24h21.75c.623 0 1.125-.502 1.125-1.125V1.125C24 .502 23.498 0 22.875 0zm17.363 9.75c.612 0 1.154.037 1.627.111a6.38 6.38 0 0 1 1.306.34v2.458a3.95 3.95 0 0 0-.643-.361 5.093 5.093 0 0 0-.717-.26 5.453 5.453 0 0 0-1.426-.2c-.3 0-.573.028-.819.086a2.1 2.1 0 0 0-.623.242c-.17.104-.3.229-.393.374a.888.888 0 0 0-.14.49c0 .196.053.373.156.529.104.156.252.304.443.444s.423.276.696.41c.273.135.582.274.926.416.47.197.892.407 1.266.628.374.222.695.473.963.753.268.279.472.598.614.957.142.359.214.776.214 1.253 0 .657-.125 1.21-.373 1.656a3.033 3.033 0 0 1-1.012 1.085 4.38 4.38 0 0 1-1.487.596c-.566.12-1.163.18-1.79.18a9.916 9.916 0 0 1-1.84-.164 5.544 5.544 0 0 1-1.512-.493v-2.63a5.033 5.033 0 0 0 3.237 1.2c.333 0 .624-.03.872-.09.249-.06.456-.144.623-.25.166-.108.29-.234.373-.38a1.023 1.023 0 0 0-.074-1.089 2.12 2.12 0 0 0-.537-.5 5.597 5.597 0 0 0-.807-.444 27.72 27.72 0 0 0-1.007-.436c-.918-.383-1.602-.852-2.053-1.405-.45-.553-.676-1.222-.676-2.005 0-.614.123-1.141.369-1.582.246-.441.58-.804 1.004-1.089a4.494 4.494 0 0 1 1.47-.629 7.536 7.536 0 0 1 1.77-.201zm-15.113.188h9.563v2.166H9.506v9.646H6.789v-9.646H3.375z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#FFFFFF" d="M126 1.637l-67 9.834v49.831l67-.534zM1.647 66.709l.003 42.404 50.791 6.983-.04-49.057zm56.82.68l.094 49.465 67.376 9.509.016-58.863zM1.61 19.297l.047 42.383 50.791-.289-.023-49.016z"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@@ -140,6 +140,117 @@ h2 {
outline: none; outline: none;
} }
.stack-carousel-shell {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 0.75rem;
align-items: center;
}
.stack-carousel {
display: flex;
gap: 1rem;
overflow-x: auto;
padding: 0.2rem;
scroll-snap-type: x mandatory;
scrollbar-width: thin;
scrollbar-color: rgba(88, 166, 255, 0.45) rgba(33, 38, 45, 0.65);
}
.stack-slide {
flex: 0 0 min(320px, calc(100% - 0.2rem));
scroll-snap-align: start;
}
.skill-heading {
display: flex;
align-items: center;
gap: 0.65rem;
}
.skill-icon {
display: inline-flex;
justify-content: center;
align-items: center;
width: 2rem;
height: 2rem;
border-radius: 10px;
border: 1px solid rgba(88, 166, 255, 0.4);
background: radial-gradient(circle at 30% 20%, rgba(88, 166, 255, 0.2), rgba(22, 27, 34, 0.95));
box-shadow: inset 0 1px 0 rgba(240, 246, 252, 0.08);
overflow: hidden;
transition: border-color 140ms ease, box-shadow 140ms ease, background-color 140ms ease;
}
.skill-logo {
width: 1.2rem;
height: 1.2rem;
object-fit: contain;
opacity: 0.92;
filter: grayscale(1) brightness(1.25) contrast(0.92);
transition: transform 140ms ease, opacity 140ms ease, filter 140ms ease;
}
.skill-fallback {
display: none;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.04em;
color: #9ecbff;
}
.skill-icon.is-fallback .skill-logo {
display: none;
}
.skill-icon.is-fallback .skill-fallback {
display: inline;
}
.skill-card .meta {
margin-top: 0.9rem;
}
.skill-card {
transition: border-color 140ms ease, box-shadow 140ms ease;
}
.stack-slide:hover .skill-icon,
.stack-slide:focus-within .skill-icon {
border-color: rgba(121, 192, 255, 0.75);
box-shadow: 0 0 0 3px rgba(56, 139, 253, 0.18), inset 0 1px 0 rgba(240, 246, 252, 0.14);
background: radial-gradient(circle at 30% 20%, rgba(121, 192, 255, 0.28), rgba(22, 27, 34, 0.95));
}
.stack-slide:hover .skill-logo,
.stack-slide:focus-within .skill-logo {
opacity: 1;
transform: scale(1.04);
filter: grayscale(1) brightness(1.4) contrast(1);
}
.carousel-btn {
width: 2.5rem;
height: 2.5rem;
border: 1px solid #3d444d;
border-radius: 999px;
background: rgba(33, 38, 45, 0.8);
color: #e6edf3;
cursor: pointer;
transition: border-color 120ms ease, background-color 120ms ease, transform 120ms ease;
}
.carousel-btn:hover {
border-color: #58a6ff;
background: rgba(56, 139, 253, 0.2);
transform: translateY(-1px);
}
.carousel-btn:focus-visible {
outline: 2px solid #58a6ff;
outline-offset: 2px;
}
.card h3 { .card h3 {
margin: 0; margin: 0;
font-size: 1.08rem; font-size: 1.08rem;
@@ -167,4 +278,22 @@ h2 {
.hero { .hero {
padding: 2rem 1.2rem; padding: 2rem 1.2rem;
} }
.stack-carousel-shell {
grid-template-columns: 1fr;
}
.carousel-btn {
display: none;
}
.stack-slide {
flex-basis: min(280px, 85%);
}
}
@media (min-width: 1000px) {
.stack-slide {
flex-basis: calc((100% - 2rem) / 3);
}
} }

View File

@@ -34,14 +34,37 @@
<h2>Tech Stack</h2> <h2>Tech Stack</h2>
<p>Primary technologies I use to deliver production-ready software.</p> <p>Primary technologies I use to deliver production-ready software.</p>
</div> </div>
<div class="card-grid"> <div class="stack-carousel-shell">
<button class="carousel-btn" type="button" aria-label="Previous technologies" (click)="scrollStack(-1)">
<span aria-hidden="true">&larr;</span>
</button>
<div class="stack-carousel" #stackCarousel role="region" aria-label="Tech stack carousel">
@for (skill of skills; track skill.name) { @for (skill of skills; track skill.name) {
<app-card cardClass="skill-card"> <app-card cardClass="skill-card stack-slide">
<div class="skill-heading">
<span class="skill-icon" role="img" [attr.aria-label]="skill.name + ' logo'">
<img
class="skill-logo"
[src]="skill.logoUrl"
[alt]="skill.name + ' logo'"
loading="lazy"
decoding="async"
(error)="onLogoError($event)"
/>
<span class="skill-fallback" aria-hidden="true">{{ skill.fallbackLabel }}</span>
</span>
<h3>{{ skill.name }}</h3> <h3>{{ skill.name }}</h3>
</div>
<p class="meta">{{ skill.category }} - {{ skill.level }}</p> <p class="meta">{{ skill.category }} - {{ skill.level }}</p>
</app-card> </app-card>
} }
</div> </div>
<button class="carousel-btn" type="button" aria-label="Next technologies" (click)="scrollStack(1)">
<span aria-hidden="true">&rarr;</span>
</button>
</div>
</section> </section>
</div> </div>

View File

@@ -37,4 +37,18 @@ describe('Home', () => {
expect(links.length).toBe(component.projects.length); expect(links.length).toBe(component.projects.length);
expect(links[0]?.getAttribute('href')).toContain('/projects/tasktimer'); expect(links[0]?.getAttribute('href')).toContain('/projects/tasktimer');
}); });
it('should render stack carousel with icon badges', () => {
const compiled = fixture.nativeElement as HTMLElement;
const carousel = compiled.querySelector('#stack .stack-carousel');
const icons = compiled.querySelectorAll('#stack .skill-icon');
const logos = compiled.querySelectorAll('#stack .skill-logo');
const controls = compiled.querySelectorAll('#stack .carousel-btn');
expect(carousel).toBeTruthy();
expect(icons.length).toBe(component.skills.length);
expect(logos.length).toBe(component.skills.length);
expect(logos[0]?.getAttribute('src')).toContain('/tech-logos/');
expect(controls.length).toBe(2);
});
}); });

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, ElementRef, ViewChild } from '@angular/core';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { CardComponent } from '../../components/card/card'; import { CardComponent } from '../../components/card/card';
import { PROJECTS, Project } from '../projects/project-data'; import { PROJECTS, Project } from '../projects/project-data';
@@ -7,6 +7,8 @@ interface Skill {
name: string; name: string;
category: 'Frontend' | 'Backend' | 'Mobile' | 'Cloud' | 'Frontend / Backend' | 'Deployment'; category: 'Frontend' | 'Backend' | 'Mobile' | 'Cloud' | 'Frontend / Backend' | 'Deployment';
level: string; level: string;
logoUrl: string;
fallbackLabel: string;
} }
@Component({ @Component({
@@ -16,6 +18,8 @@ interface Skill {
styleUrl: './home.css', styleUrl: './home.css',
}) })
export class Home { export class Home {
@ViewChild('stackCarousel') private readonly stackCarousel?: ElementRef<HTMLElement>;
readonly hero = { readonly hero = {
name: 'Tim Kainz', name: 'Tim Kainz',
role: 'Fullstack Developer from Austria', role: 'Fullstack Developer from Austria',
@@ -24,19 +28,35 @@ export class Home {
}; };
readonly skills: Skill[] = [ readonly skills: Skill[] = [
{ name: 'Angular', category: 'Frontend', level: 'Advanced' }, { name: 'Angular', category: 'Frontend', level: 'Advanced', logoUrl: '/tech-logos/angular.svg', fallbackLabel: 'NG' },
{ name: 'React', category: 'Frontend', level: 'Advanced' }, { name: 'React', category: 'Frontend', level: 'Advanced', logoUrl: '/tech-logos/react.svg', fallbackLabel: 'RE' },
{ name: 'Ionic', category: 'Frontend', level: 'Advanced' }, { name: 'Ionic', category: 'Frontend', level: 'Advanced', logoUrl: '/tech-logos/ionic.svg', fallbackLabel: 'IO' },
{ name: 'TypeScript', category: 'Frontend / Backend', level: 'Advanced' }, { name: 'TypeScript', category: 'Frontend / Backend', level: 'Advanced', logoUrl: '/tech-logos/typescript.svg', fallbackLabel: 'TS' },
{ name: 'Javascript', category: 'Frontend / Backend', level: 'Advanced' }, { name: 'Javascript', category: 'Frontend / Backend', level: 'Advanced', logoUrl: '/tech-logos/javascript.svg', fallbackLabel: 'JS' },
{ name: 'Docker', category: 'Deployment', level: 'Advanced' }, { name: 'Docker', category: 'Deployment', level: 'Advanced', logoUrl: '/tech-logos/docker.svg', fallbackLabel: 'DK' },
{ name: 'C# / .NET', category: 'Frontend / Backend', level: 'Advanced' }, { name: 'C# / .NET', category: 'Frontend / Backend', level: 'Advanced', logoUrl: '/tech-logos/csharp.svg', fallbackLabel: 'CS' },
{ name: 'ASP.NET Core', category: 'Backend', level: 'Advanced' }, { name: 'ASP.NET Core', category: 'Backend', level: 'Advanced', logoUrl: '/tech-logos/dotnet.svg', fallbackLabel: 'AS' },
{ name: 'WPF', category: 'Frontend', level: 'Advanced' }, { name: 'WPF', category: 'Frontend', level: 'Advanced', logoUrl: '/tech-logos/wpf.svg', fallbackLabel: 'WP' },
{ name: 'Java', category: 'Backend', level: 'Advanced' }, { name: 'Java', category: 'Backend', level: 'Advanced', logoUrl: '/tech-logos/java.svg', fallbackLabel: 'JV' },
{ name: 'Spring Boot', category: 'Backend', level: 'Advanced' }, { name: 'Spring Boot', category: 'Backend', level: 'Advanced', logoUrl: '/tech-logos/spring.svg', fallbackLabel: 'SB' },
{ name: 'Flutter', category: 'Mobile', level: 'Advanced' }, { name: 'Flutter', category: 'Mobile', level: 'Advanced', logoUrl: '/tech-logos/flutter.svg', fallbackLabel: 'FL' },
]; ];
readonly projects: Project[] = PROJECTS; readonly projects: Project[] = PROJECTS;
scrollStack(direction: -1 | 1): void {
const carousel = this.stackCarousel?.nativeElement;
if (!carousel) {
return;
}
const cardWidth = Math.max(carousel.clientWidth * 0.82, 260);
carousel.scrollBy({ left: cardWidth * direction, behavior: 'smooth' });
}
onLogoError(event: Event): void {
const image = event.target as HTMLImageElement | null;
const wrapper = image?.closest('.skill-icon');
wrapper?.classList.add('is-fallback');
}
} }