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:*", "@repo/ui": "workspace:*",
"framer-motion": "^11.0.0", "framer-motion": "^11.0.0",
"lucide-react": "^0.350.0", "lucide-react": "^0.350.0",
"next": "14.1.0", "next": "^15.1.3",
"next-themes": "^0.2.1", "next-themes": "^0.4.6",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
@@ -27,7 +27,7 @@
"@types/react-dom": "^18.0.0", "@types/react-dom": "^18.0.0",
"autoprefixer": "^10.0.1", "autoprefixer": "^10.0.1",
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "14.1.0", "eslint-config-next": "^15.1.3",
"postcss": "^8", "postcss": "^8",
"tailwindcss": "^3.3.0", "tailwindcss": "^3.3.0",
"tailwindcss-animate": "^1.0.7", "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", "background_color": "#000000",
"theme_color": "#000000", "theme_color": "#000000",
"icons": [ "icons": [
{
"src": "/images/favicon.svg",
"type": "image/svg+xml",
"purpose": "any"
},
{ {
"src": "/images/favicon_192.png", "src": "/images/favicon_192.png",
"sizes": "192x192", "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 { FounderSignature } from "@/components/about/founder-signature";
import { SiteLogo } from "@/components/layout/site-logo"; import { Header } from "@/components/layout/header";
import Link from "next/link";
import { FaMapMarkerAlt, FaPhone, FaBuilding, FaExternalLinkAlt } from "react-icons/fa"; import { FaMapMarkerAlt, FaPhone, FaBuilding, FaExternalLinkAlt } from "react-icons/fa";
import { FaXTwitter, FaTelegram, FaReddit } from "react-icons/fa6";
import type { Metadata } from "next"; import type { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -19,19 +17,9 @@ export const metadata: Metadata = {
export default function AboutPage() { export default function AboutPage() {
return ( return (
<main className="relative flex flex-col items-center overflow-hidden"> <main className="relative flex flex-col items-center overflow-hidden">
<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 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>
<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> <h1 className="text-4xl md:text-5xl font-bold mb-10 text-center">About Us</h1>
<div className="section-container"> <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="flex flex-col justify-center items-center bg-muted/30 rounded-xl p-6">
<div className="text-center"> <div className="text-center">
<h3 className="text-lg font-semibold mb-2">Connect</h3> <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> <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 */} <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> </div>
</div> </div>
+14 -2
View File
@@ -87,7 +87,7 @@
overflow: hidden; overflow: hidden;
transition: all 300ms; transition: all 300ms;
} }
.section-container { .section-container {
@apply mask-backdrop rounded-[2.5rem] p-12 md:p-20 text-card-foreground; @apply mask-backdrop rounded-[2.5rem] p-12 md:p-20 text-card-foreground;
} }
@@ -95,8 +95,20 @@
.glass-card { .glass-card {
@apply mask-backdrop rounded-[2rem] p-10 text-card-foreground; @apply mask-backdrop rounded-[2rem] p-10 text-card-foreground;
} }
.glass-card:hover { .glass-card:hover {
border-color: hsl(var(--primary) / 0.3); 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.", description: "Next-generation web applications engineered for scale, compliance, and performance.",
icons: { icons: {
icon: [ icon: [
{ url: "/images/favicon.svg", type: "image/svg+xml" },
{ url: "/images/favicon_192.png", sizes: "192x192", type: "image/png" }, { url: "/images/favicon_192.png", sizes: "192x192", type: "image/png" },
], ],
shortcut: "/images/favicon_192.png", shortcut: "/images/favicon.svg",
apple: [ apple: [
{ url: "/images/favicon_192.png", sizes: "192x192", type: "image/png" }, { 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 { PortfolioGrid } from "@/components/portfolio/bento-grid";
import { ContactForm } from "@/components/contact/contact-form"; import { ContactForm } from "@/components/contact/contact-form";
import { FounderSignature } from "@/components/about/founder-signature"; import { FounderSignature } from "@/components/about/founder-signature";
import { ServicesPreview } from "@/components/landing/services-preview"; 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 Link from "next/link";
import { Button } from "@repo/ui/components/button"; import { Button } from "@repo/ui/components/button";
import { Card } from "@repo/ui/components/card";
export default function Home() { export default function Home() {
return ( return (
<main className="relative flex flex-col items-center justify-between overflow-hidden"> <main className="relative flex flex-col items-center justify-between overflow-hidden">
<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 rounded-b-3xl border-b border-border/50">
<div className="flex items-center gap-2"> <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">
<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">
<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"> <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 Digital Craftsmanship
</h1> </h1>
+5 -18
View File
@@ -1,7 +1,6 @@
import { DataGrid } from "@repo/ui/procedural/data-grid"; import { DataGrid } from "@repo/ui/procedural/data-grid";
import { ThemeToggle } from "@/components/theme-toggle";
import { Button } from "@repo/ui/components/button"; 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 Link from "next/link";
import { Card } from "@repo/ui/components/card"; import { Card } from "@repo/ui/components/card";
import type { Metadata } from "next"; import type { Metadata } from "next";
@@ -19,22 +18,10 @@ export const metadata: Metadata = {
export default function ServicesPage() { export default function ServicesPage() {
return ( return (
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground overflow-hidden"> <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 /> <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 id="main-content" className="z-10 w-full max-w-5xl mx-auto py-20 px-4">
<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">
{/* Context Header */} {/* Context Header */}
<div className="mb-20"> <div className="mb-20">
<div className="text-left mb-8 section-container border-l-4 border-l-emerald-500"> <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"> <Card className="glass-card hover:border-indigo-500/30">
<h3 className="text-xl font-bold mb-3">Zero Capital Upfront</h3> <h3 className="text-xl font-bold mb-3">Zero Capital Upfront</h3>
<p className="text-base text-muted-foreground leading-relaxed"> <p className="text-base text-muted-foreground leading-relaxed">
Our "Toast" style partnership means we win when you win. Low friction onboarding Our revenue-share partnership means no setup fees or monthly subscriptions—we
with performance-based incentives. only earn when you're making sales.
</p> </p>
</Card> </Card>
<Card className="glass-card hover:border-indigo-500/30"> <Card className="glass-card hover:border-indigo-500/30">
@@ -7,12 +7,13 @@ export const FounderSignature = () => {
{/* Left Column: Bio */} {/* Left Column: Bio */}
<div className="space-y-8 text-left section-container"> <div className="space-y-8 text-left section-container">
<div className="flex items-center gap-6 relative z-10"> <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"> <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 <Image
src="/images/logo_w_circle_trans.jpg" src="/images/favicon.svg"
alt="Such Software Icon" alt="Such Software Icon"
fill width={72}
className="object-cover scale-[1.02]" height={72}
className="object-contain"
/> />
</div> </div>
<div> <div>
@@ -23,13 +24,13 @@ export const FounderSignature = () => {
<div className="prose dark:prose-invert max-w-none"> <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"> <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>
<p className="text-muted-foreground leading-relaxed mt-6"> <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>
<p className="text-muted-foreground leading-relaxed mt-4 italic border-l-4 border-primary/20 pl-6 text-lg"> <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> </p>
</div> </div>
</div> </div>
@@ -61,18 +62,18 @@ export const FounderSignature = () => {
<span className="text-emerald-500 text-[10px]">03</span> <span className="text-emerald-500 text-[10px]">03</span>
Core Competencies Core Competencies
</h4> </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"> <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>
<li className="flex items-center gap-2"> <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>
<li className="flex items-center gap-2"> <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>
<li className="flex items-center gap-2"> <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> </li>
</ul> </ul>
</Card> </Card>
@@ -1,16 +1,17 @@
'use client'; 'use client';
import { useFormState, useFormStatus } from 'react-dom'; import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
import { submitContactForm } from '@/actions/submit-contact'; import { submitContactForm } from '@/actions/submit-contact';
import { TurnstileWidget } from './turnstile-widget'; import { TurnstileWidget } from './turnstile-widget';
import { useState, useEffect } from 'react'; import { useState, useEffect, useRef } from 'react';
import { Button } from '@repo/ui/components/button'; import { Button } from '@repo/ui/components/button';
function SubmitButton() { function SubmitButton() {
const { pending } = useFormStatus(); const { pending } = useFormStatus();
return ( return (
<Button <Button
type="submit" type="submit"
disabled={pending} disabled={pending}
size="lg" size="lg"
className="w-full md:w-64 rounded-xl text-lg font-semibold" className="w-full md:w-64 rounded-xl text-lg font-semibold"
> >
@@ -20,12 +21,22 @@ function SubmitButton() {
} }
export function ContactForm() { 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 [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 ( return (
<form <form
action={formAction} ref={formRef}
action={formAction}
className="space-y-8 w-full max-w-6xl mx-auto section-container" 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" /> <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="grid md:grid-cols-2 gap-8 relative z-10">
<div className="space-y-3"> <div className="space-y-3">
<label htmlFor="name" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">Full Name</label> <label htmlFor="name" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">Full Name</label>
<input <input
name="name" name="name"
id="name" id="name"
placeholder="Jamie Doe" placeholder="Jamie Doe"
required 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" 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>
<div className="space-y-3"> <div className="space-y-3">
<label htmlFor="email" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">Work Email</label> <label htmlFor="email" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">Work Email</label>
<input <input
name="email" name="email"
id="email" id="email"
type="email" type="email"
placeholder="jamie@example.com" placeholder="jamie@example.com"
required 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" 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> </div>
<div className="space-y-3 relative z-10"> <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> <label htmlFor="message" className="text-sm font-semibold tracking-wide uppercase text-muted-foreground ml-1">Project Details</label>
<textarea <textarea
name="message" name="message"
id="message" id="message"
placeholder="What are you building? Mention any specific constraints, timelines, or technologies..." placeholder="What are you building? Mention any specific constraints, timelines, or technologies..."
required 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" 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> </div>
<input type="hidden" name="token" value={token} /> <input type="hidden" name="token" value={token} />
@@ -92,13 +109,17 @@ export function ContactForm() {
</div> </div>
{state?.message && ( {state?.message && (
<div className={`mt-8 p-6 rounded-2xl text-base border-l-4 relative z-10 ${ <div
state.success role="status"
? "bg-emerald-500/10 border-emerald-500 text-emerald-700 dark:text-emerald-400" aria-live="polite"
: "bg-red-500/10 border-red-500 text-red-700 dark:text-red-400" 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"> <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> <p className="font-medium">{state.message}</p>
</div> </div>
</div> </div>
@@ -5,19 +5,31 @@ import { NebulaField } from "@repo/ui/procedural/nebula-field";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export function DynamicBackground() { export function DynamicBackground() {
const { theme } = useTheme(); const { resolvedTheme, theme, systemTheme } = useTheme();
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
useEffect(() => { useEffect(() => {
setMounted(true); 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 ( return (
<NebulaField <NebulaField
className="fixed inset-0 z-0 h-screen w-screen" className="fixed inset-0 z-0 h-screen w-screen"
themeMode={theme === 'dark' ? 'dark' : 'light'} 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() { export function SiteLogo() {
return ( return (
<Link href="/" className="flex items-center gap-3 group"> <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) */} {/* Dark mode logo (white text) */}
<Image <Image
src="/images/logo_dark_w_name_trans.png" src="/images/logo_dark_w_name.svg"
alt="Such Software" alt="Such Software"
fill fill
className="object-contain hidden dark:block" className="object-contain hidden dark:block"
@@ -17,7 +17,7 @@ export function SiteLogo() {
/> />
{/* Light mode logo (black text) */} {/* Light mode logo (black text) */}
<Image <Image
src="/images/logo_light_w_name_trans.png" src="/images/logo_light_w_name.svg"
alt="Such Software" alt="Such Software"
fill fill
className="object-contain block dark:hidden" className="object-contain block dark:hidden"
@@ -1,81 +1,90 @@
import { Card } from "@repo/ui/components/card"; import { Card } from "@repo/ui/components/card";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link";
export const PortfolioGrid = () => { export const PortfolioGrid = () => {
return ( 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"> <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 */} {/* 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"> <Link href="https://rcryptocurrency.com" target="_blank" rel="noopener noreferrer" className="md:col-span-2 md:row-span-2">
<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"> <Card className="h-full glass-card group min-h-[300px] rounded-[2.5rem] !bg-slate-950/90 cursor-pointer">
<h3 className="text-4xl font-bold text-white shadow-black drop-shadow-xl tracking-tight">r/CryptoCurrency</h3> <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">
<p className="text-gray-200 shadow-black drop-shadow-md max-w-md mt-3 font-medium text-lg leading-relaxed"> <h3 className="text-4xl font-bold text-white shadow-black drop-shadow-xl tracking-tight">r/CryptoCurrency</h3>
Governance dashboard for 10M+ members. Real-time DAO metrics. <p className="text-gray-200 shadow-black drop-shadow-md max-w-md mt-3 font-medium text-lg leading-relaxed">
</p> Governance dashboard for 10M+ members. Real-time DAO metrics.
</div> </p>
<div className="absolute inset-x-10 top-10 bottom-48 rounded-2xl overflow-hidden z-10 bg-slate-800/30"> </div>
<Image <div className="absolute inset-x-10 top-10 bottom-48 rounded-2xl overflow-hidden z-10 bg-slate-800/30">
src="/images/r-cryptocurrency.png" <Image
alt="r/CryptoCurrency" src="/images/r-cryptocurrency.png"
fill alt="Screenshot of r/CryptoCurrency subreddit homepage showing over 10 million subscribers, community links, and a cartoon moon mascot"
className="object-contain p-8 opacity-80 transition-transform duration-1000 group-hover:scale-105" fill
sizes="(max-width: 768px) 100vw, 50vw" className="object-contain p-8 opacity-80 transition-transform duration-1000 group-hover:scale-105"
/> sizes="(max-width: 768px) 100vw, 50vw"
</div> />
</Card> </div>
</Card>
</Link>
{/* Secondary: Occupy Wallets */} {/* Secondary: Occupy Wallets */}
<Card className="md:col-span-2 glass-card group min-h-[300px] rounded-[2.5rem]"> <Link href="https://occupywallets.art" target="_blank" rel="noopener noreferrer" className="md:col-span-2">
<div className="absolute inset-0 z-20 p-8 flex flex-col justify-center items-center text-center pointer-events-none"> <Card className="h-full glass-card group min-h-[300px] rounded-[2.5rem] cursor-pointer">
<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> <div className="absolute inset-0 z-20 p-8 flex flex-col justify-center items-center text-center pointer-events-none">
<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> <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>
</div> <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 className="absolute inset-4 rounded-xl overflow-hidden z-10 bg-zinc-200/50 dark:bg-zinc-800/50"> </div>
<Image <div className="absolute inset-4 rounded-xl overflow-hidden z-10 bg-zinc-200/50 dark:bg-zinc-800/50">
src="/images/occupy-wallets.png" <Image
alt="Occupy Wallets" src="/images/occupy-wallets.png"
fill alt="Screenshot of Occupy Wallets gallery displaying illustrated artwork with political and economic commentary"
className="object-contain p-4 opacity-60 transition-transform duration-1000 group-hover:scale-105" fill
sizes="(max-width: 768px) 100vw, 50vw" className="object-contain p-4 opacity-60 transition-transform duration-1000 group-hover:scale-105"
/> sizes="(max-width: 768px) 100vw, 50vw"
</div> />
</Card> </div>
</Card>
</Link>
{/* Tertiary: Monerogue & Wowne.ro (Grouped) */} {/* 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"> <Link href="https://play.wowne.ro" target="_blank" rel="noopener noreferrer" className="md:col-span-1">
<div className="absolute inset-x-0 top-0 z-20 p-6 bg-gradient-to-b from-background/95 to-transparent pointer-events-none"> <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">
<h3 className="text-xl font-bold text-orange-700 dark:text-orange-400">Privacy Labs</h3> <div className="absolute inset-x-0 top-0 z-20 p-6 bg-gradient-to-b from-background/95 to-transparent pointer-events-none">
<ul className="mt-2 space-y-1 text-sm text-foreground font-semibold"> <h3 className="text-xl font-bold text-orange-700 dark:text-orange-400">Privacy Labs</h3>
<li className="flex items-center gap-2">Monerogue <span className="text-xs opacity-60">(Game)</span></li> <ul className="mt-2 space-y-1 text-sm text-foreground font-semibold">
<li className="flex items-center gap-2">Wowne.ro <span className="text-xs opacity-60">(WASM)</span></li> <li className="flex items-center gap-2">Monerogue <span className="text-xs opacity-60">(Game)</span></li>
</ul> <li className="flex items-center gap-2">Wowne.ro <span className="text-xs opacity-60">(WASM)</span></li>
</div> </ul>
<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"> </div>
<Image <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">
src="/images/privacy-labs.png" <Image
alt="Privacy Labs" src="/images/privacy-labs.png"
fill alt="Screenshots of Monerogue roguelike game and Wowne.ro WebAssembly wallet interface"
className="object-contain p-2 opacity-50 transition-transform duration-1000 group-hover:scale-110" fill
sizes="(max-width: 768px) 100vw, 25vw" className="object-contain p-2 opacity-50 transition-transform duration-1000 group-hover:scale-110"
/> sizes="(max-width: 768px) 100vw, 25vw"
</div> />
</Card> </div>
</Card>
</Link>
{/* Quaternary: MoonPlace */} {/* 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"> <Link href="https://moonplace.io" target="_blank" rel="noopener noreferrer" className="md:col-span-1">
<div className="absolute inset-x-0 top-0 z-20 p-6 bg-gradient-to-b from-background/95 to-transparent pointer-events-none"> <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">
<h3 className="text-xl font-bold text-indigo-700 dark:text-indigo-400">MoonPlace</h3> <div className="absolute inset-x-0 top-0 z-20 p-6 bg-gradient-to-b from-background/95 to-transparent pointer-events-none">
<p className="text-sm mt-1 text-foreground font-semibold">Collaborative pixel canvas engine.</p> <h3 className="text-xl font-bold text-indigo-700 dark:text-indigo-400">MoonPlace</h3>
</div> <p className="text-sm mt-1 text-foreground font-semibold">Collaborative pixel canvas engine.</p>
<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"> </div>
<Image <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">
src="/images/moonplace.png" <Image
alt="MoonPlace" src="/images/moonplace.png"
fill alt="Screenshot of MoonPlace collaborative pixel art canvas showing colorful community-created artwork"
className="object-contain p-2 opacity-50 transition-transform duration-1000 group-hover:scale-110" fill
sizes="(max-width: 768px) 100vw, 25vw" className="object-contain p-2 opacity-50 transition-transform duration-1000 group-hover:scale-110"
/> sizes="(max-width: 768px) 100vw, 25vw"
</div> />
</Card> </div>
</Card>
</Link>
</section> </section>
); );
}; };
+1 -1
View File
@@ -2,7 +2,7 @@
import * as React from "react" import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes" 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) { export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider> 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"; import { useEffect, useState } from "react";
export function ThemeToggle() { export function ThemeToggle() {
const { setTheme, resolvedTheme } = useTheme(); const { setTheme, resolvedTheme, theme, systemTheme } = useTheme();
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
}, []); }, []);
// Debug
useEffect(() => {
console.log('[ThemeToggle] mounted:', mounted, 'theme:', theme, 'resolvedTheme:', resolvedTheme);
}, [mounted, theme, resolvedTheme]);
const toggleTheme = (e: React.MouseEvent<HTMLButtonElement>) => { const toggleTheme = (e: React.MouseEvent<HTMLButtonElement>) => {
const isDark = resolvedTheme === "dark"; const isDark = resolvedTheme === "dark";
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// Fallback for browsers without startViewTransition
if (!document.startViewTransition) { // Skip animation if user prefers reduced motion or browser doesn't support view transitions
if (prefersReducedMotion || !document.startViewTransition) {
setTheme(isDark ? "light" : "dark"); setTheme(isDark ? "light" : "dark");
return; return;
} }
@@ -51,18 +57,21 @@ export function ThemeToggle() {
}); });
}; };
const isDark = resolvedTheme === "dark";
return ( return (
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={mounted ? toggleTheme : undefined} onClick={mounted ? toggleTheme : undefined}
className="rounded-full" className="rounded-full"
aria-label="Toggle theme" aria-label={mounted ? (isDark ? "Switch to light mode" : "Switch to dark mode") : "Toggle theme"}
> >
{mounted && resolvedTheme === "dark" ? ( {/* Show the icon representing what clicking will switch TO */}
<Moon className="h-5 w-5" /> {mounted && isDark ? (
) : (
<Sun className="h-5 w-5" /> <Sun className="h-5 w-5" />
) : (
<Moon className="h-5 w-5" />
)} )}
</Button> </Button>
); );
+1 -1
View File
@@ -24,7 +24,7 @@ export const DataGrid = () => {
width="2" height="2" fill="currentColor" width="2" height="2" fill="currentColor"
className="text-blue-500 dark:text-blue-400" className="text-blue-500 dark:text-blue-400"
> >
<motion.animateMotion <animateMotion
path="M 0 50 H 100" path="M 0 50 H 100"
dur="5s" dur="5s"
repeatCount="indefinite" repeatCount="indefinite"
+7 -14
View File
@@ -77,14 +77,9 @@ const StreamLine = ({ d, themeMode, onHit, width, index }: StreamLineProps) => {
); );
}; };
export const NebulaField = ({ className, density = 8, themeMode = 'dark' }: NebulaProps) => { export const NebulaField = ({ className, density = 8, themeMode = 'dark' }: NebulaProps) => {
const [mounted, setMounted] = useState(false);
const monitorControls = useAnimation(); const monitorControls = useAnimation();
const [monitorColor, setMonitorColor] = useState(themeMode === 'light' ? "text-slate-400" : "text-emerald-500/60"); const [monitorColor, setMonitorColor] = useState(themeMode === 'light' ? "text-slate-400" : "text-emerald-500/60");
useEffect(() => {
setMounted(true);
}, []);
// Sync color with theme if no impacts have happened yet // Sync color with theme if no impacts have happened yet
useEffect(() => { useEffect(() => {
@@ -130,25 +125,23 @@ export const NebulaField = ({ className, density = 8, themeMode = 'dark' }: Nebu
}, [density, themeMode, targetX, targetY]); }, [density, themeMode, targetX, targetY]);
const handleHit = useCallback(() => { const handleHit = useCallback(() => {
const colors = themeMode === 'light' const colors = themeMode === 'light'
? ["text-slate-400", "text-blue-400", "text-indigo-400", "text-emerald-400"] ? ["text-slate-400", "text-blue-400", "text-indigo-400", "text-emerald-400"]
: ["text-emerald-500/50", "text-blue-500/50", "text-purple-500/50", "text-cyan-500/50", "text-teal-500/50"]; : ["text-emerald-500/50", "text-blue-500/50", "text-purple-500/50", "text-cyan-500/50", "text-teal-500/50"];
setMonitorColor(colors[Math.floor(Math.random() * colors.length)]); setMonitorColor(colors[Math.floor(Math.random() * colors.length)]);
monitorControls.start({ monitorControls.start({
scale: [1, 1.1, 1], // Slightly more pronounced pulse scale: [1, 1.1, 1],
filter: [ filter: [
"brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))", "brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))",
"brightness(1.3) drop-shadow(0 0 12px currentColor)", // Stronger glow "brightness(1.3) drop-shadow(0 0 12px currentColor)",
"brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))" "brightness(1) drop-shadow(0 0 0px rgba(0,0,0,0))"
], ],
transition: { duration: 1.5, ease: "easeOut" } // Smoother, longer pulse transition: { duration: 1.5, ease: "easeOut" }
}); });
}, [monitorControls, themeMode]); }, [monitorControls, themeMode]);
if (!mounted) return null;
return ( return (
<div className={cn("absolute inset-0 overflow-hidden pointer-events-none z-0", className)}> <div className={cn("absolute inset-0 overflow-hidden pointer-events-none z-0", className)}>
+579 -97
View File
File diff suppressed because it is too large Load Diff