diff --git a/package.json b/package.json index 8dfa65f5ea..069df4f293 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/nesting": "0.0.0-insiders.565cd3e", "@tailwindcss/typography": "^0.5.15", + "arktype": "2.1.28", "autoprefixer": "^10.4.20", "calendar-link": "^2.10.0", "clsx": "^2.1.1", @@ -59,7 +60,7 @@ "leaflet": "^1.9.4", "lucide-react": "^0.469.0", "motion": "^12.11.0", - "next": "^14.2.32", + "next": "14.2.34", "next-query-params": "^5.0.1", "next-sitemap": "^4.2.3", "next-with-less": "^3.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7ce62ccfb..b9c2882449 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,7 +70,7 @@ importers: version: 15.5.6 '@plaiceholder/next': specifier: ^3.0.0 - version: 3.0.0(next@14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(plaiceholder@3.0.0(sharp@0.34.4))(sharp@0.34.4) + version: 3.0.0(next@14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(plaiceholder@3.0.0(sharp@0.34.4))(sharp@0.34.4) '@sparticuz/chromium': specifier: ^138.0.2 version: 138.0.2 @@ -83,6 +83,9 @@ importers: '@tailwindcss/typography': specifier: ^0.5.15 version: 0.5.19(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.1)) + arktype: + specifier: 2.1.28 + version: 2.1.28 autoprefixer: specifier: ^10.4.20 version: 10.4.22(postcss@8.5.6) @@ -132,23 +135,23 @@ importers: specifier: ^12.11.0 version: 12.23.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next: - specifier: ^14.2.32 - version: 14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 14.2.34 + version: 14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-query-params: specifier: ^5.0.1 - version: 5.1.0(next@14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(use-query-params@2.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 5.1.0(next@14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(use-query-params@2.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) next-sitemap: specifier: ^4.2.3 - version: 4.2.3(next@14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 4.2.3(next@14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) next-with-less: specifier: ^3.0.1 - version: 3.0.1(less-loader@12.3.0(less@4.4.1))(less@4.4.1)(next@14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 3.0.1(less-loader@12.3.0(less@4.4.1))(less@4.4.1)(next@14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) nextra: specifier: 3.3.1 - version: 3.3.1(patch_hash=a4cb9ca39251906b7635817067482091dda31729230807c156358a0561ce2bcb)(@types/react@18.3.27)(acorn@8.15.0)(next@14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + version: 3.3.1(patch_hash=a4cb9ca39251906b7635817067482091dda31729230807c156358a0561ce2bcb)(@types/react@18.3.27)(acorn@8.15.0)(next@14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) nextra-theme-docs: specifier: 3.3.1 - version: 3.3.1(patch_hash=2cafbb261163557a490b97bea35ce78a55af9ec0ae200e2545ad15543b1443e5)(next@14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.3.1(patch_hash=a4cb9ca39251906b7635817067482091dda31729230807c156358a0561ce2bcb)(@types/react@18.3.27)(acorn@8.15.0)(next@14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.3.1(patch_hash=2cafbb261163557a490b97bea35ce78a55af9ec0ae200e2545ad15543b1443e5)(next@14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.3.1(patch_hash=a4cb9ca39251906b7635817067482091dda31729230807c156358a0561ce2bcb)(@types/react@18.3.27)(acorn@8.15.0)(next@14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) numbro: specifier: 2.5.0 version: 2.5.0 @@ -326,8 +329,8 @@ importers: scripts/sync-working-groups: dependencies: arktype: - specifier: ^2.1.27 - version: 2.1.27 + specifier: 2.1.28 + version: 2.1.28 packages: @@ -351,11 +354,11 @@ packages: peerDependencies: graphql: '*' - '@ark/schema@0.55.0': - resolution: {integrity: sha512-IlSIc0FmLKTDGr4I/FzNHauMn0MADA6bCjT1wauu4k6MyxhC1R9gz0olNpIRvK7lGGDwtc/VO0RUDNvVQW5WFg==} + '@ark/schema@0.56.0': + resolution: {integrity: sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==} - '@ark/util@0.55.0': - resolution: {integrity: sha512-aWFNK7aqSvqFtVsl1xmbTjGbg91uqtJV7Za76YGNEwIO4qLjMfyY8flmmbhooYMuqPCO2jyxu8hve943D+w3bA==} + '@ark/util@0.56.0': + resolution: {integrity: sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==} '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -1974,8 +1977,8 @@ packages: '@next/env@13.5.11': resolution: {integrity: sha512-fbb2C7HChgM7CemdCY+y3N1n8pcTKdqtQLbC7/EQtPdLvlMUT9JX/dBYl8MMZAtYG4uVMyPFHXckb68q/NRwqg==} - '@next/env@14.2.33': - resolution: {integrity: sha512-CgVHNZ1fRIlxkLhIX22flAZI/HmpDaZ8vwyJ/B0SDPTBuLZ1PJ+DWMjCHhqnExfmSQzA/PbZi8OAc7PAq2w9IA==} + '@next/env@14.2.34': + resolution: {integrity: sha512-iuGW/UM+EZbn2dm+aLx+avo1rVap+ASoFr7oLpTBVW2G2DqhD5l8Fme9IsLZ6TTsp0ozVSFswidiHK1NGNO+pg==} '@next/eslint-plugin-next@15.5.6': resolution: {integrity: sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ==} @@ -2661,11 +2664,11 @@ packages: aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - arkregex@0.0.3: - resolution: {integrity: sha512-bU21QJOJEFJK+BPNgv+5bVXkvRxyAvgnon75D92newgHxkBJTgiFwQxusyViYyJkETsddPlHyspshDQcCzmkNg==} + arkregex@0.0.4: + resolution: {integrity: sha512-biS/FkvSwQq59TZ453piUp8bxMui11pgOMV9WHAnli1F8o0ayNCZzUwQadL/bGIUic5TkS/QlPcyMuI8ZIwedQ==} - arktype@2.1.27: - resolution: {integrity: sha512-enctOHxI4SULBv/TDtCVi5M8oLd4J5SVlPUblXDzSsOYQNMzmVbUosGBnJuZDKmFlN5Ie0/QVEuTE+Z5X1UhsQ==} + arktype@2.1.28: + resolution: {integrity: sha512-LVZqXl2zWRpNFnbITrtFmqeqNkPPo+KemuzbGSY6jvJwCb4v8NsDzrWOLHnQgWl26TkJeWWcUNUeBpq2Mst1/Q==} array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} @@ -2860,9 +2863,6 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001751: - resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} - caniuse-lite@1.0.30001754: resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==} @@ -4928,8 +4928,8 @@ packages: less-loader: '>= 7.0.0' next: '>= 11.0.1' - next@14.2.33: - resolution: {integrity: sha512-GiKHLsD00t4ACm1p00VgrI0rUFAC9cRDGReKyERlM57aeEZkOQGcZTpIbsGn0b562FTPJWmYfKwplfO9EaT6ng==} + next@14.2.34: + resolution: {integrity: sha512-s7mRraWlkEVRLjHHdu5khn0bSnmUh+U+YtigBc+t2Ge7jJHFIVBZna+W9Jcx7b04HhM7eJWrNJ2A+sQs9gJ3eg==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -6646,11 +6646,11 @@ snapshots: transitivePeerDependencies: - encoding - '@ark/schema@0.55.0': + '@ark/schema@0.56.0': dependencies: - '@ark/util': 0.55.0 + '@ark/util': 0.56.0 - '@ark/util@0.55.0': {} + '@ark/util@0.56.0': {} '@asamuzakjp/css-color@3.2.0': dependencies: @@ -8624,7 +8624,7 @@ snapshots: '@next/env@13.5.11': {} - '@next/env@14.2.33': {} + '@next/env@14.2.34': {} '@next/eslint-plugin-next@15.5.6': dependencies: @@ -8726,9 +8726,9 @@ snapshots: '@pkgr/core@0.2.9': {} - '@plaiceholder/next@3.0.0(next@14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(plaiceholder@3.0.0(sharp@0.34.4))(sharp@0.34.4)': + '@plaiceholder/next@3.0.0(next@14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(plaiceholder@3.0.0(sharp@0.34.4))(sharp@0.34.4)': dependencies: - next: 14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) plaiceholder: 3.0.0(sharp@0.34.4) sharp: 0.34.4 @@ -9408,15 +9408,15 @@ snapshots: dependencies: dequal: 2.0.3 - arkregex@0.0.3: + arkregex@0.0.4: dependencies: - '@ark/util': 0.55.0 + '@ark/util': 0.56.0 - arktype@2.1.27: + arktype@2.1.28: dependencies: - '@ark/schema': 0.55.0 - '@ark/util': 0.55.0 - arkregex: 0.0.3 + '@ark/schema': 0.56.0 + '@ark/util': 0.56.0 + arkregex: 0.0.4 array-buffer-byte-length@1.0.2: dependencies: @@ -9634,8 +9634,6 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001751: {} - caniuse-lite@1.0.30001754: {} capital-case@1.0.4: @@ -12281,39 +12279,39 @@ snapshots: negotiator@1.0.0: {} - next-query-params@5.1.0(next@14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(use-query-params@2.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + next-query-params@5.1.0(next@14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(use-query-params@2.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: - next: 14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 tslib: 2.8.1 use-query-params: 2.2.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - next-sitemap@4.2.3(next@14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + next-sitemap@4.2.3(next@14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: '@corex/deepmerge': 4.0.43 '@next/env': 13.5.11 fast-glob: 3.3.3 minimist: 1.2.8 - next: 14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes@0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next-with-less@3.0.1(less-loader@12.3.0(less@4.4.1))(less@4.4.1)(next@14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + next-with-less@3.0.1(less-loader@12.3.0(less@4.4.1))(less@4.4.1)(next@14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: clone-deep: 4.0.1 less: 4.4.1 less-loader: 12.3.0(less@4.4.1) - next: 14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - next@14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@next/env': 14.2.33 + '@next/env': 14.2.34 '@swc/helpers': 0.5.5 busboy: 1.6.0 - caniuse-lite: 1.0.30001751 + caniuse-lite: 1.0.30001754 graceful-fs: 4.2.11 postcss: 8.4.31 react: 18.3.1 @@ -12334,21 +12332,21 @@ snapshots: - '@babel/core' - babel-plugin-macros - nextra-theme-docs@3.3.1(patch_hash=2cafbb261163557a490b97bea35ce78a55af9ec0ae200e2545ad15543b1443e5)(next@14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.3.1(patch_hash=a4cb9ca39251906b7635817067482091dda31729230807c156358a0561ce2bcb)(@types/react@18.3.27)(acorn@8.15.0)(next@14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + nextra-theme-docs@3.3.1(patch_hash=2cafbb261163557a490b97bea35ce78a55af9ec0ae200e2545ad15543b1443e5)(next@14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.3.1(patch_hash=a4cb9ca39251906b7635817067482091dda31729230807c156358a0561ce2bcb)(@types/react@18.3.27)(acorn@8.15.0)(next@14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@headlessui/react': 2.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) clsx: 2.1.1 escape-string-regexp: 5.0.0 flexsearch: 0.7.43 - next: 14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - nextra: 3.3.1(patch_hash=a4cb9ca39251906b7635817067482091dda31729230807c156358a0561ce2bcb)(@types/react@18.3.27)(acorn@8.15.0)(next@14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + nextra: 3.3.1(patch_hash=a4cb9ca39251906b7635817067482091dda31729230807c156358a0561ce2bcb)(@types/react@18.3.27)(acorn@8.15.0)(next@14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) scroll-into-view-if-needed: 3.1.0 zod: 3.25.76 - nextra@3.3.1(patch_hash=a4cb9ca39251906b7635817067482091dda31729230807c156358a0561ce2bcb)(@types/react@18.3.27)(acorn@8.15.0)(next@14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3): + nextra@3.3.1(patch_hash=a4cb9ca39251906b7635817067482091dda31729230807c156358a0561ce2bcb)(@types/react@18.3.27)(acorn@8.15.0)(next@14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3): dependencies: '@formatjs/intl-localematcher': 0.5.10 '@headlessui/react': 2.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -12371,7 +12369,7 @@ snapshots: mdast-util-gfm: 3.1.0 mdast-util-to-hast: 13.2.0 negotiator: 1.0.0 - next: 14.2.33(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.34(@babel/core@7.28.3)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) p-limit: 6.2.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) diff --git a/scripts/sync-working-groups/package.json b/scripts/sync-working-groups/package.json index b470821c56..7659901d21 100644 --- a/scripts/sync-working-groups/package.json +++ b/scripts/sync-working-groups/package.json @@ -7,6 +7,6 @@ "start": "node ./sync-working-groups.ts" }, "dependencies": { - "arktype": "^2.1.27" + "arktype": "2.1.28" } } diff --git a/src/_design-system/breadcrumbs.tsx b/src/_design-system/breadcrumbs.tsx index 4b42a7554a..a394874b38 100644 --- a/src/_design-system/breadcrumbs.tsx +++ b/src/_design-system/breadcrumbs.tsx @@ -26,9 +26,10 @@ export const Breadcrumbs = ({ const title = extractStringsFromReactNode(item.title) const className = clsx( - "text-neu-700 dark:text-neu-400 min-w-6 last:text-neu-800 dark:last:text-neu-800 leading-none", + "text-neu-700 dark:text-neu-400 min-w-6 last:text-neu-800 dark:last:text-neu-800 leading-none whitespace-pre", href && "gql-focus-visible ring-inset hover:text-neu-900 hover:underline underline-offset-2", + item.title.length > 8 ? "overflow-hidden truncate" : "shrink-0", ) return ( diff --git a/src/app/(main)/resources/[category]/blog-posts-section.tsx b/src/app/(main)/resources/[category]/blog-posts-section.tsx new file mode 100644 index 0000000000..df478f7694 --- /dev/null +++ b/src/app/(main)/resources/[category]/blog-posts-section.tsx @@ -0,0 +1,61 @@ +"use client" + +import { Button } from "@/app/conf/_design-system/button" +import { Eyebrow } from "@/_design-system/eyebrow" +import { BlogCard } from "@/components/blog-page/blog-card" + +export interface BlogPost { + href: string + title: string + author: string + date?: Date + tags: string[] +} + +export interface BlogPostsSectionProps { + title: string + description: string + posts: BlogPost[] + readAllHref?: string + readAllLabel?: string +} + +export function BlogPostsSection({ + title, + description, + posts, + readAllHref = "/blog", + readAllLabel = "Read all GraphQL stories", +}: BlogPostsSectionProps) { + return ( +
+
+
+ Blog posts +

{title}

+

+ {description} +

+
+ +
+ +
+ {posts.map(post => ( + + ))} +
+
+ ) +} diff --git a/src/app/(main)/resources/[category]/blur-corner.webp b/src/app/(main)/resources/[category]/blur-corner.webp new file mode 100644 index 0000000000..50e58292e7 Binary files /dev/null and b/src/app/(main)/resources/[category]/blur-corner.webp differ diff --git a/src/app/(main)/resources/[category]/category-tools-libraries-section.tsx b/src/app/(main)/resources/[category]/category-tools-libraries-section.tsx new file mode 100644 index 0000000000..61b565187a --- /dev/null +++ b/src/app/(main)/resources/[category]/category-tools-libraries-section.tsx @@ -0,0 +1,210 @@ +import path from "node:path" +import { glob } from "node:fs/promises" +import { readFile } from "node:fs/promises" +import matter from "gray-matter" + +import type { CSSProperties } from "react" +import { Button } from "@/app/conf/_design-system/button" +import blurCorner from "./blur-corner.webp" +import { Eyebrow } from "@/_design-system/eyebrow" +import slugMap from "@/code/slug-map.json" +import { type Topic } from "@/resources/types" +import { StripesDecoration } from "@/app/conf/_design-system/stripes-decoration" + +import { icons } from "./icons" +import { ChevronRight } from "@/app/conf/_design-system/pixelarticons/chevron-right" + +interface LibraryEntry { + name: string + href?: string + group: string + icon: React.ReactNode + tags: string[] +} + +const librariesPromise = loadLibraries() + +async function loadLibraries(): Promise { + const entries: LibraryEntry[] = [] + + for await (const file of glob("src/code/**/*.md")) { + const relative = path.relative("src/code", file) + const segments = relative.split(path.sep) + const top = segments[0] + const group = + top === "language-support" ? (segments[1] ?? "language-support") : top + if (!group) continue + + const raw = await readFile(file, "utf8") + const { data } = matter(raw) + const tags: string[] = Array.isArray(data.tags) ? data.tags : [] + if (!tags.includes("tools-and-libraries")) continue + + const name: string | undefined = data.name + if (!name) continue + + const href: string | undefined = + data.url ?? + (data.github ? `https://github.com/${data.github}` : undefined) ?? + (data.npm ? `https://npmjs.com/package/${data.npm}` : undefined) + + entries.push({ name, href, group, tags }) + } + + const deduped = entries.filter( + (item, index, self) => + index === + self.findIndex(t => t.name.toLowerCase() === item.name.toLowerCase()), + ) + + return deduped +} + +function displayName(id: string) { + const key = id as keyof typeof slugMap + return slugMap[key] ?? id +} + +export async function CategoryToolsLibrariesSection({ + category, +}: { + category: Topic +}) { + const libraries = await librariesPromise + const filtered = libraries.filter(item => item.tags.includes(category)) + + const grouped = Array.from( + filtered.reduce>((acc, item) => { + const list = acc.get(item.group) ?? [] + list.push(item) + acc.set(item.group, list) + return acc + }, new Map()), + ) + .map(([group, items]) => ({ + id: group, + name: displayName(group), + items: items + .sort((a, b) => + a.name.localeCompare(b.name, "en", { sensitivity: "base" }), + ) + .slice(0, 20), + })) + .sort((a, b) => b.items.length - a.items.length) + + if (grouped.length === 0) { + return null + } + + return ( +
+ +
+
+
+ + key tools & libraries + +

+ Build GraphQL with tools and libraries +

+

+ Explore language and platform tooling to ship production-ready + graphs. +

+
+ +
+ +
+ {grouped.map((group, index) => { + const nextLength = grouped[index + 1]?.items.length ?? 0 + const columns = + nextLength > 0 && group.items.length >= nextLength * 1.9 ? 2 : 1 + const listStyle = { "--item-columns": columns } as CSSProperties + const breakIndex = + columns === 2 ? Math.floor(group.items.length / 2) : 0 + + const Icon = icons[group.id] + + return ( +
+
+ {Icon && ( +
+ +
+ )} + {group.name} +
+ {/* TODO: On mobile */} + +
+
+
    + {group.items.map((item, i) => ( +
  • = i ? "1px" : "", + }} + > + {item.href ? ( + + {item.name} + + ) : ( + + {item.name} + + )} +
  • + ))} +
+
+ ) + })} +
+
+
+ ) +} + +function Stripes() { + return ( +
+ +
+ ) +} diff --git a/src/app/(main)/resources/[category]/icons/ballerina.svg b/src/app/(main)/resources/[category]/icons/ballerina.svg new file mode 100644 index 0000000000..ac26d152ef --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/ballerina.svg @@ -0,0 +1,11 @@ + + + diff --git a/src/app/(main)/resources/[category]/icons/c-net.svg b/src/app/(main)/resources/[category]/icons/c-net.svg new file mode 100644 index 0000000000..462caaccb8 --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/c-net.svg @@ -0,0 +1,20 @@ + + + + + diff --git a/src/app/(main)/resources/[category]/icons/clojure.svg b/src/app/(main)/resources/[category]/icons/clojure.svg new file mode 100644 index 0000000000..da928dca14 --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/clojure.svg @@ -0,0 +1,11 @@ + + + diff --git a/src/app/(main)/resources/[category]/icons/elixir.svg b/src/app/(main)/resources/[category]/icons/elixir.svg new file mode 100644 index 0000000000..1d8b0670c6 --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/elixir.svg @@ -0,0 +1,20 @@ + + + + + + diff --git a/src/app/(main)/resources/[category]/icons/elm.svg b/src/app/(main)/resources/[category]/icons/elm.svg new file mode 100644 index 0000000000..3ecba14b6e --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/elm.svg @@ -0,0 +1,11 @@ + + + diff --git a/src/app/(main)/resources/[category]/icons/flutter.svg b/src/app/(main)/resources/[category]/icons/flutter.svg new file mode 100644 index 0000000000..3ba8a43d2c --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/flutter.svg @@ -0,0 +1,11 @@ + + + diff --git a/src/app/(main)/resources/[category]/icons/go.svg b/src/app/(main)/resources/[category]/icons/go.svg new file mode 100644 index 0000000000..040c13a73d --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/go.svg @@ -0,0 +1,14 @@ + + + + diff --git a/src/app/(main)/resources/[category]/icons/haskell.svg b/src/app/(main)/resources/[category]/icons/haskell.svg new file mode 100644 index 0000000000..7e3d97c155 --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/haskell.svg @@ -0,0 +1,11 @@ + + + diff --git a/src/app/(main)/resources/[category]/icons/index.tsx b/src/app/(main)/resources/[category]/icons/index.tsx new file mode 100644 index 0000000000..a65381792c --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/index.tsx @@ -0,0 +1,41 @@ +import type { ComponentType, SVGProps } from "react" + +import Ballerina from "./ballerina.svg?svgr" +import CNet from "./c-net.svg?svgr" +import Clojure from "./clojure.svg?svgr" +import Elixir from "./elixir.svg?svgr" +import Elm from "./elm.svg?svgr" +import Flutter from "./flutter.svg?svgr" +import GoIcon from "./go.svg?svgr" +import Haskell from "./haskell.svg?svgr" +import JavaKotlinAndroid from "./java-kotlin-android.svg?svgr" +import Javascript from "./javascript.svg?svgr" +import Julia from "./julia.svg?svgr" +import LanguageSupport from "./language-support.svg?svgr" +import Php from "./php.svg?svgr" +import Python from "./python.svg?svgr" +import Ruby from "./ruby.svg?svgr" +import Rust from "./rust.svg?svgr" +import Scala from "./scala.svg?svgr" +import SwiftObjectiveCIos from "./swift-objective-c-ios.svg?svgr" + +export const icons = { + ballerina: Ballerina, + "c-net": CNet, + clojure: Clojure, + elixir: Elixir, + elm: Elm, + flutter: Flutter, + go: GoIcon, + haskell: Haskell, + "java-kotlin-android": JavaKotlinAndroid, + javascript: Javascript, + julia: Julia, + "language-support": LanguageSupport, + php: Php, + python: Python, + ruby: Ruby, + rust: Rust, + scala: Scala, + "swift-objective-c-ios": SwiftObjectiveCIos, +} satisfies Record>> diff --git a/src/app/(main)/resources/[category]/icons/java.svg b/src/app/(main)/resources/[category]/icons/java.svg new file mode 100644 index 0000000000..c715c1721a --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/java.svg @@ -0,0 +1,11 @@ + + + diff --git a/src/app/(main)/resources/[category]/icons/javascript.svg b/src/app/(main)/resources/[category]/icons/javascript.svg new file mode 100644 index 0000000000..f6456d509a --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/javascript.svg @@ -0,0 +1,11 @@ + + + diff --git a/src/app/(main)/resources/[category]/icons/julia.svg b/src/app/(main)/resources/[category]/icons/julia.svg new file mode 100644 index 0000000000..f665e15ac6 --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/julia.svg @@ -0,0 +1,11 @@ + + + diff --git a/src/app/(main)/resources/[category]/icons/multiplatform.svg b/src/app/(main)/resources/[category]/icons/multiplatform.svg new file mode 100644 index 0000000000..dde94a7300 --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/multiplatform.svg @@ -0,0 +1,17 @@ + + + + + diff --git a/src/app/(main)/resources/[category]/icons/php.svg b/src/app/(main)/resources/[category]/icons/php.svg new file mode 100644 index 0000000000..53d4936dc7 --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/php.svg @@ -0,0 +1,11 @@ + + + diff --git a/src/app/(main)/resources/[category]/icons/python.svg b/src/app/(main)/resources/[category]/icons/python.svg new file mode 100644 index 0000000000..0251e33cb0 --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/python.svg @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/src/app/(main)/resources/[category]/icons/ruby.svg b/src/app/(main)/resources/[category]/icons/ruby.svg new file mode 100644 index 0000000000..f15548a564 --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/ruby.svg @@ -0,0 +1,13 @@ + + + diff --git a/src/app/(main)/resources/[category]/icons/rust.svg b/src/app/(main)/resources/[category]/icons/rust.svg new file mode 100644 index 0000000000..55eae58b74 --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/rust.svg @@ -0,0 +1,11 @@ + + + diff --git a/src/app/(main)/resources/[category]/icons/scala.svg b/src/app/(main)/resources/[category]/icons/scala.svg new file mode 100644 index 0000000000..176a81b929 --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/scala.svg @@ -0,0 +1,11 @@ + + + diff --git a/src/app/(main)/resources/[category]/icons/swift.svg b/src/app/(main)/resources/[category]/icons/swift.svg new file mode 100644 index 0000000000..1211a47a21 --- /dev/null +++ b/src/app/(main)/resources/[category]/icons/swift.svg @@ -0,0 +1,11 @@ + + + diff --git a/src/app/(main)/resources/[category]/page.tsx b/src/app/(main)/resources/[category]/page.tsx new file mode 100644 index 0000000000..938757d89e --- /dev/null +++ b/src/app/(main)/resources/[category]/page.tsx @@ -0,0 +1,198 @@ +import { Metadata } from "next" +import { notFound } from "next/navigation" + +import { NavbarFixed } from "@/components/navbar/navbar-fixed" +import { getResourcesByTag } from "@/resources/data" +import { + Kind, + kinds, + topics, + type ResourceMetadata, + type Topic, +} from "@/resources/types" + +import { categoryNames, categorySubtitles } from "../subtitles" +import { ResourcesHero } from "../resources-hero" +import { TocHeroContents } from "@/components/toc-hero" +import { Eyebrow } from "@/_design-system/eyebrow" +import { ResourceHubCard } from "../resource-hub-card" +import { BlogPostsSection } from "./blog-posts-section" +import { CategoryToolsLibrariesSection } from "./category-tools-libraries-section" + +const sectionKindNames: Record = { + video: "Featured videos", + blog: "Blog posts", + "tools-and-libraries": "Tools & Libraries", + guide: "Guides", + book: "Books", + "blog-or-newsletter": "Blogs & Newsletters", +} + +// TODO: I'd prefer to have this in JSX over "JSON" objects +const blogTitles: Partial> = { + frontend: "Insights for frontend devs", + backend: "Insights for backend devs", +} + +const blogDescriptions: Partial> = { + frontend: "Stay up to date with insights from the GraphQL community.", + backend: "Stay up to date with insights from the GraphQL community.", +} + +function sectionHeading( + section: { kind: Kind; resources: ResourceMetadata[] }, + category: Topic, +) { + if (section.kind === "video") { + if (category === "frontend") return "Master GraphQL on the frontend" + if (category === "backend") return "Master GraphQL on the backend" + } + + return sectionLabel(section.kind) +} + +interface PageParams { + category: string +} + +export async function generateStaticParams() { + return topics.map(category => ({ category })) +} + +export async function generateMetadata({ + params, +}: { + params: PageParams +}): Promise { + const category = params.category as Topic + if (!topics.includes(category)) return {} + + const title = `${categoryNames[category]} Resources` + const description = categorySubtitles[category] + + return { title, description } +} + +export default async function CategoryPage({ params }: { params: PageParams }) { + const category = params.category as Topic + if (!topics.includes(category)) return notFound() + + const resources = await getResourcesByTag(category) + const deduped = uniqueByTitle(resources) + const grouped = groupByKind(deduped) + + return ( +
+ + + + sectionLabel(section.kind))} + className="max-w-[528px]" + /> + + + {grouped.map(section => ( + + ))} +
+ ) +} + +function uniqueByTitle(resources: ResourceMetadata[]) { + const seen = new Set() + return resources.filter(resource => { + const key = resource.title.trim().toLowerCase() + if (seen.has(key)) return false + seen.add(key) + return true + }) +} + +function groupByKind(resources: ResourceMetadata[]) { + return kinds + .map(kind => ({ + kind, + resources: resources.filter( + resource => (resource.kind ?? getKindFromTags(resource)) === kind, + ), + })) + .filter(section => section.resources.length > 0) +} + +function getKindFromTags(resource: ResourceMetadata) { + return kinds.find(kind => resource.tags.includes(kind)) +} + +function sectionLabel(kind: Kind) { + return sectionKindNames[kind] ?? `${kind[0].toUpperCase()}${kind.slice(1)}` +} + +function CategorySection({ + section, + category, +}: { + section: { kind: Kind; resources: ResourceMetadata[] } + category: Topic +}) { + if (section.kind === "tools-and-libraries") { + return + } + + if (section.kind === "blog") { + return ( + ({ + href: resource.url, + title: resource.title, + author: resource.author ?? "GraphQL Community", + tags: resource.tags.filter(tag => tag !== "blog" && tag !== category), + }))} + /> + ) + } + + return ( +
+
+
+ {sectionKindNames[section.kind]} +

+ {sectionHeading(section, category)} +

+
+ + {section.resources.length} resources + +
+ +
    + {section.resources.map(resource => ( +
  • + +
  • + ))} +
+
+ ) +} diff --git a/src/app/(main)/resources/assets/ai.svg b/src/app/(main)/resources/assets/ai.svg new file mode 100644 index 0000000000..1a74a15a0e --- /dev/null +++ b/src/app/(main)/resources/assets/ai.svg @@ -0,0 +1,18 @@ + + + + + diff --git a/src/app/(main)/resources/assets/archive.svg b/src/app/(main)/resources/assets/archive.svg new file mode 100644 index 0000000000..bb98c0c8e5 --- /dev/null +++ b/src/app/(main)/resources/assets/archive.svg @@ -0,0 +1,14 @@ + + + + diff --git a/src/app/(main)/resources/assets/backend.svg b/src/app/(main)/resources/assets/backend.svg new file mode 100644 index 0000000000..2477c25f58 --- /dev/null +++ b/src/app/(main)/resources/assets/backend.svg @@ -0,0 +1,21 @@ + + + + + + + diff --git a/src/app/(main)/resources/assets/bookmark.svg b/src/app/(main)/resources/assets/bookmark.svg new file mode 100644 index 0000000000..422dc51a2a --- /dev/null +++ b/src/app/(main)/resources/assets/bookmark.svg @@ -0,0 +1,15 @@ + diff --git a/src/app/(main)/resources/assets/federation.svg b/src/app/(main)/resources/assets/federation.svg new file mode 100644 index 0000000000..e16b30d1a2 --- /dev/null +++ b/src/app/(main)/resources/assets/federation.svg @@ -0,0 +1,11 @@ + + + diff --git a/src/app/(main)/resources/assets/frontend.svg b/src/app/(main)/resources/assets/frontend.svg new file mode 100644 index 0000000000..135bf7899a --- /dev/null +++ b/src/app/(main)/resources/assets/frontend.svg @@ -0,0 +1,14 @@ + + + + diff --git a/src/app/(main)/resources/assets/monitoring.svg b/src/app/(main)/resources/assets/monitoring.svg new file mode 100644 index 0000000000..aa9ce67345 --- /dev/null +++ b/src/app/(main)/resources/assets/monitoring.svg @@ -0,0 +1,14 @@ + + + + diff --git a/src/app/(main)/resources/assets/newspaper.svg b/src/app/(main)/resources/assets/newspaper.svg new file mode 100644 index 0000000000..0bc736695f --- /dev/null +++ b/src/app/(main)/resources/assets/newspaper.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/src/app/(main)/resources/assets/security.svg b/src/app/(main)/resources/assets/security.svg new file mode 100644 index 0000000000..c999cc8256 --- /dev/null +++ b/src/app/(main)/resources/assets/security.svg @@ -0,0 +1,20 @@ + + + + + + diff --git a/src/app/(main)/resources/assets/tools.svg b/src/app/(main)/resources/assets/tools.svg new file mode 100644 index 0000000000..e22e782c3d --- /dev/null +++ b/src/app/(main)/resources/assets/tools.svg @@ -0,0 +1,20 @@ + + + + + + diff --git a/src/app/(main)/resources/assets/video-player.svg b/src/app/(main)/resources/assets/video-player.svg new file mode 100644 index 0000000000..6a01b6f929 --- /dev/null +++ b/src/app/(main)/resources/assets/video-player.svg @@ -0,0 +1,6 @@ + diff --git a/src/app/(main)/resources/assets/write-note.svg b/src/app/(main)/resources/assets/write-note.svg new file mode 100644 index 0000000000..6c75219572 --- /dev/null +++ b/src/app/(main)/resources/assets/write-note.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/app/(main)/resources/blog-category-links.tsx b/src/app/(main)/resources/blog-category-links.tsx new file mode 100644 index 0000000000..4ce389ef9c --- /dev/null +++ b/src/app/(main)/resources/blog-category-links.tsx @@ -0,0 +1,46 @@ +"use client" + +import Link from "next/link" +import { Collapsible } from "@base-ui-components/react/collapsible" + +import CaretDownIcon from "@/app/conf/_design-system/pixelarticons/caret-down.svg?svgr" +import { BlogTags } from "@/components/blog-page/blog-tags" +import { blogTagColors } from "@/components/blog-page/blog-tag-colors" + +const categories = Object.keys(blogTagColors) + +/** + * Shows tags on desktop and a collapsible on mobile. + */ +export function BlogCategoryLinks() { + return ( + <> + + + categories + + + + {categories.map(category => ( + + {category.replace(/-/g, " ")} + + ))} + + + +
+ + categories + +
+ +
+
+ + ) +} diff --git a/src/app/(main)/resources/blog-post-list-item.tsx b/src/app/(main)/resources/blog-post-list-item.tsx new file mode 100644 index 0000000000..9dcf236926 --- /dev/null +++ b/src/app/(main)/resources/blog-post-list-item.tsx @@ -0,0 +1,53 @@ +import Link from "next/link" + +import ArrowRightIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr" +import { BlogTags } from "@/components/blog-page/blog-tags" + +export interface BlogPostRowProps { + date: string + category: string + title: string + href: string + author: string +} + +export function BlogPostListItem({ + date, + category, + title, + href, + author, +}: BlogPostRowProps) { + const formattedDate = new Date(date) + .toLocaleDateString("en-GB", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }) + .replaceAll("/", "-") + + return ( + +
+ +
+

+ {title} +

+
+
+ + + {author} + +
+ +
+ + ) +} diff --git a/src/app/(main)/resources/blog-section.tsx b/src/app/(main)/resources/blog-section.tsx new file mode 100644 index 0000000000..91a6546f9a --- /dev/null +++ b/src/app/(main)/resources/blog-section.tsx @@ -0,0 +1,103 @@ +import fs from "node:fs/promises" +import path from "node:path" +import Link from "next/link" +import grayMatter from "gray-matter" + +import { Button } from "@/app/conf/_design-system/button" +import PlayIcon from "@/app/conf/_design-system/pixelarticons/play.svg?svgr" + +import { blogTagColors } from "@/components/blog-page/blog-tag-colors" +import { BlogCategoryLinks } from "./blog-category-links" +import { BlogPostListItem } from "./blog-post-list-item" + +interface BlogFrontMatter { + title: string + tags?: string[] + byline: string + date: string | number | Date +} + +type BlogFrontMatterWithFile = BlogFrontMatter & { + fileName: string + date: Date +} + +let cachedBlogFrontMatters: BlogFrontMatterWithFile[] | null = null + +async function getBlogFrontMatters() { + if (cachedBlogFrontMatters) return cachedBlogFrontMatters + + const files = await fs.readdir("./src/pages/blog") + + const blogs = await Promise.all( + files + .filter(filename => /\.mdx?$/.test(filename)) + .map(async (filename: string) => { + const filePath = path.join("./src/pages/blog", filename) + const content = await fs.readFile(filePath, "utf8") + const { data } = grayMatter(content) + const frontMatter = data as BlogFrontMatter + + return { + ...frontMatter, + fileName: path.parse(filePath).name, + date: new Date(frontMatter.date), + } + }), + ) + + cachedBlogFrontMatters = blogs + .filter((blog): blog is BlogFrontMatterWithFile => + Boolean(blog.fileName && blog.title && blog.date), + ) + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + + return cachedBlogFrontMatters +} + +export async function BlogSection() { + const blogs = await getBlogFrontMatters() + const blogPosts = Object.keys(blogTagColors) + .flatMap(tag => blogs.filter(blog => blog.tags?.includes(tag)).slice(0, 5)) + .reduce((unique, blog) => { + if (!unique.some(item => item.fileName === blog.fileName)) { + unique.push(blog) + } + return unique + }, []) + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + .slice(0, 5) + + return ( +
+
+
+ + + Blog + +
+

The GraphQL Blog

+
+ + + +
    + {blogPosts.map(post => ( + + ))} +
+ +
+ +
+
+ ) +} diff --git a/src/app/(main)/resources/categories-section.tsx b/src/app/(main)/resources/categories-section.tsx new file mode 100644 index 0000000000..29ff4b4350 --- /dev/null +++ b/src/app/(main)/resources/categories-section.tsx @@ -0,0 +1,120 @@ +import Link from "next/link" + +import ArrowDownIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr" + +import { Eyebrow } from "@/_design-system/eyebrow" +import { type Topic } from "@/resources/types" + +import FrontendIcon from "./assets/frontend.svg?svgr" +import BackendIcon from "./assets/backend.svg?svgr" +import FederationIcon from "./assets/federation.svg?svgr" +import SecurityIcon from "./assets/security.svg?svgr" +import AIIcon from "./assets/ai.svg?svgr" +import MonitoringIcon from "./assets/monitoring.svg?svgr" + +interface Category { + id: Topic + name: string + description: string + icon: React.ReactNode +} + +const categories: Category[] = [ + { + id: "frontend", + name: "Frontend", + description: + "Build better queries and optimize UI performance with the right client tools.", + icon: ( + + ), + }, + { + id: "backend", + name: "Backend", + description: + "From resolvers to execution — everything you need to run a GraphQL server in production.", + icon: ( + + ), + }, + { + id: "federation", + name: "Federation", + description: + "Design and manage distributed graphs that scale across teams and services.", + icon: ( + + ), + }, + { + id: "security", + name: "Security", + description: + "Secure your GraphQL API with query limits and schema protection.", + icon: ( + + ), + }, + { + id: "ai", + name: "AI", + description: + "Use GraphQL to power AI systems — patterns, tools and implementations.", + icon: ( + + ), + }, + { + id: "monitoring", + name: "Monitoring", + description: + "Track performance, usage and schema changes to keep your graph healthy.", + icon: ( + + ), + }, +] + +export function CategoriesSection() { + return ( +
+
+ Explore GraphQL by topic +
+

Choose a resource category

+

+ Dive into the topics most relevant to your work with GraphQL. Find + selected tools, videos and other resources. +

+
+
+ +
+ {categories.map(category => ( + + ))} +
+
+ ) +} + +function CategoryCard({ category }: { category: Category }) { + return ( + +
+ {category.icon} + +
+
+

{category.name}

+

+ {category.description} +

+
+ + ) +} diff --git a/src/app/(main)/resources/page.tsx b/src/app/(main)/resources/page.tsx new file mode 100644 index 0000000000..8cb5aa3652 --- /dev/null +++ b/src/app/(main)/resources/page.tsx @@ -0,0 +1,47 @@ +import { NavbarFixed } from "@/components/navbar/navbar-fixed" +import { LookingForMore } from "@/components/looking-for-more" + +import { ResourcesHero } from "./resources-hero" +import { CategoriesSection } from "./categories-section" +import { ToolsLibrariesSection } from "./tools-libraries-section" +import { SpecificationSection } from "./specification-section" +import { BlogSection } from "./blog-section" +import { VideoResourcesSection } from "./video-resources-section" +import { ReadingResourcesSection } from "./reading-resources-section" + +export const metadata = { + title: "Resource Hub", + description: + "Explore curated GraphQL resources by topic. Find tools, videos, blog posts, and more to help you build with GraphQL.", +} + +export default function ResourcesPage() { + return ( +
+ + +
+ + + + +
+
+ +
+ +
+ +
+ ) +} diff --git a/src/app/(main)/resources/reading-resources-section.tsx b/src/app/(main)/resources/reading-resources-section.tsx new file mode 100644 index 0000000000..daf6d6227c --- /dev/null +++ b/src/app/(main)/resources/reading-resources-section.tsx @@ -0,0 +1,71 @@ +import Link from "next/link" + +import { Button } from "@/app/conf/_design-system/button" +import { Eyebrow } from "@/_design-system/eyebrow" +import ArrowDownIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr" + +import NewspaperIcon from "./assets/newspaper.svg?svgr" +import WriteIcon from "./assets/write-note.svg?svgr" +import BookIcon from "./assets/bookmark.svg?svgr" + +export function ReadingResourcesSection() { + return ( +
+
+
+ reading resources library + +

+ Dive into GraphQL content +

+

+ Browse reading materials to learn best practices and stay up to date + with the ecosystem. +

+ +
+ +
+ } + label="Blogs and newsletters" + /> + } + label="Individual posts" + /> + } + label="Books" + /> +
+
+
+ ) +} + +function ReadingLink({ + href, + icon, + label, +}: { + href: string + icon: React.ReactNode + label: string +}) { + return ( + + {icon} + {label} + + + ) +} diff --git a/src/app/(main)/resources/reading/[subcategory]/page.tsx b/src/app/(main)/resources/reading/[subcategory]/page.tsx new file mode 100644 index 0000000000..eea54355fd --- /dev/null +++ b/src/app/(main)/resources/reading/[subcategory]/page.tsx @@ -0,0 +1,36 @@ +import { notFound } from "next/navigation" + +import { + ReadingLibraryPage, + readingMetadata, + subcategories, + type Subcategory, +} from "../reading-page" + +interface PageParams { + subcategory: string +} + +function isSubcategory(value: string): value is Subcategory { + return subcategories.includes(value as Subcategory) +} + +export function generateStaticParams() { + return subcategories.map(subcategory => ({ subcategory })) +} + +export function generateMetadata({ params }: { params: PageParams }) { + const subcategory = params.subcategory + if (!isSubcategory(subcategory)) return {} + return readingMetadata(subcategory) +} + +export default function ReadingSubcategoryPage({ + params, +}: { + params: PageParams +}) { + const subcategory = params.subcategory + if (!isSubcategory(subcategory)) return notFound() + return +} diff --git a/src/app/(main)/resources/reading/page.tsx b/src/app/(main)/resources/reading/page.tsx new file mode 100644 index 0000000000..a997ace6c3 --- /dev/null +++ b/src/app/(main)/resources/reading/page.tsx @@ -0,0 +1,7 @@ +import { ReadingLibraryPage, readingMetadata } from "./reading-page" + +export const metadata = readingMetadata("all") + +export default function ReadingPage() { + return +} diff --git a/src/app/(main)/resources/reading/reading-page.tsx b/src/app/(main)/resources/reading/reading-page.tsx new file mode 100644 index 0000000000..1618ed946b --- /dev/null +++ b/src/app/(main)/resources/reading/reading-page.tsx @@ -0,0 +1,200 @@ +import Link from "next/link" +import { NavbarFixed } from "@/components/navbar/navbar-fixed" +import { notFound } from "next/navigation" +import { Breadcrumbs } from "@/_design-system/breadcrumbs" +import { clsx } from "clsx" + +import { ResourcesHero } from "../resources-hero" +import { Eyebrow } from "@/_design-system/eyebrow" +import { ReadingResourcesCard } from "./reading-resources-card" +import { readResources } from "@/resources/data" +import { topics, type ResourceMetadata, type Topic } from "@/resources/types" + +export const subcategories = [ + "blogs-and-newsletters", + "individual-posts", + "books", +] as const + +export type Subcategory = (typeof subcategories)[number] + +type Variant = Subcategory | "all" + +const topicSet = new Set(topics) + +const tabs: { + label: string + href: string + variant: Variant + color: string +}[] = [ + { + label: "Blogs & newsletters", + href: "/resources/reading/blogs-and-newsletters", + variant: "blogs-and-newsletters", + color: "hsl(var(--color-pri-base))", + }, + { + label: "Individual posts", + href: "/resources/reading/individual-posts", + variant: "individual-posts", + color: "#FF8800", + }, + { + label: "Books", + href: "/resources/reading/books", + variant: "books", + color: "#00C6AC", + }, +] + +const variants: Record< + Variant, + { + title: string + description: string + eyebrow: string + filter: (resource: ResourceMetadata) => boolean + } +> = { + all: { + title: "Reading Resources Library", + description: + "Browse reading materials to learn best practices and stay up to date with the ecosystem.", + eyebrow: "Reading resources", + filter: resource => + resource.tags.some( + tag => + tag === "blog-or-newsletter" || tag === "guide" || tag === "book", + ), + }, + "blogs-and-newsletters": { + title: "Blogs & Newsletters", + description: + "Popular sources to learn and keep track of the GraphQL ecosystem.", + eyebrow: "Stay informed", + filter: resource => resource.tags.includes("blog-or-newsletter"), + }, + "individual-posts": { + title: "Individual Posts", + description: "Notable posts from the community.", + eyebrow: "Deep dives", + filter: resource => + resource.tags.some( + tag => + tag === "guide" || + (tag === "blog" && !resource.url.startsWith("/blog")), + ), + }, + books: { + title: "Books", + description: + "Books to help you level up your GraphQL knowledge and practice.", + eyebrow: "Read and learn", + filter: resource => resource.tags.includes("book"), + }, +} + +export function readingMetadata(variant: Variant) { + const config = variants[variant] + if (!config) return {} + return { + title: config.title, + description: config.description, + } +} + +function hasTopicTag(resource: ResourceMetadata) { + return resource.tags.some(tag => topicSet.has(tag as Topic)) +} + +function uniqueByTitle(resources: ResourceMetadata[]) { + const seen = new Set() + return resources.filter(resource => { + const key = resource.title.trim().toLowerCase() + if (seen.has(key)) return false + seen.add(key) + return true + }) +} + +export async function ReadingLibraryPage({ variant }: { variant: Variant }) { + const config = variants[variant] + if (!config) return notFound() + + const resources = await readResources() + const filtered = uniqueByTitle(resources) + .filter(config.filter) + .sort((a, b) => + a.title.localeCompare(b.title, "en", { sensitivity: "base" }), + ) + + const activePath = [ + { + name: "Home", + route: "/", + }, + { + name: "Resource Hub", + route: "/resources", + }, + { + name: "Reading Resources Library", + route: "/resources/reading", + }, + ].map(item => ({ + ...item, + title: item.name, + type: "page", + children: [], + frontMatter: {}, + })) + + return ( +
+ + +
+ + +
    + {filtered.map(resource => ( +
  • + +
  • + ))} +
+
+
+ ) +} diff --git a/src/app/(main)/resources/reading/reading-resources-card.tsx b/src/app/(main)/resources/reading/reading-resources-card.tsx new file mode 100644 index 0000000000..a23dc6bc7b --- /dev/null +++ b/src/app/(main)/resources/reading/reading-resources-card.tsx @@ -0,0 +1,75 @@ +import type { ComponentType, SVGProps } from "react" + +import BookmarkIcon from "../assets/bookmark.svg?svgr" +import InfoIcon from "@/app/conf/_design-system/pixelarticons/info.svg?svgr" +import NotesIcon from "@/app/conf/_design-system/pixelarticons/notes.svg?svgr" +import { ResourceHubCard } from "../resource-hub-card" +import type { ResourceMetadata } from "@/resources/types" + +type CornerIcon = ComponentType> + +type ReadingKind = "book" | "blog-or-newsletter" | "blog" | "guide" + +const readingKindConfig: Record< + ReadingKind, + { label: string; color: string; Icon: CornerIcon } +> = { + book: { label: "books", color: "#00C6AC", Icon: BookmarkIcon }, + "blog-or-newsletter": { + label: "blogs & newsletters", + color: "hsl(var(--color-pri-base))", + Icon: NotesIcon, + }, + blog: { label: "blog posts", color: "#FF8800", Icon: NotesIcon }, + guide: { label: "guides", color: "#FF8800", Icon: InfoIcon }, +} + +function pickReadingKind(resource: ResourceMetadata): ReadingKind | undefined { + const candidates: ReadingKind[] = [ + "book", + "blog-or-newsletter", + "guide", + "blog", + ] + return candidates.find((candidate): candidate is ReadingKind => + resource.tags.includes(candidate), + ) +} + +export function ReadingResourcesCard({ + resource, +}: { + resource: ResourceMetadata +}) { + const kind = pickReadingKind(resource) + const config = kind ? readingKindConfig[kind] : undefined + + return ( + + + + ) : null + } + /> + ) +} diff --git a/src/app/(main)/resources/resource-hub-card.tsx b/src/app/(main)/resources/resource-hub-card.tsx new file mode 100644 index 0000000000..2621def228 --- /dev/null +++ b/src/app/(main)/resources/resource-hub-card.tsx @@ -0,0 +1,118 @@ +import Link from "next/link" +import type { ReactNode } from "react" +import { clsx } from "clsx" + +import ArrowDownIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr" +import ClockIcon from "@/app/conf/_design-system/pixelarticons/clock.svg?svgr" +import { Tag } from "@/app/conf/_design-system/tag" +import { Topic } from "@/resources/types" +import { blogTagColors } from "@/components/blog-page/blog-tag-colors" + +export const tagColors: Record = { + ...blogTagColors, + backend: "#36C1A0", + "defies-categorization": "#894545", + "developer-experience": "#6fc9af", + "federation-and-composite-schemas": "#cbc749", + "graphql-clients": "#ca78fc", + "graphql-in-production": "#e4981f", + "graphql-security": "#CC6BB0", + "graphql-spec": "#6B73CC", + scaling: "#8D8D8D", + frontend: "violet", + documentation: "salmon", + "schema-evolution": "thistle", + security: "cornflowerblue", + "case-studies": "#B36B00", + "federation-and-distributed-systems": "#FF8F70", + federation: "#5C7CFA", + tools: "#0FA3B1", + "api-platform-and-gateways": "#F4B400", + "schema-design": "#7E57C2", + ai: "#FF5FA2", + monitoring: "#2D9CDB", + "blog-or-newsletter": "#FF8800", + book: "#00C6AC", + guide: "#FF8800", +} + +interface ResourceHubCardProps { + href: string + title: string + author?: string + duration?: string + authorPlacement?: "body" | "footer" + tags?: string[] + className?: string + icon?: ReactNode +} + +export function ResourceHubCard({ + href, + title, + author, + duration, + authorPlacement = "footer", + tags, + className, + icon, +}: ResourceHubCardProps) { + return ( + +
+
+ {tags?.length ? ( +
+ {tags.map(tag => ( + + {formatTag(tag)} + + ))} +
+ ) : null} +
+ {authorPlacement === "body" && author ? ( + {author} + ) : null} +

+ {title} +

+
+
+ {icon ? ( +
{icon}
+ ) : null} +
+
+
+ {authorPlacement === "footer" && author ? ( + {author} + ) : null} + {duration ? ( + + + {duration} + + ) : null} +
+
+ +
+
+ + ) +} + +function formatTag(tag: string) { + if (tag === "blog-or-newsletter") return "Blogs & Newsletters" + if (tag === "book") return "Books" + if (tag === "guide") return "Individual posts" + + return tag.replaceAll("-", " ") +} diff --git a/src/app/(main)/resources/resources-hero.tsx b/src/app/(main)/resources/resources-hero.tsx new file mode 100644 index 0000000000..29ac61e94c --- /dev/null +++ b/src/app/(main)/resources/resources-hero.tsx @@ -0,0 +1,28 @@ +import { LearnHeroStripes } from "@/components/learn-aggregator/learn-hero-stripes" + +export function ResourcesHero({ + heading, + text, + children, +}: { + heading: string + text: string + children?: React.ReactNode +}) { + return ( +
+ +
+

{heading}

+

{text}

+ {children} +
+
+ ) +} diff --git a/src/app/(main)/resources/specification-section.tsx b/src/app/(main)/resources/specification-section.tsx new file mode 100644 index 0000000000..45fc32edfe --- /dev/null +++ b/src/app/(main)/resources/specification-section.tsx @@ -0,0 +1,54 @@ +import { Button } from "@/app/conf/_design-system/button" +import ArchiveIcon from "./assets/archive.svg?svgr" +import { StripesDecoration } from "@/app/conf/_design-system/stripes-decoration" +import { Eyebrow } from "@/_design-system/eyebrow" + +export function SpecificationSection() { + return ( +
+
+ + specification + +
+
+ +
+ +
+

+ Read the GraphQL Specification +

+

+ The specification defines the core structure of GraphQL. It's the + foundation for building consistent servers, clients, and tools. + Read the spec to better understand how GraphQL works. +

+ +
+
+
+
+ ) +} + +function Stripes() { + return ( +
+ +
+ ) +} diff --git a/src/app/(main)/resources/subtitles.ts b/src/app/(main)/resources/subtitles.ts new file mode 100644 index 0000000000..3ce6841fab --- /dev/null +++ b/src/app/(main)/resources/subtitles.ts @@ -0,0 +1,21 @@ +import { type Topic } from "@/resources/types" + +export const categoryNames: Record = { + frontend: "Frontend", + backend: "Backend", + federation: "Federation", + security: "Security", + ai: "AI", + monitoring: "Monitoring", +} + +export const categorySubtitles: Record = { + frontend: "Learn how to integrate GraphQL on the frontend.", + backend: + "Build powerful GraphQL backends with the right tools, libraries and expert insights.", + federation: "Learn how to build and compose GraphQL graphs with federation.", + ai: "Explore how to use GraphQL for AI systems.", + security: "Learn how to secure your GraphQL APIs.", + monitoring: + "Stay ahead of performance issues by monitoring queries and watching error trends.", +} diff --git a/src/app/(main)/resources/tools-libraries-section.tsx b/src/app/(main)/resources/tools-libraries-section.tsx new file mode 100644 index 0000000000..4c69c86096 --- /dev/null +++ b/src/app/(main)/resources/tools-libraries-section.tsx @@ -0,0 +1,34 @@ +import { Button } from "@/app/conf/_design-system/button" +import ToolsIcon from "./assets/tools.svg?svgr" +import { Eyebrow } from "@/_design-system/eyebrow" + +export function ToolsLibrariesSection() { + return ( +
+
+ + tools and libraries + + +
+
+

+ Build GraphQL with Tools & Libraries +

+

+ Explore solutions and docs for building with GraphQL — across + languages, frameworks, and platforms. +

+ +
+ +
+ +
+
+
+
+ ) +} diff --git a/src/app/(main)/resources/video-resources-section.tsx b/src/app/(main)/resources/video-resources-section.tsx new file mode 100644 index 0000000000..51122b0a5a --- /dev/null +++ b/src/app/(main)/resources/video-resources-section.tsx @@ -0,0 +1,31 @@ +import { Button } from "@/app/conf/_design-system/button" +import { Eyebrow } from "@/_design-system/eyebrow" +import VideoPlayerIcon from "./assets/video-player.svg?svgr" + +export function VideoResourcesSection() { + return ( +
+ video resources library + +
+
+ +
+ +
+

+ Watch and learn GraphQL +

+

+ Build your skills with featured videos from GraphQL Conf, global + meetups, and expert engineers — keeping you up to date in a + fast-moving ecosystem. +

+ +
+
+
+ ) +} diff --git a/src/app/(main)/resources/video/page.tsx b/src/app/(main)/resources/video/page.tsx new file mode 100644 index 0000000000..2274aaedde --- /dev/null +++ b/src/app/(main)/resources/video/page.tsx @@ -0,0 +1,64 @@ +import { NavbarFixed } from "@/components/navbar/navbar-fixed" +import { getResourcesByTag } from "@/resources/data" +import VideoPlayerIcon from "../assets/video-player.svg?svgr" +import { ResourcesHero } from "../resources-hero" +import { Eyebrow } from "@/_design-system/eyebrow" +import { VideoLibrary } from "./video-library" + +export const metadata = { + title: "Video Resources Library", + description: + "Expand your expertise with curated videos to help you master GraphQL and stay up to date with its evolving ecosystem.", +} + +export default async function VideoResourcesPage() { + const resources = await getResourcesByTag("video") + const seen = new Set() + const unique = resources.filter(resource => { + const key = resource.title.trim().toLowerCase() + if (seen.has(key)) return false + seen.add(key) + return true + }) + unique.sort((a, b) => + a.title.localeCompare(b.title, "en", { sensitivity: "base" }), + ) + + return ( +
+ + + +
+
+
+
+ +
+
+ Watch and learn +

+ Curated GraphQL talks +

+

+ Browse recorded sessions from GraphQL Conf alongside community + talks and workshops. +

+
+
+ + + {unique.length} videos + +
+ +
+ +
+
+
+ ) +} diff --git a/src/app/(main)/resources/video/video-library.tsx b/src/app/(main)/resources/video/video-library.tsx new file mode 100644 index 0000000000..d49d5da274 --- /dev/null +++ b/src/app/(main)/resources/video/video-library.tsx @@ -0,0 +1,242 @@ +"use client" + +import { useMemo, useState } from "react" +import { + Combobox, + ComboboxButton, + ComboboxInput, + ComboboxOption, + ComboboxOptions, + Label, +} from "@headlessui/react" + +import { Button } from "@/app/conf/_design-system/button" +import { Tag } from "@/app/conf/_design-system/tag" +import CaretDownIcon from "@/app/conf/_design-system/pixelarticons/caret-down.svg?svgr" +import { CheckboxIcon } from "@/app/conf/_design-system/pixelarticons/checkbox-icon" +import { ResourceHubCard } from "../resource-hub-card" +import { type ResourceMetadata, topics, type Topic } from "@/resources/types" + +interface VideoLibraryProps { + resources: ResourceMetadata[] +} + +type SortOrder = "az" | "za" + +export function VideoLibrary({ resources }: VideoLibraryProps) { + const [selectedTopics, setSelectedTopics] = useState([]) + const [sortOrder, setSortOrder] = useState("az") + + const topicOptions = useMemo(() => { + const allowed = new Set(topics) + const found = new Set() + resources.forEach(resource => { + resource.tags.forEach(tag => { + if (allowed.has(tag as Topic)) { + found.add(tag) + } + }) + }) + return Array.from(found).sort((a, b) => + a.localeCompare(b, "en", { sensitivity: "base" }), + ) + }, [resources]) + + const filtered = useMemo(() => { + const filteredByTopic = + selectedTopics.length === 0 + ? resources + : resources.filter(resource => + resource.tags.some(tag => selectedTopics.includes(tag)), + ) + + const sorted = [...filteredByTopic].sort((a, b) => + sortOrder === "az" + ? a.title.localeCompare(b.title, "en", { sensitivity: "base" }) + : b.title.localeCompare(a.title, "en", { sensitivity: "base" }), + ) + + return sorted + }, [resources, selectedTopics, sortOrder]) + + return ( +
+
+
+ + +
+ +
+ + +
+
+
+ +
+ + {filtered.length} videos + + {selectedTopics.length > 0 && ( + + )} +
+
+ +
    + {filtered.map(resource => { + const tags = resource.tags.map(tag => ({ + label: tag, + color: "hsl(var(--color-neu-500))", + })) + + return ( +
  • + +
  • + ) + })} +
+
+ ) +} + +function TopicsCombobox({ + label, + options, + value, + onChange, +}: { + label: string + options: string[] + value: string[] + onChange: (next: string[]) => void +}) { + const [query, setQuery] = useState("") + + const filteredOptions = + query === "" + ? options + : options.filter(option => + option.toLowerCase().includes(query.toLowerCase()), + ) + + return ( + +
+ + + +
+ + {filteredOptions.map(option => ( + + {({ active, selected }) => ( + + )} + + ))} + +
+
+
+ ) +} + +function TopicOption({ + active, + selected, + option, +}: { + active: boolean + selected: boolean + option: string +}) { + return ( +
+ +
+ + {option} + +
+
+ ) +} diff --git a/src/app/conf/_design-system/stripes-decoration.tsx b/src/app/conf/_design-system/stripes-decoration.tsx index 8d03825612..f895baeada 100644 --- a/src/app/conf/_design-system/stripes-decoration.tsx +++ b/src/app/conf/_design-system/stripes-decoration.tsx @@ -1,21 +1,27 @@ import clsx from "clsx" const maskEven = - "repeating-linear-gradient(to right, transparent, transparent var(--stripe-width), black var(--stripe-width), black calc(var(--stripe-width) * 2))" + "repeating-linear-gradient(var(--angle), transparent, transparent var(--stripe-width), black var(--stripe-width), black calc(var(--stripe-width) * 2))" const maskOdd = - "repeating-linear-gradient(to right, black, black var(--stripe-width), transparent var(--stripe-width), transparent calc(var(--stripe-width) * 2))" + "repeating-linear-gradient(var(--angle), black, black var(--stripe-width), transparent var(--stripe-width), transparent calc(var(--stripe-width) * 2))" export interface StripesDecorationProps { evenClassName?: string oddClassName?: string stripeWidth?: string + /** + * @default "90deg" to right, + * use "-90deg" to align with right side of the container + */ + angle?: string } export function StripesDecoration({ stripeWidth = "12px", evenClassName, oddClassName, + angle = "90deg", }: StripesDecorationProps) { return ( <> @@ -23,7 +29,10 @@ export function StripesDecoration({
= { spec: "#00C6AC", grants: "#84BD01", "in-the-news": "#3F3A3D", + "developer-experience": "#6fc9af", } diff --git a/src/components/blog-page/blog-tags.tsx b/src/components/blog-page/blog-tags.tsx index 6c1a509535..618aa8282f 100644 --- a/src/components/blog-page/blog-tags.tsx +++ b/src/components/blog-page/blog-tags.tsx @@ -35,7 +35,7 @@ export function BlogTags({ key={tag} // yes, the page lives under /tags, not /blog/tags href={`/tags/${tag}`} - className="-m-1 flex p-1 ring-inset ring-neu-400 transition-opacity duration-75 hover:ring focus:!outline-offset-0 dark:ring-neu-50 [:has(>:hover)>&:not(:hover)]:opacity-70" + className="gql-focus-visible -m-1 flex p-1 ring-inset ring-neu-400 transition-opacity duration-75 hover:ring focus:!outline-offset-0 dark:ring-neu-50 [:has(>:hover)>&:not(:hover)]:opacity-70" > {tagElement} diff --git a/src/components/blog-page/featured-blog-posts.tsx b/src/components/blog-page/featured-blog-posts.tsx index fc6c7f08cc..d017dfd39b 100644 --- a/src/components/blog-page/featured-blog-posts.tsx +++ b/src/components/blog-page/featured-blog-posts.tsx @@ -47,7 +47,7 @@ export function FeaturedBlogPosts({ byline={firstFeatured.frontMatter.byline} date={firstFeatured.frontMatter.date} /> - +
diff --git a/src/components/blog-page/index.tsx b/src/components/blog-page/index.tsx index 0d597d3b7e..4f11d56fe2 100644 --- a/src/components/blog-page/index.tsx +++ b/src/components/blog-page/index.tsx @@ -5,9 +5,10 @@ import { Tag } from "@/app/conf/_design-system/tag" import { arrowsMoveSideways } from "@/app/conf/_design-system/utils/arrows-move-sideways" import { StripesDecoration } from "@/app/conf/_design-system/stripes-decoration" +import { LookingForMore } from "@/components/looking-for-more" + import { blogTagColors } from "./blog-tag-colors" import { BlogCard } from "./blog-card" -import { LookingForMore } from "./looking-for-more" import { BlogMdxContent } from "./mdx-types" import { FeaturedBlogPosts } from "./featured-blog-posts" @@ -51,14 +52,14 @@ export function BlogPage({
-
+

{currentTag || "All Posts"}

Categories

-
    +
      {Object.entries(tags) .sort((a, b) => b[1] - a[1]) .map(([tag, count], i) => ( @@ -78,7 +79,7 @@ export function BlogPage({
-
+
{blogs.map( page => (!currentTag || page.frontMatter.tags.includes(currentTag)) && ( @@ -87,7 +88,13 @@ export function BlogPage({ )}
- +
) diff --git a/src/components/blog-page/looking-for-more.tsx b/src/components/blog-page/looking-for-more.tsx deleted file mode 100644 index 2441ade4e7..0000000000 --- a/src/components/blog-page/looking-for-more.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Anchor } from "@/app/conf/_design-system/anchor" - -import ArrowDownIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr" - -export function LookingForMore() { - return ( -
-
-
-

Looking for more?

-

- Explore learning guides and best practices — or browse for tools, - libraries and other resources. -

-
-
- - Learn - - - - Resources - - -
-
-
- ) -} diff --git a/src/components/learn-aggregator/learn-hero-stripes.tsx b/src/components/learn-aggregator/learn-hero-stripes.tsx index 29d2a4e37f..b7692117a3 100644 --- a/src/components/learn-aggregator/learn-hero-stripes.tsx +++ b/src/components/learn-aggregator/learn-hero-stripes.tsx @@ -1,13 +1,24 @@ +import { clsx } from "clsx" import { StripesDecoration } from "@/app/conf/_design-system/stripes-decoration" import blurBean from "./learn-blur-bean.webp" -export function LearnHeroStripes() { +export function LearnHeroStripes({ + className, + style, + ...rest +}: { + className?: string + style?: React.CSSProperties +}) { return (
) { - return ( -
-
-
-

Looking for more?

-

- Learning is just the beginning. Discover tools and other resources — - or connect with the GraphQL community around the world. -

-
- -
-
- ) -} diff --git a/src/components/looking-for-more.tsx b/src/components/looking-for-more.tsx new file mode 100644 index 0000000000..db174b300d --- /dev/null +++ b/src/components/looking-for-more.tsx @@ -0,0 +1,46 @@ +import { clsx } from "clsx" +import { Anchor } from "@/app/conf/_design-system/anchor" +import ArrowDownIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr" + +type LinkItem = { href: string; label: string } + +interface LookingForMoreProps extends React.HTMLAttributes { + description: string + links: [LinkItem, LinkItem] +} + +export function LookingForMore({ + description, + links, + ...props +}: LookingForMoreProps) { + return ( +
+
+
+

Looking for more?

+

{description}

+
+
+ + {links[0].label} + + + + {links[1].label} + + +
+
+
+ ) +} diff --git a/src/pages/community/resources/blogs-and-newsletters.mdx b/src/pages/community/resources/blogs-and-newsletters.mdx index 46331f19e2..1927c4b4ff 100644 --- a/src/pages/community/resources/blogs-and-newsletters.mdx +++ b/src/pages/community/resources/blogs-and-newsletters.mdx @@ -35,7 +35,6 @@ Here are a list of notable blog posts to help you better understand GraphQL: - [Your First GraphQL Server](https://medium.com/the-graphqlhub/your-first-graphql-server-3c766ab4f0a2#.ovn0y19k4) - Clay Allsopp - [Tutorial: Kick start a JS API with Apollo-server, Dataloader and Knex](https://bamtech.gitbook.io/dev-standards/backend/graphql-js/getting-started-with-apollo-server-dataloader-knex.mo) - Thomas Pucci - [Tutorial: How to Build a GraphQL Server](https://medium.com/apollo-stack/tutorial-building-a-graphql-server-cddaa023c035#.bu6sdnst4) - Jonas Helfer -- [Designing Powerful APIs with GraphQL Query Parameters](https://www.graph.cool/docs/tutorials/designing-powerful-apis-with-graphql-query-parameters-aing7uech3/) - Johannes Schickling - [GraphQL and the amazing Apollo Client](https://medium.com/google-developer-experts/graphql-and-the-amazing-apollo-client-fe57e162a70c) - Gerard Sans - [GraphQL Server Basics (Part I): The Schema](https://www.prisma.io/blog/graphql-server-basics-the-schema-ac5e2950214e) - Nikolas Burk - [GraphQL Server Basics (Part II): The Network Layer](https://www.prisma.io/blog/graphql-server-basics-the-network-layer-51d97d21861) - Nikolas Burk diff --git a/src/pages/learn/index.mdx b/src/pages/learn/index.mdx index c26ab6d58e..a121f17c81 100644 --- a/src/pages/learn/index.mdx +++ b/src/pages/learn/index.mdx @@ -11,7 +11,7 @@ import { LearnHeroStripes } from '../../components/learn-aggregator/learn-hero-s import { pagesBySection } from '../../components/learn-aggregator/learn-pages' import { CommonQuestionsSection } from '../../components/learn-aggregator/common-questions' import { TrainingCoursesSection } from '../../components/learn-aggregator/training-courses' -import { LookingForMore } from "../../components/learn-aggregator/looking-for-more" +import { LookingForMore } from "../../components/looking-for-more" - + + diff --git a/src/resources/data.ts b/src/resources/data.ts new file mode 100644 index 0000000000..db37f4d1b8 --- /dev/null +++ b/src/resources/data.ts @@ -0,0 +1,127 @@ +import path from "node:path" +import { glob } from "node:fs/promises" +import { readFile } from "node:fs/promises" +import { cache } from "react" +import matter from "gray-matter" + +import { ResourceMetadata, type ResourceTag, topics } from "./types" + +const dataGlob = "src/resources/data/*.json" +const codeGlob = "src/code/**/*.md" +const blogGlob = "src/pages/blog/**/*.mdx" + +export const readResources = cache(async () => { + const resources: ResourceMetadata[] = [] + + for await (const file of glob(dataGlob)) { + const raw = await readFile(file, "utf8") + const parsed = JSON.parse(raw) + resources.push(ResourceMetadata.assert(parsed)) + } + + for await (const file of glob(blogGlob)) { + const raw = await readFile(file, "utf8") + const { data, content } = matter(raw) + + const title: string | undefined = data.title + if (!title) continue + + const slug = blogSlug(file) + + const bodyLines = content + .split(/\r?\n/) + .map(line => line.trim()) + .map(line => + line + .replace(/!\[[^\]]*\]\([^)]+\)/g, "") + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") + .replace(/`+/g, "") + .replace(/[*_~]+/g, "") + .replace(/^#+\s*/, "") + .replace(/<\/?[^>]+>/g, "") + .trim(), + ) + .filter(line => line.length > 0) + + const excerpt = bodyLines.slice(0, 2).join(" ") + + const description = + typeof data.description === "string" && data.description.length > 0 + ? data.description + : excerpt || undefined + + const topicsFromFrontmatter: ResourceTag[] = Array.isArray(data.topics) + ? data.topics.filter((tag): tag is ResourceTag => + topics.includes(tag as (typeof topics)[number]), + ) + : [] + + const topicTagsFromTags: ResourceTag[] = Array.isArray(data.tags) + ? data.tags.filter((tag): tag is ResourceTag => + topics.includes(tag as (typeof topics)[number]), + ) + : [] + + const tags: ResourceTag[] = [ + "blog", + ...topicsFromFrontmatter, + ...topicTagsFromTags, + ] + + resources.push( + ResourceMetadata.assert({ + title, + url: slug, + author: data.byline, + description, + kind: "blog", + tags, + }), + ) + } + + for await (const file of glob(codeGlob)) { + const raw = await readFile(file, "utf8") + const { data } = matter(raw) + const tags: ResourceMetadata["tags"] = Array.isArray(data.tags) + ? data.tags + : [] + + if (!tags.includes("tools-and-libraries")) { + tags.push("tools-and-libraries") + } + + const url: string | undefined = + data.url ?? + (data.github ? `https://github.com/${data.github}` : undefined) ?? + (data.npm ? `https://npmjs.com/package/${data.npm}` : undefined) + + const title = data.name ?? path.parse(file).name + + resources.push( + ResourceMetadata.assert({ + title, + url, + description: data.description, + tags, + }), + ) + } + + return resources +}) + +export async function getResourcesByTag(tag: ResourceTag) { + const resources = await readResources() + return resources.filter(resource => resource.tags.includes(tag)) +} + +function blogSlug(file: string) { + const relative = path.relative("src/pages", file) + const withoutExt = relative.replace(/\.mdx$/, "") + const normalized = withoutExt.split(path.sep).join("/") + const clean = normalized.endsWith("/index") + ? normalized.slice(0, -"index".length - 1) + : normalized + return `/${clean}` +} diff --git a/src/resources/data/a-beginner-s-guide-to-graphql.json b/src/resources/data/a-beginner-s-guide-to-graphql.json new file mode 100644 index 0000000000..2552c02efd --- /dev/null +++ b/src/resources/data/a-beginner-s-guide-to-graphql.json @@ -0,0 +1,6 @@ +{ + "title": "A Beginner’s Guide to GraphQL", + "url": "https://www.freecodecamp.org/news/a-beginners-guide-to-graphql-86f849ce1bec/", + "author": "Leonardo Maldonado", + "tags": ["blog"] +} diff --git a/src/resources/data/a-graphql-framework-for-non-js-servers-syrus-akbary.json b/src/resources/data/a-graphql-framework-for-non-js-servers-syrus-akbary.json new file mode 100644 index 0000000000..f5eeb1b82b --- /dev/null +++ b/src/resources/data/a-graphql-framework-for-non-js-servers-syrus-akbary.json @@ -0,0 +1,6 @@ +{ + "title": "A GraphQL Framework for Non-JS Servers", + "author": "Syrus Akbary", + "url": "https://www.youtube.com/watch?v=RNoyPSrQyPs", + "tags": ["video", "backend"] +} diff --git a/src/resources/data/a-graphql-framework-for-non-js-servers.json b/src/resources/data/a-graphql-framework-for-non-js-servers.json new file mode 100644 index 0000000000..b6f39fbb01 --- /dev/null +++ b/src/resources/data/a-graphql-framework-for-non-js-servers.json @@ -0,0 +1,6 @@ +{ + "title": "A GraphQL Framework for Non-JS Servers", + "author": "Syrus Akbary", + "url": "https://www.youtube.com/watch?v=RNoyPSrQyPs", + "tags": ["video"] +} diff --git a/src/resources/data/a-postgresql-backed-graphql-baas.json b/src/resources/data/a-postgresql-backed-graphql-baas.json new file mode 100644 index 0000000000..8db0be4cd4 --- /dev/null +++ b/src/resources/data/a-postgresql-backed-graphql-baas.json @@ -0,0 +1,6 @@ +{ + "title": "A PostgreSQL backed GraphQL BaaS", + "author": "Tanmai Gopal", + "url": "https://www.youtube.com/watch?v=neIZcc8y3B0", + "tags": ["video", "backend"] +} diff --git a/src/resources/data/all-talks-from-graphql-europe.json b/src/resources/data/all-talks-from-graphql-europe.json new file mode 100644 index 0000000000..fbadd36641 --- /dev/null +++ b/src/resources/data/all-talks-from-graphql-europe.json @@ -0,0 +1,5 @@ +{ + "title": "All Talks from GraphQL Europe", + "url": "https://www.youtube.com/playlist?list=PLn2e1F9Rfr6n_WFm9fPE-_wYPrYvSTySt", + "tags": ["video"] +} diff --git a/src/resources/data/apollo-client-put-graphql-data-in-your-ui.json b/src/resources/data/apollo-client-put-graphql-data-in-your-ui.json new file mode 100644 index 0000000000..080bf02325 --- /dev/null +++ b/src/resources/data/apollo-client-put-graphql-data-in-your-ui.json @@ -0,0 +1,6 @@ +{ + "title": "Apollo Client: Put GraphQL Data in Your UI", + "author": "Sashko Stubailo", + "url": "https://www.youtube.com/watch?v=u1E0CbGeICo", + "tags": ["video", "frontend"] +} diff --git a/src/resources/data/apollo-odyssey.json b/src/resources/data/apollo-odyssey.json new file mode 100644 index 0000000000..aa11a294bd --- /dev/null +++ b/src/resources/data/apollo-odyssey.json @@ -0,0 +1,5 @@ +{ + "title": "Apollo Odyssey", + "url": "https://apollographql.com/tutorials", + "tags": ["guide"] +} diff --git a/src/resources/data/apollo-s-blog.json b/src/resources/data/apollo-s-blog.json new file mode 100644 index 0000000000..e2b5979181 --- /dev/null +++ b/src/resources/data/apollo-s-blog.json @@ -0,0 +1,5 @@ +{ + "title": "Apollo's Blog", + "url": "https://apollographql.com/blog", + "tags": ["blog-or-newsletter"] +} diff --git a/src/resources/data/architecture-of-a-high-performance-graphql-to-sql-engine.json b/src/resources/data/architecture-of-a-high-performance-graphql-to-sql-engine.json new file mode 100644 index 0000000000..f107cf7911 --- /dev/null +++ b/src/resources/data/architecture-of-a-high-performance-graphql-to-sql-engine.json @@ -0,0 +1,6 @@ +{ + "title": "Architecture of a high performance GraphQL to SQL engine", + "url": "https://blog.hasura.io/architecture-of-a-high-performance-graphql-to-sql-server-58d9944b8a87", + "author": "Sandip Devarkonda", + "tags": ["blog"] +} diff --git a/src/resources/data/awesome-graphql.json b/src/resources/data/awesome-graphql.json new file mode 100644 index 0000000000..0befab9e45 --- /dev/null +++ b/src/resources/data/awesome-graphql.json @@ -0,0 +1,5 @@ +{ + "title": "awesome-graphql", + "url": "https://github.com/chentsulin/awesome-graphql", + "tags": ["guide"] +} diff --git a/src/resources/data/brand-guidelines.json b/src/resources/data/brand-guidelines.json new file mode 100644 index 0000000000..f32f7804f8 --- /dev/null +++ b/src/resources/data/brand-guidelines.json @@ -0,0 +1,5 @@ +{ + "title": "brand guidelines", + "url": "/brand", + "tags": ["guide"] +} diff --git a/src/resources/data/build-a-full-graphql-backend-in-under-5-minutes-michael-paris.json b/src/resources/data/build-a-full-graphql-backend-in-under-5-minutes-michael-paris.json new file mode 100644 index 0000000000..4fc31c17c3 --- /dev/null +++ b/src/resources/data/build-a-full-graphql-backend-in-under-5-minutes-michael-paris.json @@ -0,0 +1,6 @@ +{ + "title": "Build a Full GraphQL Backend in Under 5 Minutes", + "author": "Michael Paris", + "url": "https://www.youtube.com/watch?v=bJ8pnYd6jPQ", + "tags": ["video", "backend"] +} diff --git a/src/resources/data/build-a-full-graphql-backend-in-under-5-minutes.json b/src/resources/data/build-a-full-graphql-backend-in-under-5-minutes.json new file mode 100644 index 0000000000..ce02a20361 --- /dev/null +++ b/src/resources/data/build-a-full-graphql-backend-in-under-5-minutes.json @@ -0,0 +1,6 @@ +{ + "title": "Build a Full GraphQL Backend in Under 5 Minutes", + "author": "Michael Paris", + "url": "https://www.youtube.com/watch?v=bJ8pnYd6jPQ", + "tags": ["video"] +} diff --git a/src/resources/data/build-a-graphql-backend-with-the-serverless-framework-ryan-brown.json b/src/resources/data/build-a-graphql-backend-with-the-serverless-framework-ryan-brown.json new file mode 100644 index 0000000000..d744bed512 --- /dev/null +++ b/src/resources/data/build-a-graphql-backend-with-the-serverless-framework-ryan-brown.json @@ -0,0 +1,6 @@ +{ + "title": "Build a GraphQL Backend with the Serverless Framework", + "author": "Ryan Brown", + "url": "https://acloud.guru/learn/serverless-with-graphql", + "tags": ["video", "backend"] +} diff --git a/src/resources/data/build-a-graphql-backend-with-the-serverless-framework.json b/src/resources/data/build-a-graphql-backend-with-the-serverless-framework.json new file mode 100644 index 0000000000..cb2b1a0b88 --- /dev/null +++ b/src/resources/data/build-a-graphql-backend-with-the-serverless-framework.json @@ -0,0 +1,6 @@ +{ + "title": "Build a GraphQL Backend with the Serverless Framework", + "author": "Ryan Brown", + "url": "https://acloud.guru/learn/serverless-with-graphql", + "tags": ["video"] +} diff --git a/src/resources/data/build-a-graphql-server-for-node-js-using-postgresql-mysql-lee-benson.json b/src/resources/data/build-a-graphql-server-for-node-js-using-postgresql-mysql-lee-benson.json new file mode 100644 index 0000000000..499b02f370 --- /dev/null +++ b/src/resources/data/build-a-graphql-server-for-node-js-using-postgresql-mysql-lee-benson.json @@ -0,0 +1,6 @@ +{ + "title": "Build a GraphQL server for Node.js, using PostgreSQL/MySQL", + "author": "Lee Benson", + "url": "https://www.youtube.com/watch?v=DNPVqK_woRQ", + "tags": ["video", "backend"] +} diff --git a/src/resources/data/build-a-graphql-server-for-node-js-using-postgresql-mysql.json b/src/resources/data/build-a-graphql-server-for-node-js-using-postgresql-mysql.json new file mode 100644 index 0000000000..86b3417360 --- /dev/null +++ b/src/resources/data/build-a-graphql-server-for-node-js-using-postgresql-mysql.json @@ -0,0 +1,6 @@ +{ + "title": "Build a GraphQL server for Node.js, using PostgreSQL/MySQL", + "author": "Lee Benson", + "url": "https://www.youtube.com/watch?v=DNPVqK_woRQ", + "tags": ["video"] +} diff --git a/src/resources/data/building-native-mobile-apps-with-graphql-martjin-walraven-react-europe-2016.json b/src/resources/data/building-native-mobile-apps-with-graphql-martjin-walraven-react-europe-2016.json new file mode 100644 index 0000000000..4c2f0ea1e0 --- /dev/null +++ b/src/resources/data/building-native-mobile-apps-with-graphql-martjin-walraven-react-europe-2016.json @@ -0,0 +1,6 @@ +{ + "title": "Building Native Mobile Apps with GraphQL", + "author": "Martjin Walraven", + "url": "https://www.youtube.com/watch?v=z5rz3saDPJ8", + "tags": ["video", "frontend"] +} diff --git a/src/resources/data/building-native-mobile-apps-with-graphql.json b/src/resources/data/building-native-mobile-apps-with-graphql.json new file mode 100644 index 0000000000..810cb2a13b --- /dev/null +++ b/src/resources/data/building-native-mobile-apps-with-graphql.json @@ -0,0 +1,6 @@ +{ + "title": "Building Native Mobile Apps with GraphQL", + "author": "Martjin Walraven", + "url": "https://www.youtube.com/watch?v=z5rz3saDPJ8", + "tags": ["video"] +} diff --git a/src/resources/data/building-the-f8-app-using-graphql-relay.json b/src/resources/data/building-the-f8-app-using-graphql-relay.json new file mode 100644 index 0000000000..3c36df9045 --- /dev/null +++ b/src/resources/data/building-the-f8-app-using-graphql-relay.json @@ -0,0 +1,5 @@ +{ + "title": "Building the f8 App: Using GraphQL & Relay", + "url": "http://makeitopen.com/docs/en/1-A2-relay.html", + "tags": ["blog"] +} diff --git a/src/resources/data/chillicream-s-blog.json b/src/resources/data/chillicream-s-blog.json new file mode 100644 index 0000000000..68a2657d38 --- /dev/null +++ b/src/resources/data/chillicream-s-blog.json @@ -0,0 +1,5 @@ +{ + "title": "ChilliCream's Blog", + "url": "https://chillicream.com/blog", + "tags": ["blog-or-newsletter"] +} diff --git a/src/resources/data/community-events-section.json b/src/resources/data/community-events-section.json new file mode 100644 index 0000000000..2c3af619b8 --- /dev/null +++ b/src/resources/data/community-events-section.json @@ -0,0 +1,5 @@ +{ + "title": "community events section", + "url": "/community/upcoming-events/#meetups", + "tags": ["guide"] +} diff --git a/src/resources/data/craft-graphql-apis-in-elixir-with-absinthe.json b/src/resources/data/craft-graphql-apis-in-elixir-with-absinthe.json new file mode 100644 index 0000000000..b8c5cf6344 --- /dev/null +++ b/src/resources/data/craft-graphql-apis-in-elixir-with-absinthe.json @@ -0,0 +1,6 @@ +{ + "title": "Craft GraphQL APIs in Elixir with Absinthe", + "url": "https://pragprog.com/titles/wwgraphql/craft-graphql-apis-in-elixir-with-absinthe/", + "author": "Bruce Williams & Ben Wilson", + "tags": ["book"] +} diff --git a/src/resources/data/designing-powerful-apis-with-graphql-query-parameters.json b/src/resources/data/designing-powerful-apis-with-graphql-query-parameters.json new file mode 100644 index 0000000000..d3f99c4a56 --- /dev/null +++ b/src/resources/data/designing-powerful-apis-with-graphql-query-parameters.json @@ -0,0 +1,5 @@ +{ + "title": "Designing Powerful APIs with GraphQL Query Parameters", + "url": "https://www.graph.cool/docs/tutorials/designing-powerful-apis-with-graphql-query-parameters-aing7uech3/", + "tags": ["blog"] +} diff --git a/src/resources/data/dev-to-graphql-tag.json b/src/resources/data/dev-to-graphql-tag.json new file mode 100644 index 0000000000..7ce2241769 --- /dev/null +++ b/src/resources/data/dev-to-graphql-tag.json @@ -0,0 +1,5 @@ +{ + "title": "DEV.to GraphQL tag", + "url": "https://dev.to/t/graphql", + "tags": ["blog-or-newsletter"] +} diff --git a/src/resources/data/development-of-real-time-apps-with-graphql-node-js-vince-ning-michael-paris-sf-node-meetup-february-2017.json b/src/resources/data/development-of-real-time-apps-with-graphql-node-js-vince-ning-michael-paris-sf-node-meetup-february-2017.json new file mode 100644 index 0000000000..94c31bff6d --- /dev/null +++ b/src/resources/data/development-of-real-time-apps-with-graphql-node-js-vince-ning-michael-paris-sf-node-meetup-february-2017.json @@ -0,0 +1,6 @@ +{ + "title": "Development of real-time apps with GraphQL Node.js", + "author": "Vince Ning & Michael Paris", + "url": "https://youtu.be/yh_A6CEqsSM", + "tags": ["video", "backend"] +} diff --git a/src/resources/data/development-of-real-time-apps-with-graphql-node-js.json b/src/resources/data/development-of-real-time-apps-with-graphql-node-js.json new file mode 100644 index 0000000000..2d94848143 --- /dev/null +++ b/src/resources/data/development-of-real-time-apps-with-graphql-node-js.json @@ -0,0 +1,6 @@ +{ + "title": "Development of real-time apps with GraphQL Node.js", + "author": "Vince Ning & Michael Paris", + "url": "https://youtu.be/yh_A6CEqsSM", + "tags": ["video"] +} diff --git a/src/resources/data/escape-security-blog.json b/src/resources/data/escape-security-blog.json new file mode 100644 index 0000000000..a0190d21a3 --- /dev/null +++ b/src/resources/data/escape-security-blog.json @@ -0,0 +1,5 @@ +{ + "title": "Escape Security Blog", + "url": "https://escape.tech/blog", + "tags": ["blog-or-newsletter"] +} diff --git a/src/resources/data/exploring-graphql.json b/src/resources/data/exploring-graphql.json new file mode 100644 index 0000000000..611d7a37dc --- /dev/null +++ b/src/resources/data/exploring-graphql.json @@ -0,0 +1,6 @@ +{ + "title": "Exploring GraphQL", + "url": "https://youtube.com/watch?v=WQLzZf34FJ8", + "author": "Lee Byron", + "tags": ["video"] +} diff --git a/src/resources/data/from-rest-to-graphql.json b/src/resources/data/from-rest-to-graphql.json new file mode 100644 index 0000000000..b672e423aa --- /dev/null +++ b/src/resources/data/from-rest-to-graphql.json @@ -0,0 +1,6 @@ +{ + "title": "From REST to GraphQL", + "url": "https://0x2a.sh/from-rest-to-graphql-b4e95e94c26b#.tag7nzkrb", + "author": "Garen J. Torikian", + "tags": ["blog"] +} diff --git a/src/resources/data/from-zero-to-graphql-in-30-minutes.json b/src/resources/data/from-zero-to-graphql-in-30-minutes.json new file mode 100644 index 0000000000..b7897ec52b --- /dev/null +++ b/src/resources/data/from-zero-to-graphql-in-30-minutes.json @@ -0,0 +1,6 @@ +{ + "title": "From Zero to GraphQL in 30 Minutes", + "url": "https://youtube.com/watch?v=UBGzsb2UkeY", + "author": "Steven Luscher", + "tags": ["video"] +} diff --git a/src/resources/data/fullstack-graphql.json b/src/resources/data/fullstack-graphql.json new file mode 100644 index 0000000000..0f163bf5ec --- /dev/null +++ b/src/resources/data/fullstack-graphql.json @@ -0,0 +1,6 @@ +{ + "title": "Fullstack GraphQL", + "url": "https://www.graphqladmin.com/books/fullstack-graphql", + "author": "Julian Mayorga", + "tags": ["book"] +} diff --git a/src/resources/data/graphql-and-the-amazing-apollo-client.json b/src/resources/data/graphql-and-the-amazing-apollo-client.json new file mode 100644 index 0000000000..e74ff64ec1 --- /dev/null +++ b/src/resources/data/graphql-and-the-amazing-apollo-client.json @@ -0,0 +1,6 @@ +{ + "title": "GraphQL and the amazing Apollo Client", + "url": "https://medium.com/google-developer-experts/graphql-and-the-amazing-apollo-client-fe57e162a70c", + "author": "Gerard Sans", + "tags": ["blog"] +} diff --git a/src/resources/data/graphql-apis.json b/src/resources/data/graphql-apis.json new file mode 100644 index 0000000000..6c7365fdae --- /dev/null +++ b/src/resources/data/graphql-apis.json @@ -0,0 +1,5 @@ +{ + "title": "graphql-apis", + "url": "https://github.com/APIs-guru/graphql-apis", + "tags": ["guide"] +} diff --git a/src/resources/data/graphql-at-facebook.json b/src/resources/data/graphql-at-facebook.json new file mode 100644 index 0000000000..8f5a73d96d --- /dev/null +++ b/src/resources/data/graphql-at-facebook.json @@ -0,0 +1,6 @@ +{ + "title": "GraphQL at Facebook", + "url": "https://youtube.com/watch?v=etax3aEe2dA", + "author": "Dan Schafer", + "tags": ["video"] +} diff --git a/src/resources/data/graphql-best-practices-hands-on-experience-with-schema-design-security-and-error-handling-for-developers.json b/src/resources/data/graphql-best-practices-hands-on-experience-with-schema-design-security-and-error-handling-for-developers.json new file mode 100644 index 0000000000..c5fba326c9 --- /dev/null +++ b/src/resources/data/graphql-best-practices-hands-on-experience-with-schema-design-security-and-error-handling-for-developers.json @@ -0,0 +1,6 @@ +{ + "title": "GraphQL Best Practices: Hands-on experience with schema design, security, and error handling for developers", + "url": "https://www.amazon.com/dp/B0D9H7MJQV", + "author": "Marc-André Giroux & Apoorva Pandey", + "tags": ["book"] +} diff --git a/src/resources/data/graphql-code-of-conduct.json b/src/resources/data/graphql-code-of-conduct.json new file mode 100644 index 0000000000..64d708b1d2 --- /dev/null +++ b/src/resources/data/graphql-code-of-conduct.json @@ -0,0 +1,5 @@ +{ + "title": "GraphQL Code of Conduct", + "url": "/codeofconduct/", + "tags": ["guide"] +} diff --git a/src/resources/data/graphql-concepts-visualized.json b/src/resources/data/graphql-concepts-visualized.json new file mode 100644 index 0000000000..0e8e235f89 --- /dev/null +++ b/src/resources/data/graphql-concepts-visualized.json @@ -0,0 +1,6 @@ +{ + "title": "GraphQL Concepts Visualized", + "url": "https://medium.com/apollo-stack/the-concepts-of-graphql-bc68bd819be3#.hfczgtdsj", + "author": "Dhaivat Pandya", + "tags": ["blog"] +} diff --git a/src/resources/data/graphql-editor-blog.json b/src/resources/data/graphql-editor-blog.json new file mode 100644 index 0000000000..e178be9bcc --- /dev/null +++ b/src/resources/data/graphql-editor-blog.json @@ -0,0 +1,5 @@ +{ + "title": "GraphQL Editor Blog", + "url": "https://blog.graphqleditor.com", + "tags": ["blog-or-newsletter"] +} diff --git a/src/resources/data/graphql-explained.json b/src/resources/data/graphql-explained.json new file mode 100644 index 0000000000..8f969374d4 --- /dev/null +++ b/src/resources/data/graphql-explained.json @@ -0,0 +1,6 @@ +{ + "title": "GraphQL Explained", + "url": "https://medium.com/apollo-stack/graphql-explained-5844742f195e#.zdykxos6i", + "author": "JH", + "tags": ["blog"] +} diff --git a/src/resources/data/graphql-from-zero-to-scala.json b/src/resources/data/graphql-from-zero-to-scala.json new file mode 100644 index 0000000000..f3a10cfedc --- /dev/null +++ b/src/resources/data/graphql-from-zero-to-scala.json @@ -0,0 +1,6 @@ +{ + "title": "GraphQL: From Zero to Scala", + "author": "Jérémie Astori", + "url": "https://www.youtube.com/watch?v=6ttypoLyRaU", + "tags": ["video", "backend"] +} diff --git a/src/resources/data/graphql-future.json b/src/resources/data/graphql-future.json new file mode 100644 index 0000000000..7b789c35be --- /dev/null +++ b/src/resources/data/graphql-future.json @@ -0,0 +1,6 @@ +{ + "title": "GraphQL Future", + "url": "https://youtube.com/watch?v=ViXL0YQnioU", + "author": "Lee Byron", + "tags": ["video"] +} diff --git a/src/resources/data/graphql-in-native-applications.json b/src/resources/data/graphql-in-native-applications.json new file mode 100644 index 0000000000..8da2e8cc2c --- /dev/null +++ b/src/resources/data/graphql-in-native-applications.json @@ -0,0 +1,6 @@ +{ + "title": "GraphQL in native applications", + "author": "Igor Canadi & Alex Langenfeld", + "url": "https://atscaleconference.com/videos/graphql-in-native-applications-at-scale/", + "tags": ["video", "frontend"] +} diff --git a/src/resources/data/graphql-in-production-backend-as-a-service-michael-paris-vince-ning-graphql-in-production-meetup-sf-august-2016.json b/src/resources/data/graphql-in-production-backend-as-a-service-michael-paris-vince-ning-graphql-in-production-meetup-sf-august-2016.json new file mode 100644 index 0000000000..8d1469e85e --- /dev/null +++ b/src/resources/data/graphql-in-production-backend-as-a-service-michael-paris-vince-ning-graphql-in-production-meetup-sf-august-2016.json @@ -0,0 +1,6 @@ +{ + "title": "GraphQL in Production: Backend as a Service", + "author": "Michael Paris & Vince Ning", + "url": "https://www.youtube.com/watch?v=U2NKoStGBvE", + "tags": ["video", "backend"] +} diff --git a/src/resources/data/graphql-in-production-backend-as-a-service.json b/src/resources/data/graphql-in-production-backend-as-a-service.json new file mode 100644 index 0000000000..05dbe33642 --- /dev/null +++ b/src/resources/data/graphql-in-production-backend-as-a-service.json @@ -0,0 +1,6 @@ +{ + "title": "GraphQL in Production: Backend as a Service", + "author": "Michael Paris & Vince Ning", + "url": "https://www.youtube.com/watch?v=U2NKoStGBvE", + "tags": ["video"] +} diff --git a/src/resources/data/graphql-js-tutorial.json b/src/resources/data/graphql-js-tutorial.json new file mode 100644 index 0000000000..8ad1d6af05 --- /dev/null +++ b/src/resources/data/graphql-js-tutorial.json @@ -0,0 +1,5 @@ +{ + "title": "GraphQL-JS tutorial", + "url": "/graphql-js", + "tags": ["guide"] +} diff --git a/src/resources/data/graphql-screencasts.json b/src/resources/data/graphql-screencasts.json new file mode 100644 index 0000000000..7f21b01ad3 --- /dev/null +++ b/src/resources/data/graphql-screencasts.json @@ -0,0 +1,5 @@ +{ + "title": "GraphQL Screencasts", + "url": "https://graphql.wtf", + "tags": ["guide"] +} diff --git a/src/resources/data/graphql-server-tutorial-for-node-js-with-sql-mongodb-and-rest-jonas-helfer.json b/src/resources/data/graphql-server-tutorial-for-node-js-with-sql-mongodb-and-rest-jonas-helfer.json new file mode 100644 index 0000000000..6a120eaf18 --- /dev/null +++ b/src/resources/data/graphql-server-tutorial-for-node-js-with-sql-mongodb-and-rest-jonas-helfer.json @@ -0,0 +1,6 @@ +{ + "title": "GraphQL server tutorial for Node.js with SQL, MongoDB and REST", + "author": "Jonas Helfer", + "url": "https://www.youtube.com/watch?v=PHabPhgRUuU", + "tags": ["video", "backend"] +} diff --git a/src/resources/data/graphql-server-tutorial-for-node-js-with-sql-mongodb-and-rest.json b/src/resources/data/graphql-server-tutorial-for-node-js-with-sql-mongodb-and-rest.json new file mode 100644 index 0000000000..c67dd937a2 --- /dev/null +++ b/src/resources/data/graphql-server-tutorial-for-node-js-with-sql-mongodb-and-rest.json @@ -0,0 +1,6 @@ +{ + "title": "GraphQL server tutorial for Node.js with SQL, MongoDB and REST", + "author": "Jonas Helfer", + "url": "https://www.youtube.com/watch?v=PHabPhgRUuU", + "tags": ["video"] +} diff --git a/src/resources/data/graphql-servers.json b/src/resources/data/graphql-servers.json new file mode 100644 index 0000000000..c5f8fa7806 --- /dev/null +++ b/src/resources/data/graphql-servers.json @@ -0,0 +1,6 @@ +{ + "title": "GraphQL Servers", + "author": "Nick Schrock", + "url": "https://www.youtube.com/watch?v=KOudxKJXsjc", + "tags": ["video", "backend"] +} diff --git a/src/resources/data/graphql-source-code-overview.json b/src/resources/data/graphql-source-code-overview.json new file mode 100644 index 0000000000..fdc3c0a830 --- /dev/null +++ b/src/resources/data/graphql-source-code-overview.json @@ -0,0 +1,6 @@ +{ + "title": "GraphQL Source Code Overview", + "author": "Lee Byron", + "url": "https://youtube.com/watch?v=IqtYr6RX32Q", + "tags": ["video", "backend"] +} diff --git a/src/resources/data/graphql-tutorials.json b/src/resources/data/graphql-tutorials.json new file mode 100644 index 0000000000..994d2ff091 --- /dev/null +++ b/src/resources/data/graphql-tutorials.json @@ -0,0 +1,5 @@ +{ + "title": "GraphQL Tutorials", + "url": "https://hasura.io/learn", + "tags": ["guide"] +} diff --git a/src/resources/data/graphql-weekly.json b/src/resources/data/graphql-weekly.json new file mode 100644 index 0000000000..2d90509692 --- /dev/null +++ b/src/resources/data/graphql-weekly.json @@ -0,0 +1,5 @@ +{ + "title": "GraphQL Weekly", + "url": "https://graphqlweekly.com", + "tags": ["blog-or-newsletter"] +} diff --git a/src/resources/data/graphql-wtf-episodes-feed.json b/src/resources/data/graphql-wtf-episodes-feed.json new file mode 100644 index 0000000000..f59eceabdb --- /dev/null +++ b/src/resources/data/graphql-wtf-episodes-feed.json @@ -0,0 +1,5 @@ +{ + "title": "GraphQL WTF Episodes Feed", + "url": "https://graphql.wtf", + "tags": ["blog-or-newsletter"] +} diff --git a/src/resources/data/hands-on-full-stack-web-development-with-graphql-and-react.json b/src/resources/data/hands-on-full-stack-web-development-with-graphql-and-react.json new file mode 100644 index 0000000000..57c9172b6b --- /dev/null +++ b/src/resources/data/hands-on-full-stack-web-development-with-graphql-and-react.json @@ -0,0 +1,6 @@ +{ + "title": "Hands-on Full-Stack Web Development with GraphQL and React", + "url": "https://www.packtpub.com/en-us/product/hands-on-full-stack-web-development-with-graphql-and-react-9781789135763", + "author": "Sebastian Grebe", + "tags": ["guide"] +} diff --git a/src/resources/data/hands-on-graphql-for-better-restful-web-services-video-by-ashwin-hegde.json b/src/resources/data/hands-on-graphql-for-better-restful-web-services-video-by-ashwin-hegde.json new file mode 100644 index 0000000000..99619acd0f --- /dev/null +++ b/src/resources/data/hands-on-graphql-for-better-restful-web-services-video-by-ashwin-hegde.json @@ -0,0 +1,6 @@ +{ + "title": "Hands-on GraphQL for Better RESTful Web Services (Video)", + "author": "Ashwin Hegde", + "url": "https://www.packtpub.com/application-development/hands-graphql-better-restful-web-services-video", + "tags": ["video", "federation", "backend"] +} diff --git a/src/resources/data/hands-on-graphql-for-better-restful-web-services-video.json b/src/resources/data/hands-on-graphql-for-better-restful-web-services-video.json new file mode 100644 index 0000000000..4cd82b919f --- /dev/null +++ b/src/resources/data/hands-on-graphql-for-better-restful-web-services-video.json @@ -0,0 +1,6 @@ +{ + "title": "Hands-on GraphQL for Better RESTful Web Services (Video)", + "author": "Ashwin Hegde", + "url": "https://www.packtpub.com/application-development/hands-graphql-better-restful-web-services-video", + "tags": ["video"] +} diff --git a/src/resources/data/hasura-s-blog.json b/src/resources/data/hasura-s-blog.json new file mode 100644 index 0000000000..7addc43662 --- /dev/null +++ b/src/resources/data/hasura-s-blog.json @@ -0,0 +1,5 @@ +{ + "title": "Hasura's Blog", + "url": "https://hasura.io/blog", + "tags": ["blog-or-newsletter"] +} diff --git a/src/resources/data/inigo-s-security-blog.json b/src/resources/data/inigo-s-security-blog.json new file mode 100644 index 0000000000..ef9f0f1c82 --- /dev/null +++ b/src/resources/data/inigo-s-security-blog.json @@ -0,0 +1,5 @@ +{ + "title": "Inigo's Security Blog", + "url": "https://inigo.io/blog", + "tags": ["blog-or-newsletter"] +} diff --git a/src/resources/data/learning-graphql-and-relay.json b/src/resources/data/learning-graphql-and-relay.json new file mode 100644 index 0000000000..a1f831ede2 --- /dev/null +++ b/src/resources/data/learning-graphql-and-relay.json @@ -0,0 +1,6 @@ +{ + "title": "Learning GraphQL and Relay", + "url": "https://www.amazon.com/Learning-GraphQL-Relay-Samer-Buna/dp/1786465752", + "author": "Samer Buna", + "tags": ["guide"] +} diff --git a/src/resources/data/learning-graphql-with-react-and-relay.json b/src/resources/data/learning-graphql-with-react-and-relay.json new file mode 100644 index 0000000000..103f4f0cdd --- /dev/null +++ b/src/resources/data/learning-graphql-with-react-and-relay.json @@ -0,0 +1,6 @@ +{ + "title": "Learning GraphQL with React and Relay", + "author": "Divyendu Singh", + "url": "https://www.packtpub.com/application-development/learning-graphql-react-and-relay-video", + "tags": ["video", "frontend"] +} diff --git a/src/resources/data/learning-graphql.json b/src/resources/data/learning-graphql.json new file mode 100644 index 0000000000..4287e2bdc7 --- /dev/null +++ b/src/resources/data/learning-graphql.json @@ -0,0 +1,6 @@ +{ + "title": "Learning GraphQL", + "url": "https://www.amazon.com/Learning-GraphQL-Declarative-Fetching-Modern/dp/1492030716/", + "author": "Eve Porcello & Alex Banks", + "tags": ["guide"] +} diff --git a/src/resources/data/modernize-your-angular-app-with-graphql.json b/src/resources/data/modernize-your-angular-app-with-graphql.json new file mode 100644 index 0000000000..7f6364cb55 --- /dev/null +++ b/src/resources/data/modernize-your-angular-app-with-graphql.json @@ -0,0 +1,6 @@ +{ + "title": "Modernize Your Angular App with GraphQL", + "author": "Uri Goldshtein", + "url": "https://www.youtube.com/watch?v=E8feZBidZcs", + "tags": ["video", "frontend"] +} diff --git a/src/resources/data/official-graphql-blog.json b/src/resources/data/official-graphql-blog.json new file mode 100644 index 0000000000..63976c18d7 --- /dev/null +++ b/src/resources/data/official-graphql-blog.json @@ -0,0 +1,5 @@ +{ + "title": "Official GraphQL Blog", + "url": "https://graphql.org/blog", + "tags": ["blog-or-newsletter"] +} diff --git a/src/resources/data/production-ready-graphql.json b/src/resources/data/production-ready-graphql.json new file mode 100644 index 0000000000..48022997b1 --- /dev/null +++ b/src/resources/data/production-ready-graphql.json @@ -0,0 +1,6 @@ +{ + "title": "Production Ready GraphQL", + "url": "https://book.productionreadygraphql.com/", + "author": "Marc-Andre Giroux", + "tags": ["guide"] +} diff --git a/src/resources/data/relay-2-simpler-faster-and-more-predictable-greg-hurrell.json b/src/resources/data/relay-2-simpler-faster-and-more-predictable-greg-hurrell.json new file mode 100644 index 0000000000..5af112c51d --- /dev/null +++ b/src/resources/data/relay-2-simpler-faster-and-more-predictable-greg-hurrell.json @@ -0,0 +1,6 @@ +{ + "title": "Relay 2 - simpler, faster, and more predictable", + "author": "Greg Hurrell", + "url": "https://www.youtube.com/watch?v=OEfUBN9dAI8", + "tags": ["video", "frontend"] +} diff --git a/src/resources/data/relay-2-simpler-faster-and-more-predictable.json b/src/resources/data/relay-2-simpler-faster-and-more-predictable.json new file mode 100644 index 0000000000..7bfb22a4b4 --- /dev/null +++ b/src/resources/data/relay-2-simpler-faster-and-more-predictable.json @@ -0,0 +1,6 @@ +{ + "title": "Relay 2 - simpler, faster, and more predictable", + "author": "Greg Hurrell", + "url": "https://www.youtube.com/watch?v=OEfUBN9dAI8", + "tags": ["video"] +} diff --git a/src/resources/data/relicensing-the-graphql-specification.json b/src/resources/data/relicensing-the-graphql-specification.json new file mode 100644 index 0000000000..334cfdf4c0 --- /dev/null +++ b/src/resources/data/relicensing-the-graphql-specification.json @@ -0,0 +1,6 @@ +{ + "title": "Relicensing the GraphQL specification", + "url": "https://medium.com/@leeb/relicensing-the-graphql-specification-e7d07a52301b", + "author": "Lee Byron", + "tags": ["blog"] +} diff --git a/src/resources/data/stepzen-s-blog.json b/src/resources/data/stepzen-s-blog.json new file mode 100644 index 0000000000..208aca0ebb --- /dev/null +++ b/src/resources/data/stepzen-s-blog.json @@ -0,0 +1,5 @@ +{ + "title": "StepZen's Blog", + "url": "https://stepzen.com/blog", + "tags": ["blog-or-newsletter"] +} diff --git a/src/resources/data/the-community-discord-channel.json b/src/resources/data/the-community-discord-channel.json new file mode 100644 index 0000000000..e694c5020e --- /dev/null +++ b/src/resources/data/the-community-discord-channel.json @@ -0,0 +1,5 @@ +{ + "title": "the community Discord channel", + "url": "/community/#official-channels", + "tags": ["guide"] +} diff --git a/src/resources/data/the-graphql-guide.json b/src/resources/data/the-graphql-guide.json new file mode 100644 index 0000000000..b34d66f420 --- /dev/null +++ b/src/resources/data/the-graphql-guide.json @@ -0,0 +1,6 @@ +{ + "title": "The GraphQL Guide", + "url": "https://graphql.guide", + "author": "Loren Sands-Ramshaw", + "tags": ["guide"] +} diff --git a/src/resources/data/the-guild-s-blog.json b/src/resources/data/the-guild-s-blog.json new file mode 100644 index 0000000000..461761c85b --- /dev/null +++ b/src/resources/data/the-guild-s-blog.json @@ -0,0 +1,5 @@ +{ + "title": "The Guild's Blog", + "url": "https://the-guild.dev/blog", + "tags": ["blog-or-newsletter"] +} diff --git a/src/resources/data/the-guild-s-newsletter.json b/src/resources/data/the-guild-s-newsletter.json new file mode 100644 index 0000000000..c34b24e874 --- /dev/null +++ b/src/resources/data/the-guild-s-newsletter.json @@ -0,0 +1,5 @@ +{ + "title": "The Guild's Newsletter", + "url": "https://getrevue.co/profile/TheGuild", + "tags": ["blog-or-newsletter"] +} diff --git a/src/resources/data/the-road-to-graphql.json b/src/resources/data/the-road-to-graphql.json new file mode 100644 index 0000000000..a9a97f091f --- /dev/null +++ b/src/resources/data/the-road-to-graphql.json @@ -0,0 +1,6 @@ +{ + "title": "The Road to GraphQL", + "url": "https://www.robinwieruch.de/the-road-to-graphql-book/", + "author": "Robin Wieruch", + "tags": ["guide"] +} diff --git a/src/resources/data/trademark-policy.json b/src/resources/data/trademark-policy.json new file mode 100644 index 0000000000..415fe86826 --- /dev/null +++ b/src/resources/data/trademark-policy.json @@ -0,0 +1,5 @@ +{ + "title": "trademark policy", + "url": "https://lfprojects.org/policies/trademark-policy/", + "tags": ["guide"] +} diff --git a/src/resources/data/tutorial-how-to-build-a-graphql-server.json b/src/resources/data/tutorial-how-to-build-a-graphql-server.json new file mode 100644 index 0000000000..270598c7f5 --- /dev/null +++ b/src/resources/data/tutorial-how-to-build-a-graphql-server.json @@ -0,0 +1,6 @@ +{ + "title": "Tutorial: How to Build a GraphQL Server", + "url": "https://medium.com/apollo-stack/tutorial-building-a-graphql-server-cddaa023c035#.bu6sdnst4", + "author": "Jonas Helfer & Johanna Griffin", + "tags": ["blog"] +} diff --git a/src/resources/data/tutorial-kick-start-a-js-api-with-apollo-server-dataloader-and-knex.json b/src/resources/data/tutorial-kick-start-a-js-api-with-apollo-server-dataloader-and-knex.json new file mode 100644 index 0000000000..48eaef09bb --- /dev/null +++ b/src/resources/data/tutorial-kick-start-a-js-api-with-apollo-server-dataloader-and-knex.json @@ -0,0 +1,6 @@ +{ + "title": "Tutorial: Kick start a JS API with Apollo-server, Dataloader and Knex", + "url": "https://bamtech.gitbook.io/dev-standards/backend/graphql-js/getting-started-with-apollo-server-dataloader-knex.mo", + "author": "Thomas Pucci", + "tags": ["blog"] +} diff --git a/src/resources/data/unleashing-the-power-of-graphql-using-angular-2-gerard-sans-ng-be-2016.json b/src/resources/data/unleashing-the-power-of-graphql-using-angular-2-gerard-sans-ng-be-2016.json new file mode 100644 index 0000000000..af39944189 --- /dev/null +++ b/src/resources/data/unleashing-the-power-of-graphql-using-angular-2-gerard-sans-ng-be-2016.json @@ -0,0 +1,6 @@ +{ + "title": "Unleashing the power of GraphQL using Angular 2", + "author": "Gerard Sans", + "url": "https://www.youtube.com/watch?v=VYpJ9pfugM8", + "tags": ["video", "frontend"] +} diff --git a/src/resources/data/webinar-series-graphql-around-the-world.json b/src/resources/data/webinar-series-graphql-around-the-world.json new file mode 100644 index 0000000000..b723cb2ccc --- /dev/null +++ b/src/resources/data/webinar-series-graphql-around-the-world.json @@ -0,0 +1,5 @@ +{ + "title": "Webinar Series: GraphQL Around The World", + "url": "https://graphql-world.com/webinar", + "tags": ["video"] +} diff --git a/src/resources/data/wundergraph-s-blog.json b/src/resources/data/wundergraph-s-blog.json new file mode 100644 index 0000000000..645953db5d --- /dev/null +++ b/src/resources/data/wundergraph-s-blog.json @@ -0,0 +1,5 @@ +{ + "title": "WunderGraph's Blog", + "url": "https://wundergraph.com/blog", + "tags": ["blog-or-newsletter"] +} diff --git a/src/resources/data/yoga-graphql-server-tutorial.json b/src/resources/data/yoga-graphql-server-tutorial.json new file mode 100644 index 0000000000..c1bf2f20ae --- /dev/null +++ b/src/resources/data/yoga-graphql-server-tutorial.json @@ -0,0 +1,5 @@ +{ + "title": "Yoga GraphQL Server Tutorial", + "url": "https://the-guild.dev/graphql/yoga-server/tutorial", + "tags": ["guide"] +} diff --git a/src/resources/data/your-first-graphql-server.json b/src/resources/data/your-first-graphql-server.json new file mode 100644 index 0000000000..0af9082ceb --- /dev/null +++ b/src/resources/data/your-first-graphql-server.json @@ -0,0 +1,6 @@ +{ + "title": "Your First GraphQL Server", + "url": "https://medium.com/the-graphqlhub/your-first-graphql-server-3c766ab4f0a2#.ovn0y19k4", + "author": "Clay Allsopp", + "tags": ["blog"] +} diff --git a/src/resources/types.ts b/src/resources/types.ts new file mode 100644 index 0000000000..5bebea9c79 --- /dev/null +++ b/src/resources/types.ts @@ -0,0 +1,39 @@ +import { type } from "arktype" + +export const topics = [ + "frontend", + "backend", + "federation", + "schema-design", + "api-platform-and-gateways", + "developer-experience", + "security", + "ai", + "monitoring", + "tools", +] as const +export type Topic = (typeof topics)[number] + +export const kinds = [ + "video", + "blog", + "tools-and-libraries", + "guide", + "book", + "blog-or-newsletter", +] as const +export type Kind = (typeof kinds)[number] + +export type ResourceTag = Topic | Kind + +export const ResourceMetadata = type({ + title: "string>0", + url: type("string.url").or("/^\\/.+$/"), + "author?": "string", + "kind?": type.enumerated(...kinds), + "topics?": type.enumerated(...topics).array(), + "description?": "string>0", + tags: type.enumerated(...topics, ...kinds).array(), +}) + +export type ResourceMetadata = typeof ResourceMetadata.inferOut diff --git a/tailwind.config.ts b/tailwind.config.ts index 16649f5949..1dbf34a0fe 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -128,9 +128,10 @@ const config: Config = { plugin(({ addBase }) => { // heading styles addBase({ - ".typography-d1, .typography-h1, .typography-h2, .typography-h3": { - lineHeight: "1.2", - }, + ".typography-d1, .typography-h1, .typography-h2, .typography-h3, .typography-h4": + { + lineHeight: "1.2", + }, ".typography-d1": { fontSize: "48px", "@screen lg": { @@ -155,6 +156,12 @@ const config: Config = { fontSize: "32px", }, }, + ".typography-h4": { + fontSize: "20px", + "@screen md": { + fontSize: "28px", + }, + }, }) // paragraph styles diff --git a/test/e2e/resources-hub.spec.ts b/test/e2e/resources-hub.spec.ts new file mode 100644 index 0000000000..bc1816f630 --- /dev/null +++ b/test/e2e/resources-hub.spec.ts @@ -0,0 +1,24 @@ +import { expect, test } from "@playwright/test" + +const pages = [ + "/resources", + "/resources/frontend", + "/resources/backend", + "/resources/federation", + "/resources/ai", + "/resources/security", + "/resources/monitoring", + "/code", + "/conf", + "/resources/reading", + "/resources/video", +] + +test.describe("Resource hub pages exist", () => { + for (const path of pages) { + test(`renders ${path}`, async ({ page }) => { + const response = await page.goto(path) + expect(response?.ok()).toBeTruthy() + }) + } +}) diff --git a/vercel.json b/vercel.json index 244e7c6f2c..87a8d6f90a 100644 --- a/vercel.json +++ b/vercel.json @@ -400,6 +400,46 @@ "destination": "/blog/2020-10-15-newsletter-september-2020", "permanent": true }, + { + "source": "/community/resources/official-channels", + "destination": "/resources/official-channels", + "permanent": true + }, + { + "source": "/community/resources/training-courses", + "destination": "/resources/training-courses", + "permanent": true + }, + { + "source": "/community/resources/community-channels", + "destination": "/resources/community-channels", + "permanent": true + }, + { + "source": "/community/resources/blogs-and-newsletters", + "destination": "/resources/blogs-and-newsletters", + "permanent": true + }, + { + "source": "/community/resources/videos", + "destination": "/resources/videos", + "permanent": true + }, + { + "source": "/community/resources/vendor-channels", + "destination": "/resources/vendor-channels", + "permanent": true + }, + { + "source": "/community/resources/books", + "destination": "/resources/books", + "permanent": true + }, + { + "source": "/community/resources/more-resources", + "destination": "/resources", + "permanent": true + }, { "source": "/blog/2020-10-15-graphql-foundation-monthly-newsletter-september-2020/", "destination": "/blog/2020-10-15-newsletter-september-2020",