[website] Add blog (#417)

* add ghost api

* connect blog overview page

* connect and render post detail

* add processing to server

* update blog detail page

* add eslint-plugin-tailwindcss

* add @tanstack/react-query

* truncate text by numberOfLines

* use ComponentPropsWithRef to infer style prop type

* update ghost fns

* update index

* add tag page

* add author page

* update /

* update /tag

* update /author

* update detail page

* fix posts type

* disable redirect

* remove global background

- not found pages
- pages without common layout (preview)

* tmp: hide nav on smaller screens

* update app layout spacing

* remove text truncating

* update / spacing and sizing

* update .vscode/settings.json

* update .eslintrc

* update prettier-plugin-tailwindcss

* sort tailwind classes

* add packages/eslint-config-custom

* use turbo lint

* use tailwind.config.cjs

see https://github.com/francoismassart/eslint-plugin-tailwindcss/issues/212

* reset global line-height

* fix lint-staged

* update color tokens

* update spacing and sizes

* Update apps/website/src/lib/ghost.ts

Co-authored-by: Pavel <14926950+prichodko@users.noreply.github.com>

* update layout max width

* update card min width

* set line-height

* set overflow on pre

* use flex for markdown content to prevent overflow

* collect follow-ups

* add visibility filter

* update page count

* rename var

* remove filter

* use prod ghost api key

* update ghost api

* revert line-height

* add limit to getPosts params

* update visible posts

* add related articles

* add env vars to gh

* rename eslint config package

* update gh vars

* rename envs

* set emtpy array to related posts

* fix lint-staged

* prevent importing server envs on client

* set limit

---------

Co-authored-by: Felicio Mununga <felicio@users.noreply.github.com>
This commit is contained in:
Pavel 2023-06-21 12:35:48 +02:00 committed by GitHub
parent 820eafadde
commit 45e36b2360
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 1787 additions and 365 deletions

119
.eslintrc
View File

@ -1,119 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
// TODO: Enable type-aware linting (https://typescript-eslint.io/docs/linting/type-linting)
// "tsconfigRootDir": ".",
// "project": ["./packages/*/tsconfig.json"],
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
},
"warnOnUnsupportedTypeScriptVersion": true
},
"env": {
"browser": true,
"node": true
},
"plugins": [
"@typescript-eslint",
"import",
"simple-import-sort",
"react",
"jsx-a11y"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
// "plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:eslint-comments/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:jsx-a11y/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:react/jsx-runtime",
"prettier"
],
"overrides": [
{
"files": ["examples/**/*.tsx"],
"rules": {
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off"
}
},
{
// TODO: add https://github.com/francoismassart/eslint-plugin-tailwindcss
"files": ["./apps/website/"],
"extends": ["next", "next/core-web-vitals"]
// "settings": {
// "import/resolver": {
// "typescript": {
// "extensions": [".js", ".jsx", ".ts", ".tsx"],
// "project": ["tsconfig.json", "apps/website/tsconfig.json"]
// }
// }
// }
}
],
"rules": {
"react/prop-types": 0,
// "@typescript-eslint/consistent-type-definitions": ["error", "interface"],
"@typescript-eslint/consistent-type-imports": "error",
// TODO: turn on this rul
"@typescript-eslint/no-non-null-assertion": "off",
// "@typescript-eslint/consistent-type-exports": "error",
"simple-import-sort/imports": [
"error",
{
"groups": [
// Side effect imports.
["^\\u0000"],
// `react` related packages come first.
["^react$", "^react-dom$"],
// Things that start with a letter (or digit or underscore), or `@` followed by a letter.
["^@?\\w"],
// Absolute imports and other imports such as Vue-style `@/foo`.
// Anything not matched in another group.
["^"],
// Relative imports.
// Anything that starts with a dot.
["^\\."],
// type imports last as a separate group
["^.+\\u0000$"]
]
}
],
"simple-import-sort/exports": "error",
"import/first": "error",
"import/newline-after-import": "error",
"import/no-duplicates": "error"
},
"settings": {
"react": {
"version": "detect"
},
"import/resolver": {
"node": {
"extensions": [".js", ".jsx", ".ts", ".tsx"],
"project": [
"tsconfig.base.json",
"packages/*/tsconfig.json",
"apps/*/tsconfig.json"
]
},
"typescript": {
"alwaysTryTypes": true,
"project": [
"tsconfig.base.json",
"packages/*/tsconfig.json",
"apps/*/tsconfig.json"
]
}
},
"import/ignore": ["react-native"]
}
}
// appp/website; extend eslint-config-next

View File

@ -6,6 +6,11 @@ on:
pull_request:
types: [opened, synchronize]
env:
NEXT_PUBLIC_GHOST_API_KEY: ''
INFURA_API_KEY: ''
TAMAGUI_TARGET: 'web'
jobs:
build:
name: Build and Test

View File

@ -1,5 +1,11 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"eslint.packageManager": "yarn",
"npm.packageManager": "yarn"
"npm.packageManager": "yarn",
"eslint.workingDirectories": [
{
"mode": "auto",
"#comment": "See https://github.com/microsoft/vscode-eslint/issues/1161 for reason (i.e. multiple .eslintrc config files)"
}
]
}

4
apps/mobile/.eslintrc Normal file
View File

@ -0,0 +1,4 @@
{
"root": true,
"extends": ["@status-im/eslint-config"]
}

View File

@ -38,6 +38,7 @@
"@types/react-native": "~0.70.6",
"babel-plugin-module-resolver": "^4.1.0",
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
"@status-im/eslint-config": "*",
"typescript": "^5.0.3"
}
}

9
apps/web/.eslintrc Normal file
View File

@ -0,0 +1,9 @@
{
"root": true,
"extends": [
"@status-im/eslint-config",
"plugin:tailwindcss/recommended",
"next",
"next/core-web-vitals"
]
}

View File

@ -24,6 +24,7 @@
"@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react-swc": "^3.2.0",
"@status-im/eslint-config": "*",
"typescript": "^5.0.3",
"vite": "^4.2.1"
}

View File

@ -1,4 +1,3 @@
IGNORE_TS_CONFIG_PATHS=true
TAMAGUI_TARGET=web
TAMAGUI_DISABLE_WARN_DYNAMIC_LOAD=1

9
apps/website/.eslintrc Normal file
View File

@ -0,0 +1,9 @@
{
"root": true,
"extends": [
"@status-im/eslint-config",
"plugin:tailwindcss/recommended",
"next",
"next/core-web-vitals"
]
}

6
apps/website/.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"arrowParens": "avoid",
"tailwindConfig": "./tailwind.config.cjs"
}

View File

@ -1,6 +1,7 @@
import type { env } from './src/config/env.mjs'
import type { clientEnv } from './src/config/env.client.mjs'
import type { serverEnv } from './src/config/env.server.mjs'
type Env = typeof env
type Env = typeof clientEnv & typeof serverEnv
declare global {
namespace NodeJS {

View File

@ -1,7 +1,8 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable import/default */
import './src/config/env.mjs'
import './src/config/env.server.mjs'
import './src/config/env.client.mjs'
import tamagui_next_plugin from '@tamagui/next-plugin'
import { join } from 'node:path'

View File

@ -8,7 +8,7 @@
"start": "next start",
"lint": "next lint",
"typecheck": "tsc",
"clean": "rimraf .next .tamagui .vercel/output node_modules .turbo",
"clean": "rimraf .next .tamagui .turbo .vercel/output node_modules",
"preview": "next start --port 8151"
},
"dependencies": {
@ -23,6 +23,7 @@
"@tamagui/next-theme": "1.11.1",
"@tanstack/react-query": "^4.29.7",
"@vercel/og": "^0.5.4",
"@tryghost/content-api": "^1.11.13",
"@visx/visx": "^2.18.0",
"class-variance-authority": "^0.6.0",
"d3-array": "^3.2.3",
@ -37,17 +38,24 @@
},
"devDependencies": {
"@achingbrain/ssdp": "^4.0.1",
"@tailwindcss/typography": "^0.5.9",
"@tamagui/next-plugin": "1.11.1",
"@types/d3-array": "^3.0.4",
"@types/d3-time-format": "^4.0.0",
"@types/node": "^18.11.11",
"@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11",
"@types/tryghost__content-api": "^1.3.11",
"autoprefixer": "^10.4.14",
"@status-im/eslint-config": "*",
"postcss": "^8.4.21",
"rehype-parse": "^8.0.4",
"rehype-react": "^7.2.0",
"rehype-stringify": "^9.0.3",
"tailwindcss": "^3.3.1",
"tailwindcss-animate": "^1.0.5",
"typescript": "^5.0.3"
"typescript": "^5.0.3",
"unified": "^10.1.2"
},
"engines": {
"node": "^18.x"

View File

@ -0,0 +1,22 @@
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_1111_39343)">
<circle cx="10" cy="10" r="10" fill="#647084" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.5625 19.8785C11.0534 19.9584 10.5315 19.9999 10 19.9999C9.46846 19.9999 8.94661 19.9584 8.4375 19.8785V12.9694H5.89844V10.0611H8.4375V7.84446C8.4375 5.32289 9.93044 3.93005 12.2146 3.93005C13.3087 3.93005 14.4531 4.12656 14.4531 4.12656V6.60254H13.1921C11.9499 6.60254 11.5625 7.37809 11.5625 8.17376V10.0611H14.3359L13.8926 12.9694H11.5625V19.8785Z"
fill="white"
/>
</g>
<defs>
<clipPath id="clip0_1111_39343">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 790 B

View File

@ -0,0 +1,19 @@
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_1111_39344)">
<path
d="M11.865 12.79C11.9358 12.86 11.9358 12.9742 11.865 13.045C11.4775 13.43 10.87 13.6175 10.0058 13.6175L9.99917 13.6158L9.9925 13.6175C9.12917 13.6175 8.52083 13.43 8.13333 13.0442C8.0625 12.9742 8.0625 12.86 8.13333 12.79C8.20333 12.72 8.31833 12.72 8.38917 12.79C8.705 13.1042 9.22917 13.2575 9.9925 13.2575L9.99917 13.2592L10.0058 13.2575C10.7683 13.2575 11.2925 13.1042 11.6092 12.79C11.68 12.72 11.795 12.72 11.865 12.79ZM8.99833 10.775C8.99833 10.3525 8.65333 10.0092 8.23 10.0092C7.80583 10.0092 7.46083 10.3525 7.46083 10.775C7.46083 11.1967 7.80583 11.54 8.23 11.54C8.65333 11.5408 8.99833 11.1975 8.99833 10.775ZM20 10C20 15.5225 15.5225 20 10 20C4.4775 20 0 15.5225 0 10C0 4.4775 4.4775 0 10 0C15.5225 0 20 4.4775 20 10ZM15.8333 9.8925C15.8333 9.18333 15.2542 8.60667 14.5417 8.60667C14.1942 8.60667 13.8792 8.74583 13.6467 8.96917C12.7667 8.39 11.5758 8.02167 10.2583 7.97417L10.9792 5.70417L12.9317 6.16167L12.9292 6.19C12.9292 6.77 13.4033 7.24167 13.9858 7.24167C14.5683 7.24167 15.0417 6.77 15.0417 6.19C15.0417 5.61 14.5683 5.13833 13.9858 5.13833C13.5383 5.13833 13.1575 5.4175 13.0033 5.80833L10.8992 5.315C10.8075 5.2925 10.7133 5.34583 10.685 5.43583L9.88083 7.9675C8.50083 7.98417 7.25167 8.35583 6.3325 8.95167C6.10167 8.73917 5.79583 8.60583 5.4575 8.60583C4.74583 8.60667 4.16667 9.18333 4.16667 9.8925C4.16667 10.3642 4.42583 10.7725 4.80667 10.9967C4.78167 11.1333 4.765 11.2725 4.765 11.4133C4.765 13.3142 7.1025 14.8608 9.97583 14.8608C12.8492 14.8608 15.1867 13.3142 15.1867 11.4133C15.1867 11.28 15.1725 11.1492 15.15 11.02C15.555 10.8025 15.8333 10.3817 15.8333 9.8925ZM11.7733 10.01C11.3492 10.01 11.005 10.3533 11.005 10.7758C11.005 11.1975 11.35 11.5408 11.7733 11.5408C12.1967 11.5408 12.5417 11.1975 12.5417 10.7758C12.5417 10.3533 12.1975 10.01 11.7733 10.01Z"
fill="#647084"
/>
</g>
<defs>
<clipPath id="clip0_1111_39344">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,108 @@
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_1111_39335)">
<g clip-path="url(#clip1_1111_39335)">
<mask
id="mask0_1111_39335"
style="mask-type: alpha"
maskUnits="userSpaceOnUse"
x="0"
y="-1"
width="20"
height="21"
>
<path
d="M10 -0.000366211C2.5 -0.000366211 0 2.49963 0 9.99963C0 17.4996 2.5 19.9996 10 19.9996C17.5 19.9996 20 17.4996 20 9.99963C20 2.49963 17.5 -0.000366211 10 -0.000366211Z"
fill="white"
/>
</mask>
<g mask="url(#mask0_1111_39335)">
<g filter="url(#filter0_f_1111_39335)">
<circle cx="9.0625" cy="13.4375" r="17.8125" fill="#647084" />
</g>
<g filter="url(#filter1_d_1111_39335)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12.6063 10.6139C12.4204 12.9416 10.7481 14.9991 8.48319 15.1285C7.0925 15.2078 5.70345 14.3591 5.62842 12.9809C5.57025 11.8718 6.25689 11.0625 7.45512 10.7524C7.73822 10.6783 8.02824 10.6331 8.32063 10.6176C9.59334 10.5459 10.3903 10.8376 11.6631 10.7654C11.9567 10.7505 12.2481 10.7066 12.5329 10.6344C12.5558 10.6279 12.5835 10.6209 12.6063 10.6139Z"
fill="white"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.39258 9.38378C7.57851 7.05609 9.25081 4.99853 11.5157 4.86912C12.9064 4.78986 14.2954 5.63855 14.3705 7.01672C14.4314 8.12584 13.742 8.93517 12.5438 9.24574C12.2607 9.31992 11.9707 9.36509 11.6783 9.38054C10.4055 9.45225 9.60854 9.16055 8.33583 9.23173C8.0422 9.24659 7.75081 9.29049 7.46597 9.36275C7.44314 9.36976 7.41541 9.37677 7.39258 9.38378Z"
fill="white"
/>
</g>
</g>
</g>
</g>
<defs>
<filter
id="filter0_f_1111_39335"
x="-14.6439"
y="-10.2689"
width="47.4128"
height="47.4128"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
/>
<feGaussianBlur
stdDeviation="2.94696"
result="effect1_foregroundBlur_1111_39335"
/>
</filter>
<filter
id="filter1_d_1111_39335"
x="-15.8517"
y="-10.8855"
width="51.7033"
height="53.2229"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dy="5.72711" />
<feGaussianBlur stdDeviation="10.7383" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0.0352941 0 0 0 0 0.0627451 0 0 0 0 0.109804 0 0 0 0.12 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_1111_39335"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_1111_39335"
result="shape"
/>
</filter>
<clipPath id="clip0_1111_39335">
<rect width="20" height="20" fill="white" />
</clipPath>
<clipPath id="clip1_1111_39335">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,12 @@
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20 3.51238C19.2477 3.85565 18.452 4.08379 17.6375 4.18977C18.4958 3.65705 19.1376 2.8135 19.4412 1.81891C18.6375 2.31758 17.7582 2.66875 16.8412 2.85722C16.2791 2.22923 15.5493 1.79265 14.7469 1.60417C13.9444 1.4157 13.1063 1.48406 12.3415 1.80037C11.5766 2.11667 10.9204 2.66631 10.458 3.37786C9.99566 4.08942 9.74853 4.93 9.74875 5.79039C9.74875 6.13039 9.77625 6.45732 9.84375 6.76855C8.2126 6.68489 6.6167 6.242 5.16038 5.46885C3.70406 4.6957 2.42012 3.60969 1.3925 2.28183C0.866024 3.22581 0.703261 4.34422 0.937354 5.40933C1.17145 6.47445 1.78479 7.40617 2.6525 8.01479C2.00345 7.99658 1.36809 7.81539 0.8 7.48648V7.53356C0.801087 8.52405 1.12833 9.48391 1.72665 10.2516C2.32496 11.0192 3.15781 11.5478 4.085 11.7483C3.73423 11.845 3.37273 11.8925 3.01 11.8895C2.74949 11.8944 2.48923 11.8698 2.23375 11.8163C2.49868 12.6676 3.00936 13.4121 3.69549 13.9472C4.38162 14.4824 5.20945 14.7818 6.065 14.8044C4.61367 15.9918 2.82372 16.636 0.98125 16.6338C0.645 16.6338 0.3225 16.6182 0 16.575C1.87485 17.8389 4.05959 18.5075 6.29 18.4999C13.835 18.4999 17.96 11.9614 17.96 6.29386C17.96 6.10424 17.9538 5.92116 17.945 5.73939C18.7537 5.13391 19.4501 4.37923 20 3.51238Z"
fill="#647084"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,8 +1,8 @@
import NextLink from 'next/link'
import type { ComponentProps } from 'react'
import type { ComponentPropsWithRef } from 'react'
export const Link = (props: ComponentProps<typeof NextLink>) => {
export const Link = (props: ComponentPropsWithRef<typeof NextLink>) => {
const external =
typeof props.href === 'string'
? props.href.startsWith('http')

View File

@ -16,7 +16,7 @@ export const Logo = (props: Props) => {
const { pathname } = useRouter()
return (
<div className="flex flex-shrink-0 items-center gap-2">
<div className="flex shrink-0 items-center gap-2">
{match(pathname)
.with(
P.when(p => p.startsWith('/insights')),

View File

@ -38,7 +38,7 @@ export const NavMenu = () => {
data-visible={visible}
className={cx([
'fixed left-1/2 top-5 z-10 min-w-[746px] -translate-x-1/2 overflow-hidden',
'bg-blur-neutral-80/80 border-neutral-80/5 rounded-2xl border backdrop-blur-md',
'rounded-2xl border border-neutral-80/5 bg-blur-neutral-80/80 backdrop-blur-md',
'data-[visible=false]:pointer-events-none',
'opacity-0 transition-opacity data-[visible=true]:opacity-100',
])}
@ -58,7 +58,7 @@ export const NavMenu = () => {
className={cx([
'grid gap-3 pb-12 pl-[60px] pt-6',
'data-[state=open]:animate-in',
'data-[state=closed]:animate-out fade-out-20',
'fade-out-20 data-[state=closed]:animate-out',
])}
>
{links.map(link => {
@ -91,8 +91,8 @@ export const NavMenu = () => {
<NavigationMenu.Viewport
className={cx([
'data-[state=open]:animate-heightIn data-[state=closed]:animate-heightOut',
'transition-height h-[var(--radix-navigation-menu-viewport-height)]',
'data-[state=closed]:animate-heightOut data-[state=open]:animate-heightIn',
'h-[var(--radix-navigation-menu-viewport-height)] transition-height',
])}
/>
</NavigationMenu.Root>

View File

@ -118,7 +118,7 @@ const ActionCard = (props: ActionCardProps) => {
const { title, description, action } = props
return (
<div className="bg-netural-95 border-neutral-90 flex items-center rounded-[20px] border px-5 py-3">
<div className="bg-netural-95 flex items-center rounded-[20px] border border-neutral-90 px-5 py-3">
<div className="grid flex-1 gap-px">
<Text size={19} color="$white-100" weight="semibold">
{title}

View File

@ -36,7 +36,7 @@ const SideBar = (props: Props) => {
}, [defaultLabel])
return (
<div className="border-neutral-10 border-r p-5">
<div className="border-r border-neutral-10 p-5">
<aside className=" sticky top-5 min-w-[320px]">
<Accordion.Root
type="single"

View File

@ -31,19 +31,19 @@ const issues = [
export const TableIssues = () => {
return (
<div className="border-neutral-10 overflow-hidden rounded-2xl border">
<div className="bg-neutral-5 border-neutral-10 border-b p-3">
<div className="overflow-hidden rounded-2xl border border-neutral-10">
<div className="border-b border-neutral-10 bg-neutral-5 p-3">
<Text size={15} weight="medium">
784 Open
</Text>
</div>
<div className="divide-neutral-10 divide-y">
<div className="divide-y divide-neutral-10">
{issues.map(issue => (
<Link
key={issue.id}
href={`https://github.com/status-im/status-react/issues/${issue.id}`}
className="hover:bg-neutral-5 flex items-center justify-between px-4 py-3 transition-colors duration-200"
className="flex items-center justify-between px-4 py-3 transition-colors duration-200 hover:bg-neutral-5"
>
<div className="flex flex-col">
<Text size={15} weight="medium">

View File

@ -0,0 +1,9 @@
import { z } from 'zod'
export const envSchema = z.object({
NEXT_PUBLIC_GHOST_API_KEY: z.string(),
})
export const clientEnv = envSchema.parse({
NEXT_PUBLIC_GHOST_API_KEY: process.env.NEXT_PUBLIC_GHOST_API_KEY,
})

View File

@ -1,8 +0,0 @@
import { z } from 'zod'
export const envSchema = z.object({
INFURA_API_KEY: z.string(),
TAMAGUI_TARGET: z.literal('web'),
})
export const env = envSchema.parse(process.env)

View File

@ -0,0 +1,15 @@
import { z } from 'zod'
if (typeof window !== 'undefined') {
throw new Error(
'❌ Attempted to access a server-side environment variable on the client'
)
}
export const envSchema = z.object({
INFURA_API_KEY: z.string(),
TAMAGUI_TARGET: z.literal('web'),
NEXT_PUBLIC_GHOST_API_KEY: z.string(),
})
export const serverEnv = envSchema.parse(process.env)

View File

@ -15,21 +15,23 @@ import type { PageLayout } from 'next'
export const AppLayout: PageLayout = page => {
return (
<>
<NavMenu />
<div className="hidden lg:block">
<NavMenu />
</div>
<div className="min-h-full bg-neutral-100">
<NavigationMenu.Root>
<div className="flex items-center px-6">
<div className="flex items-center px-6 py-3">
<div className="mr-5">
<Link href="/">
<Logo />
</Link>
</div>
<div className="flex-1">
<div className="hidden flex-1 lg:flex">
<NavigationMenu.List className="flex items-center">
{Object.entries(LINKS).map(([name, links]) => (
<NavigationMenu.Item key={name}>
<NavigationMenu.Trigger className="py-4 pr-5 aria-expanded:opacity-50">
<NavigationMenu.Trigger className="pr-5 aria-expanded:opacity-50">
<Text size={15} weight="medium" color="$white-100">
{name}
</Text>
@ -64,7 +66,7 @@ export const AppLayout: PageLayout = page => {
</NavigationMenu.List>
</div>
<div className="flex justify-end">
<div className="hidden justify-end lg:flex">
<Button
size={32}
variant="darkGrey"
@ -76,8 +78,8 @@ export const AppLayout: PageLayout = page => {
</div>
<NavigationMenu.Viewport
className={cx([
'data-[state=open]:animate-heightIn data-[state=closed]:animate-heightOut',
'transition-height h-[var(--radix-navigation-menu-viewport-height)]',
'data-[state=closed]:animate-heightOut data-[state=open]:animate-heightIn',
'h-[var(--radix-navigation-menu-viewport-height)] transition-height',
// 'data-[state=open]:animate-heightIn animate-',
// 'data-[state=closed]:animate-heightOut',
// 'transition-height h-[var(--radix-navigation-menu-viewport-height)]',

View File

@ -83,7 +83,7 @@ const MENU_LINKS = [
export const InsightsLayout: PageLayout = page => {
return AppLayout(
<div className="bg-white-100 relative mx-1 flex min-h-[calc(100vh-56px-4px)] rounded-3xl">
<div className="relative mx-1 flex min-h-[calc(100vh-56px-4px)] rounded-3xl bg-white-100">
<SideBar data={MENU_LINKS} />
<main className="flex-1">{page}</main>
</div>

View File

@ -1,13 +1,13 @@
import { EthereumClient } from '@status-im/js'
import { env } from '@/config/env.mjs'
import { serverEnv } from '@/config/env.server.mjs'
let client: EthereumClient | undefined
export function getEthereumClient(): EthereumClient | undefined {
if (!client) {
client = new EthereumClient(
`https://mainnet.infura.io/v3/${env.INFURA_API_KEY}`
`https://mainnet.infura.io/v3/${serverEnv.INFURA_API_KEY}`
)
return client

View File

@ -0,0 +1,98 @@
import GhostContentAPI from '@tryghost/content-api'
import { clientEnv } from '@/config/env.client.mjs'
/** @see https://ghost.org/docs/content-api# */
const ghost = new GhostContentAPI({
url: 'https://our.status.im',
key: clientEnv.NEXT_PUBLIC_GHOST_API_KEY,
version: 'v5.0',
})
type Params = { page?: number; limit?: number; tag?: string }
export const getPosts = async (params: Params = {}) => {
const { page = 1, limit = 7, tag } = params
const response = await ghost.posts.browse({
include: ['tags', 'authors'],
order: 'published_at DESC',
limit,
page,
...(tag && { filter: `tag:${tag}` }),
filter: 'visibility:public',
})
return { posts: [...response], meta: response.meta }
}
export const getPostBySlug = async (slug: string) => {
return await ghost.posts.read(
{ slug },
{
include: ['tags', 'authors'],
}
)
}
export const getPostsByTagSlug = async (slug: string, page = 1) => {
const response = await ghost.posts.browse({
filter: `tag:${slug}+visibility:public`,
include: ['tags', 'authors'],
limit: 6,
order: 'published_at DESC',
page,
})
return { posts: [...response], meta: response.meta }
}
export const getPostsByAuthorSlug = async (slug: string, page = 1) => {
const response = await ghost.posts.browse({
filter: `author:${slug}+visibility:public`,
include: ['tags', 'authors'],
limit: 6,
order: 'published_at DESC',
page,
})
return { posts: [...response], meta: response.meta }
}
export const getPostSlugs = async (): Promise<string[]> => {
const posts = await ghost.posts.browse({
limit: '7',
fields: 'slug',
filter: 'visibility:public',
})
return posts.map(post => post.slug)
}
export const getTags = async () => {
return await ghost.tags.browse({
limit: 'all',
fields: 'name,slug',
filter: 'visibility:public',
})
}
export const getTagSlugs = async (): Promise<string[]> => {
const tags = await ghost.tags.browse({
limit: 'all',
fields: 'slug',
filter: 'visibility:public',
})
return tags.map(tag => tag.slug)
}
export const getAuthorSlugs = async (): Promise<string[]> => {
const authors = await ghost.authors.browse({
limit: 'all',
fields: 'slug',
filter: 'visibility:public',
})
return authors.map(author => author.slug)
}

View File

@ -1,11 +1,249 @@
import { createElement, Fragment, useMemo } from 'react'
import { Avatar, Provider, Tag, Text } from '@status-im/components'
import { renderToString } from 'react-dom/server'
import rehypeParse from 'rehype-parse'
import rehypeReact from 'rehype-react'
import { unified } from 'unified'
import { Breadcrumbs } from '@/components'
import { formatDate } from '@/components/chart/utils/format-time'
import { AppLayout } from '@/layouts/app-layout'
import { getPostBySlug, getPostsByTagSlug, getPostSlugs } from '@/lib/ghost'
import type { Page } from 'next'
import { PostCard } from '.'
import type { PostOrPage } from '@tryghost/content-api'
import type {
GetStaticPaths,
GetStaticProps,
InferGetStaticPropsType,
Page,
} from 'next'
import type React from 'react'
type Params = { slug: string }
export const getStaticPaths: GetStaticPaths<Params> = async () => {
const slugs = await getPostSlugs()
return {
paths: slugs.map(slug => ({ params: { slug } })),
fallback: 'blocking',
}
}
export const getStaticProps: GetStaticProps<
{
post: PostOrPage
relatedPosts: PostOrPage[]
},
Params
> = async context => {
const post = await getPostBySlug(context.params!.slug)
if (!post) {
return {
// notFound: true,
redirect: { destination: '/blog', permanent: false },
}
}
let relatedPosts: PostOrPage[] = []
if (post.primary_tag) {
const { posts } = await getPostsByTagSlug(post.primary_tag.slug)
const filteredPosts = posts.filter(p => p.slug !== post.slug).slice(0, 4)
relatedPosts = filteredPosts
}
const r = unified()
.use(rehypeParse, { fragment: true })
.use(rehypeReact, {
createElement,
Fragment,
components: {
a: (props: React.ComponentProps<'a'>) => (
<a {...props}>
<Text size={19} weight="regular" color="$blue-50">
{props.children!}
</Text>
</a>
),
p: ({ children }: React.ComponentProps<'p'>) => (
<p className="">
<Text size={19} weight="regular">
{children}
</Text>
</p>
),
img: (props: React.ComponentProps<'img'>) => (
<img {...props} className="rounded-[20px]" /> // eslint-disable-line jsx-a11y/alt-text
),
h2: ({ children, ...rest }: React.ComponentProps<'h2'>) => (
<h2 {...rest}>
<Text size={27} weight="semibold">
{children}
</Text>
</h2>
),
ul: (props: React.ComponentProps<'ul'>) => (
<ul {...props} className="list-inside list-disc" />
),
pre: (props: React.ComponentProps<'pre'>) => (
<pre {...props} className="overflow-scroll" />
),
},
})
.processSync(post.html!).result
const html = renderToString(<Provider>{r}</Provider>)
return {
props: {
post: {
...post,
// fixme?: name html
hhh: html,
},
relatedPosts,
},
}
}
type Props = InferGetStaticPropsType<typeof getStaticProps>
const BlogDetailPage: Page<Props> = ({ post, relatedPosts }) => {
const author = post.primary_author!
// todo?: MOVE TO SERVER SIDE
const result = useMemo(() => {
return unified()
.use(rehypeParse, { fragment: true })
.use(rehypeReact, {
createElement,
Fragment,
components: {
a: (props: React.ComponentProps<'a'>) => (
<a {...props}>
<Text size={19} weight="regular" color="$blue-50">
{props.children!}
</Text>
</a>
),
p: ({ children }: React.ComponentProps<'p'>) => (
<p className="">
<Text size={19} weight="regular">
{children}
</Text>
</p>
),
img: (props: React.ComponentProps<'img'>) => (
<img {...props} className="rounded-[20px]" /> // eslint-disable-line jsx-a11y/alt-text
),
h2: ({ children, ...rest }: React.ComponentProps<'h2'>) => (
<h2 {...rest}>
<Text size={27} weight="semibold">
{children}
</Text>
</h2>
),
ul: (props: React.ComponentProps<'ul'>) => (
<ul {...props} className="list-inside list-disc" />
),
pre: (props: React.ComponentProps<'pre'>) => (
<pre {...props} className="overflow-scroll" />
),
},
})
.processSync(post.html!).result
}, [post.html])
const BlogDetailPage: Page = () => {
return (
<div>
<h1>Blog</h1>
<div className="min-h-[900px] rounded-3xl bg-white-100 lg:mx-1">
<div className="border-b border-neutral-10 px-5 py-[13px]">
<Breadcrumbs cutFirstSegment={false} />
</div>
<div className="mx-auto flex max-w-2xl flex-col gap-3 px-5 pb-6 pt-12 lg:pt-20">
<div className="flex">
{post.primary_tag && (
<Tag size={32} label={post.primary_tag!.name!} />
)}
</div>
<h1 className="text-[40px] font-bold leading-[44px] tracking-[-.02em] lg:text-[64px] lg:leading-[68px]">
{post.title!}
</h1>
<div className="mt-auto flex h-5 items-center gap-1">
<Avatar
type="user"
size={20}
name={author.name ?? author.slug}
src={author.profile_image ?? undefined}
/>
<Text size={15} weight="semibold">
{author.name ?? author.slug}
</Text>
<Text size={15} color="$neutral-50">
on {formatDate(new Date(post.published_at!))}
</Text>
</div>
</div>
<div className="w-full px-2 py-4 lg:px-6 lg:py-10">
<img
src={post.feature_image!}
className="aspect-[374/182] h-full w-full rounded-[20px] object-cover lg:aspect-[1456/470]"
alt={post.feature_image_alt!}
/>
</div>
<div className="mx-auto flex max-w-2xl flex-col gap-12 px-5 py-6">
{result}
</div>
<div className="mx-auto flex max-w-2xl flex-col gap-[17px] px-5 py-6">
<div className="flex flex-row items-center gap-2">
<Avatar
type="user"
size={32}
name={author.name ?? author.slug}
src={author.profile_image ?? undefined}
/>
<div className="flex flex-col">
<Text size={15} weight="semibold">
{author.name ?? author.slug}
</Text>
<Text size={13} color="$neutral-50">
{author.meta_description}
</Text>
</div>
</div>
</div>
{relatedPosts.length && (
<div className="border-t border-neutral-10 bg-neutral-5 px-5 pb-[64px] pt-12 lg:px-10">
<div className="mb-6">
<Text size={27} weight="semibold">
Related articles
</Text>
</div>
{/* <div className="grid auto-rows-[1fr] grid-cols-[repeat(auto-fill,minmax(350px,1fr))] gap-5"> */}
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
{relatedPosts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
</div>
)}
{/* <div
className="mx-auto grid max-w-2xl gap-4"
dangerouslySetInnerHTML={{ __html: post.hhh }}
/> */}
</div>
)
}

View File

@ -0,0 +1,137 @@
import { Avatar, Button, Text } from '@status-im/components'
import { useInfiniteQuery } from '@tanstack/react-query'
// import { redirect } from 'next/navigation'
// import { useRouter } from 'next/router'
import { Breadcrumbs } from '@/components'
import { AppLayout } from '@/layouts/app-layout'
import { getAuthorSlugs, getPostsByAuthorSlug } from '@/lib/ghost'
import { PostCard } from '..'
import type { PostOrPage, PostsOrPages } from '@tryghost/content-api'
import type {
GetStaticPaths,
GetStaticProps,
InferGetStaticPropsType,
Page,
} from 'next'
type Params = { slug: string }
export const getStaticPaths: GetStaticPaths<Params> = async () => {
const slugs = await getAuthorSlugs()
return {
paths: slugs.map(slug => ({ params: { slug } })),
fallback: false,
}
}
export const getStaticProps: GetStaticProps<
{
posts: PostOrPage[]
meta: PostsOrPages['meta']
},
Params
> = async context => {
const { posts, meta } = await getPostsByAuthorSlug(context.params!.slug)
if (!posts || !posts.length) {
return {
notFound: true,
// redirect: { destination: '/blog', permanent: false },
}
}
return {
props: {
posts,
meta,
},
}
}
type Props = InferGetStaticPropsType<typeof getStaticProps>
const BlogAuthorPage: Page<Props> = ({ posts, meta }) => {
// const { isFallback } = useRouter()
// if (isFallback || !posts.length) {
// redirect('/blog')
// }
const author = posts[0].primary_author!
const {
data,
// error,
fetchNextPage,
hasNextPage,
// isFetching,
// isFetchingNextPage,
// status,
// isFetched,
} = useInfiniteQuery({
queryKey: ['posts', author.slug],
queryFn: async ({ pageParam: page, queryKey }) => {
const [, tag] = queryKey
const { posts, meta } = await getPostsByAuthorSlug(tag, page)
return { posts, meta }
},
getNextPageParam: ({ meta }) => meta.pagination.next,
initialData: { pages: [{ posts, meta }], pageParams: [1] },
staleTime: Infinity,
})
if (!data) {
return null
}
const allPosts = data.pages.flatMap(page => page.posts)
return (
<div className="min-h-[900px] rounded-3xl bg-white-100 lg:mx-1">
<div className="border-b border-neutral-10 px-5 py-[13px]">
<Breadcrumbs cutFirstSegment={false} />
</div>
<div className="px-5">
<div className="mx-auto max-w-[1184px] pb-24 pt-12 lg:pb-32 lg:pt-20">
<div className="mb-12 grid gap-2">
<Avatar
type="user"
size={56}
name={author.name ?? author.slug}
src={author.profile_image ?? undefined}
/>
<h1 className="text-[40px] font-bold leading-[44px] tracking-[-.02em] lg:text-[64px] lg:leading-[68px]">
{author.name}
</h1>
<Text size={19}>{author.meta_description}</Text>
</div>
<div className="grid auto-rows-[1fr] grid-cols-[repeat(auto-fill,minmax(350px,1fr))] gap-5">
{allPosts.map(post => (
<PostCard key={post.id} post={post} showAuthor={false} />
))}
</div>
{hasNextPage && (
<div className="mt-8 flex justify-center">
<Button variant="outline" onPress={() => fetchNextPage()}>
Load more posts
</Button>
</div>
)}
</div>
</div>
</div>
)
}
BlogAuthorPage.getLayout = AppLayout
export default BlogAuthorPage

View File

@ -1,64 +1,223 @@
import { Button, Shadow, Tag, Text } from '@status-im/components'
import { useMemo } from 'react'
import { Avatar, Button, Shadow, Tag, Text } from '@status-im/components'
import { useInfiniteQuery } from '@tanstack/react-query'
// import Image from 'next/image'
import { formatDate } from '@/components/chart/utils/format-time'
import { Link } from '@/components/link'
import { AppLayout } from '@/layouts/app-layout'
import { getPosts } from '@/lib/ghost'
import type { Page } from 'next'
import type { PostOrPage, PostsOrPages } from '@tryghost/content-api'
import type { GetStaticProps, InferGetStaticPropsType, Page } from 'next'
export const getStaticProps: GetStaticProps<{
posts: PostOrPage[]
meta: PostsOrPages['meta']
}> = async () => {
const { posts, meta } = await getPosts()
return {
props: {
posts,
meta,
},
}
}
type Props = InferGetStaticPropsType<typeof getStaticProps>
const BlogPage: Page<Props> = props => {
const { posts, meta } = props
const {
data,
// error,
fetchNextPage,
hasNextPage,
// isFetching,
// isFetchingNextPage,
// status,
// isFetched,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam: page }) => await getPosts({ page }),
getNextPageParam: ({ meta }) => meta.pagination.next,
initialData: { pages: [{ posts, meta }], pageParams: [1] },
staleTime: Infinity,
})
const { highlightedPost, visiblePosts } = useMemo(() => {
const [highlightedPost, ...posts] = data!.pages.flatMap(page => page.posts)
const maxLength = posts.length - (posts.length % 3) // the number of posts should be divisible by 3
const visiblePosts = posts.slice(0, maxLength)
return { highlightedPost, visiblePosts }
}, [data])
const BlogPage: Page = () => {
return (
<div className="bg-white-100 mx-1 min-h-[900px] rounded-3xl">
<div className="mx-auto max-w-[1184px] py-32">
<div className="grid gap-2">
<h1 className="text-7xl font-bold">Blog</h1>
<Text size={19}>Long form articles, thoughts, and ideas.</Text>
</div>
<div>
<div className="mt-16 grid grid-cols-3 gap-5">
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map(v => (
<PostCard key={v} />
))}
<div className="min-h-[900px] rounded-3xl bg-white-100 lg:mx-1">
<div className="px-5">
<div className="mx-auto max-w-[1184px] pb-24 pt-12 lg:pb-32 lg:pt-20">
<div className="mb-10 grid gap-2">
<h1 className="text-[40px] font-bold leading-[44px] tracking-[-.02em] lg:text-[64px] lg:leading-[68px]">
Blog.
</h1>
<Text size={19}>Long form articles, thoughts, and ideas.</Text>
</div>
</div>
<div className="flex justify-center pt-8">
<Button variant="outline">Load more posts</Button>
<div>
<div className="mb-[44px] xl:mb-12">
<HighlightedPostCard post={highlightedPost} />
</div>
<div className="grid auto-rows-[1fr] grid-cols-[repeat(auto-fill,minmax(350px,1fr))] gap-5">
{visiblePosts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
</div>
{hasNextPage && (
<div className="mt-8 flex justify-center">
<Button variant="outline" onPress={() => fetchNextPage()}>
Load more posts
</Button>
</div>
)}
</div>
</div>
</div>
)
}
const PostCard = () => {
type HighlightedPostCardProps = {
post: PostOrPage
}
export const HighlightedPostCard = (props: HighlightedPostCardProps) => {
const { post } = props
const author = post.primary_author!
return (
<Shadow className="border-neutral-5 rounded-[20px] border">
<div className="flex flex-col gap-2 p-4">
<div className="self-start">
<Tag size={24} label="Updates" />
</div>
<Text size={19} weight="semibold">
Long form articles, thoughts, and ideas.
</Text>
<div className="flex gap-1">
<Text size={15} weight="semibold">
Status
</Text>
<Text size={15} color="$neutral-50">
on Jul 12, 2022
</Text>
</div>
</div>
<div className="px-2 pb-2">
{/* eslint-disable-next-line jsx-a11y/alt-text */}
// <Link href={`/blog/${post.slug}`} className="flex flex-row-reverse gap-7">
<Link
href={`/blog/${post.slug}`}
className="grid grid-cols-1 gap-5 xl:grid-cols-3 xl:gap-7"
>
<div className="col-span-2 w-full flex-[2] shrink-0">
<img
className="rounded-2xl"
style={{
aspectRatio: '366/206',
objectFit: 'cover',
}}
src="https://images.unsplash.com/photo-1683053243792-28e9d984c25a?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1780&q=80"
className="aspect-[366/206] h-full w-full rounded-2xl object-cover"
src={post.feature_image!}
alt={post.feature_image_alt!}
/>
</div>
<div className="flex flex-[1] flex-col gap-2 xl:py-5 xl:pr-5">
<div className="h-6 overflow-hidden">
{post.primary_tag && (
<div className="flex">
<Tag size={24} label={post.primary_tag.name!} />
</div>
)}
</div>
<div>
<span className="text-[27px] font-semibold leading-[32px] tracking-[-.021em] lg:text-[40px] lg:font-bold lg:leading-[44px] lg:tracking-[-.02em]">
{post.title}
</span>
</div>
<div>
<Text size={19} weight="regular">
{post.excerpt}
</Text>
</div>
<div className="mt-auto flex h-5 items-center gap-1">
<Avatar
type="user"
size={20}
name={author.name ?? author.slug}
src={author.profile_image ?? undefined}
/>
<Text size={15} weight="semibold">
{author.name ?? author.slug}
</Text>
<Text size={15} color="$neutral-50">
on {formatDate(new Date(post.published_at!))}
</Text>
</div>
</div>
</Link>
)
}
type PostCardProps = {
post: PostOrPage
showTag?: boolean
showAuthor?: boolean
}
export const PostCard = (props: PostCardProps) => {
const { post, showTag = true, showAuthor = true } = props
const author = post.primary_author!
return (
<Shadow className="h-full rounded-[20px]">
<Link
href={`/blog/${post.slug}`}
className="flex h-full w-full flex-col rounded-[20px] border border-neutral-5 bg-white-100"
>
<div className="flex grow flex-col gap-2 p-4">
{showTag && (
<div className="h-6 overflow-hidden">
{post.primary_tag && (
<div className="flex">
<Tag size={24} label={post.primary_tag.name!} />
</div>
)}
</div>
)}
<div>
<Text size={19} weight="semibold">
{post.title}
</Text>
</div>
{showAuthor ? (
<div className="mt-auto flex h-5 gap-1">
<Avatar
type="user"
size={20}
name={author.name ?? author.slug}
src={author.profile_image ?? undefined}
/>
<Text size={15} weight="semibold">
{author.name ?? author.slug}
</Text>
<Text size={15} color="$neutral-50">
on {formatDate(new Date(post.published_at!))}
</Text>
</div>
) : (
<div className="mt-auto h-5">
<Text size={15} color="$neutral-50">
{formatDate(new Date(post.published_at!))}
</Text>
</div>
)}
</div>
<div className="w-full px-2 pb-2">
{/* eslint-disable-next-line jsx-a11y/alt-text */}
<img
className="aspect-[334/188] h-full w-full rounded-2xl object-cover"
src={post.feature_image!}
/>
</div>
</Link>
</Shadow>
)
}

View File

@ -0,0 +1,132 @@
import { Button, Text } from '@status-im/components'
import { useInfiniteQuery } from '@tanstack/react-query'
import { Breadcrumbs } from '@/components'
import { AppLayout } from '@/layouts/app-layout'
import { getPostsByTagSlug, getTagSlugs } from '@/lib/ghost'
import { PostCard } from '..'
import type { PostOrPage, PostsOrPages } from '@tryghost/content-api'
import type {
GetStaticPaths,
GetStaticProps,
InferGetStaticPropsType,
Page,
} from 'next'
type Params = { slug: string }
export const getStaticPaths: GetStaticPaths<Params> = async () => {
const slugs = await getTagSlugs()
return {
paths: slugs.map(slug => ({ params: { slug } })),
/** If fallback is false, then any paths not returned by getStaticPaths will result in a 404 page.
*
* @see https://nextjs.org/docs/pages/api-reference/functions/get-static-paths#fallback-false
*/
fallback: false,
}
}
export const getStaticProps: GetStaticProps<
{
posts: PostOrPage[]
meta: PostsOrPages['meta']
},
Params
> = async context => {
const { posts, meta } = await getPostsByTagSlug(context.params!.slug)
if (!posts || !posts.length) {
return {
notFound: true,
// redirect: { destination: '/blog', permanent: false },
}
}
return {
props: {
posts,
meta,
},
}
}
type Props = InferGetStaticPropsType<typeof getStaticProps>
const BlogTagPage: Page<Props> = ({ posts, meta }) => {
const tag = posts[0].primary_tag!
const {
data,
// error,
fetchNextPage,
hasNextPage,
// isFetching,
// isFetchingNextPage,
// status,
// isFetched,
} = useInfiniteQuery({
queryKey: ['posts', tag.slug],
queryFn: async ({ pageParam: page, queryKey }) => {
const [, tag] = queryKey
const { posts, meta } = await getPostsByTagSlug(tag, page)
return { posts, meta }
},
getNextPageParam: ({ meta }) => meta.pagination.next,
initialData: { pages: [{ posts, meta }], pageParams: [1] },
staleTime: Infinity,
})
if (!data) {
return null
}
const allPosts = data.pages.flatMap(page => page.posts)
return (
// layout 1 (showBreadcrumbs, showHighlightedPostCard, ?posts=renderPosts())
<div className="min-h-[900px] rounded-3xl bg-white-100 lg:mx-1">
{/* layout 2 */}
{/* breadcumbs */}
<div className="border-b border-neutral-10 px-5 py-[13px]">
<Breadcrumbs cutFirstSegment={false} />
</div>
<div className="px-5">
<div className="mx-auto max-w-[1184px] pb-24 pt-12 lg:pb-32 lg:pt-20">
{/* content */}
{/* note: diff mb than index.ts (mb-12 vs. mb-10) */}
<div className="mb-12 grid gap-2">
<h1 className="text-[40px] font-bold leading-[44px] tracking-[-.02em] lg:text-[64px] lg:leading-[68px]">
{tag.name}
</h1>
<Text size={19}>{tag.description}</Text>
</div>
<div className="grid auto-rows-[1fr] grid-cols-[repeat(auto-fill,minmax(350px,1fr))] gap-5">
{allPosts.map(post => (
<PostCard key={post.id} post={post} showTag={false} />
))}
</div>
{hasNextPage && (
<div className="mt-8 flex justify-center">
<Button variant="outline" onPress={() => fetchNextPage()}>
Load more posts
</Button>
</div>
)}
</div>
</div>
</div>
)
}
BlogTagPage.getLayout = AppLayout
export default BlogTagPage

View File

@ -8,7 +8,7 @@ import type { Page } from 'next'
const HomePage: Page = () => {
return (
<>
<div className="bg-white-100 mx-1 rounded-3xl py-32">
<div className="mx-1 rounded-3xl bg-white-100 py-32">
<div className="mx-40 mb-40 grid gap-8">
<div className="grid gap-6">
<h1 className="text-7xl font-bold">
@ -44,7 +44,7 @@ const HomePage: Page = () => {
</div>
<div className="text-center">
<h2 className="text-white-100 text-4xl">
<h2 className="text-4xl text-white-100">
Own a community? Time to take back control!
</h2>
<Text size={19} color="$white-100">
@ -52,7 +52,7 @@ const HomePage: Page = () => {
</Text>
</div>
<div className="bg-white-100 mx-1 space-y-[200px] rounded-3xl py-32">
<div className="mx-1 space-y-[200px] rounded-3xl bg-white-100 py-32">
<FeatureSection
title={`Chat privately\nwith friends`}
description="Protect your right to free speech with e2e encryption & metadata privacy."
@ -101,22 +101,22 @@ const FeatureSection = ({ title, description }: FeatureSectionProps) => {
const FeatureGrid = () => {
return (
<div className="grid h-[800px] grid-cols-3 grid-rows-2 gap-5">
<div className="border-neutral-80/5 row-span-2 rounded-[32px] border">
<div className="row-span-2 rounded-[32px] border border-neutral-80/5">
<Text size={27} weight="semibold">
Title
</Text>
</div>
<div className="border-neutral-80/5 rounded-[32px] border">
<div className="rounded-[32px] border border-neutral-80/5">
<Text size={27} weight="semibold">
Title
</Text>
</div>
<div className="border-neutral-80/5 row-span-2 rounded-[32px] border">
<div className="row-span-2 rounded-[32px] border border-neutral-80/5">
<Text size={27} weight="semibold">
Title
</Text>
</div>
<div className="border-neutral-80/5 rounded-[32px] border">
<div className="rounded-[32px] border border-neutral-80/5">
<Text size={27} weight="semibold">
Title
</Text>

View File

@ -8,7 +8,7 @@ import type { Page } from 'next'
const EpicsDetailPage: Page = () => {
return (
<div>
<div className="border-neutral-10 border-b px-5 py-3">
<div className="border-b border-neutral-10 px-5 py-3">
<Breadcrumbs />
</div>
<div className="px-10 py-6">
@ -18,7 +18,7 @@ const EpicsDetailPage: Page = () => {
fullscreen
/>
<div role="separator" className="bg-neutral-10 -mx-6 my-6 h-px" />
<div role="separator" className="-mx-6 my-6 h-px bg-neutral-10" />
<TableIssues />
</div>

View File

@ -58,7 +58,7 @@ const ReposPage: Page = () => {
<Link
key={repo.name}
href={`https://github.com/status-im/${repo.name}`}
className="border-neutral-10 hover:border-neutral-40 flex h-[124px] flex-col rounded-2xl border px-4 py-3 transition-colors duration-200"
className="flex h-[124px] flex-col rounded-2xl border border-neutral-10 px-4 py-3 transition-colors duration-200 hover:border-neutral-40"
>
<Text size={15} weight="semibold">
{repo.name}

View File

@ -8,7 +8,7 @@ import type { Page } from 'next'
const WorkstreamDetailPage: Page = () => {
return (
<div>
<div className="border-neutral-10 border-b px-5 py-3">
<div className="border-b border-neutral-10 px-5 py-3">
<Breadcrumbs />
</div>
<div className="px-10 py-6">
@ -18,7 +18,7 @@ const WorkstreamDetailPage: Page = () => {
fullscreen
/>
<div role="separator" className="bg-neutral-10 -mx-6 my-6 h-px" />
<div role="separator" className="-mx-6 my-6 h-px bg-neutral-10" />
<TableIssues />
</div>

View File

@ -2,12 +2,6 @@
@tailwind components;
@tailwind utilities;
@layer base {
body {
@apply bg-neutral-100;
}
}
@keyframes gradient {
0% {
background-position: 0% 50%;

View File

@ -14,7 +14,7 @@ module.exports = {
fontFamily: {
sans: ['var(--font-inter)', ...fontFamily.sans],
},
colors,
colors: colors,
// use <Text /> from @status-im/components or arbitrary values
// fontSize: {},

View File

@ -3,6 +3,7 @@
"private": true,
"workspaces": {
"packages": [
"packages/eslint-config*",
"packages/status-js",
"packages/colors",
"packages/icons",
@ -17,7 +18,7 @@
"test": "turbo run test --filter=@status-im/* -- --run",
"dev": "turbo run dev --filter=@status-im/* --parallel",
"build": "turbo run build --filter=@status-im/*",
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint/.eslint-cache .",
"lint": "turbo run lint --filter=@status-im/* --filter=website --filter=web",
"typecheck": "turbo run typecheck",
"format": "prettier --ignore-path .gitignore --write .",
"clean": "turbo run clean && rimraf node_modules",
@ -35,24 +36,12 @@
"@changesets/cli": "^2.23.0",
"@tsconfig/strictest": "^2.0.0",
"@types/prettier": "^2.7.2",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"eslint": "^8.37.0",
"eslint-config-next": "^13.2.4",
"eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-node": "^0.3.7",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"@status-im/eslint-config": "*",
"husky": "^8.0.3",
"lint-staged": "^13.2.0",
"patch-package": "^6.5.1",
"prettier": "^2.8.8",
"prettier-plugin-tailwindcss": "^0.2.8",
"prettier-plugin-tailwindcss": "0.3.0",
"rimraf": "^4.4.1",
"turbo": "^1.8.8",
"typescript": "^5.0.3",
@ -61,7 +50,7 @@
"vitest": "^0.29.8"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"*.{ts,tsx,js,jsx,mjs}": [
"eslint",
"prettier --write"
],

View File

@ -0,0 +1,4 @@
{
"root": true,
"extends": ["@status-im/eslint-config"]
}

View File

@ -35,6 +35,7 @@
"devDependencies": {
"@clack/prompts": "^0.6.3",
"colorjs.io": "^0.4.3",
"@status-im/eslint-config": "*",
"figma-api": "^1.11.0",
"fs-extra": "^11.1.1",
"vite": "^4.1.4",

View File

@ -0,0 +1,4 @@
{
"root": true,
"extends": ["@status-im/eslint-config"]
}

View File

@ -63,6 +63,7 @@
"@storybook/testing-library": "^0.1.0",
"@tamagui/vite-plugin": "1.11.1",
"@vitejs/plugin-react-swc": "^3.2.0",
"@status-im/eslint-config": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-native-svg": "^13.8.0",

View File

@ -84,13 +84,13 @@ export const tokens = createTokens({
'turquoise-50-opa-20': 'hsla(193, 41%, 45%, 0.2)',
'turquoise-50-opa-30': 'hsla(193, 41%, 45%, 0.3)',
'turquoise-50-opa-40': 'hsla(193, 41%, 45%, 0.4)',
'blue-50': 'hsla(202, 84%, 62%, 1)',
'blue-60': 'hsla(202, 56%, 52%, 1)',
'blue-50-opa-5': 'hsla(202, 84%, 62%, 0.05)',
'blue-50-opa-10': 'hsla(202, 84%, 62%, 0.1)',
'blue-50-opa-20': 'hsla(202, 84%, 62%, 0.2)',
'blue-50-opa-30': 'hsla(202, 84%, 62%, 0.3)',
'blue-50-opa-40': 'hsla(202, 84%, 62%, 0.4)',
'blue-50': 'hsla(231, 91%, 56%, 1)',
'blue-60': 'hsla(231, 70%, 45%, 1)',
'blue-50-opa-5': 'hsla(231, 91%, 56%, 0.05)',
'blue-50-opa-10': 'hsla(231, 91%, 56%, 0.1)',
'blue-50-opa-20': 'hsla(231, 91%, 56%, 0.2)',
'blue-50-opa-30': 'hsla(231, 91%, 56%, 0.3)',
'blue-50-opa-40': 'hsla(231, 91%, 56%, 0.4)',
'green-50': 'hsla(151, 53%, 58%, 1)',
'green-60': 'hsla(151, 38%, 48%, 1)',
'green-50-opa-5': 'hsla(151, 53%, 58%, 0.05)',

View File

@ -0,0 +1,104 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
// TODO: Enable type-aware linting (https://typescript-eslint.io/docs/linting/type-linting)
// "tsconfigRootDir": ".",
// "project": ["./packages/*/tsconfig.json"],
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
warnOnUnsupportedTypeScriptVersion: true,
},
env: {
browser: true,
node: true,
},
plugins: [
'@typescript-eslint',
'import',
'simple-import-sort',
'react',
'jsx-a11y',
],
extends: [
'plugin:@typescript-eslint/recommended',
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
// "plugin:@typescript-eslint/recommended-requiring-type-checking",
'plugin:eslint-comments/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:jsx-a11y/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:react/jsx-runtime',
'prettier',
],
overrides: [
{
files: ['examples/**/*.tsx'],
rules: {
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
},
},
],
rules: {
'react/prop-types': 0,
// "@typescript-eslint/consistent-type-definitions": ["error", "interface"],
'@typescript-eslint/consistent-type-imports': 'error',
// TODO: turn on this rul
'@typescript-eslint/no-non-null-assertion': 'off',
// "@typescript-eslint/consistent-type-exports": "error",
'simple-import-sort/imports': [
'error',
{
groups: [
// Side effect imports.
['^\\u0000'],
// `react` related packages come first.
['^react$', '^react-dom$'],
// Things that start with a letter (or digit or underscore), or `@` followed by a letter.
['^@?\\w'],
// Absolute imports and other imports such as Vue-style `@/foo`.
// Anything not matched in another group.
['^'],
// Relative imports.
// Anything that starts with a dot.
['^\\.'],
// type imports last as a separate group
['^.+\\u0000$'],
],
},
],
'simple-import-sort/exports': 'error',
'import/first': 'error',
'import/newline-after-import': 'error',
'import/no-duplicates': 'error',
},
settings: {
react: {
version: 'detect',
},
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
project: [
'tsconfig.base.json',
'packages/*/tsconfig.json',
'apps/*/tsconfig.json',
],
},
typescript: {
alwaysTryTypes: true,
project: [
'tsconfig.base.json',
'packages/*/tsconfig.json',
'apps/*/tsconfig.json',
],
},
},
'import/ignore': ['react-native'],
},
}

View File

@ -0,0 +1,31 @@
{
"version": "0.1.0",
"name": "@status-im/eslint-config",
"main": "index.js",
"exports": {
".": {
"import": "./index.js",
"require": "./index.js"
},
"./package.json": "./package.json"
},
"scripts": {
"clean": "rimraf node_modules"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.59.11",
"@typescript-eslint/parser": "^5.59.11",
"eslint": "^8.42.0",
"eslint-config-next": "^13.2.4",
"eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-node": "^0.3.7",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-tailwindcss": "^3.12.1"
}
}

4
packages/icons/.eslintrc Normal file
View File

@ -0,0 +1,4 @@
{
"root": true,
"extends": ["@status-im/eslint-config"]
}

View File

@ -46,6 +46,7 @@
"@svgr/plugin-prettier": "^7.0.0",
"@svgr/plugin-svgo": "^7.0.0",
"@types/fs-extra": "^11.0.1",
"@status-im/eslint-config": "*",
"figma-api": "^1.11.0",
"fs-extra": "^11.1.1",
"svgo": "^3.0.2",

View File

@ -0,0 +1,13 @@
{
"root": true,
"extends": ["@status-im/eslint-config"],
"overrides": [
{
"files": ["./src/protos/**/*_pb.ts"],
"rules": {
"eslint-comments/disable-enable-pair": "off",
"eslint-comments/no-unlimited-disable": "off"
}
}
]
}

View File

@ -55,6 +55,7 @@
},
"devDependencies": {
"@bufbuild/protoc-gen-es": "^1.0.0",
"@status-im/eslint-config": "*",
"happy-dom": "^9.1.7"
},
"files": [

View File

@ -0,0 +1,4 @@
{
"root": true,
"extends": ["@status-im/eslint-config"]
}

View File

@ -63,6 +63,7 @@
"@types/node": "^16.9.6",
"@types/react": "^18.0.28",
"@vitejs/plugin-react": "^1.3.2",
"@status-im/eslint-config": "*",
"happy-dom": "^5.3.1"
},
"peerDependencies": {

632
yarn.lock

File diff suppressed because it is too large Load Diff