this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add basics for fullstack spa in gleam blog

+1024 -2
+1
package.json
··· 28 28 "mysql2": "^3.9.7", 29 29 "next": "^14.2.4", 30 30 "react": "^18.3.1", 31 + "react-code-blocks": "^0.1.6", 31 32 "react-dom": "^18.3.1", 32 33 "react-markdown": "^9.0.1", 33 34 "rehype-raw": "^7.0.0",
+277
pnpm-lock.yaml
··· 50 50 react: 51 51 specifier: ^18.3.1 52 52 version: 18.3.1 53 + react-code-blocks: 54 + specifier: ^0.1.6 55 + version: 0.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 53 56 react-dom: 54 57 specifier: ^18.3.1 55 58 version: 18.3.1(react@18.3.1) ··· 123 126 '@alloc/quick-lru@5.2.0': 124 127 resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} 125 128 engines: {node: '>=10'} 129 + 130 + '@babel/runtime@7.25.0': 131 + resolution: {integrity: sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==} 132 + engines: {node: '>=6.9.0'} 133 + 134 + '@emotion/is-prop-valid@1.2.2': 135 + resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==} 136 + 137 + '@emotion/memoize@0.8.1': 138 + resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} 139 + 140 + '@emotion/unitless@0.8.1': 141 + resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} 126 142 127 143 '@esbuild-kit/core-utils@3.3.2': 128 144 resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} ··· 598 614 '@types/estree@1.0.5': 599 615 resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} 600 616 617 + '@types/hast@2.3.10': 618 + resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} 619 + 601 620 '@types/hast@3.0.4': 602 621 resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} 603 622 ··· 627 646 628 647 '@types/slug@5.0.8': 629 648 resolution: {integrity: sha512-mblTWR1OST257k1gZ3QvqG+ERSr8Ea6dyM1FH6Jtm4jeXi0/r0/95VNctofuiywPxCVQuE8AuFoqmvJ4iVUlXQ==} 649 + 650 + '@types/stylis@4.2.5': 651 + resolution: {integrity: sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==} 630 652 631 653 '@types/unist@2.0.10': 632 654 resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} ··· 870 892 resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} 871 893 engines: {node: '>= 6'} 872 894 895 + camelize@1.0.1: 896 + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} 897 + 873 898 caniuse-lite@1.0.30001642: 874 899 resolution: {integrity: sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==} 875 900 ··· 883 908 character-entities-html4@2.1.0: 884 909 resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} 885 910 911 + character-entities-legacy@1.1.4: 912 + resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} 913 + 886 914 character-entities-legacy@3.0.0: 887 915 resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} 888 916 917 + character-entities@1.2.4: 918 + resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} 919 + 889 920 character-entities@2.0.2: 890 921 resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} 922 + 923 + character-reference-invalid@1.1.4: 924 + resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} 891 925 892 926 character-reference-invalid@2.0.1: 893 927 resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} ··· 910 944 color-name@1.1.4: 911 945 resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 912 946 947 + comma-separated-tokens@1.0.8: 948 + resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} 949 + 913 950 comma-separated-tokens@2.0.3: 914 951 resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} 915 952 ··· 931 968 cross-spawn@7.0.3: 932 969 resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 933 970 engines: {node: '>= 8'} 971 + 972 + css-color-keywords@1.0.0: 973 + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} 974 + engines: {node: '>=4'} 975 + 976 + css-to-react-native@3.2.0: 977 + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} 934 978 935 979 cssesc@3.0.0: 936 980 resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} ··· 1349 1393 fastq@1.17.1: 1350 1394 resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} 1351 1395 1396 + fault@1.0.4: 1397 + resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} 1398 + 1352 1399 file-entry-cache@6.0.1: 1353 1400 resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} 1354 1401 engines: {node: ^10.12.0 || >=12.0.0} ··· 1374 1421 foreground-child@3.2.1: 1375 1422 resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} 1376 1423 engines: {node: '>=14'} 1424 + 1425 + format@0.2.2: 1426 + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} 1427 + engines: {node: '>=0.4.x'} 1377 1428 1378 1429 fs.realpath@1.0.0: 1379 1430 resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} ··· 1495 1546 hast-util-from-parse5@8.0.1: 1496 1547 resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==} 1497 1548 1549 + hast-util-parse-selector@2.2.5: 1550 + resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} 1551 + 1498 1552 hast-util-parse-selector@4.0.0: 1499 1553 resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} 1500 1554 ··· 1509 1563 1510 1564 hast-util-whitespace@3.0.0: 1511 1565 resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} 1566 + 1567 + hastscript@6.0.0: 1568 + resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} 1512 1569 1513 1570 hastscript@8.0.0: 1514 1571 resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==} ··· 1516 1573 heap@0.2.7: 1517 1574 resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} 1518 1575 1576 + highlight.js@10.7.3: 1577 + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} 1578 + 1519 1579 html-url-attributes@3.0.0: 1520 1580 resolution: {integrity: sha512-/sXbVCWayk6GDVg3ctOX6nxaVj7So40FcFAnWlWGNAB1LpYKcV5Cd10APjPjW80O7zYW2MsjBV4zZ7IZO5fVow==} 1521 1581 ··· 1552 1612 resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} 1553 1613 engines: {node: '>= 0.4'} 1554 1614 1615 + is-alphabetical@1.0.4: 1616 + resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} 1617 + 1555 1618 is-alphabetical@2.0.1: 1556 1619 resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} 1557 1620 1621 + is-alphanumerical@1.0.4: 1622 + resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} 1623 + 1558 1624 is-alphanumerical@2.0.1: 1559 1625 resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} 1560 1626 ··· 1597 1663 resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} 1598 1664 engines: {node: '>= 0.4'} 1599 1665 1666 + is-decimal@1.0.4: 1667 + resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} 1668 + 1600 1669 is-decimal@2.0.1: 1601 1670 resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} 1602 1671 ··· 1623 1692 resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 1624 1693 engines: {node: '>=0.10.0'} 1625 1694 1695 + is-hexadecimal@1.0.4: 1696 + resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} 1697 + 1626 1698 is-hexadecimal@2.0.1: 1627 1699 resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} 1628 1700 ··· 1795 1867 loose-envify@1.4.0: 1796 1868 resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 1797 1869 hasBin: true 1870 + 1871 + lowlight@1.20.0: 1872 + resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} 1798 1873 1799 1874 lru-cache@10.4.3: 1800 1875 resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} ··· 2048 2123 resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 2049 2124 engines: {node: '>=6'} 2050 2125 2126 + parse-entities@2.0.0: 2127 + resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} 2128 + 2051 2129 parse-entities@4.0.1: 2052 2130 resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} 2053 2131 ··· 2137 2215 resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} 2138 2216 engines: {node: ^10 || ^12 || >=14} 2139 2217 2218 + postcss@8.4.38: 2219 + resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} 2220 + engines: {node: ^10 || ^12 || >=14} 2221 + 2140 2222 postcss@8.4.39: 2141 2223 resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==} 2142 2224 engines: {node: ^10 || ^12 || >=14} ··· 2202 2284 engines: {node: '>=14'} 2203 2285 hasBin: true 2204 2286 2287 + prismjs@1.27.0: 2288 + resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==} 2289 + engines: {node: '>=6'} 2290 + 2291 + prismjs@1.29.0: 2292 + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} 2293 + engines: {node: '>=6'} 2294 + 2205 2295 prop-types@15.8.1: 2206 2296 resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} 2297 + 2298 + property-information@5.6.0: 2299 + resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} 2207 2300 2208 2301 property-information@6.5.0: 2209 2302 resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} ··· 2215 2308 queue-microtask@1.2.3: 2216 2309 resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 2217 2310 2311 + react-code-blocks@0.1.6: 2312 + resolution: {integrity: sha512-ENNuxG07yO+OuX1ChRje3ieefPRz6yrIpHmebQlaFQgzcAHbUfVeTINpOpoI9bSRSObeYo/OdHsporeToZ7fcg==} 2313 + engines: {node: '>=16'} 2314 + peerDependencies: 2315 + react: '>=16' 2316 + 2218 2317 react-dom@18.3.1: 2219 2318 resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} 2220 2319 peerDependencies: ··· 2229 2328 '@types/react': '>=18' 2230 2329 react: '>=18' 2231 2330 2331 + react-syntax-highlighter@15.5.0: 2332 + resolution: {integrity: sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==} 2333 + peerDependencies: 2334 + react: '>= 0.14.0' 2335 + 2232 2336 react@18.3.1: 2233 2337 resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} 2234 2338 engines: {node: '>=0.10.0'} ··· 2244 2348 resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} 2245 2349 engines: {node: '>= 0.4'} 2246 2350 2351 + refractor@3.6.0: 2352 + resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} 2353 + 2354 + regenerator-runtime@0.14.1: 2355 + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} 2356 + 2247 2357 regexp.prototype.flags@1.5.2: 2248 2358 resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} 2249 2359 engines: {node: '>= 0.4'} ··· 2325 2435 resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} 2326 2436 engines: {node: '>= 0.4'} 2327 2437 2438 + shallowequal@1.1.0: 2439 + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} 2440 + 2328 2441 shebang-command@2.0.0: 2329 2442 resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 2330 2443 engines: {node: '>=8'} ··· 2363 2476 resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 2364 2477 engines: {node: '>=0.10.0'} 2365 2478 2479 + space-separated-tokens@1.1.5: 2480 + resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} 2481 + 2366 2482 space-separated-tokens@2.0.2: 2367 2483 resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} 2368 2484 ··· 2436 2552 style-to-object@1.0.6: 2437 2553 resolution: {integrity: sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA==} 2438 2554 2555 + styled-components@6.1.12: 2556 + resolution: {integrity: sha512-n/O4PzRPhbYI0k1vKKayfti3C/IGcPf+DqcrOB7O/ab9x4u/zjqraneT5N45+sIe87cxrCApXM8Bna7NYxwoTA==} 2557 + engines: {node: '>= 16'} 2558 + peerDependencies: 2559 + react: '>= 16.8.0' 2560 + react-dom: '>= 16.8.0' 2561 + 2439 2562 styled-jsx@5.1.1: 2440 2563 resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} 2441 2564 engines: {node: '>= 12.0.0'} ··· 2448 2571 optional: true 2449 2572 babel-plugin-macros: 2450 2573 optional: true 2574 + 2575 + stylis@4.3.2: 2576 + resolution: {integrity: sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==} 2451 2577 2452 2578 sucrase@3.35.0: 2453 2579 resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} ··· 2510 2636 2511 2637 tsconfig-paths@3.15.0: 2512 2638 resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} 2639 + 2640 + tslib@2.6.2: 2641 + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} 2513 2642 2514 2643 tslib@2.6.3: 2515 2644 resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} ··· 2629 2758 wrappy@1.0.2: 2630 2759 resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 2631 2760 2761 + xtend@4.0.2: 2762 + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} 2763 + engines: {node: '>=0.4'} 2764 + 2632 2765 yaml@2.4.5: 2633 2766 resolution: {integrity: sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==} 2634 2767 engines: {node: '>= 14'} ··· 2648 2781 2649 2782 '@alloc/quick-lru@5.2.0': {} 2650 2783 2784 + '@babel/runtime@7.25.0': 2785 + dependencies: 2786 + regenerator-runtime: 0.14.1 2787 + 2788 + '@emotion/is-prop-valid@1.2.2': 2789 + dependencies: 2790 + '@emotion/memoize': 0.8.1 2791 + 2792 + '@emotion/memoize@0.8.1': {} 2793 + 2794 + '@emotion/unitless@0.8.1': {} 2795 + 2651 2796 '@esbuild-kit/core-utils@3.3.2': 2652 2797 dependencies: 2653 2798 esbuild: 0.18.20 ··· 2964 3109 2965 3110 '@types/estree@1.0.5': {} 2966 3111 3112 + '@types/hast@2.3.10': 3113 + dependencies: 3114 + '@types/unist': 2.0.10 3115 + 2967 3116 '@types/hast@3.0.4': 2968 3117 dependencies: 2969 3118 '@types/unist': 3.0.2 ··· 2994 3143 csstype: 3.1.3 2995 3144 2996 3145 '@types/slug@5.0.8': {} 3146 + 3147 + '@types/stylis@4.2.5': {} 2997 3148 2998 3149 '@types/unist@2.0.10': {} 2999 3150 ··· 3289 3440 3290 3441 camelcase-css@2.0.1: {} 3291 3442 3443 + camelize@1.0.1: {} 3444 + 3292 3445 caniuse-lite@1.0.30001642: {} 3293 3446 3294 3447 ccount@2.0.1: {} ··· 3300 3453 3301 3454 character-entities-html4@2.1.0: {} 3302 3455 3456 + character-entities-legacy@1.1.4: {} 3457 + 3303 3458 character-entities-legacy@3.0.0: {} 3459 + 3460 + character-entities@1.2.4: {} 3304 3461 3305 3462 character-entities@2.0.2: {} 3306 3463 3464 + character-reference-invalid@1.1.4: {} 3465 + 3307 3466 character-reference-invalid@2.0.1: {} 3308 3467 3309 3468 chokidar@3.6.0: ··· 3334 3493 3335 3494 color-name@1.1.4: {} 3336 3495 3496 + comma-separated-tokens@1.0.8: {} 3497 + 3337 3498 comma-separated-tokens@2.0.3: {} 3338 3499 3339 3500 commander@4.1.1: {} ··· 3351 3512 path-key: 3.1.1 3352 3513 shebang-command: 2.0.0 3353 3514 which: 2.0.2 3515 + 3516 + css-color-keywords@1.0.0: {} 3517 + 3518 + css-to-react-native@3.2.0: 3519 + dependencies: 3520 + camelize: 1.0.1 3521 + css-color-keywords: 1.0.0 3522 + postcss-value-parser: 4.2.0 3354 3523 3355 3524 cssesc@3.0.0: {} 3356 3525 ··· 3938 4107 dependencies: 3939 4108 reusify: 1.0.4 3940 4109 4110 + fault@1.0.4: 4111 + dependencies: 4112 + format: 0.2.2 4113 + 3941 4114 file-entry-cache@6.0.1: 3942 4115 dependencies: 3943 4116 flat-cache: 3.2.0 ··· 3968 4141 cross-spawn: 7.0.3 3969 4142 signal-exit: 4.1.0 3970 4143 4144 + format@0.2.2: {} 4145 + 3971 4146 fs.realpath@1.0.0: {} 3972 4147 3973 4148 fsevents@2.3.3: ··· 4121 4296 vfile-location: 5.0.3 4122 4297 web-namespaces: 2.0.1 4123 4298 4299 + hast-util-parse-selector@2.2.5: {} 4300 + 4124 4301 hast-util-parse-selector@4.0.0: 4125 4302 dependencies: 4126 4303 '@types/hast': 3.0.4 ··· 4175 4352 dependencies: 4176 4353 '@types/hast': 3.0.4 4177 4354 4355 + hastscript@6.0.0: 4356 + dependencies: 4357 + '@types/hast': 2.3.10 4358 + comma-separated-tokens: 1.0.8 4359 + hast-util-parse-selector: 2.2.5 4360 + property-information: 5.6.0 4361 + space-separated-tokens: 1.1.5 4362 + 4178 4363 hastscript@8.0.0: 4179 4364 dependencies: 4180 4365 '@types/hast': 3.0.4 ··· 4184 4369 space-separated-tokens: 2.0.2 4185 4370 4186 4371 heap@0.2.7: {} 4372 + 4373 + highlight.js@10.7.3: {} 4187 4374 4188 4375 html-url-attributes@3.0.0: {} 4189 4376 ··· 4217 4404 hasown: 2.0.2 4218 4405 side-channel: 1.0.6 4219 4406 4407 + is-alphabetical@1.0.4: {} 4408 + 4220 4409 is-alphabetical@2.0.1: {} 4221 4410 4411 + is-alphanumerical@1.0.4: 4412 + dependencies: 4413 + is-alphabetical: 1.0.4 4414 + is-decimal: 1.0.4 4415 + 4222 4416 is-alphanumerical@2.0.1: 4223 4417 dependencies: 4224 4418 is-alphabetical: 2.0.1 ··· 4265 4459 dependencies: 4266 4460 has-tostringtag: 1.0.2 4267 4461 4462 + is-decimal@1.0.4: {} 4463 + 4268 4464 is-decimal@2.0.1: {} 4269 4465 4270 4466 is-extendable@0.1.1: {} ··· 4284 4480 is-glob@4.0.3: 4285 4481 dependencies: 4286 4482 is-extglob: 2.1.1 4483 + 4484 + is-hexadecimal@1.0.4: {} 4287 4485 4288 4486 is-hexadecimal@2.0.1: {} 4289 4487 ··· 4439 4637 loose-envify@1.4.0: 4440 4638 dependencies: 4441 4639 js-tokens: 4.0.0 4640 + 4641 + lowlight@1.20.0: 4642 + dependencies: 4643 + fault: 1.0.4 4644 + highlight.js: 10.7.3 4442 4645 4443 4646 lru-cache@10.4.3: {} 4444 4647 ··· 4845 5048 dependencies: 4846 5049 callsites: 3.1.0 4847 5050 5051 + parse-entities@2.0.0: 5052 + dependencies: 5053 + character-entities: 1.2.4 5054 + character-entities-legacy: 1.1.4 5055 + character-reference-invalid: 1.1.4 5056 + is-alphanumerical: 1.0.4 5057 + is-decimal: 1.0.4 5058 + is-hexadecimal: 1.0.4 5059 + 4848 5060 parse-entities@4.0.1: 4849 5061 dependencies: 4850 5062 '@types/unist': 2.0.10 ··· 4922 5134 picocolors: 1.0.1 4923 5135 source-map-js: 1.2.0 4924 5136 5137 + postcss@8.4.38: 5138 + dependencies: 5139 + nanoid: 3.3.7 5140 + picocolors: 1.0.1 5141 + source-map-js: 1.2.0 5142 + 4925 5143 postcss@8.4.39: 4926 5144 dependencies: 4927 5145 nanoid: 3.3.7 ··· 4936 5154 4937 5155 prettier@3.3.3: {} 4938 5156 5157 + prismjs@1.27.0: {} 5158 + 5159 + prismjs@1.29.0: {} 5160 + 4939 5161 prop-types@15.8.1: 4940 5162 dependencies: 4941 5163 loose-envify: 1.4.0 4942 5164 object-assign: 4.1.1 4943 5165 react-is: 16.13.1 5166 + 5167 + property-information@5.6.0: 5168 + dependencies: 5169 + xtend: 4.0.2 4944 5170 4945 5171 property-information@6.5.0: {} 4946 5172 ··· 4948 5174 4949 5175 queue-microtask@1.2.3: {} 4950 5176 5177 + react-code-blocks@0.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): 5178 + dependencies: 5179 + '@babel/runtime': 7.25.0 5180 + react: 18.3.1 5181 + react-syntax-highlighter: 15.5.0(react@18.3.1) 5182 + styled-components: 6.1.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 5183 + tslib: 2.6.3 5184 + transitivePeerDependencies: 5185 + - react-dom 5186 + 4951 5187 react-dom@18.3.1(react@18.3.1): 4952 5188 dependencies: 4953 5189 loose-envify: 1.4.0 ··· 4973 5209 transitivePeerDependencies: 4974 5210 - supports-color 4975 5211 5212 + react-syntax-highlighter@15.5.0(react@18.3.1): 5213 + dependencies: 5214 + '@babel/runtime': 7.25.0 5215 + highlight.js: 10.7.3 5216 + lowlight: 1.20.0 5217 + prismjs: 1.29.0 5218 + react: 18.3.1 5219 + refractor: 3.6.0 5220 + 4976 5221 react@18.3.1: 4977 5222 dependencies: 4978 5223 loose-envify: 1.4.0 ··· 4995 5240 globalthis: 1.0.4 4996 5241 which-builtin-type: 1.1.3 4997 5242 5243 + refractor@3.6.0: 5244 + dependencies: 5245 + hastscript: 6.0.0 5246 + parse-entities: 2.0.0 5247 + prismjs: 1.27.0 5248 + 5249 + regenerator-runtime@0.14.1: {} 5250 + 4998 5251 regexp.prototype.flags@1.5.2: 4999 5252 dependencies: 5000 5253 call-bind: 1.0.7 ··· 5099 5352 functions-have-names: 1.2.3 5100 5353 has-property-descriptors: 1.0.2 5101 5354 5355 + shallowequal@1.1.0: {} 5356 + 5102 5357 shebang-command@2.0.0: 5103 5358 dependencies: 5104 5359 shebang-regex: 3.0.0 ··· 5129 5384 5130 5385 source-map@0.6.1: {} 5131 5386 5387 + space-separated-tokens@1.1.5: {} 5388 + 5132 5389 space-separated-tokens@2.0.2: {} 5133 5390 5134 5391 sprintf-js@1.0.3: {} ··· 5220 5477 dependencies: 5221 5478 inline-style-parser: 0.2.3 5222 5479 5480 + styled-components@6.1.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1): 5481 + dependencies: 5482 + '@emotion/is-prop-valid': 1.2.2 5483 + '@emotion/unitless': 0.8.1 5484 + '@types/stylis': 4.2.5 5485 + css-to-react-native: 3.2.0 5486 + csstype: 3.1.3 5487 + postcss: 8.4.38 5488 + react: 18.3.1 5489 + react-dom: 18.3.1(react@18.3.1) 5490 + shallowequal: 1.1.0 5491 + stylis: 4.3.2 5492 + tslib: 2.6.2 5493 + 5223 5494 styled-jsx@5.1.1(react@18.3.1): 5224 5495 dependencies: 5225 5496 client-only: 0.0.1 5226 5497 react: 18.3.1 5498 + 5499 + stylis@4.3.2: {} 5227 5500 5228 5501 sucrase@3.35.0: 5229 5502 dependencies: ··· 5309 5582 json5: 1.0.2 5310 5583 minimist: 1.2.8 5311 5584 strip-bom: 3.0.0 5585 + 5586 + tslib@2.6.2: {} 5312 5587 5313 5588 tslib@2.6.3: {} 5314 5589 ··· 5484 5759 strip-ansi: 7.1.0 5485 5760 5486 5761 wrappy@1.0.2: {} 5762 + 5763 + xtend@4.0.2: {} 5487 5764 5488 5765 yaml@2.4.5: {} 5489 5766
+2 -2
src/app/page.tsx
··· 32 32 </div> 33 33 </Link> 34 34 35 - <div className="mx-auto flex max-w-[800px] flex-col items-center gap-8"> 35 + <div className="mx-auto flex max-w-[1000px] flex-col items-center gap-8"> 36 36 <p className="flex max-w-[60ch] flex-wrap items-center justify-center gap-2 text-center text-lg font-medium"> 37 37 Welcome to my little corner of the internet.{" "} 38 38 <Heart className="rotate-12 rounded-lg fill-[#ee2c05] stroke-[#ee2c05]" /> ··· 59 59 </span>{" "} 60 60 Posts 61 61 </h2> 62 - <ul className="flex w-full flex-col"> 62 + <ul className="flex w-full flex-col gap-3"> 63 63 {posts 64 64 .sort( 65 65 (a, b) =>
+26
src/app/posts/[post]/_components/code.tsx
··· 1 + /* eslint-disable @typescript-eslint/no-unsafe-call */ 2 + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 3 + /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 4 + /* eslint-disable @typescript-eslint/no-explicit-any */ 5 + "use client"; 6 + 7 + import React from "react"; 8 + import { CodeBlock } from "react-code-blocks"; 9 + 10 + const InternalCodeBlock = ({ rest }: { rest: any }) => 11 + (rest.children ?? "").split("\n").length <= 1 ? ( 12 + <span className="rounded-md bg-neutral-200 px-1 font-mono"> 13 + {rest.children} 14 + </span> 15 + ) : ( 16 + <CodeBlock 17 + text={rest.children} 18 + language={ 19 + ({ rs: "rust" } as Record<string, string>)[ 20 + rest.className.split("-")[1] 21 + ] ?? undefined 22 + } 23 + /> 24 + ); 25 + 26 + export default InternalCodeBlock;
+17
src/app/posts/[post]/page.tsx
··· 9 9 import Link from "next/link"; 10 10 import { Roboto_Condensed } from "next/font/google"; 11 11 import localFont from "next/font/local"; 12 + import CodeBlock from "./_components/code"; 12 13 13 14 const robotoCondensed = Roboto_Condensed({ subsets: ["latin"] }); 14 15 const PlaywriteCU = localFont({ src: "../../../styles/PlaywriteCU.ttf" }); ··· 38 39 const { node, ...rest } = props; 39 40 return <h1 className="text-4xl font-semibold" {...rest} />; 40 41 }, 42 + h2(props) { 43 + const { node, ...rest } = props; 44 + return <h2 className="text-3xl font-semibold" {...rest} />; 45 + }, 46 + h3(props) { 47 + const { node, ...rest } = props; 48 + return <h3 className="text-2xl font-semibold" {...rest} />; 49 + }, 50 + h4(props) { 51 + const { node, ...rest } = props; 52 + return <h4 className="text-xl font-semibold" {...rest} />; 53 + }, 41 54 a(props) { 42 55 const { node, ...rest } = props; 43 56 return <a className="text-sky-600 hover:underline" {...rest} />; ··· 45 58 i(props) { 46 59 const { node, ...rest } = props; 47 60 return <i className="text-neutral-700" {...rest} />; 61 + }, 62 + code(props) { 63 + const { node, ...rest } = props; 64 + return <CodeBlock rest={rest} />; 48 65 }, 49 66 img(props) { 50 67 const { node, src, ...rest } = props;
+701
src/posts/the-basics-for-a-fullstack-spa-in-gleam.md
··· 1 + --- 2 + title: "The basics for a fullstack SPA in Gleam" 3 + published: 2024/7/31 4 + --- 5 + 6 + # The basics for a fullstack SPA in Gleam 7 + 8 + 2024/7/31 9 + 10 + Gleam is a functional programming language that, contrary to many other popular functional languages, features a syntax which will feel familiar to those accustomed to C-style programming languges like C#, Javascript, and Rust. This however is where a lot of similarities end for those unfamiliar with functional programming as common constructs like `for` and `while` are missing and replaced with functional recursion while `if` and `switch` have been combined into a more powerful `case`. I won't go any more indepth on the syntax of Gleam in this article but if you're interested i'd recommend checking out the [language tour](https://tour.gleam.run/) for a good starting point. 11 + 12 + With that being said this article goes into the stack and libraries used to create a fully [functional fullstack application in Gleam](https://kirakira.keii.dev) however I will not go into any specific details on how the website was made as the code is [available on github](https://github.com/dinkelspiel/kirakira). I will instead explain the basics of routing, ajax requests (fetch), and effects. This article also assumes that you have gone through the tour and know the basics like creating a gleam project and installing libraries as I will not go in to that here. 13 + 14 + Before we get into all the code, the entiry codebase for this blogpost is available [on github](https://github.com/dinkelspiel/basic-fullstack-gleam) if you prefer to just look at the code. 15 + 16 + ## The Frontend 17 + 18 + ### State 19 + 20 + In gleam the current reigning champ for frontend development is hands down the [lustre](https://github.com/lustre-labs/lustre) framework by the amazing [Hayleigh Thompson](https://blog.hayleigh.dev/) that follows the Model-View-Update architecture. 21 + 22 + > This means that the state of the application is stored in a single, immutable data structure called the model, and updated as messages are dispatched to the runtime. 23 + 24 + Coming from a background of React, this felt quite daunting. I'm used to co-locating my state with the view by using the `const [value, setValue] = useState("")` syntax in React and updating it as simply as `setValue("Hello World")` and moving from this to modelling the state as types, initing the model, then defining messages was quite the ask. Lets see the difference and similarities between these two options. 25 + 26 + This is a simple counter as a React component that you've probably seen 100s of times. 27 + 28 + ```js 29 + function Counter() { 30 + const [value, setValue] = useState(0); 31 + 32 + return ( 33 + <div> 34 + <button onClick={() => setValue(value + 1)}>+</button> 35 + {value} 36 + <button onClick={() => setValue(value - 1)}>-</button> 37 + </div> 38 + ); 39 + } 40 + ``` 41 + 42 + and this is the equivalent code in Lustre 43 + 44 + ```rs 45 + type Model = 46 + Int 47 + 48 + fn init(initial_count: Int) -> Model { 49 + 0 50 + } 51 + 52 + pub opaque type Msg { 53 + Increment 54 + Decrement 55 + } 56 + 57 + fn update(model: Model, msg: Msg) -> Model { 58 + case msg { 59 + Increment -> model + 1 60 + Decrement -> model - 1 61 + } 62 + } 63 + 64 + fn view(model: Model) -> Element(Msg) { 65 + let count = int.to_string(model) 66 + 67 + div([], [ 68 + button([on_click(Increment)], [text("+")]), 69 + p([], [ 70 + text(count), 71 + ]), 72 + button([on_click(Decrement)], [text("-")]), 73 + ]) 74 + } 75 + ``` 76 + 77 + If we break this down then we first define the model or the "State" for our project. For the counter we only need an integer so we will alias the Model type to an Int but for any project bigger than this you'd want to define a Constructor with named variables like so 78 + 79 + ```rs 80 + type Model { 81 + Model(counter: Int, username: String, /* Any other variables here */) 82 + } 83 + ``` 84 + 85 + Then we initalize the state as 0 and define our messages and we only need two effects: Increment and Decrement. These will be what call our updates and is the way you define messages between the html and the update function which is just a simple `case` (similar to a `switch`) over the recieved message. Here we want to add 1 to our model if the `Increment` message is sent and remove 1 from our model if the `Decrement` message is sent. 86 + 87 + Then we define our view which returns a Lustre element type which is a Gleam appropriated syntax meant to be similar to html. It should be completely understandeable to anyone atleast somewhat familiar to html. The standouts to what might be considered wierd are the `[]` after the `div`/`p`/`button`, the `text` function and the type in the `on_click`. We will go through them one by one. 88 + 89 + First the `[]` after our elements is where we put our attributes. The code I've written doesn't include styling so they look remarkeable empty but if we were to add some classes to our `div` then it might look something like this: `div([class("container")], [])`. Then in the middle we have the function `text` this is simply because Gleam needs explicit types and the `String` type does not convert to the lustre `Element`. Therefor lustre provides the function as a way to add text to the html. Lastly the `on_click` function takes in a `Constructor` of the `Msg` type and it is how lustre handles events. This code will send the `Increment` message to the `update` function when the + button is pressed as an example. This might seem primitive but can be quite powerful when combinding it with data in the constructors so you might send a `Increment(step: 2)` instead of just an `Increment`. 90 + 91 + For this small example it is obviosuly more verbose compared to the React example but where this model really starts to shine is when you want to start breaking out components for a larger project than just a counter. In React [prop drilling](https://www.freecodecamp.org/news/avoid-prop-drilling-in-react/) is a common bad practice in React codebases so much so that [entire libraries](https://github.com/pmndrs/zustand) have been made to avoid it. And if you look at some sample zustand code: 92 + 93 + ```js 94 + const useStore = create((set) => ({ 95 + counter: 0, 96 + incr: () => set((state) => ({ counter: state.counter + 1 })), 97 + decr: () => set((state) => ({ counter: state.counter - 1 })), 98 + })); 99 + 100 + function Counter() { 101 + const { counter, incr, decr } = useStore(); 102 + 103 + return ( 104 + <div> 105 + <button onClick={() => incr()}>+</button> 106 + {counter} 107 + <button onClick={() => decr()}>-</button> 108 + </div> 109 + ); 110 + } 111 + ``` 112 + 113 + You might notice that it looks remarkeably like our Lustre example. Lustre promotes a similar state management strategy out of the box allowing [scalability](https://hackernoon.com/scalability-the-lost-level-of-react-state-management) from the start. Defining the messages and models in this way means that any function in our codebase can read from the `model` as long as it is provided in the function by the parent and each function can send out any messages to be handles by a central structure, the `update`. 114 + 115 + ### Create our app 116 + 117 + Now when we've got the state out of the way we want to create our app. Since we are going to have a backend and a frontend we want to create folder for our two projects and then init our two projects inside of there, but we'll start with the frontend. 118 + 119 + ```bash 120 + $ mkdir my-app # Create our folder 121 + $ cd my-app # Enter the folder 122 + $ gleam new frontend # Create our gleam app named frontend 123 + $ cd frontend # Enter the new frontend project 124 + $ gleam add lustre lustre_dev_tools # Add the lustre and lustre_dev_tools dependencies 125 + ``` 126 + 127 + And before we do anything else we have to go into the `/frontend/gleam.toml` and add `target = "javascript"` below the name and version. It should look something like this: 128 + 129 + ```toml 130 + name = "frontend" 131 + version = "1.0.0" 132 + target = "javascript" 133 + ``` 134 + 135 + Another thing that is also required is adding a `ffi.mjs` in the `/frontend/src` directory that contains which will be our way of getting the current url of our browser inside the lustre app. 136 + 137 + ```js 138 + export function get_route() { 139 + return window.location.pathname; 140 + } 141 + ``` 142 + 143 + Throughout this article you can run the frontend by using 144 + 145 + ```bash 146 + $ gleam run -m lustre/dev start 147 + ``` 148 + 149 + ### Routing 150 + 151 + Although state management can go a long way defining different pages is neccesary for a webapp and one thing missing from both Lustre and React is a provided routing strategy. In React this is often solved by using a framework like [Next.JS](https://nextjs.org/) or a routing specific library like [React Router](https://reactrouter.com/en/main). Lustre provides a library called [Modem](https://hexdocs.pm/modem/) which is a simple client-side routing library that provides a router and some wrappers around `window.history.pushState` to allow for routing through Lustres Messages. 152 + 153 + To add `modem` to our app we can run the following command in the frontend folder 154 + 155 + ```bash 156 + $ gleam add modem 157 + ``` 158 + 159 + The Modem library doesn't however provide routing for when the page loads and always defaults to `/` or the default route defined in your init method. This is because Modem only interupts route change messages and the requested route does not send one. It can be implemented rather easily however and below is the simple solution present in https://kirakira.keii.dev. First we define our Route type. Everything in Gleam should be modeled using the type system and that includes our router. Lets create a simple page that has two pages, a landing page and an about page. 160 + 161 + ```rs 162 + import gleam/uri.{type Uri} 163 + import lustre 164 + import lustre/effect.{type Effect} 165 + import lustre/element.{type Element} 166 + import lustre/element/html.{div, text, a, form, input, button} 167 + import lustre/attribute.{href, type_} 168 + import modem 169 + 170 + // This is the entrypoint for our app and wont change much 171 + pub fn main() { 172 + lustre.application(init, update, view) 173 + |> lustre.start("#app", Nil) 174 + } 175 + 176 + // Define our route type 177 + pub type Route { 178 + Home 179 + About 180 + NotFound 181 + } 182 + 183 + // Include that route in our model 184 + type Model { 185 + Model(route: Route) 186 + } 187 + 188 + // Define our OnRouteChange message in our messages 189 + pub type Msg { 190 + OnRouteChange(Route) 191 + // In gleam we can include data in our types so here we add Route data to our OnRouteChange message 192 + } 193 + 194 + // Gleam doesn't expose any functions for getting the current url so we will use the ffi functionality to import this function from javascript later. In laymans terms this makes Gleam be able to import any javascript and use it as a function. 195 + @external(javascript, "./ffi.mjs", "get_route") 196 + fn do_get_route() -> String 197 + 198 + // Define our function where we get our route 199 + fn get_route() -> Route { 200 + let uri = case do_get_route() |> uri.parse { 201 + Ok(uri) -> uri 202 + _ -> panic as "Invalid uri" 203 + // The uri is coming from our javascript integration so an invalid uri should be unreachable state so we can safely panic here 204 + } 205 + 206 + case uri.path |> uri.path_segments { 207 + // Here we match for the route in the uri split on the slashes so / becomes [] and /about becomes ["about"] and so on 208 + [] -> Home 209 + ["about"] -> About 210 + _ -> NotFound 211 + } 212 + } 213 + 214 + // Define our function for handling when the route changes 215 + fn on_url_change(uri: Uri) -> Msg { 216 + OnRouteChange(get_route()) 217 + // When the url changes dispatch the message for when the route changes with the new route that we get from our get_route() function 218 + } 219 + 220 + // Create our model initialization 221 + fn init(_) -> #(Model, Effect(Msg)) { 222 + #( 223 + Model( 224 + route: get_route(), 225 + // Here we can get the current route when the page is initialized in the browser 226 + ), 227 + modem.init(on_url_change), 228 + ) 229 + } 230 + 231 + // Create our update method 232 + fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { 233 + case msg { 234 + OnRouteChange(route) -> #( 235 + Model( 236 + ..model, 237 + // This isn't neccesary currently but is required to keep the state between the route changes 238 + route: route, 239 + ), 240 + effect.none(), 241 + // This just tells our program to not do anything after 242 + ) 243 + } 244 + } 245 + 246 + // Now we can define our view with our html 247 + fn view(model: Model) -> Element(Msg) { 248 + case model.route { 249 + // Here we match the current route in the state and return different html based on what route is recieved 250 + Home -> div([], [text("You are on the homepage")]) 251 + About -> div([], [text("You are on the about page")]) 252 + NotFound -> div([], [text("404 Not Found")]) 253 + } 254 + } 255 + ``` 256 + 257 + That is the code neccesary for a router in client-side Gleam. You have to do some scaffolding youself but it gives you much more control. I expect we will see more abstractions come out over the following years like the currently planned work on a framework called [Pevensive](https://github.com/Pevensie/pevensie/discussions/1) which will feature ready-to-use routing among other things similar to a framework like [Laravel](https://laravel.com/). 258 + 259 + ### Data Fetching 260 + 261 + The datafetching in Lustre is mostly done using the [lustre_http](https://hexdocs.pm/lustre_http/index.html) library. It provides a simple function for getting and posting data in lustre Effects and Messages that I've covered previously but adding a simple data fetch to our routing example above to show posts would be to add `Post` type. 262 + 263 + First we run 264 + 265 + ```bash 266 + $ gleam add lustre_http 267 + ``` 268 + 269 + to add `lustre_http` to our dependecies 270 + 271 + ```rs 272 + pub type Post { 273 + Post(id: Int, title: String, body: String) 274 + } 275 + 276 + // And then if you want to show the data in your state you could add it to your model 277 + type Model { 278 + Model( 279 + ..other data 280 + posts: List(Post) 281 + ) 282 + } 283 + ``` 284 + 285 + Then add a `fn get_posts() -> Effect(msg)` function for getting our data from the api 286 + 287 + ```rs 288 + import gleam/dynamic 289 + import lustre_http 290 + import gleam/int 291 + 292 + fn get_posts() { 293 + let decoder = 294 + dynamic.list( // We want to decode a list so we use a dynamic.list here 295 + dynamic.decode3( // And it is a list of json that looks like this {id: 1, title: "title", body: "body"} so we use a decodeN matching the number of arguments 296 + Post, // You input the type of your data here 297 + dynamic.field("id", dynamic.int), // Then here and for the following lines you define the field with the name and the type 298 + dynamic.field("title", dynamic.string), 299 + dynamic.field("body", dynamic.string), 300 + ) 301 + ) 302 + 303 + lustre_http.get( // Then you call lustre_http get 304 + "http://localhost:8000/posts", // This will be a call to our future backend 305 + lustre_http.expect_json(decoder, GotPosts), // Then lustre_http exposes a method to parse the resulting data as json that takes in our json decoder from earlier with the Msg that signals that the data was recieved 306 + ) 307 + } 308 + ``` 309 + 310 + this function can then in turn be added into our `Msg` type 311 + 312 + ```rs 313 + pub type Msg { 314 + ..other messages 315 + GotPosts(Result(List(Post), lustre_http.HttpError)) 316 + } 317 + 318 + // And subsequently our update method 319 + fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { 320 + case msg { 321 + ..other messages 322 + GotPosts(posts_result) -> case posts_result { 323 + Ok(posts) -> #(Model(..model, posts: posts), effect.none()) // Here we set the state to our current state + our new posts 324 + Error(_) -> panic 325 + } 326 + } 327 + } 328 + ``` 329 + 330 + And to call this function on page start if you are doing something like authentication that shouldn't be done via interaction but instead automatically then you can run this function in your lustre init method. 331 + 332 + ```rs 333 + fn init(_) -> #(Model, Effect(Msg)) { 334 + #( 335 + Model( 336 + route: get_route(), 337 + posts: [] // This is our list of posts 338 + ), 339 + effect.batch([ 340 + modem.init(on_url_change), // Move the modem.init here inside the new effect.batch 341 + get_posts(), 342 + ]) 343 + ) 344 + } 345 + ``` 346 + 347 + If we want to now show our posts in our rudamentary frontend fron the rounting section then we can just do this: 348 + 349 + ```rs 350 + type Route { 351 + ..other routes 352 + ShowPost(post_id: Int) // Add a post page that takes in a post_id 353 + } 354 + 355 + fn get_route() -> Route { 356 + let uri = case do_get_route() |> uri.parse { 357 + Ok(uri) -> uri 358 + _ -> panic as "Invalid uri" 359 + } 360 + 361 + case uri.path |> uri.path_segments { 362 + ..other routes 363 + ["post", post_id_string] -> { 364 + let assert Ok(post_id) = int.parse(post_id_string) // Here we parse our post_id from our url and require it to be an int. Ideally in a production application you'd do some error handling here but we only care if it's an integer. 365 + ShowPost(post_id) // Return the route Post with our post_id 366 + } 367 + } 368 + } 369 + 370 + fn view(model: Model) -> Element(Msg) { 371 + case model.route { 372 + Home -> div([], // If we are on the homepage 373 + list.map(model.posts, fn(post) { // Loop over all posts in our model 374 + a([href("/post/" <> int.to_string(post.id))], [ // Return a link to /post/(post_id) 375 + text(post.title), // With the post title as the link value 376 + ]) 377 + }) 378 + ) 379 + ShowPost(post_id) -> { // If we are on the post page with a valid post_id 380 + let assert Ok(post) = list.find(model.posts, fn(post) { post.id == post_id }) // We find the post matching our post_id. Same as the post_id parsing but we only care if the value is valid so we don't care about error handling. 381 + 382 + div([], [ // Show our target post 383 + text(post.title), 384 + text(": "), 385 + text(post.body) 386 + ]) 387 + } 388 + About -> div([], [text("You are on the about page")]) 389 + NotFound -> div([], [text("404 Not Found")]) 390 + } 391 + } 392 + ``` 393 + 394 + which will show all posts from our coming backend on our frontend. 395 + 396 + And now we can use our model in our view using the `model: Model` variable passed to our view function. This is the bulk of the work that is used to allow a functioning frontend in kirakira. Now if we want to create a post request that adds a post then we can do the following. 397 + 398 + #### Creating a post 399 + 400 + ```rs 401 + import gleam/json 402 + import gleam/option.{type Option} 403 + import lustre/event 404 + 405 + type Model { // Update our model 406 + Model( 407 + ..previous data 408 + title: String, // Add title and body to our model. These will be the values we create our post with 409 + body: String 410 + ) 411 + } 412 + 413 + pub type MessageErrorResponse { // Add a new type for our responses that can only have a message or an error 414 + MessageErrorResponse(message: Option(String), error: Option(String)) 415 + } 416 + 417 + pub type Msg { // We also update our messages 418 + ..other messages 419 + TitleUpdated(value: String) // Add Title and Body updated to handle the input updating in the frontend to sync it with the state of our lustre application 420 + BodyUpdated(value: String) 421 + RequestCreatePost // Create a message for our form to create the post 422 + CreatePostResponded(Result(MessageErrorResponse, lustre_http.HttpError)) // Create a message for when the backend send back a result 423 + } 424 + 425 + fn init(_) -> #(Model, Effect(Msg)) { // We update our init function accordingly 426 + #( 427 + Model( 428 + ..previous data 429 + title: "", // Initalize the title and body to empty string 430 + body: "" 431 + ), 432 + get_posts() 433 + ) 434 + } 435 + 436 + fn create_post(model: Model) { 437 + lustre_http.post( 438 + "http://localhost:8000/posts", // This will be a call to our future backends create post route 439 + json.object([ 440 + #("title", json.string(model.title)), 441 + #("body", json.string(model.body)) 442 + ]), 443 + lustre_http.expect_json( 444 + dynamic.decode2( 445 + MessageErrorResponse, 446 + dynamic.optional_field("message", dynamic.string), 447 + dynamic.optional_field("error", dynamic.string), 448 + ), 449 + CreatePostResponded 450 + ) 451 + ) 452 + } 453 + 454 + fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { 455 + case msg { 456 + ..other messages 457 + TitleUpdated(value) -> #( // If the user updates the title input 458 + Model(..model, title: value), // Then we update the current model with the current state and we modify the title to the new value 459 + effect.none(), 460 + ) 461 + BodyUpdated(value) -> #( // Same with the body 462 + Model(..model, body: value), 463 + effect.none(), 464 + ) 465 + RequestCreatePost -> #(model, create_post(model)) // Run the create_post function if the RequestCreatePost message was recieved from the frontend. 466 + CreatePostResponded(response) -> #(model, get_posts()) // If the create post responded then we want to refetch our posts 467 + } 468 + } 469 + 470 + fn view(model: Model) -> Element(Msg) { 471 + case model.route { 472 + ..other routes 473 + Home -> 474 + div([], list.append([ 475 + form([event.on_submit(RequestCreatePost)], [ // If the user submits the form by clicking on the button we request gleam to create our post 476 + text("Title"), 477 + input([event.on_input(TitleUpdated)]), // event.on_input sends the message TitleUpdated each time the user updates the input 478 + text("Body"), 479 + input([event.on_input(BodyUpdated)]), // Same here but for BodyUpdated 480 + button([type_("submit")], [ 481 + text("Create Post") 482 + ]) 483 + ]) 484 + ], 485 + list.map(model.posts, fn(post) { // Loop over all posts in our model 486 + a([href("/post/" <> int.to_string(post.id))], [ // Return a link to /post/(post_id) 487 + text(post.title), // With the post title as the link value 488 + ]) 489 + }) 490 + ) 491 + } 492 + } 493 + ``` 494 + 495 + ## The Backend 496 + 497 + As in most programming languages, interpreted or not, Gleam features an easy to use webserver similar to the likes of [ExpressJS](https://expressjs.com/) and [Go Fiber](https://gofiber.io/) offering a simple and robost way to serve data over the wire and this solution is called [Wisp](https://gleam-wisp.github.io/wisp/). Wisp includes good examples on how you would handle the basics and for most webapps they're really all you need. A backend for a lot of projects will only really need routing, database interfacing, and responding to the incomming request with json. Most of these are covered in their [examples](https://github.com/gleam-wisp/wisp/tree/main/examples) however I will go through how I utalized these in [Kirakira](https://kirakira.keii.dev). 498 + 499 + ### Creating our backend app 500 + 501 + We will do the same spiel so enter the `/my-app` directory (or whatever you called it) and run the following commands. 502 + 503 + ```bash 504 + $ gleam new backend # Create our gleam app named backend 505 + $ cd backend # Enter the new frontend project 506 + $ gleam add wisp mist gleam_http gleam_erlang simplifile gleam_json cors_builder # Add the lustre and lustre_dev_tools dependencies 507 + ``` 508 + 509 + And create a `data.json` file in `/backend` containing a `[]` to initialize our "database". 510 + 511 + ### Routing 512 + 513 + First we have the routing, arguably the most important part of any backend. Following the routing examples from the examples link above we use the same `app.gleam` with the main method and `/app/web.gleam` as they will not change throughout the project (atleast not until a substantial part if you aren't doing anything wierd). 514 + 515 + Here is the `backend.gleam` we will use 516 + 517 + ```rs 518 + import backend/router 519 + import gleam/erlang/process 520 + import mist 521 + import wisp 522 + 523 + pub fn main() { 524 + wisp.configure_logger() 525 + let secret_key_base = wisp.random_string(64) 526 + 527 + let assert Ok(_) = 528 + wisp.mist_handler(router.handle_request, secret_key_base) 529 + |> mist.new 530 + |> mist.port(8000) 531 + |> mist.start_http 532 + 533 + process.sleep_forever() 534 + } 535 + ``` 536 + 537 + and subsequent `/backend/web.gleam` 538 + 539 + ```rs 540 + import wisp 541 + 542 + pub fn middleware( 543 + req: wisp.Request, 544 + handle_request: fn(wisp.Request) -> wisp.Response, 545 + ) -> wisp.Response { 546 + let req = wisp.method_override(req) 547 + use <- wisp.log_request(req) 548 + use <- wisp.rescue_crashes 549 + use req <- wisp.handle_head(req) 550 + 551 + handle_request(req) 552 + } 553 + ``` 554 + 555 + The `router.gleam` is also unchanged but we will remove the default route and add a `/posts` route. 556 + 557 + ```rs 558 + import wisp.{type Request, type Response} 559 + import gleam/string_builder 560 + import gleam/http.{Get, Post as WispPost} 561 + import cors_builder as cors 562 + import backend/web 563 + import gleam/result 564 + import gleam/dynamic 565 + import gleam/json 566 + import gleam/list 567 + 568 + pub fn handle_request(req: Request) -> Response { 569 + use req <- web.middleware(req) 570 + 571 + case wisp.path_segments(req) { 572 + ["posts"] -> case req.method { // If the user requests the posts route 573 + Get -> list_posts(req) // And the method is GET, return a list of all posts, we will create this function later 574 + WispPost -> create_post(req) // And if the method is POST create a post, we will create this function later 575 + _ -> wisp.method_not_allowed([Get, WispPost]) // And if its neither return an invalid method error 576 + } 577 + _ -> wisp.not_found() // If the route is not /posts return a 404 not found 578 + } 579 + } 580 + ``` 581 + 582 + ### Creating our List Post Controller 583 + 584 + To handle the route we will create our `controllers` just a fancy word for function that handles a request really. We will start with our `list_posts` function from before. 585 + 586 + ```rs 587 + type Post { // Create a type that models our post 588 + Post(id: Int, title: String, body: String) 589 + } 590 + 591 + fn list_posts(req: Request) -> Response { 592 + // Here we will use blocks and use statements and i will explain them more in detail later 593 + 594 + let result = { 595 + use file_data <- result.try(simplifile.read(from: "./data.json") // To avoid this post getting even *longer* i will use a file as a database. Gleam and databases is for another article. Simplifile is a standard for filesystem usage in Gleam so we use it here 596 + |> result.replace_error("Problem reading data.json")) // Here we also replace the error with a string so it can be returned later in the error 597 + 598 + // Here we will parse our data from json to a type and then back into json to simulate this coming from a database of some sort but this could really just be a simple returning of the file_data if you wanted to if you are just doing files that map directly to the response. 599 + 600 + let posts_decoder = // Create a decoder that parses a list of posts eg. [{id: 1, title: "Post", body: "Body"}] 601 + dynamic.list(dynamic.decode3( 602 + Post, 603 + dynamic.field("id", dynamic.int), 604 + dynamic.field("title", dynamic.string), 605 + dynamic.field("body", dynamic.string) 606 + )) 607 + 608 + use posts <- result.try(json.decode(from: file_data, using: posts_decoder) // Take our string file_data and turn it into our Post type using our decoder 609 + |> result.replace_error("Problem decoding file_data to posts")) 610 + 611 + Ok(json.array(posts, fn(post) { // Encode our 612 + json.object([ 613 + #("id", json.int(post.id)), 614 + #("title", json.string(post.title)), 615 + #("body", json.string(post.body)) 616 + ]) 617 + })) 618 + } 619 + 620 + case result { 621 + Ok(json) -> wisp.json_response(json |> json.to_string_builder, 200) // Return our json posts that we turn into a string_builder as thats whats required with a code of 200 meaning OK. 622 + Error(_) -> wisp.unprocessable_entity() // If we encounter an error we send an empty response. If this were a real application it'd probably be best to send a json_response back. 623 + } 624 + } 625 + ``` 626 + 627 + So what do we do here? Well we use some syntax that might be new to you. We create our result variable and then we open a block. In gleam everything is a statement so you can assign a block with a return to a variable without breaking anything out to a function. Then we have our first problem, the use statement. It has its own [page on the tour](https://tour.gleam.run/advanced-features/use/) that I recommend checking out if you want to learn more but what it effectively does here is taking the [Result](https://tour.gleam.run/data-types/results/) that is returned from a function, here `simplifile.read`. To ignore going into details we then give the result to `result.try` and this makes it so we can do `use` on it. Now we can do our use like so `use file_data <- result.try(simplifile.read(filepath))` which will put the `Ok` value of our simplifile.read into the `file_data` variable or return an `Error` from the block. This is effectively like an early return as we continue if the result is True or exit the block with the Error if it failed. 628 + 629 + Then we create our decoder which should be fairly straight-forward. We use the dynamic library to decode a list of an object that has the fields id of type Int, title of type String, and body of type String into the type `List(Post)` because we marked the object to be decoded into the `Post` type. Then we do the same use magic with our json decoder to return an error if it fails or put the parsed List of Posts in our posts variable. 630 + 631 + Then we return an `Ok` where we decode the `List(Post)` into json. This has to be surrounded in the `Ok` because of our `use` statements which return `Error` types and we have to comply with the `Result` returntype. And finally we do a `case` on our `result` variable that contains the `Result` type from out block, returning a json response with the status 200 if the `result` variable is `Ok` and an error if the `result` variable from our block is an `Error`. 632 + 633 + ### Creating our Create Post Controller 634 + 635 + Now to handle our `create_post` route we do essentially the same thing. 636 + 637 + ```rs 638 + // Create a type for our create post request data 639 + type CreatePost { 640 + CreatePost(title: String, body: String) 641 + } 642 + 643 + fn create_post(req: Request) -> Response { 644 + // We will use the same scaffolding as we use in the list_posts example with our result so that can go unchanged 645 + 646 + // Get the json body from our request 647 + use body <- wisp.require_json(req) 648 + 649 + let result = { 650 + // Create a decoder for our request data 651 + let create_post_decoder = dynamic.decode2( 652 + CreatePost, 653 + dynamic.field("title", dynamic.string), 654 + dynamic.field("body", dynamic.string), 655 + ) 656 + 657 + use parsed_request <- result.try(case create_post_decoder(body) { // Decode our body to the CreatePost type 658 + Ok(parsed) -> Ok(parsed) 659 + Error(_) -> Error("Invalid body recieved") 660 + }) 661 + 662 + use file_data <- result.try(simplifile.read(from: "./data.json")) // Load the posts again from the file 663 + 664 + let posts_decoder = // Create a decoder that parses a list of posts eg. [{id: 1, title: "Post", body: "Body"}] 665 + dynamic.list(dynamic.decode3( 666 + Post, 667 + dynamic.field("id", dynamic.int), 668 + dynamic.field("title", dynamic.string), 669 + dynamic.field("body", dynamic.string) 670 + )) 671 + 672 + use posts <- result.try(json.decode(from: file_data, using: posts_decoder)) // Take our string file_data and turn it into our Post type using our decoder 673 + 674 + // Add the new post to the old posts 675 + let new_posts = list.append(posts, [Post(id: list.length(posts), title: parsed_request.title, body: parsed_request.body)]) 676 + 677 + let new_posts_as_json = json.array(new_posts, fn(post) { // Encode our posts to json 678 + json.object([ 679 + #("id", json.int(post.id)), 680 + #("title", json.string(post.title)), 681 + #("body", json.string(post.body)) 682 + ]) 683 + }) 684 + 685 + let _ = new_posts_as_json // let _ = syntax just discards the value 686 + |> json.to_string // Turn the new posts json into a string 687 + |> simplifile.write(to: "./data.json") // And write it to our data.json file 688 + 689 + Ok("Successfully created post") // Return a success message 690 + } 691 + 692 + case result { 693 + Ok(message) -> wisp.json_response(json.object([#("message", json.string(message))]) |> json.to_string_builder, 200) // Return our success 694 + Error(_) -> wisp.unprocessable_entity() // If we encounter an error we send an empty response. If this were a real application it'd probably be best to send a json_response back. 695 + } 696 + } 697 + ``` 698 + 699 + And that should be it for our backend. Now if you enter the frontend you should be able to create posts and read them! I'll probably write up an article about database interfacing soon(tm) but for now this is my article on how to create a simple system to post posts between a frontend and a backend in Gleam! 700 + 701 + Thank you for reading, and I humbly wish you good night as it's 4:23am <3.