├── .nvmrc ├── .npmrc ├── packages ├── cli │ ├── src │ │ ├── pages │ │ │ ├── index.ts │ │ │ ├── Country.tsx │ │ │ ├── Product │ │ │ │ ├── Model │ │ │ │ │ ├── DateView.tsx │ │ │ │ │ ├── SizeItem.tsx │ │ │ │ │ └── Model.tsx │ │ │ │ └── Product.tsx │ │ │ ├── Pages.tsx │ │ │ └── Home.tsx │ │ ├── layout │ │ │ ├── index.ts │ │ │ ├── Layout.tsx │ │ │ ├── Footer.tsx │ │ │ └── Header.tsx │ │ ├── utils │ │ │ ├── clearOutput.ts │ │ │ ├── theme.ts │ │ │ ├── useIsTooShortHeight.ts │ │ │ ├── inputDictionary.ts │ │ │ ├── useScreenSize.ts │ │ │ ├── logger.ts │ │ │ └── isEmpty.ts │ │ ├── devEntry.ts │ │ ├── index.tsx │ │ ├── components │ │ │ ├── Select │ │ │ │ ├── SelectItem.tsx │ │ │ │ ├── SelectIndicator.tsx │ │ │ │ └── Select.tsx │ │ │ ├── ChangeSizeScreen.tsx │ │ │ └── Image.tsx │ │ ├── hooks │ │ │ └── useInputProcess.ts │ │ ├── router.ts │ │ └── store │ │ │ ├── country.ts │ │ │ ├── country.test.ts │ │ │ └── product.ts │ ├── tsconfig.json │ ├── sea-config.json │ ├── rspack.config.js │ ├── scripts │ │ ├── pack-sea.ps1 │ │ └── pack-sea.sh │ └── package.json └── sdk │ ├── tsconfig.json │ ├── mocks │ ├── handlers │ │ ├── handlers.ts │ │ └── mockProductFeed │ │ │ └── mockProductFeed.ts │ └── node.ts │ ├── CHANGELOG.md │ ├── index.ts │ ├── models │ ├── snkrsRootResponse.ts │ └── availableCountries.ts │ ├── utils │ ├── Error.ts │ ├── delay.ts │ ├── rest.ts │ ├── HttpError.ts │ ├── HttpError.test.ts │ ├── Error.test.ts │ ├── jsonRequest.ts │ ├── delay.test.ts │ └── jsonRequest.test.ts │ ├── productFeed │ ├── api.ts │ ├── url.ts │ ├── format.ts │ └── schema.ts │ ├── package.json │ └── scripts │ └── ci-check-product-feed.ts ├── .vscode ├── settings.json └── launch.json ├── .gitignore ├── .prettierignore ├── .changeset └── config.json ├── DEPENDENCIES.txt ├── vercel.json ├── .github ├── composite-actions │ ├── install │ │ └── action.yml │ └── ensure-release-package │ │ └── action.yml └── workflows │ ├── changesets-release-pr.yml │ ├── product-feed-health.yml │ ├── sdk-tarball-release.yml │ └── cli-sea-release.yml ├── .prettierrc.json ├── tsconfig.json ├── api └── upcoming-releases │ └── [countryCode].ts └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 24.11.0 -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | save-exact=true 3 | prefer-dedupe=true -------------------------------------------------------------------------------- /packages/cli/src/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { Pages } from './Pages.ts' 2 | -------------------------------------------------------------------------------- /packages/cli/src/layout/index.ts: -------------------------------------------------------------------------------- 1 | export { Layout } from './Layout.tsx' 2 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "terminal.integrated.enableImages": true 4 | } -------------------------------------------------------------------------------- /packages/cli/src/utils/clearOutput.ts: -------------------------------------------------------------------------------- 1 | export const clearOutput = () => { 2 | process.stdout.write('\u001b[3J\u001b[1J') 3 | console.clear() 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | .yalc 5 | yalc.lock 6 | 7 | #db 8 | *.db* 9 | .vercel 10 | .env*.local 11 | 12 | 13 | #logs 14 | cli*.log -------------------------------------------------------------------------------- /packages/sdk/mocks/handlers/handlers.ts: -------------------------------------------------------------------------------- 1 | import { mockProductFeed } from './mockProductFeed/mockProductFeed.ts' 2 | 3 | export const handlers = [...mockProductFeed] 4 | -------------------------------------------------------------------------------- /packages/cli/src/devEntry.ts: -------------------------------------------------------------------------------- 1 | import { mockServer } from '@nike-release-checker/sdk/mocks' 2 | 3 | mockServer.listen({ onUnhandledRequest: 'bypass' }) 4 | import('./index.tsx') 5 | -------------------------------------------------------------------------------- /packages/sdk/mocks/node.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node' 2 | 3 | import { handlers } from './handlers/handlers.ts' 4 | 5 | export const mockServer = setupServer(...handlers) 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | package-lock.json 4 | 5 | # build 6 | dist 7 | 8 | # code editors 9 | .idea 10 | .vscode 11 | 12 | # misc 13 | .yalc 14 | yalc.lock 15 | .DS_Store -------------------------------------------------------------------------------- /packages/cli/src/utils/theme.ts: -------------------------------------------------------------------------------- 1 | export const theme = { 2 | color: { 3 | snkrsRed: '#FF5C7D', 4 | }, 5 | sizes: { 6 | image: 5, // 5 for iTerm2, and 4 for VSCode due Image render bug, which adds extra empty lines to bottom 7 | fullHeight: 27, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /packages/sdk/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @nike-release-checker/sdk 2 | 3 | ## 0.2.0 4 | 5 | ### Minor Changes 6 | 7 | - Add new handlers entprypoint to sdk package 8 | 9 | ### Patch Changes 10 | 11 | - 9315319: Seed SDK release tracking with the first Changeset entry. 12 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json", 3 | "commit": false, 4 | "fixed": [], 5 | "linked": [], 6 | "access": "restricted", 7 | "baseBranch": "master", 8 | "ignore": ["@nike-release-checker/cli"] 9 | } 10 | -------------------------------------------------------------------------------- /DEPENDENCIES.txt: -------------------------------------------------------------------------------- 1 | Dependency notes 2 | ---------------- 3 | - Keep `ink-select-input` at 6.1.0. Do not bump to 6.2.0 or newer because 6.2.0 breaks number input processing. If a newer version is required, either propose a bugfix upstream or vendor the fixed source into this project. 4 | 5 | -------------------------------------------------------------------------------- /packages/cli/sea-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "./dist/bundle.cjs", 3 | "output": "./dist/sea-prep.blob", 4 | "disableExperimentalSEAWarning": true, 5 | "useSnapshot": false, 6 | "useCodeCache": true, 7 | "execArgv": ["--experimental-webstorage", "--localstorage-file=local.db"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/cli/src/utils/useIsTooShortHeight.ts: -------------------------------------------------------------------------------- 1 | import { theme } from './theme.ts' 2 | import { useScreenSize } from './useScreenSize.ts' 3 | 4 | export const useIsTooShortHeight = (): boolean => { 5 | const { height } = useScreenSize() 6 | return height < theme.sizes.fullHeight 7 | } 8 | -------------------------------------------------------------------------------- /packages/cli/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { render } from 'ink' 2 | 3 | import { Layout } from './layout/index.ts' 4 | import { Pages } from './pages/Pages.tsx' 5 | import { clearOutput } from './utils/clearOutput.ts' 6 | 7 | clearOutput() 8 | render( 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "functions": { 4 | "api/**/*.ts": { 5 | "includeFiles": "packages/sdk/**" 6 | } 7 | }, 8 | "rewrites": [ 9 | { 10 | "source": "/api/upcoming-releases/:countryCode", 11 | "destination": "/api/upcoming-releases/[countryCode]" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/cli/src/utils/inputDictionary.ts: -------------------------------------------------------------------------------- 1 | export const inputDictionary = { 2 | HOME: { 3 | key: '1', 4 | routeName: 'home', 5 | url: '/', 6 | }, 7 | COUNTRY: { 8 | key: '2', 9 | routeName: 'selectCountry', 10 | url: '/select-country', 11 | }, 12 | PRODUCT: { 13 | key: '3', 14 | routeName: 'product', 15 | url: '/product', 16 | }, 17 | } as const 18 | -------------------------------------------------------------------------------- /packages/cli/src/components/Select/SelectItem.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'ink' 2 | 3 | import { theme } from '../../utils/theme.ts' 4 | 5 | export type SelectItemProps = { 6 | readonly isSelected?: boolean 7 | readonly label: string 8 | } 9 | 10 | export const SelectItem = ({ isSelected = false, label }: SelectItemProps) => { 11 | return {label} 12 | } 13 | -------------------------------------------------------------------------------- /packages/sdk/index.ts: -------------------------------------------------------------------------------- 1 | export { getProductFeed } from './productFeed/api.ts' 2 | export { formatProductFeedResponse } from './productFeed/format.ts' 3 | export type { ProductFeedUrlParams } from './productFeed/url.ts' 4 | export { availableCountries } from './models/availableCountries.ts' 5 | export type * from './models/availableCountries.ts' 6 | export type * from './productFeed/schema.ts' 7 | export type * from './models/snkrsRootResponse.ts' 8 | -------------------------------------------------------------------------------- /.github/composite-actions/install/action.yml: -------------------------------------------------------------------------------- 1 | name: Install workspace 2 | description: Checkout, set Node from .nvmrc, and install dependencies 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Use Node version from .nvmrc 7 | uses: actions/setup-node@v6 8 | with: 9 | node-version-file: ".nvmrc" 10 | cache: npm 11 | 12 | - name: Install dependencies 13 | run: npm ci 14 | shell: bash 15 | -------------------------------------------------------------------------------- /packages/sdk/mocks/handlers/mockProductFeed/mockProductFeed.ts: -------------------------------------------------------------------------------- 1 | import { delay, http, HttpResponse } from 'msw' 2 | 3 | import { mockGetProductFeedData } from './mockProductFeedData.ts' 4 | 5 | const mockGetProductFeed = http.get(`https://api.nike.com/product_feed/threads/v3/`, async () => { 6 | const body = mockGetProductFeedData 7 | await delay() 8 | return HttpResponse.json(body) 9 | }) 10 | 11 | export const mockProductFeed = [mockGetProductFeed] 12 | -------------------------------------------------------------------------------- /packages/cli/src/hooks/useInputProcess.ts: -------------------------------------------------------------------------------- 1 | import { useInput } from 'ink' 2 | 3 | import { $router } from '../router.ts' 4 | import { $country } from '../store/country.ts' 5 | import { inputDictionary } from '../utils/inputDictionary.ts' 6 | 7 | const { HOME, COUNTRY } = inputDictionary 8 | 9 | export const useInputProcess = () => { 10 | useInput((input) => { 11 | if (input === HOME.key) $router.open(HOME.url) 12 | else if (input === COUNTRY.key) $country.reset() 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "useTabs": true, 4 | "singleQuote": true, 5 | "semi": false, 6 | "trailingComma": "es5", 7 | "plugins": ["@ianvs/prettier-plugin-sort-imports"], 8 | "importOrder": [ 9 | "", 10 | "", 11 | "^react", 12 | "", 13 | "", 14 | "^[.]", 15 | "", 16 | "", 17 | "^[.]" 18 | ], 19 | "importOrderParserPlugins": ["typescript", "jsx", "importAttributes"], 20 | "importOrderTypeScriptVersion": "5.0.0" 21 | } 22 | -------------------------------------------------------------------------------- /packages/cli/src/components/Select/SelectIndicator.tsx: -------------------------------------------------------------------------------- 1 | import figures from 'figures' 2 | import { Box, Text } from 'ink' 3 | 4 | import { theme } from '../../utils/theme.ts' 5 | 6 | export type SelectIndicatorProps = { 7 | readonly isSelected?: boolean 8 | } 9 | 10 | export const SelectIndicator = ({ isSelected = false }: SelectIndicatorProps) => { 11 | return ( 12 | 13 | {isSelected ? {figures.pointer} : } 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /packages/sdk/models/snkrsRootResponse.ts: -------------------------------------------------------------------------------- 1 | import { array, number, object, string } from 'valibot' 2 | 3 | import type { BaseSchema } from 'valibot' 4 | 5 | const pagesSchema = object({ 6 | next: string(), 7 | prev: string(), 8 | totalPages: number(), 9 | totalResources: number(), 10 | }) 11 | 12 | export const createSnkrsRootResponseSchema = >( 13 | objectsSchema: Schema 14 | ) => 15 | object({ 16 | objects: array(objectsSchema), 17 | pages: pagesSchema, 18 | }) 19 | -------------------------------------------------------------------------------- /packages/cli/src/components/Select/Select.tsx: -------------------------------------------------------------------------------- 1 | import SelectInput from 'ink-select-input' 2 | 3 | import { SelectIndicator } from './SelectIndicator.tsx' 4 | import { SelectItem } from './SelectItem.tsx' 5 | 6 | import type { ComponentProps } from 'react' 7 | 8 | export const Select = ({ 9 | itemComponent = SelectItem, 10 | indicatorComponent = SelectIndicator, 11 | ...rest 12 | }: ComponentProps>) => { 13 | return ( 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /packages/sdk/utils/Error.ts: -------------------------------------------------------------------------------- 1 | export class CustomError extends Error { 2 | static isAbortError(error: unknown) { 3 | return error instanceof Error && error.name === 'AbortError' 4 | } 5 | 6 | static createAbortError(reason?: any) { 7 | return reason ?? new DOMException('This operation was aborted', 'AbortError') 8 | } 9 | 10 | static isTimeoutError(error: unknown) { 11 | return error instanceof Error && error.name === 'TimeoutError' 12 | } 13 | 14 | constructor(message: string) { 15 | super(message) 16 | this.name = this.constructor.name 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/sdk/utils/delay.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from './Error.ts' 2 | 3 | export const delay = (ms: number, { signal }: { signal?: AbortSignal } = {}) => { 4 | const { promise, resolve, reject } = Promise.withResolvers() 5 | 6 | const abortHandler = () => { 7 | clearTimeout(timeout) 8 | reject(CustomError.createAbortError(signal?.reason)) 9 | } 10 | signal?.addEventListener('abort', abortHandler, { once: true }) 11 | 12 | const timeout = setTimeout(() => { 13 | signal?.removeEventListener('abort', abortHandler) 14 | resolve(undefined) 15 | }, ms) 16 | 17 | return promise 18 | } 19 | -------------------------------------------------------------------------------- /packages/sdk/productFeed/api.ts: -------------------------------------------------------------------------------- 1 | import { rest } from '../utils/rest.ts' 2 | import { getProductFeedUrl } from './url.ts' 3 | 4 | import type { GetOptions } from '../utils/rest.ts' 5 | import type { ProductFeedResponseOutput } from './schema.ts' 6 | import type { ProductFeedUrlParams } from './url.ts' 7 | 8 | type GetProductFeedParams = ProductFeedUrlParams & Omit 9 | 10 | export const getProductFeed = async (params: GetProductFeedParams) => { 11 | const url = getProductFeedUrl(params) 12 | const response = await rest.get({ url, ...params }) 13 | return response.objects 14 | } 15 | -------------------------------------------------------------------------------- /packages/cli/src/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Spacer } from 'ink' 2 | 3 | import { useInputProcess } from '../hooks/useInputProcess.ts' 4 | import { theme } from '../utils/theme.ts' 5 | import { Footer } from './Footer.tsx' 6 | import { Header } from './Header.tsx' 7 | 8 | import type { ReactElement } from 'react' 9 | 10 | interface LayoutProps { 11 | children: ReactElement 12 | } 13 | 14 | export const Layout = ({ children }: LayoutProps) => { 15 | useInputProcess() 16 | 17 | return ( 18 | 19 |
20 | {children} 21 | 22 |