Upgrade Next.js to 15, fix first-load theme bug, add SVG assets

- Upgrade next 14.1.0 → 15.5.9, next-themes 0.2.1 → 0.4.6
- Fix theme not initializing on first load after server restart
- Add SVG logos and favicon, update site-logo to use SVGs
- Create shared Header component with skip-to-main link
- Add portfolio links to bento grid cards
- Improve accessibility: aria-describedby, prefers-reduced-motion
- Fix useFormState → useActionState for React 19 compatibility
- Fix founder-signature icon (use square favicon instead of wide logo)
This commit is contained in:
jwinterm
2026-01-16 21:02:05 -05:00
parent b81bea6297
commit 0d284b5c7d
23 changed files with 1331 additions and 303 deletions
+3 -3
View File
@@ -14,8 +14,8 @@
"@repo/ui": "workspace:*",
"framer-motion": "^11.0.0",
"lucide-react": "^0.350.0",
"next": "14.1.0",
"next-themes": "^0.2.1",
"next": "^15.1.3",
"next-themes": "^0.4.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.5.0",
@@ -27,7 +27,7 @@
"@types/react-dom": "^18.0.0",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"eslint-config-next": "^15.1.3",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"tailwindcss-animate": "^1.0.7",
+119
View File
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="190.59711mm"
height="190.59711mm"
viewBox="0 0 190.59711 190.59711"
version="1.1"
id="svg5"
xml:space="preserve"
sodipodi:docname="favicon.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
inkscape:export-filename="favicon.png"
inkscape:export-xdpi="25.586958"
inkscape:export-ydpi="25.586958"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.91490602"
inkscape:cx="360.14628"
inkscape:cy="207.1251"
inkscape:window-width="2318"
inkscape:window-height="1365"
inkscape:window-x="4768"
inkscape:window-y="177"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" /><defs
id="defs2" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-49.436114,-100.65314)"><g
id="g1403"
transform="translate(160.4222,89.628141)"><g
id="g464"
transform="translate(0,-102.13792)"><rect
style="fill:#3baf8a;fill-opacity:1;stroke-width:2"
id="rect2694"
width="10.037269"
height="101.95406"
x="-72.188728"
y="157.86139"
ry="1.4290794" /><rect
style="fill:#2190a4;fill-opacity:1;stroke-width:2"
id="rect2694-3"
width="10.037269"
height="101.95406"
x="-64.071434"
y="157.86139"
ry="1.4290794" /><rect
style="fill:#2190a4;fill-opacity:1;stroke-width:1.6256"
id="rect2694-3-2"
width="6.6310406"
height="101.95406"
x="-64.071434"
y="157.86139"
ry="0" /></g><g
id="g468"
transform="translate(0,-102.28801)"><rect
style="fill:#3baf8a;fill-opacity:1;stroke-width:2.02482"
id="rect2694-6"
width="17.863615"
height="58.716511"
x="-40.229259"
y="201.24904"
ry="0.8230232" /><rect
style="fill:#2190a4;fill-opacity:1;stroke-width:1.78573"
id="rect2694-6-7"
width="17.863615"
height="45.668602"
x="-40.30563"
y="157.67754"
ry="0.64013201"
inkscape:transform-center-x="3.412718"
inkscape:transform-center-y="23.898841" /></g><rect
style="fill:#286442;fill-opacity:1;stroke-width:2.67119"
id="rect2694-6-5"
width="17.863615"
height="102.18733"
x="-8.6201973"
y="55.490204"
ry="1.4323492" /><g
id="g472"
transform="translate(0,-102.85913)"><rect
style="fill:#ea62a0;fill-opacity:1;stroke-width:2.14284"
id="rect2694-6-3"
width="17.863615"
height="65.760574"
x="22.950037"
y="157.82869"
ry="0.92175913" /><rect
style="fill:#3ca5d4;fill-opacity:1;stroke-width:1.8415"
id="rect2694-6-56"
width="17.863615"
height="48.565922"
x="22.929722"
y="211.97075"
ry="0.68074346" /></g></g><circle
style="fill:none;stroke:#3ca5d4;stroke-width:3.40352;stroke-dasharray:none;stroke-opacity:1"
id="path353"
cx="144.73466"
cy="195.95169"
r="87.84816" /><circle
style="fill:none;stroke:#3baf8a;stroke-width:1.46551;stroke-opacity:1"
id="path353-3"
cx="144.73466"
cy="195.95169"
r="94.565796" /></g></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+55
View File
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="152.49817mm"
height="42.107525mm"
viewBox="0 0 152.49817 42.107525"
version="1.1"
id="svg5"
xml:space="preserve"
sodipodi:docname="logo_dark.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="1.829812"
inkscape:cx="150.56191"
inkscape:cy="-4.6452859"
inkscape:window-width="2318"
inkscape:window-height="1365"
inkscape:window-x="4768"
inkscape:window-y="177"
inkscape:window-maximized="0"
inkscape:current-layer="text9525" /><defs
id="defs2" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-25.834871,-46.19669)"><g
aria-label="SUCH "
id="text9525"
style="font-size:56.4444px;letter-spacing:0.637646px;fill:#39bfd7;stroke-width:2"><path
d="m 58.290401,59.122465 c -0.112889,-4.28978 -1.580443,-7.450667 -4.402663,-9.595554 -2.878664,-2.144887 -6.54755,-3.217331 -11.063103,-3.217331 -4.684885,0 -8.410215,1.128888 -11.232435,3.273775 -2.82222,2.201332 -4.23333,5.136441 -4.23333,8.80533 0,1.12889 0.112888,2.14489 0.39511,3.10444 0.282222,0.95956 0.620889,1.74978 0.959555,2.42711 0.338667,0.67734 0.846666,1.29822 1.636888,1.91911 0.790222,0.56445 1.41111,1.07245 1.975554,1.41111 0.508,0.33867 1.354666,0.67734 2.483554,1.12889 1.128888,0.39511 1.975554,0.67733 2.483553,0.84667 0.564444,0.16933 1.467555,0.45155 2.82222,0.84666 0.169333,0 0.338667,0.0564 0.508,0.11289 0.225777,0.0564 0.338666,0.11289 0.451555,0.16933 0.112889,0 0.282222,0.0564 0.508,0.11289 3.951108,1.12889 6.491106,1.97556 7.732883,2.65289 1.241776,0.62089 1.80622,1.63689 1.80622,3.048 0,1.63689 -0.620888,2.93511 -1.975554,3.89466 -1.354665,0.95956 -3.160886,1.46756 -5.475107,1.46756 -5.870217,0 -8.918215,-2.54 -9.087548,-7.56355 h -8.748882 c 0.05644,4.51555 1.636887,8.07155 4.684885,10.5551 3.047998,2.48355 7.337772,3.78177 12.756435,3.78177 2.144887,0 4.176885,-0.28222 6.095995,-0.73377 1.91911,-0.45156 3.668886,-1.18534 5.249329,-2.20133 1.580443,-1.016 2.878665,-2.37067 3.781775,-4.064 0.959555,-1.69333 1.41111,-3.72533 1.41111,-5.98311 0,-3.16088 -0.959555,-5.58799 -2.878664,-7.28133 -1.91911,-1.69333 -4.910663,-3.10444 -8.861771,-4.23333 -0.903111,-0.28222 -1.91911,-0.56444 -3.160887,-0.84666 -1.185332,-0.33867 -2.088443,-0.56445 -2.765775,-0.73378 -0.677333,-0.22578 -1.41111,-0.45155 -2.201332,-0.67733 -0.790222,-0.22578 -1.41111,-0.45156 -1.862665,-0.67733 -0.395111,-0.22578 -0.790222,-0.45156 -1.241777,-0.79023 -0.395111,-0.33866 -0.677333,-0.67733 -0.846666,-1.016 -0.112889,-0.39511 -0.225778,-0.84666 -0.225778,-1.298221 0,-1.467555 0.620889,-2.596443 1.749777,-3.386664 1.128888,-0.790222 2.709331,-1.185333 4.741329,-1.185333 4.74133,0 7.281328,1.975554 7.619995,5.983108 z"
style="-inkscape-font-specification:'Helvetica Neue Bold';fill:#3f9f68;fill-opacity:1"
id="path2392" /><path
d="m 98.45513,46.796413 h -8.805327 v 24.045315 c 0,3.27378 -0.620888,5.70088 -1.749776,7.28133 -1.128888,1.58044 -3.273775,2.37066 -6.434662,2.37066 -3.273775,0 -5.531551,-0.79022 -6.773328,-2.37066 -1.241777,-1.58045 -1.862665,-4.00755 -1.862665,-7.28133 V 46.796413 h -8.805327 v 24.045315 c 0,5.75733 1.354666,10.0471 4.176886,12.81288 2.82222,2.76577 7.168439,4.12044 13.151545,4.12044 5.870218,0 10.159992,-1.35467 12.925768,-4.064 2.765776,-2.70933 4.176886,-6.9991 4.176886,-12.75643 z"
style="-inkscape-font-specification:'Helvetica Neue Bold';fill:#ea62a0;fill-opacity:1"
id="path2394" /><path
d="m 140.08742,71.766005 h -8.74888 c -0.79022,6.03955 -3.83822,9.0311 -9.20044,9.0311 -3.27377,0 -5.81377,-1.18533 -7.73288,-3.66888 -1.91911,-2.48356 -2.87866,-5.75733 -2.87866,-9.93422 0,-2.37066 0.33866,-4.51555 1.01599,-6.54755 0.67734,-1.97555 1.80623,-3.668885 3.49956,-5.02355 1.69333,-1.354666 3.72533,-2.031999 6.20888,-2.031999 2.37067,0 4.28978,0.564444 5.87022,1.806221 1.58044,1.241777 2.59644,2.935108 3.048,5.192888 h 8.63599 c -0.33867,-4.176889 -2.14489,-7.619997 -5.36222,-10.329328 -3.27378,-2.709332 -7.28133,-4.063997 -12.02266,-4.063997 -5.9831,0 -10.78088,1.975554 -14.39332,5.983106 -3.61244,3.951108 -5.36222,8.974659 -5.36222,15.014209 0,6.32178 1.74978,11.45822 5.30578,15.29644 3.556,3.83821 8.24088,5.81377 14.1111,5.81377 5.36222,0 9.59555,-1.524 12.69999,-4.45911 3.048,-2.99155 4.85422,-6.99911 5.30577,-12.0791 z"
style="-inkscape-font-specification:'Helvetica Neue Bold'"
id="path2396" /><path
d="m 178.33304,47.586633 h -8.80533 V 62.713738 H 153.10239 V 47.586633 h -8.80533 v 40.188415 h 8.80533 v -17.32843 h 16.42532 v 17.32843 h 8.80533 z"
style="-inkscape-font-specification:'Helvetica Neue Bold';fill:#5cc7a5;fill-opacity:1"
id="path2398" /></g></g></svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

+119
View File
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="374.69867mm"
height="102.70798mm"
viewBox="0 0 374.69867 102.70798"
version="1.1"
id="svg5"
xml:space="preserve"
sodipodi:docname="logo_dark_w_name.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.64693625"
inkscape:cx="680.90171"
inkscape:cy="-139.89014"
inkscape:window-width="2318"
inkscape:window-height="1365"
inkscape:window-x="4768"
inkscape:window-y="177"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" /><defs
id="defs2" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(72.188728,-54.96956)"><g
id="g464"
transform="translate(0,-102.13792)"><rect
style="fill:#5cc7a5;fill-opacity:1;stroke-width:2"
id="rect2694"
width="10.037269"
height="101.95406"
x="-72.188728"
y="157.86139"
ry="1.4290794" /><rect
style="fill:#39bfd7;fill-opacity:1;stroke-width:2"
id="rect2694-3"
width="10.037269"
height="101.95406"
x="-64.071434"
y="157.86139"
ry="1.4290794" /><rect
style="fill:#39bfd7;fill-opacity:1;stroke-width:1.6256"
id="rect2694-3-2"
width="6.6310406"
height="101.95406"
x="-64.071434"
y="157.86139"
ry="0" /></g><g
id="g468"
transform="translate(0,-102.28801)"><rect
style="fill:#5cc7a5;fill-opacity:1;stroke-width:2.02482"
id="rect2694-6"
width="17.863615"
height="58.716511"
x="-40.229259"
y="201.24904"
ry="0.8230232" /><rect
style="fill:#39bfd7;fill-opacity:1;stroke-width:1.78573"
id="rect2694-6-7"
width="17.863615"
height="45.668602"
x="-40.30563"
y="157.67754"
ry="0.64013201"
inkscape:transform-center-x="3.412718"
inkscape:transform-center-y="23.898841" /></g><rect
style="fill:#3f9f68;fill-opacity:1;stroke-width:2.67119"
id="rect2694-6-5"
width="17.863615"
height="102.18733"
x="-8.6201973"
y="55.490204"
ry="1.4323492" /><g
id="g472"
transform="translate(0,-102.85913)"><rect
style="fill:#ea62a0;fill-opacity:1;stroke-width:2.14284"
id="rect2694-6-3"
width="17.863615"
height="65.760574"
x="22.950037"
y="157.82869"
ry="0.92175913" /><rect
style="fill:#39bfd7;fill-opacity:1;stroke-width:1.8415"
id="rect2694-6-56"
width="17.863615"
height="48.565922"
x="22.929722"
y="211.97075"
ry="0.68074346" /></g><text
xml:space="preserve"
style="font-size:56.4444px;line-height:55.5625px;text-align:start;letter-spacing:0.637646px;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke-width:2"
x="58.101341"
y="98.397858"
id="text9525"><tspan
sodipodi:role="line"
id="tspan9523"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:56.4444px;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Bold';text-align:start;text-anchor:start;fill:#ffffff;stroke-width:2"
x="58.101341"
y="98.397858">Such</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:56.4444px;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Bold';text-align:start;text-anchor:start;fill:#ffffff;stroke-width:2"
x="58.101341"
y="154.07214"
id="tspan9527">Software</tspan></text></g></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

+55
View File
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="152.49817mm"
height="42.107525mm"
viewBox="0 0 152.49817 42.107525"
version="1.1"
id="svg5"
xml:space="preserve"
sodipodi:docname="logo_light.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="2.587745"
inkscape:cx="328.66453"
inkscape:cy="69.75185"
inkscape:window-width="1960"
inkscape:window-height="1365"
inkscape:window-x="5398"
inkscape:window-y="277"
inkscape:window-maximized="0"
inkscape:current-layer="text9525" /><defs
id="defs2" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-25.834871,-46.19669)"><g
aria-label="SUCH "
id="text9525"
style="font-size:56.4444px;letter-spacing:0.637646px;fill:#39bfd7;stroke-width:2"><path
d="m 58.290401,59.122465 c -0.112889,-4.28978 -1.580443,-7.450667 -4.402663,-9.595554 -2.878664,-2.144887 -6.54755,-3.217331 -11.063103,-3.217331 -4.684885,0 -8.410215,1.128888 -11.232435,3.273775 -2.82222,2.201332 -4.23333,5.136441 -4.23333,8.80533 0,1.12889 0.112888,2.14489 0.39511,3.10444 0.282222,0.95956 0.620889,1.74978 0.959555,2.42711 0.338667,0.67734 0.846666,1.29822 1.636888,1.91911 0.790222,0.56445 1.41111,1.07245 1.975554,1.41111 0.508,0.33867 1.354666,0.67734 2.483554,1.12889 1.128888,0.39511 1.975554,0.67733 2.483553,0.84667 0.564444,0.16933 1.467555,0.45155 2.82222,0.84666 0.169333,0 0.338667,0.0564 0.508,0.11289 0.225777,0.0564 0.338666,0.11289 0.451555,0.16933 0.112889,0 0.282222,0.0564 0.508,0.11289 3.951108,1.12889 6.491106,1.97556 7.732883,2.65289 1.241776,0.62089 1.80622,1.63689 1.80622,3.048 0,1.63689 -0.620888,2.93511 -1.975554,3.89466 -1.354665,0.95956 -3.160886,1.46756 -5.475107,1.46756 -5.870217,0 -8.918215,-2.54 -9.087548,-7.56355 h -8.748882 c 0.05644,4.51555 1.636887,8.07155 4.684885,10.5551 3.047998,2.48355 7.337772,3.78177 12.756435,3.78177 2.144887,0 4.176885,-0.28222 6.095995,-0.73377 1.91911,-0.45156 3.668886,-1.18534 5.249329,-2.20133 1.580443,-1.016 2.878665,-2.37067 3.781775,-4.064 0.959555,-1.69333 1.41111,-3.72533 1.41111,-5.98311 0,-3.16088 -0.959555,-5.58799 -2.878664,-7.28133 -1.91911,-1.69333 -4.910663,-3.10444 -8.861771,-4.23333 -0.903111,-0.28222 -1.91911,-0.56444 -3.160887,-0.84666 -1.185332,-0.33867 -2.088443,-0.56445 -2.765775,-0.73378 -0.677333,-0.22578 -1.41111,-0.45155 -2.201332,-0.67733 -0.790222,-0.22578 -1.41111,-0.45156 -1.862665,-0.67733 -0.395111,-0.22578 -0.790222,-0.45156 -1.241777,-0.79023 -0.395111,-0.33866 -0.677333,-0.67733 -0.846666,-1.016 -0.112889,-0.39511 -0.225778,-0.84666 -0.225778,-1.298221 0,-1.467555 0.620889,-2.596443 1.749777,-3.386664 1.128888,-0.790222 2.709331,-1.185333 4.741329,-1.185333 4.74133,0 7.281328,1.975554 7.619995,5.983108 z"
style="-inkscape-font-specification:'Helvetica Neue Bold';fill:#286442;fill-opacity:1"
id="path2392" /><path
d="m 98.45513,46.796413 h -8.805327 v 24.045315 c 0,3.27378 -0.620888,5.70088 -1.749776,7.28133 -1.128888,1.58044 -3.273775,2.37066 -6.434662,2.37066 -3.273775,0 -5.531551,-0.79022 -6.773328,-2.37066 -1.241777,-1.58045 -1.862665,-4.00755 -1.862665,-7.28133 V 46.796413 h -8.805327 v 24.045315 c 0,5.75733 1.354666,10.0471 4.176886,12.81288 2.82222,2.76577 7.168439,4.12044 13.151545,4.12044 5.870218,0 10.159992,-1.35467 12.925768,-4.064 2.765776,-2.70933 4.176886,-6.9991 4.176886,-12.75643 z"
style="-inkscape-font-specification:'Helvetica Neue Bold';fill:#ea62a0;fill-opacity:1"
id="path2394" /><path
d="m 140.08742,71.766005 h -8.74888 c -0.79022,6.03955 -3.83822,9.0311 -9.20044,9.0311 -3.27377,0 -5.81377,-1.18533 -7.73288,-3.66888 -1.91911,-2.48356 -2.87866,-5.75733 -2.87866,-9.93422 0,-2.37066 0.33866,-4.51555 1.01599,-6.54755 0.67734,-1.97555 1.80623,-3.668885 3.49956,-5.02355 1.69333,-1.354666 3.72533,-2.031999 6.20888,-2.031999 2.37067,0 4.28978,0.564444 5.87022,1.806221 1.58044,1.241777 2.59644,2.935108 3.048,5.192888 h 8.63599 c -0.33867,-4.176889 -2.14489,-7.619997 -5.36222,-10.329328 -3.27378,-2.709332 -7.28133,-4.063997 -12.02266,-4.063997 -5.9831,0 -10.78088,1.975554 -14.39332,5.983106 -3.61244,3.951108 -5.36222,8.974659 -5.36222,15.014209 0,6.32178 1.74978,11.45822 5.30578,15.29644 3.556,3.83821 8.24088,5.81377 14.1111,5.81377 5.36222,0 9.59555,-1.524 12.69999,-4.45911 3.048,-2.99155 4.85422,-6.99911 5.30577,-12.0791 z"
style="-inkscape-font-specification:'Helvetica Neue Bold';fill:#3ca5d4;fill-opacity:1"
id="path2396" /><path
d="m 178.33304,47.586633 h -8.80533 V 62.713738 H 153.10239 V 47.586633 h -8.80533 v 40.188415 h 8.80533 v -17.32843 h 16.42532 v 17.32843 h 8.80533 z"
style="-inkscape-font-specification:'Helvetica Neue Bold';fill:#3baf8a;fill-opacity:1"
id="path2398" /></g></g></svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="374.69867mm"
height="102.70798mm"
viewBox="0 0 374.69867 102.70798"
version="1.1"
id="svg5"
xml:space="preserve"
sodipodi:docname="logo_light_w_name.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.64693625"
inkscape:cx="257.36694"
inkscape:cy="186.26256"
inkscape:window-width="2318"
inkscape:window-height="1365"
inkscape:window-x="4768"
inkscape:window-y="177"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" /><defs
id="defs2" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(72.188728,-54.96956)"><g
id="g464"
transform="translate(0,-102.13792)"><rect
style="fill:#3baf8a;fill-opacity:1;stroke-width:2"
id="rect2694"
width="10.037269"
height="101.95406"
x="-72.188728"
y="157.86139"
ry="1.4290794" /><rect
style="fill:#2190a4;fill-opacity:1;stroke-width:2"
id="rect2694-3"
width="10.037269"
height="101.95406"
x="-64.071434"
y="157.86139"
ry="1.4290794" /><rect
style="fill:#2190a4;fill-opacity:1;stroke-width:1.6256"
id="rect2694-3-2"
width="6.6310406"
height="101.95406"
x="-64.071434"
y="157.86139"
ry="0" /></g><g
id="g468"
transform="translate(0,-102.28801)"><rect
style="fill:#3baf8a;fill-opacity:1;stroke-width:2.02482"
id="rect2694-6"
width="17.863615"
height="58.716511"
x="-40.229259"
y="201.24904"
ry="0.8230232" /><rect
style="fill:#2190a4;fill-opacity:1;stroke-width:1.78573"
id="rect2694-6-7"
width="17.863615"
height="45.668602"
x="-40.30563"
y="157.67754"
ry="0.64013201"
inkscape:transform-center-x="3.412718"
inkscape:transform-center-y="23.898841" /></g><rect
style="fill:#286442;fill-opacity:1;stroke-width:2.67119"
id="rect2694-6-5"
width="17.863615"
height="102.18733"
x="-8.6201973"
y="55.490204"
ry="1.4323492" /><g
id="g472"
transform="translate(0,-102.85913)"><rect
style="fill:#ea62a0;fill-opacity:1;stroke-width:2.14284"
id="rect2694-6-3"
width="17.863615"
height="65.760574"
x="22.950037"
y="157.82869"
ry="0.92175913" /><rect
style="fill:#3ca5d4;fill-opacity:1;stroke-width:1.8415"
id="rect2694-6-56"
width="17.863615"
height="48.565922"
x="22.929722"
y="211.97075"
ry="0.68074346" /></g><text
xml:space="preserve"
style="font-size:56.4444px;line-height:55.5625px;text-align:start;letter-spacing:0.637646px;text-anchor:start;fill:#000000;fill-opacity:1;stroke-width:2"
x="58.101341"
y="98.397858"
id="text9525"><tspan
sodipodi:role="line"
id="tspan9523"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:56.4444px;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Bold';text-align:start;text-anchor:start;fill:#000000;stroke-width:2"
x="58.101341"
y="98.397858">Such</tspan><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:56.4444px;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Bold';text-align:start;text-anchor:start;fill:#000000;stroke-width:2"
x="58.101341"
y="154.07214"
id="tspan9527">Software</tspan></text></g></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

+5
View File
@@ -7,6 +7,11 @@
"background_color": "#000000",
"theme_color": "#000000",
"icons": [
{
"src": "/images/favicon.svg",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/images/favicon_192.png",
"sizes": "192x192",
+19 -18
View File
@@ -1,9 +1,7 @@
import { DynamicBackground } from "@/components/landing/dynamic-background";
import { ThemeToggle } from "@/components/theme-toggle";
import { FounderSignature } from "@/components/about/founder-signature";
import { SiteLogo } from "@/components/layout/site-logo";
import Link from "next/link";
import { Header } from "@/components/layout/header";
import { FaMapMarkerAlt, FaPhone, FaBuilding, FaExternalLinkAlt } from "react-icons/fa";
import { FaXTwitter, FaTelegram, FaReddit } from "react-icons/fa6";
import type { Metadata } from "next";
export const metadata: Metadata = {
@@ -19,19 +17,9 @@ export const metadata: Metadata = {
export default function AboutPage() {
return (
<main className="relative flex flex-col items-center overflow-hidden">
<header className="z-50 w-full max-w-7xl mx-auto flex justify-between items-center p-6 bg-background/50 backdrop-blur-3xl sticky top-0 rounded-b-3xl border-b border-border/50">
<div className="flex items-center gap-2">
<SiteLogo />
<nav className="hidden md:flex gap-6 text-sm font-medium">
<Link href="/services" className="hover:text-primary transition-colors">Services</Link>
<Link href="/about" className="text-primary transition-colors">About</Link>
</nav>
</div>
<ThemeToggle />
</header>
<Header />
<div className="z-10 w-full max-w-6xl mx-auto py-20 px-4">
<div id="main-content" className="z-10 w-full max-w-6xl mx-auto py-20 px-4">
<h1 className="text-4xl md:text-5xl font-bold mb-10 text-center">About Us</h1>
<div className="section-container">
@@ -78,8 +66,21 @@ export default function AboutPage() {
<div className="flex flex-col justify-center items-center bg-muted/30 rounded-xl p-6">
<div className="text-center">
<h3 className="text-lg font-semibold mb-2">Connect</h3>
<p className="text-sm text-muted-foreground mb-4">Find us on your favorite platforms</p>
{/* Social links are in the footer, but we can highlight them here or leave for footer */}
<p className="text-sm text-muted-foreground mb-4">Find us on your favorite platforms</p>
<div className="flex gap-6 justify-center">
<a href="https://x.com/such_software" target="_blank" rel="noopener noreferrer" className="text-muted-foreground hover:text-primary transition-colors">
<FaXTwitter className="h-6 w-6" />
<span className="sr-only">X (Twitter)</span>
</a>
<a href="https://t.me/j_winter_m" target="_blank" rel="noopener noreferrer" className="text-muted-foreground hover:text-primary transition-colors">
<FaTelegram className="h-6 w-6" />
<span className="sr-only">Telegram</span>
</a>
<a href="https://reddit.com/r/SuchSoftware" target="_blank" rel="noopener noreferrer" className="text-muted-foreground hover:text-primary transition-colors">
<FaReddit className="h-6 w-6" />
<span className="sr-only">Reddit</span>
</a>
</div>
</div>
</div>
</div>
+14 -2
View File
@@ -87,7 +87,7 @@
overflow: hidden;
transition: all 300ms;
}
.section-container {
@apply mask-backdrop rounded-[2.5rem] p-12 md:p-20 text-card-foreground;
}
@@ -95,8 +95,20 @@
.glass-card {
@apply mask-backdrop rounded-[2rem] p-10 text-card-foreground;
}
.glass-card:hover {
border-color: hsl(var(--primary) / 0.3);
}
}
/* Respect users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
+2 -1
View File
@@ -12,9 +12,10 @@ export const metadata: Metadata = {
description: "Next-generation web applications engineered for scale, compliance, and performance.",
icons: {
icon: [
{ url: "/images/favicon.svg", type: "image/svg+xml" },
{ url: "/images/favicon_192.png", sizes: "192x192", type: "image/png" },
],
shortcut: "/images/favicon_192.png",
shortcut: "/images/favicon.svg",
apple: [
{ url: "/images/favicon_192.png", sizes: "192x192", type: "image/png" },
],
+4 -17
View File
@@ -1,30 +1,17 @@
import { DynamicBackground } from "@/components/landing/dynamic-background";
import { ThemeToggle } from "@/components/theme-toggle";
import { PortfolioGrid } from "@/components/portfolio/bento-grid";
import { ContactForm } from "@/components/contact/contact-form";
import { FounderSignature } from "@/components/about/founder-signature";
import { ServicesPreview } from "@/components/landing/services-preview";
import { SiteLogo } from "@/components/layout/site-logo";
import { Header } from "@/components/layout/header";
import Link from "next/link";
import { Button } from "@repo/ui/components/button";
import { Card } from "@repo/ui/components/card";
export default function Home() {
return (
<main className="relative flex flex-col items-center justify-between overflow-hidden">
<header className="z-50 w-full max-w-7xl mx-auto flex justify-between items-center p-6 bg-background/50 backdrop-blur-3xl sticky top-0 rounded-b-3xl border-b border-border/50">
<div className="flex items-center gap-2">
<SiteLogo />
<nav className="hidden md:flex gap-6 text-sm font-medium">
<Link href="/services" className="hover:text-primary transition-colors">Services</Link>
<Link href="/about" className="hover:text-primary transition-colors">About</Link>
</nav>
</div>
<ThemeToggle />
</header>
<div className="z-10 w-full max-w-7xl mx-auto flex flex-col items-center pt-20 pb-10 px-4 text-center">
<Header />
<div id="main-content" className="z-10 w-full max-w-7xl mx-auto flex flex-col items-center pt-20 pb-10 px-4 text-center">
<h1 className="text-5xl md:text-7xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-blue-500 to-emerald-500 pb-2">
Digital Craftsmanship
</h1>
+5 -18
View File
@@ -1,7 +1,6 @@
import { DataGrid } from "@repo/ui/procedural/data-grid";
import { ThemeToggle } from "@/components/theme-toggle";
import { Button } from "@repo/ui/components/button";
import { SiteLogo } from "@/components/layout/site-logo";
import { Header } from "@/components/layout/header";
import Link from "next/link";
import { Card } from "@repo/ui/components/card";
import type { Metadata } from "next";
@@ -19,22 +18,10 @@ export const metadata: Metadata = {
export default function ServicesPage() {
return (
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground overflow-hidden">
{/* Option B: Data Grid Background for Technical Feel */}
<DataGrid />
<Header />
<header className="z-50 w-full max-w-7xl mx-auto flex justify-between items-center p-6 bg-background/50 backdrop-blur-3xl sticky top-0 border-b border-border/50 rounded-b-3xl">
<div className="flex items-center gap-2">
<SiteLogo />
<nav className="hidden md:flex gap-6 text-sm font-medium">
<Link href="/services" className="text-primary transition-colors">Services</Link>
<Link href="/about" className="hover:text-primary transition-colors">About</Link>
</nav>
</div>
<ThemeToggle />
</header>
<div className="z-10 w-full max-w-5xl mx-auto py-20 px-4">
<div id="main-content" className="z-10 w-full max-w-5xl mx-auto py-20 px-4">
{/* Context Header */}
<div className="mb-20">
<div className="text-left mb-8 section-container border-l-4 border-l-emerald-500">
@@ -70,8 +57,8 @@ export default function ServicesPage() {
<Card className="glass-card hover:border-indigo-500/30">
<h3 className="text-xl font-bold mb-3">Zero Capital Upfront</h3>
<p className="text-base text-muted-foreground leading-relaxed">
Our "Toast" style partnership means we win when you win. Low friction onboarding
with performance-based incentives.
Our revenue-share partnership means no setup fees or monthly subscriptions—we
only earn when you're making sales.
</p>
</Card>
<Card className="glass-card hover:border-indigo-500/30">
@@ -7,12 +7,13 @@ export const FounderSignature = () => {
{/* Left Column: Bio */}
<div className="space-y-8 text-left section-container">
<div className="flex items-center gap-6 relative z-10">
<div className="relative w-24 h-24 rounded-full overflow-hidden border-2 border-primary/10 bg-white dark:bg-zinc-900 shadow-xl ring-4 ring-primary/5">
<Image
src="/images/logo_w_circle_trans.jpg"
alt="Such Software Icon"
fill
className="object-cover scale-[1.02]"
<div className="relative w-24 h-24 rounded-full overflow-hidden border-2 border-primary/10 bg-white dark:bg-zinc-900 shadow-xl ring-4 ring-primary/5 flex items-center justify-center">
<Image
src="/images/favicon.svg"
alt="Such Software Icon"
width={72}
height={72}
className="object-contain"
/>
</div>
<div>
@@ -23,13 +24,13 @@ export const FounderSignature = () => {
<div className="prose dark:prose-invert max-w-none">
<p className="text-muted-foreground leading-relaxed italic border-l-4 border-primary/20 pl-6 text-lg">
"During my three years at <span className="text-foreground font-semibold">Cecil College</span>, I didn't just teach web engineering—I re-engineered how it was taught.
"During my three years at <span className="text-foreground font-semibold">Cecil College</span>, I didn't just teach engineering—I re-engineered how it was taught.
</p>
<p className="text-muted-foreground leading-relaxed mt-6">
Transitioning the curriculum toward from PDFs and Powerpoints to web-based content, I focused on transforming complex technical lessons into highly accessible HTML using <span className="text-foreground font-semibold underline decoration-primary/50">Quarto markdown</span> as our source. This allowed us to consistently generate semantic content including accessible equations and guaranteed descriptive alt text for every visual asset.
Transitioning the curriculum from PDFs and Powerpoints to web-based content, I focused on transforming complex technical lessons into highly accessible HTML using <span className="text-foreground font-semibold underline decoration-primary/50">Quarto markdown</span>. This allowed us to consistently generate semantic content including accessible equations and guaranteed descriptive alt text for every visual asset.
</p>
<p className="text-muted-foreground leading-relaxed mt-4 italic border-l-4 border-primary/20 pl-6 text-lg">
This wasn't just academic theory; it was a rigorous discipline of semantic architecture, ARIA compliance, and cognitive load management. Today, I bring that same technical rigor to Such Software’s commercial projects. We don't retroactively fix accessibility; we architect for it from line one."
This wasn't just theory; it was a methodical implementation of semantic architecture, ARIA compliance, and cognitive load management. Today, I bring that same technical rigor to Such Software’s commercial projects. We don't retroactively fix accessibility; we architect for it from line one."
</p>
</div>
</div>
@@ -61,18 +62,18 @@ export const FounderSignature = () => {
<span className="text-emerald-500 text-[10px]">03</span>
Core Competencies
</h4>
<ul className="grid grid-cols-1 gap-2 font-mono text-[11px] uppercase tracking-wider text-muted-foreground/80">
<ul className="grid grid-cols-1 gap-2 font-mono text-xs uppercase tracking-wider text-muted-foreground">
<li className="flex items-center gap-2">
<span className="text-emerald-500 text-sm">✓</span> WCAG 2.2 AA Compliance
<span className="text-emerald-500">✓</span> WCAG 2.2 AA Compliance
</li>
<li className="flex items-center gap-2">
<span className="text-emerald-500 text-sm">✓</span> Section 508 & ADA
<span className="text-emerald-500">✓</span> Section 508 & ADA
</li>
<li className="flex items-center gap-2">
<span className="text-emerald-500 text-sm">✓</span> Semantic HTML5 Architecture
<span className="text-emerald-500">✓</span> Semantic HTML5 Architecture
</li>
<li className="flex items-center gap-2">
<span className="text-emerald-500 text-sm">✓</span> Playwright A11y Automation
<span className="text-emerald-500">✓</span> Playwright A11y Automation
</li>
</ul>
</Card>
@@ -1,16 +1,17 @@
'use client';
import { useFormState, useFormStatus } from 'react-dom';
import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
import { submitContactForm } from '@/actions/submit-contact';
import { TurnstileWidget } from './turnstile-widget';
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { Button } from '@repo/ui/components/button';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<Button
type="submit"
disabled={pending}
<Button
type="submit"
disabled={pending}
size="lg"
className="w-full md:w-64 rounded-xl text-lg font-semibold"
>
@@ -20,12 +21,22 @@ function SubmitButton() {
}
export function ContactForm() {
const [state, formAction] = useFormState(submitContactForm, { success: false, message: '' });
const [state, formAction] = useActionState(submitContactForm, { success: false, message: '' });
const [token, setToken] = useState<string>('');
const formRef = useRef<HTMLFormElement>(null);
// Reset form on successful submission
useEffect(() => {
if (state.success && formRef.current) {
formRef.current.reset();
setToken('');
}
}, [state.success]);
return (
<form
action={formAction}
<form
ref={formRef}
action={formAction}
className="space-y-8 w-full max-w-6xl mx-auto section-container"
>
<div className="absolute top-0 right-0 w-64 h-64 bg-primary/5 rounded-full blur-3xl -mr-32 -mt-32" />
@@ -41,40 +52,46 @@ export function ContactForm() {
<div className="grid md:grid-cols-2 gap-8 relative z-10">
<div className="space-y-3">
<label htmlFor="name" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">Full Name</label>
<input
name="name"
id="name"
<input
name="name"
id="name"
placeholder="Jamie Doe"
required
className="w-full p-4 border-2 border-border/50 rounded-2xl bg-background/50 focus:ring-4 focus:ring-primary/10 focus:border-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary transition-all text-lg"
required
aria-describedby={state?.errors?.name ? "name-error" : undefined}
aria-invalid={state?.errors?.name ? "true" : undefined}
className="w-full p-4 border-2 border-border/50 rounded-2xl bg-background/50 focus:ring-4 focus:ring-primary/10 focus:border-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary transition-all text-lg"
/>
{state?.errors?.name && <p className="text-red-500 text-sm px-1">{state.errors.name}</p>}
{state?.errors?.name && <p id="name-error" className="text-red-500 text-sm px-1" role="alert">{state.errors.name}</p>}
</div>
<div className="space-y-3">
<label htmlFor="email" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">Work Email</label>
<input
name="email"
id="email"
type="email"
<input
name="email"
id="email"
type="email"
placeholder="jamie@example.com"
required
className="w-full p-4 border-2 border-border/50 rounded-2xl bg-background/50 focus:ring-4 focus:ring-primary/10 focus:border-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary transition-all text-lg"
required
aria-describedby={state?.errors?.email ? "email-error" : undefined}
aria-invalid={state?.errors?.email ? "true" : undefined}
className="w-full p-4 border-2 border-border/50 rounded-2xl bg-background/50 focus:ring-4 focus:ring-primary/10 focus:border-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary transition-all text-lg"
/>
{state?.errors?.email && <p className="text-red-500 text-sm px-1">{state.errors.email}</p>}
{state?.errors?.email && <p id="email-error" className="text-red-500 text-sm px-1" role="alert">{state.errors.email}</p>}
</div>
</div>
<div className="space-y-3 relative z-10">
<label htmlFor="message" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">Project Details</label>
<textarea
name="message"
id="message"
<textarea
name="message"
id="message"
placeholder="What are you building? Mention any specific constraints, timelines, or technologies..."
required
className="w-full p-6 border-2 border-border/50 rounded-2xl bg-background/50 h-56 resize-none focus:ring-4 focus:ring-primary/10 focus:border-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary transition-all text-lg leading-relaxed"
required
aria-describedby={state?.errors?.message ? "message-error" : undefined}
aria-invalid={state?.errors?.message ? "true" : undefined}
className="w-full p-6 border-2 border-border/50 rounded-2xl bg-background/50 h-56 resize-none focus:ring-4 focus:ring-primary/10 focus:border-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary transition-all text-lg leading-relaxed"
/>
{state?.errors?.message && <p className="text-red-500 text-sm px-1">{state.errors.message}</p>}
{state?.errors?.message && <p id="message-error" className="text-red-500 text-sm px-1" role="alert">{state.errors.message}</p>}
</div>
<input type="hidden" name="token" value={token} />
@@ -92,13 +109,17 @@ export function ContactForm() {
</div>
{state?.message && (
<div className={`mt-8 p-6 rounded-2xl text-base border-l-4 relative z-10 ${
state.success
? "bg-emerald-500/10 border-emerald-500 text-emerald-700 dark:text-emerald-400"
: "bg-red-500/10 border-red-500 text-red-700 dark:text-red-400"
}`}>
<div
role="status"
aria-live="polite"
className={`mt-8 p-6 rounded-2xl text-base border-l-4 relative z-10 ${
state.success
? "bg-emerald-500/10 border-emerald-500 text-emerald-700 dark:text-emerald-400"
: "bg-red-500/10 border-red-500 text-red-700 dark:text-red-400"
}`}
>
<div className="flex items-center gap-3">
<span className="text-xl">{state.success ? "✓" : "⚠"}</span>
<span className="text-xl" aria-hidden="true">{state.success ? "✓" : "⚠"}</span>
<p className="font-medium">{state.message}</p>
</div>
</div>
@@ -5,19 +5,31 @@ import { NebulaField } from "@repo/ui/procedural/nebula-field";
import { useEffect, useState } from "react";
export function DynamicBackground() {
const { theme } = useTheme();
const { resolvedTheme, theme, systemTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
// Debug on first load
useEffect(() => {
console.log('[DynamicBackground] mounted:', mounted, 'theme:', theme, 'resolvedTheme:', resolvedTheme, 'systemTheme:', systemTheme);
}, [mounted, theme, resolvedTheme, systemTheme]);
// Don't render until mounted (standard hydration fix)
if (!mounted) {
return <div className="fixed inset-0 z-0 h-screen w-screen" />;
}
// After mount, determine the theme - fall back to system preference or 'dark'
const effectiveTheme = resolvedTheme || systemTheme || 'dark';
const themeMode = effectiveTheme === 'dark' ? 'dark' : 'light';
return (
<NebulaField
className="fixed inset-0 z-0 h-screen w-screen"
themeMode={theme === 'dark' ? 'dark' : 'light'}
<NebulaField
className="fixed inset-0 z-0 h-screen w-screen"
themeMode={themeMode}
/>
);
}
+41
View File
@@ -0,0 +1,41 @@
'use client';
import Link from "next/link";
import { usePathname } from "next/navigation";
import { SiteLogo } from "./site-logo";
import { ThemeToggle } from "@/components/theme-toggle";
export function Header() {
const pathname = usePathname();
return (
<>
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-[100] focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-lg focus:outline-none focus:ring-2 focus:ring-ring"
>
Skip to main content
</a>
<header className="z-50 w-full max-w-7xl mx-auto flex justify-between items-center p-6 bg-background/50 backdrop-blur-3xl sticky top-0 rounded-b-3xl border-b border-border/50">
<div className="flex items-center gap-4">
<SiteLogo />
<nav className="hidden md:flex gap-6 text-sm font-medium" aria-label="Main navigation">
<Link
href="/services"
className={pathname === "/services" ? "text-primary" : "hover:text-primary transition-colors"}
>
Services
</Link>
<Link
href="/about"
className={pathname === "/about" ? "text-primary" : "hover:text-primary transition-colors"}
>
About
</Link>
</nav>
</div>
<ThemeToggle />
</header>
</>
);
}
+3 -3
View File
@@ -6,10 +6,10 @@ import Link from "next/link";
export function SiteLogo() {
return (
<Link href="/" className="flex items-center gap-3 group">
<div className="relative w-52 h-14 lg:w-64 lg:h-16">
<div className="relative w-40 h-11 lg:w-48 lg:h-12">
{/* Dark mode logo (white text) */}
<Image
src="/images/logo_dark_w_name_trans.png"
src="/images/logo_dark_w_name.svg"
alt="Such Software"
fill
className="object-contain hidden dark:block"
@@ -17,7 +17,7 @@ export function SiteLogo() {
/>
{/* Light mode logo (black text) */}
<Image
src="/images/logo_light_w_name_trans.png"
src="/images/logo_light_w_name.svg"
alt="Such Software"
fill
className="object-contain block dark:hidden"
@@ -1,81 +1,90 @@
import { Card } from "@repo/ui/components/card";
import Image from "next/image";
import Link from "next/link";
export const PortfolioGrid = () => {
return (
<section className="grid grid-cols-1 md:grid-cols-4 auto-rows-[300px] gap-6 w-full max-w-7xl mx-auto p-4 z-10 relative">
{/* Hero Project: r/CryptoCurrency */}
<Card className="md:col-span-2 md:row-span-2 glass-card group min-h-[300px] rounded-[2.5rem] !bg-slate-950/90">
<div className="absolute inset-0 z-20 bg-gradient-to-t from-black/95 via-black/20 to-transparent p-12 flex flex-col justify-end items-center text-center pointer-events-none pb-20">
<h3 className="text-4xl font-bold text-white shadow-black drop-shadow-xl tracking-tight">r/CryptoCurrency</h3>
<p className="text-gray-200 shadow-black drop-shadow-md max-w-md mt-3 font-medium text-lg leading-relaxed">
Governance dashboard for 10M+ members. Real-time DAO metrics.
</p>
</div>
<div className="absolute inset-x-10 top-10 bottom-48 rounded-2xl overflow-hidden z-10 bg-slate-800/30">
<Image
src="/images/r-cryptocurrency.png"
alt="r/CryptoCurrency"
fill
className="object-contain p-8 opacity-80 transition-transform duration-1000 group-hover:scale-105"
sizes="(max-width: 768px) 100vw, 50vw"
/>
</div>
</Card>
<Link href="https://rcryptocurrency.com" target="_blank" rel="noopener noreferrer" className="md:col-span-2 md:row-span-2">
<Card className="h-full glass-card group min-h-[300px] rounded-[2.5rem] !bg-slate-950/90 cursor-pointer">
<div className="absolute inset-0 z-20 bg-gradient-to-t from-black/95 via-black/20 to-transparent p-12 flex flex-col justify-end items-center text-center pointer-events-none pb-20">
<h3 className="text-4xl font-bold text-white shadow-black drop-shadow-xl tracking-tight">r/CryptoCurrency</h3>
<p className="text-gray-200 shadow-black drop-shadow-md max-w-md mt-3 font-medium text-lg leading-relaxed">
Governance dashboard for 10M+ members. Real-time DAO metrics.
</p>
</div>
<div className="absolute inset-x-10 top-10 bottom-48 rounded-2xl overflow-hidden z-10 bg-slate-800/30">
<Image
src="/images/r-cryptocurrency.png"
alt="Screenshot of r/CryptoCurrency subreddit homepage showing over 10 million subscribers, community links, and a cartoon moon mascot"
fill
className="object-contain p-8 opacity-80 transition-transform duration-1000 group-hover:scale-105"
sizes="(max-width: 768px) 100vw, 50vw"
/>
</div>
</Card>
</Link>
{/* Secondary: Occupy Wallets */}
<Card className="md:col-span-2 glass-card group min-h-[300px] rounded-[2.5rem]">
<div className="absolute inset-0 z-20 p-8 flex flex-col justify-center items-center text-center pointer-events-none">
<h3 className="text-2xl font-bold text-zinc-900 dark:text-white drop-shadow-sm bg-white/60 dark:bg-black/60 backdrop-blur-3xl rounded-xl px-6 py-2">Occupy Wallets</h3>
<p className="text-zinc-900 dark:text-zinc-200 font-bold drop-shadow-sm bg-white/60 dark:bg-black/60 backdrop-blur-3xl rounded-xl px-6 py-2 mt-4">High-fidelity gallery for fiscal graffiti.</p>
</div>
<div className="absolute inset-4 rounded-xl overflow-hidden z-10 bg-zinc-200/50 dark:bg-zinc-800/50">
<Image
src="/images/occupy-wallets.png"
alt="Occupy Wallets"
fill
className="object-contain p-4 opacity-60 transition-transform duration-1000 group-hover:scale-105"
sizes="(max-width: 768px) 100vw, 50vw"
/>
</div>
</Card>
<Link href="https://occupywallets.art" target="_blank" rel="noopener noreferrer" className="md:col-span-2">
<Card className="h-full glass-card group min-h-[300px] rounded-[2.5rem] cursor-pointer">
<div className="absolute inset-0 z-20 p-8 flex flex-col justify-center items-center text-center pointer-events-none">
<h3 className="text-2xl font-bold text-zinc-900 dark:text-white drop-shadow-sm bg-white/60 dark:bg-black/60 backdrop-blur-3xl rounded-xl px-6 py-2">Occupy Wallets</h3>
<p className="text-zinc-900 dark:text-zinc-200 font-bold drop-shadow-sm bg-white/60 dark:bg-black/60 backdrop-blur-3xl rounded-xl px-6 py-2 mt-4">High-fidelity gallery for fiscal graffiti.</p>
</div>
<div className="absolute inset-4 rounded-xl overflow-hidden z-10 bg-zinc-200/50 dark:bg-zinc-800/50">
<Image
src="/images/occupy-wallets.png"
alt="Screenshot of Occupy Wallets gallery displaying illustrated artwork with political and economic commentary"
fill
className="object-contain p-4 opacity-60 transition-transform duration-1000 group-hover:scale-105"
sizes="(max-width: 768px) 100vw, 50vw"
/>
</div>
</Card>
</Link>
{/* Tertiary: Monerogue & Wowne.ro (Grouped) */}
<Card className="md:col-span-1 glass-card group min-h-[300px] rounded-[2.5rem] !bg-orange-50/95 dark:!bg-orange-950/95 !border-orange-500/20">
<div className="absolute inset-x-0 top-0 z-20 p-6 bg-gradient-to-b from-background/95 to-transparent pointer-events-none">
<h3 className="text-xl font-bold text-orange-700 dark:text-orange-400">Privacy Labs</h3>
<ul className="mt-2 space-y-1 text-sm text-foreground font-semibold">
<li className="flex items-center gap-2">Monerogue <span className="text-xs opacity-60">(Game)</span></li>
<li className="flex items-center gap-2">Wowne.ro <span className="text-xs opacity-60">(WASM)</span></li>
</ul>
</div>
<div className="absolute inset-x-4 bottom-4 top-24 rounded-lg overflow-hidden z-10 bg-black/5 dark:bg-white/5 border border-orange-500/10 shadow-inner">
<Image
src="/images/privacy-labs.png"
alt="Privacy Labs"
fill
className="object-contain p-2 opacity-50 transition-transform duration-1000 group-hover:scale-110"
sizes="(max-width: 768px) 100vw, 25vw"
/>
</div>
</Card>
<Link href="https://play.wowne.ro" target="_blank" rel="noopener noreferrer" className="md:col-span-1">
<Card className="h-full glass-card group min-h-[300px] rounded-[2.5rem] !bg-orange-50/95 dark:!bg-orange-950/95 !border-orange-500/20 cursor-pointer">
<div className="absolute inset-x-0 top-0 z-20 p-6 bg-gradient-to-b from-background/95 to-transparent pointer-events-none">
<h3 className="text-xl font-bold text-orange-700 dark:text-orange-400">Privacy Labs</h3>
<ul className="mt-2 space-y-1 text-sm text-foreground font-semibold">
<li className="flex items-center gap-2">Monerogue <span className="text-xs opacity-60">(Game)</span></li>
<li className="flex items-center gap-2">Wowne.ro <span className="text-xs opacity-60">(WASM)</span></li>
</ul>
</div>
<div className="absolute inset-x-4 bottom-4 top-24 rounded-lg overflow-hidden z-10 bg-black/5 dark:bg-white/5 border border-orange-500/10 shadow-inner">
<Image
src="/images/privacy-labs.png"
alt="Screenshots of Monerogue roguelike game and Wowne.ro WebAssembly wallet interface"
fill
className="object-contain p-2 opacity-50 transition-transform duration-1000 group-hover:scale-110"
sizes="(max-width: 768px) 100vw, 25vw"
/>
</div>
</Card>
</Link>
{/* Quaternary: MoonPlace */}
<Card className="md:col-span-1 glass-card group min-h-[300px] rounded-[2.5rem] !bg-indigo-50/95 dark:!bg-indigo-950/95 !border-indigo-500/20">
<div className="absolute inset-x-0 top-0 z-20 p-6 bg-gradient-to-b from-background/95 to-transparent pointer-events-none">
<h3 className="text-xl font-bold text-indigo-700 dark:text-indigo-400">MoonPlace</h3>
<p className="text-sm mt-1 text-foreground font-semibold">Collaborative pixel canvas engine.</p>
</div>
<div className="absolute inset-x-4 bottom-4 top-24 rounded-lg overflow-hidden z-10 bg-black/5 dark:bg-white/5 border border-indigo-500/10 shadow-inner">
<Image
src="/images/moonplace.png"
alt="MoonPlace"
fill
className="object-contain p-2 opacity-50 transition-transform duration-1000 group-hover:scale-110"
sizes="(max-width: 768px) 100vw, 25vw"
/>
</div>
</Card>
<Link href="https://moonplace.io" target="_blank" rel="noopener noreferrer" className="md:col-span-1">
<Card className="h-full glass-card group min-h-[300px] rounded-[2.5rem] !bg-indigo-50/95 dark:!bg-indigo-950/95 !border-indigo-500/20 cursor-pointer">
<div className="absolute inset-x-0 top-0 z-20 p-6 bg-gradient-to-b from-background/95 to-transparent pointer-events-none">
<h3 className="text-xl font-bold text-indigo-700 dark:text-indigo-400">MoonPlace</h3>
<p className="text-sm mt-1 text-foreground font-semibold">Collaborative pixel canvas engine.</p>
</div>
<div className="absolute inset-x-4 bottom-4 top-24 rounded-lg overflow-hidden z-10 bg-black/5 dark:bg-white/5 border border-indigo-500/10 shadow-inner">
<Image
src="/images/moonplace.png"
alt="Screenshot of MoonPlace collaborative pixel art canvas showing colorful community-created artwork"
fill
className="object-contain p-2 opacity-50 transition-transform duration-1000 group-hover:scale-110"
sizes="(max-width: 768px) 100vw, 25vw"
/>
</div>
</Card>
</Link>
</section>
);
};
+1 -1
View File
@@ -2,7 +2,7 @@
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
import { type ThemeProviderProps } from "next-themes"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
+20 -11
View File
@@ -6,18 +6,24 @@ import { Button } from "@repo/ui/components/button";
import { useEffect, useState } from "react";
export function ThemeToggle() {
const { setTheme, resolvedTheme } = useTheme();
const { setTheme, resolvedTheme, theme, systemTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
// Debug
useEffect(() => {
console.log('[ThemeToggle] mounted:', mounted, 'theme:', theme, 'resolvedTheme:', resolvedTheme);
}, [mounted, theme, resolvedTheme]);
const toggleTheme = (e: React.MouseEvent<HTMLButtonElement>) => {
const isDark = resolvedTheme === "dark";
// Fallback for browsers without startViewTransition
if (!document.startViewTransition) {
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// Skip animation if user prefers reduced motion or browser doesn't support view transitions
if (prefersReducedMotion || !document.startViewTransition) {
setTheme(isDark ? "light" : "dark");
return;
}
@@ -51,18 +57,21 @@ export function ThemeToggle() {
});
};
const isDark = resolvedTheme === "dark";
return (
<Button
variant="ghost"
size="icon"
<Button
variant="ghost"
size="icon"
onClick={mounted ? toggleTheme : undefined}
className="rounded-full"
aria-label="Toggle theme"
aria-label={mounted ? (isDark ? "Switch to light mode" : "Switch to dark mode") : "Toggle theme"}
>
{mounted && resolvedTheme === "dark" ? (
<Moon className="h-5 w-5" />
) : (
{/* Show the icon representing what clicking will switch TO */}
{mounted && isDark ? (
<Sun className="h-5 w-5" />
) : (
<Moon className="h-5 w-5" />
)}
</Button>
);