mirror of
https://github.com/morten-olsen/refocus.dev.git
synced 2026-02-08 00:46:25 +01:00
init
This commit is contained in:
12
.eslintrc
Normal file
12
.eslintrc
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "@react-native-community",
|
||||||
|
"rules": {
|
||||||
|
"react/react-in-jsx-scope": 0,
|
||||||
|
"prettier/prettier": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
59
.github/workflows/publish.yml
vendored
Normal file
59
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: Deploy static content to Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['main']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||||
|
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||||
|
concurrency:
|
||||||
|
group: 'pages'
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Setup corepack
|
||||||
|
run: corepack enable
|
||||||
|
|
||||||
|
- name: setup pnpm config
|
||||||
|
run: pnpm config set store-dir $PNPM_CACHE_FOLDER
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm turbo bundle
|
||||||
|
env:
|
||||||
|
ASSET_URL: 'https://refocus.dev'
|
||||||
|
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v3
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v1
|
||||||
|
with:
|
||||||
|
path: './packages/app/dist'
|
||||||
|
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v2
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/node_modules/
|
||||||
|
/.pnpm-store/
|
||||||
|
.turbo/
|
||||||
13
.prettierrc.json
Normal file
13
.prettierrc.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
6
.yo-rc.json
Normal file
6
.yo-rc.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"generator-x-repo": {
|
||||||
|
"root": "@refocus",
|
||||||
|
"config": "@refocus/config"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
package.json
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@react-native-community/eslint-config": "^3.2.0",
|
||||||
|
"eslint": "^8.33.0",
|
||||||
|
"prettier": "^2.8.3",
|
||||||
|
"turbo": "^1.9.9"
|
||||||
|
},
|
||||||
|
"workspaces": ["packages/*"],
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "turbo build",
|
||||||
|
"dev": "turbo dev",
|
||||||
|
"lint": "eslint packages/*/src",
|
||||||
|
"start": "turbo start",
|
||||||
|
"test": "turbo test"
|
||||||
|
},
|
||||||
|
"version": "0.0.1",
|
||||||
|
"name": "@refocus/repo",
|
||||||
|
"devDependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.59.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
packages/app/.eslintrc.cjs
Normal file
14
packages/app/.eslintrc.cjs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': 'warn',
|
||||||
|
},
|
||||||
|
}
|
||||||
24
packages/app/.gitignore
vendored
Normal file
24
packages/app/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
20
packages/app/index.html
Normal file
20
packages/app/index.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Loading</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script>
|
||||||
|
window.process = {
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
45
packages/app/package.json
Normal file
45
packages/app/package.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"bundle": "tsc && vite build",
|
||||||
|
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@refocus/sdk": "workspace:^",
|
||||||
|
"@refocus/ui": "workspace:^",
|
||||||
|
"@refocus/widgets": "workspace:^",
|
||||||
|
"framer": "^2.3.0",
|
||||||
|
"framer-motion": "^10.12.16",
|
||||||
|
"localforage": "^1.10.0",
|
||||||
|
"match-sorter": "^6.3.1",
|
||||||
|
"popmotion": "^11.0.5",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-masonry-component": "^6.3.0",
|
||||||
|
"react-responsive-masonry": "^2.1.7",
|
||||||
|
"react-router-dom": "^6.13.0",
|
||||||
|
"sort-by": "^1.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.0.37",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@types/react-responsive-masonry": "^2.1.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.59.0",
|
||||||
|
"@typescript-eslint/parser": "^5.59.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.0.0",
|
||||||
|
"eslint": "^8.38.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.3.4",
|
||||||
|
"isomorphic-fetch": "^3.0.0",
|
||||||
|
"os-browserify": "^0.3.0",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
|
"stream-browserify": "^3.0.0",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.3.9"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/app/public/vite.svg
Normal file
1
packages/app/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
12
packages/app/src/app.tsx
Normal file
12
packages/app/src/app.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { widgets } from '@refocus/widgets';
|
||||||
|
import { FocusProvider, Interface } from '@refocus/ui';
|
||||||
|
|
||||||
|
const App: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<FocusProvider widgets={widgets as any}>
|
||||||
|
<Interface.App />
|
||||||
|
</FocusProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
9
packages/app/src/main.tsx
Normal file
9
packages/app/src/main.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './app';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
1
packages/app/src/vite-env.d.ts
vendored
Normal file
1
packages/app/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
25
packages/app/tsconfig.json
Normal file
25
packages/app/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
packages/app/tsconfig.node.json
Normal file
10
packages/app/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
28
packages/app/vite.config.ts
Normal file
28
packages/app/vite.config.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react-swc';
|
||||||
|
|
||||||
|
const ASSET_URL = process.env.ASSET_URL || '';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
base: ASSET_URL ? `${ASSET_URL}/` : './',
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
fetch: 'isomorphic-fetch',
|
||||||
|
stream: 'stream-browserify',
|
||||||
|
path: 'path-browserify',
|
||||||
|
os: 'os-browserify',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
global: 'globalThis',
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
esbuildOptions: {
|
||||||
|
// Node.js global to browser globalThis
|
||||||
|
define: {
|
||||||
|
global: 'globalThis',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
9
packages/configs/cjs/tsconfig.json
Normal file
9
packages/configs/cjs/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ES2019", "DOM"],
|
||||||
|
"target": "ES2019",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "Node"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
packages/configs/esm/tsconfig.json
Normal file
9
packages/configs/esm/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "NodeNext"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
packages/configs/package.json
Normal file
4
packages/configs/package.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "@refocus/config",
|
||||||
|
"version": "0.0.1"
|
||||||
|
}
|
||||||
19
packages/configs/tsconfig.json
Normal file
19
packages/configs/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": [],
|
||||||
|
"ts-node": {
|
||||||
|
"files": true
|
||||||
|
}
|
||||||
|
}
|
||||||
2
packages/sdk/.gitignore
vendored
Normal file
2
packages/sdk/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/node_modules/
|
||||||
|
/dist/
|
||||||
38
packages/sdk/package.json
Normal file
38
packages/sdk/package.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@refocus/config": "workspace:^",
|
||||||
|
"@types/react": "^18.0.37",
|
||||||
|
"@types/uuid": "^9.0.2",
|
||||||
|
"typescript": "^5.0.4"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"default": "./dist/esm/index.js",
|
||||||
|
"types": "./dist/esm/types/index.d.ts"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"default": "./dist/cjs/index.js",
|
||||||
|
"types": "./dist/cjs/types/index.d.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/**/*"
|
||||||
|
],
|
||||||
|
"main": "./dist/cjs/index.js",
|
||||||
|
"name": "@refocus/sdk",
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm build:esm && pnpm build:cjs",
|
||||||
|
"build:cjs": "tsc -p tsconfig.json",
|
||||||
|
"build:esm": "tsc -p tsconfig.esm.json"
|
||||||
|
},
|
||||||
|
"types": "./dist/cjs/types/index.d.ts",
|
||||||
|
"dependencies": {
|
||||||
|
"@linear/sdk": "^5.0.0",
|
||||||
|
"@sinclair/typebox": "^0.28.15",
|
||||||
|
"@slack/web-api": "^6.8.1",
|
||||||
|
"octokit": "^2.0.19",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
140
packages/sdk/src/boards/context.tsx
Normal file
140
packages/sdk/src/boards/context.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { Boards, BoardsLoad, BoardsSave } from './types';
|
||||||
|
|
||||||
|
type BoardsContextValue = {
|
||||||
|
selected?: string;
|
||||||
|
boards: Boards;
|
||||||
|
addBoard: (name: string) => void;
|
||||||
|
selectBoard: (id: string) => void;
|
||||||
|
removeBoard: (id: string) => void;
|
||||||
|
addWidget: (boardId: string, type: string, data: string) => void;
|
||||||
|
removeWidget: (boardId: string, widgetId: string) => void;
|
||||||
|
updateWidget: (boardId: string, widgetId: string, data: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BoardsProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
load: BoardsLoad;
|
||||||
|
save: BoardsSave;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BoardsContext = createContext<BoardsContextValue | null>(null);
|
||||||
|
|
||||||
|
const BoardsProvider: React.FC<BoardsProviderProps> = ({
|
||||||
|
children,
|
||||||
|
load,
|
||||||
|
save,
|
||||||
|
}) => {
|
||||||
|
const [boards, setBoards] = useState<Boards>(load().boards);
|
||||||
|
const [selected, setSelected] = useState<string | undefined>(load().selected);
|
||||||
|
|
||||||
|
const addBoard = useCallback((name: string) => {
|
||||||
|
const id = uuid();
|
||||||
|
setBoards((currentBoards) => ({
|
||||||
|
...currentBoards,
|
||||||
|
[id]: {
|
||||||
|
name,
|
||||||
|
widgets: {},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeBoard = useCallback((id: string) => {
|
||||||
|
setBoards((currentBoards) => {
|
||||||
|
const copy = { ...currentBoards };
|
||||||
|
delete copy[id];
|
||||||
|
return copy;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addWidget = useCallback(
|
||||||
|
(boardId: string, type: string, data: string) => {
|
||||||
|
const id = uuid();
|
||||||
|
setBoards((currentBoards) => ({
|
||||||
|
...currentBoards,
|
||||||
|
[boardId]: {
|
||||||
|
...currentBoards[boardId],
|
||||||
|
widgets: {
|
||||||
|
...currentBoards[boardId].widgets,
|
||||||
|
[id]: {
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeWidget = useCallback((boardId: string, widgetId: string) => {
|
||||||
|
setBoards((currentBoards) => {
|
||||||
|
const copy = { ...currentBoards };
|
||||||
|
delete copy[boardId].widgets[widgetId];
|
||||||
|
return copy;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateWidget = useCallback(
|
||||||
|
(boardId: string, widgetId: string, data: string) => {
|
||||||
|
setBoards((currentBoards) => ({
|
||||||
|
...currentBoards,
|
||||||
|
[boardId]: {
|
||||||
|
...currentBoards[boardId],
|
||||||
|
widgets: {
|
||||||
|
...currentBoards[boardId].widgets,
|
||||||
|
[widgetId]: {
|
||||||
|
...currentBoards[boardId].widgets[widgetId],
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectBoard = useCallback((id: string) => {
|
||||||
|
setSelected(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
save({ boards, selected });
|
||||||
|
}, [boards, selected, save]);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
boards,
|
||||||
|
selected,
|
||||||
|
addBoard,
|
||||||
|
removeBoard,
|
||||||
|
addWidget,
|
||||||
|
removeWidget,
|
||||||
|
selectBoard,
|
||||||
|
updateWidget,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
boards,
|
||||||
|
selected,
|
||||||
|
addBoard,
|
||||||
|
removeBoard,
|
||||||
|
addWidget,
|
||||||
|
removeWidget,
|
||||||
|
selectBoard,
|
||||||
|
updateWidget,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BoardsContext.Provider value={value}>{children}</BoardsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { BoardsContext, BoardsProvider };
|
||||||
77
packages/sdk/src/boards/hooks.ts
Normal file
77
packages/sdk/src/boards/hooks.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { BoardsContext } from './context';
|
||||||
|
|
||||||
|
const useBoards = () => {
|
||||||
|
const context = useContext(BoardsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useBoards must be used within a BoardsProvider');
|
||||||
|
}
|
||||||
|
return context.boards;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSelectedBoard = () => {
|
||||||
|
const context = useContext(BoardsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useCurrentBoard must be used within a BoardsProvider');
|
||||||
|
}
|
||||||
|
return context.selected;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAddWidget = () => {
|
||||||
|
const context = useContext(BoardsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAddWidget must be used within a BoardsProvider');
|
||||||
|
}
|
||||||
|
return context.addWidget;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useRemoveWidget = () => {
|
||||||
|
const context = useContext(BoardsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useRemoveWidget must be used within a BoardsProvider');
|
||||||
|
}
|
||||||
|
return context.removeWidget;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAddBoard = () => {
|
||||||
|
const context = useContext(BoardsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAddBoard must be used within a BoardsProvider');
|
||||||
|
}
|
||||||
|
return context.addBoard;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useRemoveBoard = () => {
|
||||||
|
const context = useContext(BoardsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useRemoveBoard must be used within a BoardsProvider');
|
||||||
|
}
|
||||||
|
return context.removeBoard;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSelectBoard = () => {
|
||||||
|
const context = useContext(BoardsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useSelectBoard must be used within a BoardsProvider');
|
||||||
|
}
|
||||||
|
return context.selectBoard;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useUpdateWidget = () => {
|
||||||
|
const context = useContext(BoardsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useUpdateWidget must be used within a BoardsProvider');
|
||||||
|
}
|
||||||
|
return context.updateWidget;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
useBoards,
|
||||||
|
useSelectedBoard,
|
||||||
|
useAddWidget,
|
||||||
|
useRemoveWidget,
|
||||||
|
useAddBoard,
|
||||||
|
useRemoveBoard,
|
||||||
|
useSelectBoard,
|
||||||
|
useUpdateWidget,
|
||||||
|
};
|
||||||
12
packages/sdk/src/boards/index.ts
Normal file
12
packages/sdk/src/boards/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export { BoardsProvider } from './context';
|
||||||
|
export {
|
||||||
|
useBoards,
|
||||||
|
useSelectedBoard,
|
||||||
|
useAddWidget,
|
||||||
|
useRemoveWidget,
|
||||||
|
useAddBoard,
|
||||||
|
useRemoveBoard,
|
||||||
|
useSelectBoard,
|
||||||
|
useUpdateWidget,
|
||||||
|
} from './hooks';
|
||||||
|
export * from './types';
|
||||||
23
packages/sdk/src/boards/types.ts
Normal file
23
packages/sdk/src/boards/types.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
type BoardData = { boards: Boards; selected?: string };
|
||||||
|
|
||||||
|
type BoardsLoad = () => BoardData;
|
||||||
|
|
||||||
|
type BoardsSave = (data: BoardData) => void;
|
||||||
|
|
||||||
|
type BoardWidget = {
|
||||||
|
type: string;
|
||||||
|
data: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Board = {
|
||||||
|
name: string;
|
||||||
|
widgets: {
|
||||||
|
[key: string]: BoardWidget;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type Boards = {
|
||||||
|
[key: string]: Board;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { Board, Boards, BoardWidget, BoardsLoad, BoardsSave };
|
||||||
71
packages/sdk/src/clients/github/context.tsx
Normal file
71
packages/sdk/src/clients/github/context.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { createContext, useCallback, useMemo, useState } from 'react';
|
||||||
|
import { Octokit } from 'octokit';
|
||||||
|
|
||||||
|
type GithubLogin = React.ComponentType<{
|
||||||
|
setToken: (token: string) => void;
|
||||||
|
cancel: () => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type GithubClientContextValue = {
|
||||||
|
octokit?: Octokit;
|
||||||
|
login?: () => void;
|
||||||
|
logout: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GithubClientProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
login: GithubLogin;
|
||||||
|
};
|
||||||
|
|
||||||
|
const GithubClientContext = createContext<GithubClientContextValue | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const GithubClientProvider: React.FC<GithubClientProviderProps> = ({
|
||||||
|
children,
|
||||||
|
login: GithubLoginComponent,
|
||||||
|
}) => {
|
||||||
|
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||||
|
const [pat, setPat] = useState(localStorage.getItem('github_pat') || '');
|
||||||
|
const octokit = useMemo(() => {
|
||||||
|
if (!pat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('github_pat', pat);
|
||||||
|
return new Octokit({ auth: pat });
|
||||||
|
}, [pat]);
|
||||||
|
|
||||||
|
const logout = useCallback(() => {
|
||||||
|
setPat('');
|
||||||
|
localStorage.removeItem('github_pat');
|
||||||
|
}, [setPat]);
|
||||||
|
|
||||||
|
const login = useCallback(() => {
|
||||||
|
setIsLoggingIn(true);
|
||||||
|
}, [setIsLoggingIn]);
|
||||||
|
|
||||||
|
const onLogin = useCallback(
|
||||||
|
(nextPat: string) => {
|
||||||
|
setPat(nextPat);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
},
|
||||||
|
[setPat, setIsLoggingIn],
|
||||||
|
);
|
||||||
|
|
||||||
|
const cancelLogin = useCallback(() => {
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
}, [setIsLoggingIn]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GithubClientContext.Provider value={{ octokit, logout, login }}>
|
||||||
|
{isLoggingIn && (
|
||||||
|
<GithubLoginComponent cancel={cancelLogin} setToken={onLogin} />
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</GithubClientContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { GithubLogin };
|
||||||
|
export { GithubClientContext, GithubClientProvider };
|
||||||
64
packages/sdk/src/clients/github/hooks.ts
Normal file
64
packages/sdk/src/clients/github/hooks.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||||
|
import { GithubClientContext } from './context';
|
||||||
|
import { Octokit } from 'octokit';
|
||||||
|
|
||||||
|
const useGithub = () => {
|
||||||
|
const context = useContext(GithubClientContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useGithubClient must be used within a GithubClientProvider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
client: context.octokit,
|
||||||
|
logout: context.logout,
|
||||||
|
login: context.login,
|
||||||
|
}),
|
||||||
|
[context.octokit, context.logout, context.login],
|
||||||
|
);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useGithubQuery = <P, T = unknown>(
|
||||||
|
query: (client: Octokit, params: P) => Promise<T>,
|
||||||
|
) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<unknown | null>(null);
|
||||||
|
const [data, setData] = useState<T>();
|
||||||
|
const { client } = useGithub();
|
||||||
|
|
||||||
|
const fetch = useCallback(
|
||||||
|
async (params: P) => {
|
||||||
|
if (!client) {
|
||||||
|
throw new Error('Github client is not initialized');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const data = await query(client, params);
|
||||||
|
setData(data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
setError(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[client, query],
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
data,
|
||||||
|
fetch,
|
||||||
|
}),
|
||||||
|
[loading, error, data, fetch],
|
||||||
|
);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useGithub, useGithubQuery };
|
||||||
4
packages/sdk/src/clients/github/index.ts
Normal file
4
packages/sdk/src/clients/github/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { GithubClientProvider, GithubLogin } from './context';
|
||||||
|
export { useGithub, useGithubQuery } from './hooks';
|
||||||
|
export { withGithub } from './with-github';
|
||||||
|
export * as GithubTypes from './types';
|
||||||
12
packages/sdk/src/clients/github/types.ts
Normal file
12
packages/sdk/src/clients/github/types.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Octokit } from 'octokit';
|
||||||
|
import { AsyncResponse } from '../../utils/types';
|
||||||
|
|
||||||
|
type PullRequest = AsyncResponse<Octokit['rest']['pulls']['get']>['data'];
|
||||||
|
type Commit = AsyncResponse<Octokit['rest']['repos']['getCommit']>['data'];
|
||||||
|
type WorkflowRun = AsyncResponse<
|
||||||
|
Octokit['rest']['actions']['listWorkflowRuns']
|
||||||
|
>['data']['workflow_runs'][number];
|
||||||
|
type Profile = PullRequest['user'];
|
||||||
|
type Repository = PullRequest['base']['repo'];
|
||||||
|
|
||||||
|
export { PullRequest, Profile, Repository, WorkflowRun, Commit };
|
||||||
19
packages/sdk/src/clients/github/with-github.tsx
Normal file
19
packages/sdk/src/clients/github/with-github.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useGithub } from '.';
|
||||||
|
|
||||||
|
const withGithub = <TProps extends object>(
|
||||||
|
Component: React.ComponentType<TProps>,
|
||||||
|
Fallback: React.ComponentType<object>,
|
||||||
|
) => {
|
||||||
|
const WrappedComponent: React.FC<TProps> = (props) => {
|
||||||
|
const github = useGithub();
|
||||||
|
|
||||||
|
if (!github.client) {
|
||||||
|
return <Fallback />;
|
||||||
|
}
|
||||||
|
return <Component {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
return WrappedComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { withGithub };
|
||||||
17
packages/sdk/src/clients/index.ts
Normal file
17
packages/sdk/src/clients/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export {
|
||||||
|
useGithub,
|
||||||
|
useGithubQuery,
|
||||||
|
withGithub,
|
||||||
|
GithubTypes,
|
||||||
|
GithubLogin,
|
||||||
|
} from './github';
|
||||||
|
export { useLinear, useLinearQuery, withLinear, LinearLogin } from './linear';
|
||||||
|
export {
|
||||||
|
useSlack,
|
||||||
|
useSlackQuery,
|
||||||
|
withSlack,
|
||||||
|
SlackClient,
|
||||||
|
SlackTypes,
|
||||||
|
SlackLogin,
|
||||||
|
} from './slack';
|
||||||
|
export { ClientProvider } from './provider';
|
||||||
73
packages/sdk/src/clients/linear/context.tsx
Normal file
73
packages/sdk/src/clients/linear/context.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { LinearClient } from '@linear/sdk';
|
||||||
|
import { createContext, useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
type LinearLogin = React.ComponentType<{
|
||||||
|
setApiKey: (apiKey: string) => void;
|
||||||
|
cancel: () => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type LinearClientContextValue = {
|
||||||
|
client?: LinearClient;
|
||||||
|
logout: () => void;
|
||||||
|
login?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LinearClientProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
login: LinearLogin;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LinearClientContext = createContext<LinearClientContextValue | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const LinearClientProvider: React.FC<LinearClientProviderProps> = ({
|
||||||
|
children,
|
||||||
|
login: LinearLoginComponent,
|
||||||
|
}) => {
|
||||||
|
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||||
|
const [apiKey, setApiKey] = useState(
|
||||||
|
localStorage.getItem('linear_token') || '',
|
||||||
|
);
|
||||||
|
const client = useMemo(() => {
|
||||||
|
if (!apiKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('linear_token', apiKey);
|
||||||
|
return new LinearClient({ apiKey: apiKey });
|
||||||
|
}, [apiKey]);
|
||||||
|
|
||||||
|
const logout = useCallback(() => {
|
||||||
|
setApiKey('');
|
||||||
|
localStorage.removeItem('linear_token');
|
||||||
|
}, [setApiKey]);
|
||||||
|
|
||||||
|
const login = useCallback(() => {
|
||||||
|
setIsLoggingIn(true);
|
||||||
|
}, [setIsLoggingIn]);
|
||||||
|
|
||||||
|
const onLogin = useCallback(
|
||||||
|
(nextApiKey: string) => {
|
||||||
|
setApiKey(nextApiKey);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
},
|
||||||
|
[setApiKey, setIsLoggingIn],
|
||||||
|
);
|
||||||
|
|
||||||
|
const cancelLogin = useCallback(() => {
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
}, [setIsLoggingIn]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LinearClientContext.Provider value={{ client, login, logout }}>
|
||||||
|
{isLoggingIn && (
|
||||||
|
<LinearLoginComponent setApiKey={onLogin} cancel={cancelLogin} />
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</LinearClientContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { LinearLogin };
|
||||||
|
export { LinearClientContext, LinearClientProvider };
|
||||||
61
packages/sdk/src/clients/linear/hooks.ts
Normal file
61
packages/sdk/src/clients/linear/hooks.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||||
|
import { LinearClientContext } from './context';
|
||||||
|
import { LinearClient } from '@linear/sdk';
|
||||||
|
|
||||||
|
const useLinear = () => {
|
||||||
|
const context = useContext(LinearClientContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useLinearClient must be used within a LinearClientProvider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
client: context.client,
|
||||||
|
logout: context.logout,
|
||||||
|
login: context.login,
|
||||||
|
}),
|
||||||
|
[context.client, context.logout, context.login],
|
||||||
|
);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useLinearQuery = <T = unknown>(
|
||||||
|
query: (client: LinearClient) => Promise<T>,
|
||||||
|
) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<unknown | null>(null);
|
||||||
|
const [data, setData] = useState<T | null>(null);
|
||||||
|
const { client } = useLinear();
|
||||||
|
|
||||||
|
const fetch = useCallback(async () => {
|
||||||
|
if (!client) {
|
||||||
|
throw new Error('Linear client is not initialized');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const data = await query(client);
|
||||||
|
setData(data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
setError(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [client, query]);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
data,
|
||||||
|
fetch,
|
||||||
|
}),
|
||||||
|
[loading, error, data, fetch],
|
||||||
|
);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useLinear, useLinearQuery };
|
||||||
3
packages/sdk/src/clients/linear/index.ts
Normal file
3
packages/sdk/src/clients/linear/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { LinearClientProvider, LinearLogin } from './context';
|
||||||
|
export { useLinear, useLinearQuery } from './hooks';
|
||||||
|
export { withLinear } from './with-linear';
|
||||||
19
packages/sdk/src/clients/linear/with-linear.tsx
Normal file
19
packages/sdk/src/clients/linear/with-linear.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useLinear } from '.';
|
||||||
|
|
||||||
|
const withLinear = <TProps extends object>(
|
||||||
|
Component: React.ComponentType<TProps>,
|
||||||
|
FallBack: React.ComponentType<object>,
|
||||||
|
) => {
|
||||||
|
const WrappedComponent: React.FC<TProps> = (props) => {
|
||||||
|
const linear = useLinear();
|
||||||
|
|
||||||
|
if (!linear.client) {
|
||||||
|
return <FallBack />;
|
||||||
|
}
|
||||||
|
return <Component {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
return WrappedComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { withLinear };
|
||||||
29
packages/sdk/src/clients/provider.tsx
Normal file
29
packages/sdk/src/clients/provider.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { GithubClientProvider, GithubLogin } from './github';
|
||||||
|
import { LinearClientProvider, LinearLogin } from './linear';
|
||||||
|
import { SlackClientProvider, SlackLogin } from './slack';
|
||||||
|
|
||||||
|
type ClientProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
logins: {
|
||||||
|
github: GithubLogin;
|
||||||
|
linear: LinearLogin;
|
||||||
|
slack: SlackLogin;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const ClientProvider: React.FC<ClientProviderProps> = ({
|
||||||
|
children,
|
||||||
|
logins,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<LinearClientProvider login={logins.linear}>
|
||||||
|
<GithubClientProvider login={logins.github}>
|
||||||
|
<SlackClientProvider login={logins.slack}>
|
||||||
|
{children}
|
||||||
|
</SlackClientProvider>
|
||||||
|
</GithubClientProvider>
|
||||||
|
</LinearClientProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ClientProvider };
|
||||||
59
packages/sdk/src/clients/slack/client.ts
Normal file
59
packages/sdk/src/clients/slack/client.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import type { WebClient } from '@slack/web-api';
|
||||||
|
import type { Expand } from '../../utils/types';
|
||||||
|
|
||||||
|
type MethodFromString<T extends `${string}.${string}`> =
|
||||||
|
T extends `${infer A}.${infer B}`
|
||||||
|
? A extends keyof WebClient
|
||||||
|
? B extends keyof WebClient[A]
|
||||||
|
? WebClient[A][B] extends (...args: any) => any
|
||||||
|
? (
|
||||||
|
...args: Parameters<WebClient[A][B]>
|
||||||
|
) => ReturnType<WebClient[A][B]>
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type ParamsFromString<T extends `${string}.${string}`> =
|
||||||
|
MethodFromString<T> extends (arg: infer P) => unknown
|
||||||
|
? P extends Record<string, any>
|
||||||
|
? P
|
||||||
|
: never
|
||||||
|
: never;
|
||||||
|
type ReturnFromString<T extends `${string}.${string}`> =
|
||||||
|
MethodFromString<T> extends () => infer R
|
||||||
|
? R extends Promise<infer P>
|
||||||
|
? P
|
||||||
|
: never
|
||||||
|
: never;
|
||||||
|
|
||||||
|
class SlackClient {
|
||||||
|
#token: string;
|
||||||
|
|
||||||
|
constructor(token: string) {
|
||||||
|
this.#token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public send = async <K extends `${string}.${string}`>(
|
||||||
|
action: K,
|
||||||
|
params: Expand<ParamsFromString<K>>,
|
||||||
|
): Promise<Expand<ReturnFromString<K>>> => {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('token', this.#token);
|
||||||
|
Object.keys(params).forEach((key) => {
|
||||||
|
form.append(key, params[key as keyof typeof params]);
|
||||||
|
});
|
||||||
|
const response = await fetch(`https://slack.com/api/${action}`, {
|
||||||
|
mode: 'cors',
|
||||||
|
method: 'POST',
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch: ${response.statusText} ${action}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { ReturnFromString };
|
||||||
|
export { SlackClient };
|
||||||
69
packages/sdk/src/clients/slack/context.tsx
Normal file
69
packages/sdk/src/clients/slack/context.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { createContext, useCallback, useMemo, useState } from 'react';
|
||||||
|
import { SlackClient } from './client';
|
||||||
|
|
||||||
|
type SlackLogin = React.ComponentType<{
|
||||||
|
setToken: (token: string) => void;
|
||||||
|
cancel: () => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type SlackClientContextValue = {
|
||||||
|
client?: SlackClient;
|
||||||
|
logout: () => void;
|
||||||
|
login: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SlackClientProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
login: SlackLogin;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SlackClientContext = createContext<SlackClientContextValue | null>(null);
|
||||||
|
|
||||||
|
const SlackClientProvider: React.FC<SlackClientProviderProps> = ({
|
||||||
|
children,
|
||||||
|
login: SlackLoginComponent,
|
||||||
|
}) => {
|
||||||
|
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||||
|
const [token, setToken] = useState(localStorage.getItem('slack_token') || '');
|
||||||
|
const client = useMemo(() => {
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('slack_token', token);
|
||||||
|
return new SlackClient(token);
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
const logout = useCallback(() => {
|
||||||
|
setToken('');
|
||||||
|
localStorage.removeItem('slack_token');
|
||||||
|
}, [setToken]);
|
||||||
|
|
||||||
|
const login = useCallback(() => {
|
||||||
|
setIsLoggingIn(true);
|
||||||
|
}, [setIsLoggingIn]);
|
||||||
|
|
||||||
|
const onLogin = useCallback(
|
||||||
|
(nextToken: string) => {
|
||||||
|
setToken(nextToken);
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
},
|
||||||
|
[setToken, setIsLoggingIn],
|
||||||
|
);
|
||||||
|
|
||||||
|
const cancelLogin = useCallback(() => {
|
||||||
|
setIsLoggingIn(false);
|
||||||
|
}, [setIsLoggingIn]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SlackClientContext.Provider value={{ client, login, logout }}>
|
||||||
|
{isLoggingIn && (
|
||||||
|
<SlackLoginComponent setToken={onLogin} cancel={cancelLogin} />
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</SlackClientContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { SlackLogin };
|
||||||
|
export { SlackClientContext, SlackClientProvider };
|
||||||
62
packages/sdk/src/clients/slack/hooks.ts
Normal file
62
packages/sdk/src/clients/slack/hooks.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||||
|
import { SlackClientContext } from './context';
|
||||||
|
import { SlackClient } from './client';
|
||||||
|
|
||||||
|
const useSlack = () => {
|
||||||
|
const context = useContext(SlackClientContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useSlack must be used within a SlackClientProvider');
|
||||||
|
}
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
client: context.client,
|
||||||
|
logout: context.logout,
|
||||||
|
login: context.login,
|
||||||
|
}),
|
||||||
|
[context.client, context.logout, context.login],
|
||||||
|
);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSlackQuery = <P, T = unknown>(
|
||||||
|
query: (client: SlackClient, params: P) => Promise<T>,
|
||||||
|
) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<unknown | null>(null);
|
||||||
|
const [data, setData] = useState<T | null>(null);
|
||||||
|
const { client } = useSlack();
|
||||||
|
|
||||||
|
const fetch = useCallback(
|
||||||
|
async (params: P) => {
|
||||||
|
if (!client) {
|
||||||
|
throw new Error('Slack client is not initialized');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const data = await query(client, params);
|
||||||
|
setData(data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
setError(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[client, query],
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
data,
|
||||||
|
fetch,
|
||||||
|
}),
|
||||||
|
[loading, error, data, fetch],
|
||||||
|
);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useSlack, useSlackQuery };
|
||||||
5
packages/sdk/src/clients/slack/index.ts
Normal file
5
packages/sdk/src/clients/slack/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export { SlackClientProvider, SlackLogin } from './context';
|
||||||
|
export { SlackClient } from './client';
|
||||||
|
export { useSlack, useSlackQuery } from './hooks';
|
||||||
|
export { withSlack } from './with-slack';
|
||||||
|
export * as SlackTypes from './types';
|
||||||
5
packages/sdk/src/clients/slack/types.ts
Normal file
5
packages/sdk/src/clients/slack/types.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { ReturnFromString } from './client';
|
||||||
|
|
||||||
|
type Profile = Exclude<ReturnFromString<'users.info'>['user'], undefined>;
|
||||||
|
|
||||||
|
export type { Profile };
|
||||||
19
packages/sdk/src/clients/slack/with-slack.tsx
Normal file
19
packages/sdk/src/clients/slack/with-slack.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useSlack } from '.';
|
||||||
|
|
||||||
|
const withSlack = <TProps extends object>(
|
||||||
|
Component: React.ComponentType<TProps>,
|
||||||
|
Fallback: React.ComponentType<object>,
|
||||||
|
) => {
|
||||||
|
const WrappedComponent: React.FC<TProps> = (props) => {
|
||||||
|
const slack = useSlack();
|
||||||
|
|
||||||
|
if (!slack.client) {
|
||||||
|
return <Fallback />;
|
||||||
|
}
|
||||||
|
return <Component {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
return WrappedComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { withSlack };
|
||||||
42
packages/sdk/src/hooks/index.ts
Normal file
42
packages/sdk/src/hooks/index.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
type AutoUpdateOptions<TReturn> = {
|
||||||
|
interval: number;
|
||||||
|
action: () => Promise<TReturn>;
|
||||||
|
callback?: (next: TReturn, prev?: TReturn) => void;
|
||||||
|
};
|
||||||
|
const useAutoUpdate = <T>(
|
||||||
|
{ interval, action, callback = () => {} }: AutoUpdateOptions<T>,
|
||||||
|
deps: any[],
|
||||||
|
) => {
|
||||||
|
const prev = useRef<T>();
|
||||||
|
const actionWithCallback = useCallback(action, [...deps]);
|
||||||
|
const callbackWithCallback = useCallback(callback, [...deps]);
|
||||||
|
|
||||||
|
const update = useCallback(async () => {
|
||||||
|
const next = await actionWithCallback();
|
||||||
|
callbackWithCallback(next, prev.current);
|
||||||
|
prev.current = next;
|
||||||
|
}, [actionWithCallback, callbackWithCallback]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let intervalId: NodeJS.Timeout;
|
||||||
|
|
||||||
|
const update = async () => {
|
||||||
|
const next = await actionWithCallback();
|
||||||
|
callbackWithCallback(next, prev.current);
|
||||||
|
prev.current = next;
|
||||||
|
intervalId = setTimeout(update, interval);
|
||||||
|
};
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(intervalId);
|
||||||
|
};
|
||||||
|
}, [interval, actionWithCallback, callbackWithCallback]);
|
||||||
|
|
||||||
|
return update;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useAutoUpdate };
|
||||||
6
packages/sdk/src/index.ts
Normal file
6
packages/sdk/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export * from './clients';
|
||||||
|
export * from './widgets';
|
||||||
|
export * from './provider';
|
||||||
|
export * from './notifications';
|
||||||
|
export * from './hooks';
|
||||||
|
export * from './boards';
|
||||||
65
packages/sdk/src/notifications/context.tsx
Normal file
65
packages/sdk/src/notifications/context.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { createContext, useCallback, useMemo, useState } from 'react';
|
||||||
|
import type { Notification } from './types';
|
||||||
|
|
||||||
|
type NotificationsContextValue = {
|
||||||
|
notifications: Notification[];
|
||||||
|
add: (notification: Notification) => void;
|
||||||
|
dismiss: (id: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NotificationsProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NotificationsContext = createContext<NotificationsContextValue | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
let nextId = 0;
|
||||||
|
|
||||||
|
const NotificationsProvider: React.FC<NotificationsProviderProps> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||||
|
|
||||||
|
const add = useCallback((notification: Notification) => {
|
||||||
|
setNotifications((current) => [
|
||||||
|
...current,
|
||||||
|
{
|
||||||
|
id: String(nextId++),
|
||||||
|
...notification,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
if ('Notification' in window) {
|
||||||
|
const notify = async () => {
|
||||||
|
if (Notification.permission !== 'granted') {
|
||||||
|
await Notification.requestPermission();
|
||||||
|
}
|
||||||
|
if (Notification.permission === 'granted') {
|
||||||
|
const n = new Notification(notification.title || 'Notification', {
|
||||||
|
body: notification.message,
|
||||||
|
});
|
||||||
|
setTimeout(() => n.close(), 10 * 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
notify();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const dismiss = useCallback((id: string) => {
|
||||||
|
setNotifications((current) => current.filter((n) => n.id !== id));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({ notifications, add, dismiss }),
|
||||||
|
[notifications, add, dismiss],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationsContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</NotificationsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NotificationsContext, NotificationsProvider };
|
||||||
37
packages/sdk/src/notifications/hooks.ts
Normal file
37
packages/sdk/src/notifications/hooks.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { NotificationsContext } from './context';
|
||||||
|
|
||||||
|
const useNotifications = () => {
|
||||||
|
const context = useContext(NotificationsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useNotifications must be used within a NotificationsProvider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.notifications;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useNotificationAdd = () => {
|
||||||
|
const context = useContext(NotificationsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useNotificationAdd must be used within a NotificationsProvider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.add;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useNotificationDismiss = () => {
|
||||||
|
const context = useContext(NotificationsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useNotificationDismiss must be used within a NotificationsProvider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.dismiss;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useNotifications, useNotificationAdd, useNotificationDismiss };
|
||||||
7
packages/sdk/src/notifications/index.ts
Normal file
7
packages/sdk/src/notifications/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export { NotificationsProvider } from './context';
|
||||||
|
export type { Notification } from './types';
|
||||||
|
export {
|
||||||
|
useNotifications,
|
||||||
|
useNotificationAdd,
|
||||||
|
useNotificationDismiss,
|
||||||
|
} from './hooks';
|
||||||
12
packages/sdk/src/notifications/types.ts
Normal file
12
packages/sdk/src/notifications/types.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
type Notification = {
|
||||||
|
view: Symbol;
|
||||||
|
id?: string;
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
actions?: {
|
||||||
|
label: string;
|
||||||
|
callback: () => void;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { Notification };
|
||||||
40
packages/sdk/src/provider.tsx
Normal file
40
packages/sdk/src/provider.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { TSchema } from '@sinclair/typebox';
|
||||||
|
import {
|
||||||
|
ClientProvider,
|
||||||
|
GithubLogin,
|
||||||
|
LinearLogin,
|
||||||
|
SlackLogin,
|
||||||
|
} from './clients';
|
||||||
|
import { Widget, WidgetsProvider } from './widgets';
|
||||||
|
import { NotificationsProvider } from './notifications';
|
||||||
|
import { BoardsLoad, BoardsProvider, BoardsSave } from './boards';
|
||||||
|
|
||||||
|
type DashboardProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
widgets?: Widget<TSchema>[];
|
||||||
|
load: BoardsLoad;
|
||||||
|
save: BoardsSave;
|
||||||
|
logins: {
|
||||||
|
github: GithubLogin;
|
||||||
|
linear: LinearLogin;
|
||||||
|
slack: SlackLogin;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const DashboardProvider: React.FC<DashboardProviderProps> = ({
|
||||||
|
children,
|
||||||
|
widgets,
|
||||||
|
load,
|
||||||
|
save,
|
||||||
|
logins,
|
||||||
|
}) => (
|
||||||
|
<WidgetsProvider widgets={widgets}>
|
||||||
|
<BoardsProvider load={load} save={save}>
|
||||||
|
<NotificationsProvider>
|
||||||
|
<ClientProvider logins={logins}>{children}</ClientProvider>
|
||||||
|
</NotificationsProvider>
|
||||||
|
</BoardsProvider>
|
||||||
|
</WidgetsProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { DashboardProvider };
|
||||||
16
packages/sdk/src/utils/types.ts
Normal file
16
packages/sdk/src/utils/types.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
|
||||||
|
|
||||||
|
// expands object types recursively
|
||||||
|
type ExpandRecursively<T> = T extends object
|
||||||
|
? T extends infer O
|
||||||
|
? { [K in keyof O]: ExpandRecursively<O[K]> }
|
||||||
|
: never
|
||||||
|
: T;
|
||||||
|
|
||||||
|
type AsyncResponse<T> = T extends () => Promise<infer U> ? U : never;
|
||||||
|
|
||||||
|
type FirstParameter<T> = T extends (arg1: infer U, ...args: any[]) => any
|
||||||
|
? U
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export type { Expand, ExpandRecursively, AsyncResponse, FirstParameter };
|
||||||
29
packages/sdk/src/widgets/context.tsx
Normal file
29
packages/sdk/src/widgets/context.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { TSchema } from '@sinclair/typebox';
|
||||||
|
import { createContext, useState } from 'react';
|
||||||
|
import { Widget } from './types';
|
||||||
|
|
||||||
|
type WidgetsContextValue = {
|
||||||
|
widgets: Widget<TSchema>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type WidgetsProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
widgets?: Widget<TSchema>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const WidgetsContext = createContext<WidgetsContextValue | null>(null);
|
||||||
|
|
||||||
|
const WidgetsProvider: React.FC<WidgetsProviderProps> = ({
|
||||||
|
children,
|
||||||
|
widgets: initialWidgets,
|
||||||
|
}) => {
|
||||||
|
const [widgets] = useState<Widget<TSchema>[]>(initialWidgets || []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WidgetsContext.Provider value={{ widgets }}>
|
||||||
|
{children}
|
||||||
|
</WidgetsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { WidgetsContext, WidgetsProvider };
|
||||||
26
packages/sdk/src/widgets/editor.tsx
Normal file
26
packages/sdk/src/widgets/editor.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { useWidget } from '.';
|
||||||
|
import { useWidgetData, useWidgetId } from './hooks';
|
||||||
|
|
||||||
|
type WidgetRenderProps = {
|
||||||
|
onSave: (data: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WidgetEditor: React.FC<WidgetRenderProps> = ({ onSave }) => {
|
||||||
|
const id = useWidgetId();
|
||||||
|
const data = useWidgetData();
|
||||||
|
const widget = useWidget(id);
|
||||||
|
|
||||||
|
if (!widget) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Component = widget.edit;
|
||||||
|
|
||||||
|
if (!Component) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Component value={data} save={onSave} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { WidgetEditor };
|
||||||
124
packages/sdk/src/widgets/hooks.ts
Normal file
124
packages/sdk/src/widgets/hooks.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||||
|
import { WidgetsContext } from './context';
|
||||||
|
import { WidgetContext } from './widget-context';
|
||||||
|
|
||||||
|
const useWidget = (id: string) => {
|
||||||
|
const context = useContext(WidgetsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useWidget must be used within a WidgetsProvider');
|
||||||
|
}
|
||||||
|
const current = useMemo(() => {
|
||||||
|
return context.widgets.find((widget) => widget.id === id);
|
||||||
|
}, [context.widgets, id]);
|
||||||
|
|
||||||
|
return current;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useWidgets = () => {
|
||||||
|
const context = useContext(WidgetsContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useWidgets must be used within a WidgetsProvider');
|
||||||
|
}
|
||||||
|
return context.widgets;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WidgetResult = {
|
||||||
|
id: string;
|
||||||
|
data: any;
|
||||||
|
description?: string;
|
||||||
|
name: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useGetWidgetsFromUrl = () => {
|
||||||
|
const [current, setCurrent] = useState<WidgetResult[]>([]);
|
||||||
|
const widgets = useWidgets();
|
||||||
|
|
||||||
|
const update = useCallback(
|
||||||
|
(url: URL) => {
|
||||||
|
const result = widgets.map((widget) => {
|
||||||
|
const parsed = widget.parseUrl?.(url);
|
||||||
|
if (!parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: widget.id,
|
||||||
|
name: widget.name,
|
||||||
|
description: widget.description,
|
||||||
|
icon: widget.icon,
|
||||||
|
data: parsed,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setCurrent(result.filter(Boolean) as WidgetResult[]);
|
||||||
|
},
|
||||||
|
[widgets],
|
||||||
|
);
|
||||||
|
|
||||||
|
return [current, update] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDismissWidgetNotification = () => {
|
||||||
|
const context = useContext(WidgetContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useDismissWidgetNotification must be used within a WidgetProvider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context.dismissNotification;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAddWidgetNotification = () => {
|
||||||
|
const context = useContext(WidgetContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useAddWidgetNotification must be used within a WidgetProvider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context.addNotification;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useWidgetNotifications = () => {
|
||||||
|
const context = useContext(WidgetContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useWidgetNotifications must be used within a WidgetProvider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context.notifications;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useWidgetData = () => {
|
||||||
|
const context = useContext(WidgetContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useWidgetData must be used within a WidgetProvider');
|
||||||
|
}
|
||||||
|
return context.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSetWidgetData = () => {
|
||||||
|
const context = useContext(WidgetContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useSetWidgetData must be used within a WidgetProvider');
|
||||||
|
}
|
||||||
|
return context.setData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useWidgetId = () => {
|
||||||
|
const context = useContext(WidgetContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useWidgetId must be used within a WidgetProvider');
|
||||||
|
}
|
||||||
|
return context.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
useWidget,
|
||||||
|
useWidgets,
|
||||||
|
useGetWidgetsFromUrl,
|
||||||
|
useWidgetNotifications,
|
||||||
|
useDismissWidgetNotification,
|
||||||
|
useAddWidgetNotification,
|
||||||
|
useWidgetData,
|
||||||
|
useSetWidgetData,
|
||||||
|
useWidgetId,
|
||||||
|
};
|
||||||
16
packages/sdk/src/widgets/index.ts
Normal file
16
packages/sdk/src/widgets/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export { WidgetsProvider } from './context';
|
||||||
|
export type { Widget } from './types';
|
||||||
|
export {
|
||||||
|
useWidget,
|
||||||
|
useWidgets,
|
||||||
|
useGetWidgetsFromUrl,
|
||||||
|
useAddWidgetNotification,
|
||||||
|
useDismissWidgetNotification,
|
||||||
|
useWidgetNotifications,
|
||||||
|
useWidgetId,
|
||||||
|
useWidgetData,
|
||||||
|
useSetWidgetData,
|
||||||
|
} from './hooks';
|
||||||
|
export { WidgetProvider } from './widget-context';
|
||||||
|
export { WidgetView } from './view';
|
||||||
|
export { WidgetEditor } from './editor';
|
||||||
17
packages/sdk/src/widgets/types.ts
Normal file
17
packages/sdk/src/widgets/types.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { TSchema, Static } from '@sinclair/typebox';
|
||||||
|
|
||||||
|
type Widget<TConfig extends TSchema> = {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
schema: TSchema;
|
||||||
|
parseUrl?: (url: URL) => Static<TConfig> | undefined;
|
||||||
|
component: React.ComponentType<Static<TConfig>>;
|
||||||
|
edit?: React.ComponentType<{
|
||||||
|
value?: Static<TConfig>;
|
||||||
|
save: (next: Static<TConfig>) => void;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { Widget };
|
||||||
18
packages/sdk/src/widgets/view.tsx
Normal file
18
packages/sdk/src/widgets/view.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useWidget } from '.';
|
||||||
|
import { useWidgetData, useWidgetId } from './hooks';
|
||||||
|
|
||||||
|
const WidgetView: React.FC = () => {
|
||||||
|
const id = useWidgetId();
|
||||||
|
const data = useWidgetData();
|
||||||
|
const widget = useWidget(id);
|
||||||
|
|
||||||
|
if (!widget) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Component = widget.component;
|
||||||
|
|
||||||
|
return <Component {...data} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { WidgetView };
|
||||||
73
packages/sdk/src/widgets/widget-context.tsx
Normal file
73
packages/sdk/src/widgets/widget-context.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { createContext, useCallback, useMemo, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
Notification as BaseNotification,
|
||||||
|
useNotificationAdd,
|
||||||
|
useNotifications,
|
||||||
|
useNotificationDismiss,
|
||||||
|
} from '../notifications';
|
||||||
|
type Notification = Omit<BaseNotification, 'view'>;
|
||||||
|
|
||||||
|
type WidgetContextValue = {
|
||||||
|
id: string;
|
||||||
|
data?: any;
|
||||||
|
addNotification: (notification: Notification) => void;
|
||||||
|
dismissNotification: (id: string) => void;
|
||||||
|
notifications: Notification[];
|
||||||
|
setData?: (data: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WidgetProviderProps = {
|
||||||
|
id: string;
|
||||||
|
data?: any;
|
||||||
|
setData?: (data: any) => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WidgetContext = createContext<WidgetContextValue | null>(null);
|
||||||
|
|
||||||
|
const WidgetProvider = ({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
setData,
|
||||||
|
children,
|
||||||
|
}: WidgetProviderProps) => {
|
||||||
|
const ref = useRef(Symbol('WidgetRender'));
|
||||||
|
const globalNotifications = useNotifications();
|
||||||
|
const addGlobalNotification = useNotificationAdd();
|
||||||
|
const dissmissGlobalNotification = useNotificationDismiss();
|
||||||
|
const notifications = useMemo(() => {
|
||||||
|
return globalNotifications.filter((n) => n.view !== ref.current);
|
||||||
|
}, [globalNotifications]);
|
||||||
|
|
||||||
|
const addNotification = useCallback(
|
||||||
|
(notification: Notification) => {
|
||||||
|
addGlobalNotification({ ...notification, view: ref.current });
|
||||||
|
},
|
||||||
|
[addGlobalNotification],
|
||||||
|
);
|
||||||
|
|
||||||
|
const dismissNotification = useCallback(
|
||||||
|
(dismissId: string) => {
|
||||||
|
dissmissGlobalNotification(dismissId);
|
||||||
|
},
|
||||||
|
[dissmissGlobalNotification],
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
addNotification,
|
||||||
|
dismissNotification,
|
||||||
|
notifications,
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
setData,
|
||||||
|
}),
|
||||||
|
[addNotification, notifications, id, data, setData, dismissNotification],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WidgetContext.Provider value={value}>{children}</WidgetContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { WidgetContext, WidgetProvider };
|
||||||
10
packages/sdk/tsconfig.esm.json
Normal file
10
packages/sdk/tsconfig.esm.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"declarationDir": "./dist/esm/types",
|
||||||
|
"outDir": "dist/esm"
|
||||||
|
},
|
||||||
|
"extends": "@refocus/config/esm",
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
10
packages/sdk/tsconfig.json
Normal file
10
packages/sdk/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"declarationDir": "./dist/cjs/types",
|
||||||
|
"outDir": "dist/cjs"
|
||||||
|
},
|
||||||
|
"extends": "@refocus/config/cjs",
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
2
packages/ui/.gitignore
vendored
Normal file
2
packages/ui/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/node_modules/
|
||||||
|
/dist/
|
||||||
17
packages/ui/.storybook/main.ts
Normal file
17
packages/ui/.storybook/main.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { StorybookConfig } from '@storybook/react-vite';
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||||
|
addons: [
|
||||||
|
'@storybook/addon-links',
|
||||||
|
'@storybook/addon-essentials',
|
||||||
|
'@storybook/addon-interactions',
|
||||||
|
],
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/react-vite',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
autodocs: 'tag',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
24
packages/ui/.storybook/preview.tsx
Normal file
24
packages/ui/.storybook/preview.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { Preview } from '@storybook/react';
|
||||||
|
import { UIProvider } from '../src/theme/provider';
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<UIProvider>
|
||||||
|
<Story />
|
||||||
|
</UIProvider>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
parameters: {
|
||||||
|
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
53
packages/ui/package.json
Normal file
53
packages/ui/package.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@refocus/config": "workspace:^",
|
||||||
|
"@storybook/addon-essentials": "^7.0.20",
|
||||||
|
"@storybook/addon-interactions": "^7.0.20",
|
||||||
|
"@storybook/addon-links": "^7.0.20",
|
||||||
|
"@storybook/blocks": "^7.0.20",
|
||||||
|
"@storybook/react": "^7.0.20",
|
||||||
|
"@storybook/react-vite": "^7.0.20",
|
||||||
|
"@storybook/testing-library": "^0.0.14-next.2",
|
||||||
|
"@types/react": "^18.0.37",
|
||||||
|
"@types/styled-components": "^5.1.26",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"storybook": "^7.0.20",
|
||||||
|
"typescript": "^5.0.4"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"default": "./dist/esm/index.js",
|
||||||
|
"types": "./dist/esm/types/index.d.ts"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"default": "./dist/cjs/index.js",
|
||||||
|
"types": "./dist/cjs/types/index.d.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/**/*"
|
||||||
|
],
|
||||||
|
"main": "./dist/cjs/index.js",
|
||||||
|
"name": "@refocus/ui",
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm build:esm && pnpm build:cjs",
|
||||||
|
"build:cjs": "tsc -p tsconfig.json",
|
||||||
|
"build:esm": "tsc -p tsconfig.esm.json",
|
||||||
|
"storybook": "storybook dev -p 6006 --no-open",
|
||||||
|
"build-storybook": "storybook build"
|
||||||
|
},
|
||||||
|
"types": "./dist/cjs/types/index.d.ts",
|
||||||
|
"dependencies": {
|
||||||
|
"@refocus/sdk": "workspace:^",
|
||||||
|
"@radix-ui/react-dialog": "^1.0.4",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
|
"react-icons": "^4.9.0",
|
||||||
|
"react-markdown": "^6.0.3",
|
||||||
|
"styled-components": "6.0.0-rc.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
84
packages/ui/src/base/avatar/index.tsx
Normal file
84
packages/ui/src/base/avatar/index.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
type AvatarProps = {
|
||||||
|
url?: string;
|
||||||
|
name?: string;
|
||||||
|
decal?: React.ReactNode;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
sm: 28,
|
||||||
|
md: 50,
|
||||||
|
lg: 75,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fontSizes = {
|
||||||
|
sm: 10,
|
||||||
|
md: 24,
|
||||||
|
lg: 32,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapper = styled.div<{
|
||||||
|
size: AvatarProps['size'];
|
||||||
|
}>`
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
${({ size }) => (size ? `width: ${sizes[size]}px;` : '')}
|
||||||
|
${({ size }) => (size ? `height: ${sizes[size]}px;` : '')}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Image = styled.img`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 3px solid ${({ theme }) => theme.colors.text.base};
|
||||||
|
border-radius: 50%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const WithoutImage = styled.div<{
|
||||||
|
size: AvatarProps['size'];
|
||||||
|
}>`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 3px solid ${({ theme }) => theme.colors.text.base};
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.colors.text.base};
|
||||||
|
${({ size }) => (size ? `font-size: ${fontSizes[size]}px;` : '')}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Decal = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: -5px;
|
||||||
|
min-width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0 5px;
|
||||||
|
background-color: ${({ theme }) => theme.colors.text.base};
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.colors.bg.base};
|
||||||
|
font-size: 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Avatar: React.FC<AvatarProps> = ({ url, name, decal, size = 'md' }) => {
|
||||||
|
const initials = useMemo(() => {
|
||||||
|
const [firstName, lastName] = name?.split(' ') || [];
|
||||||
|
return `${firstName?.[0] || ''}${lastName?.[0] || ''}`;
|
||||||
|
}, [name]);
|
||||||
|
return (
|
||||||
|
<Wrapper size={size}>
|
||||||
|
{!url && <WithoutImage size={size}>{initials}</WithoutImage>}
|
||||||
|
{url && <Image src={url} alt={name || ''} />}
|
||||||
|
{decal && <Decal>{decal}</Decal>}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Avatar };
|
||||||
30
packages/ui/src/base/button/index.tsx
Normal file
30
packages/ui/src/base/button/index.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { styled } from 'styled-components';
|
||||||
|
import { View } from '../view';
|
||||||
|
|
||||||
|
type ButtonProps = {
|
||||||
|
title: React.ReactNode;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ButtonWrapper = styled(View)`
|
||||||
|
background-color: ${({ theme }) => theme.colors.bg.highlight};
|
||||||
|
display: inline-flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Button: React.FC<ButtonProps> = ({ title, onClick, icon }) => {
|
||||||
|
return (
|
||||||
|
<ButtonWrapper
|
||||||
|
$p="sm"
|
||||||
|
as="button"
|
||||||
|
onClick={onClick}
|
||||||
|
$items="center"
|
||||||
|
$gap="sm"
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{title}
|
||||||
|
</ButtonWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Button };
|
||||||
24
packages/ui/src/base/card/index.tsx
Normal file
24
packages/ui/src/base/card/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
import { View } from '../view';
|
||||||
|
|
||||||
|
const Card = styled(View)`
|
||||||
|
border-radius: ${({ theme }) => `${theme.radii.md}${theme.units.radii}`};
|
||||||
|
|
||||||
|
${({ theme, ...rest }) =>
|
||||||
|
'onClick' in rest &&
|
||||||
|
`
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 0 5px 2px ${theme.colors.bg.highlight};
|
||||||
|
background: ${theme.colors.bg.highlight100};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
box-shadow: 0 0 3px 2px ${theme.colors.bg.highlight100};
|
||||||
|
background: ${theme.colors.bg.highlight100};
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export { Card };
|
||||||
30
packages/ui/src/base/dialog/index.stories.tsx
Normal file
30
packages/ui/src/base/dialog/index.stories.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { StoryObj, Meta } from '@storybook/react';
|
||||||
|
import { Dialog } from '.';
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof Dialog>;
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Interface/Dialog',
|
||||||
|
component: Dialog,
|
||||||
|
} satisfies Meta<typeof Dialog>;
|
||||||
|
|
||||||
|
const docs: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Dialog>
|
||||||
|
<Dialog.Trigger asChild>
|
||||||
|
<button>Open Dialog</button>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay />
|
||||||
|
<Dialog.Content>
|
||||||
|
<Dialog.Title>Dialog Title</Dialog.Title>
|
||||||
|
<Dialog.Description>Dialog Description</Dialog.Description>
|
||||||
|
<Dialog.CloseButton />
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
export { docs };
|
||||||
102
packages/ui/src/base/dialog/index.tsx
Normal file
102
packages/ui/src/base/dialog/index.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import * as DialogPrimitives from '@radix-ui/react-dialog';
|
||||||
|
import React, { forwardRef } from 'react';
|
||||||
|
import { styled } from 'styled-components';
|
||||||
|
import { FiX } from 'react-icons/fi';
|
||||||
|
import { styles } from '../../typography';
|
||||||
|
import { View } from '../view';
|
||||||
|
|
||||||
|
const Root = styled(DialogPrimitives.Root)``;
|
||||||
|
|
||||||
|
const Overlay = styled(DialogPrimitives.Overlay)`
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Portal = styled(DialogPrimitives.Portal)``;
|
||||||
|
|
||||||
|
const Content = styled(DialogPrimitives.Content)`
|
||||||
|
z-index: 1000;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: ${({ theme }) => theme.colors.bg.base100};
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px,
|
||||||
|
hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 90vw;
|
||||||
|
max-width: 450px;
|
||||||
|
max-height: 85vh;
|
||||||
|
padding: 25px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Title = styled(DialogPrimitives.Title)`
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 17px;
|
||||||
|
${styles.dialogTitle}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Description = styled(DialogPrimitives.Description)`
|
||||||
|
margin: 10px 0 20px;
|
||||||
|
color: var(--mauve11);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Trigger = styled(DialogPrimitives.Trigger)``;
|
||||||
|
|
||||||
|
const Close = styled(DialogPrimitives.Close)`
|
||||||
|
all: unset;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CloseButtonWrapper = styled(DialogPrimitives.Close)`
|
||||||
|
all: unset;
|
||||||
|
font-family: inherit;
|
||||||
|
border-radius: 100%;
|
||||||
|
height: 25px;
|
||||||
|
width: 25px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--violet4);
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 2px var(--violet7);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Buttons = styled(View)`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CloseButton = forwardRef<
|
||||||
|
React.ComponentProps<typeof CloseButtonWrapper>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CloseButtonWrapper>
|
||||||
|
>((props, forwardedRef) => (
|
||||||
|
<CloseButtonWrapper {...props} ref={forwardedRef as any}>
|
||||||
|
<FiX />
|
||||||
|
</CloseButtonWrapper>
|
||||||
|
));
|
||||||
|
|
||||||
|
const Dialog = Object.assign(Root, {
|
||||||
|
Overlay,
|
||||||
|
Portal,
|
||||||
|
Content,
|
||||||
|
Title,
|
||||||
|
Description,
|
||||||
|
Trigger,
|
||||||
|
CloseButton,
|
||||||
|
Close,
|
||||||
|
Buttons,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { Dialog };
|
||||||
43
packages/ui/src/base/dropdown/index.stories.tsx
Normal file
43
packages/ui/src/base/dropdown/index.stories.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { StoryObj, Meta } from '@storybook/react';
|
||||||
|
import { DropdownMenu } from '.';
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof DropdownMenu>;
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/DropDown',
|
||||||
|
component: DropdownMenu,
|
||||||
|
} satisfies Meta<typeof DropdownMenu>;
|
||||||
|
|
||||||
|
const docs: Story = {
|
||||||
|
render: () => (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<button>Open Dialog</button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
Item 1<DropdownMenu.RightSlot>Foo</DropdownMenu.RightSlot>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Sub>
|
||||||
|
<DropdownMenu.SubTrigger>
|
||||||
|
Item 2<DropdownMenu.RightSlot>⌘+A</DropdownMenu.RightSlot>
|
||||||
|
</DropdownMenu.SubTrigger>
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.SubContent sideOffset={2} alignOffset={-5}>
|
||||||
|
<DropdownMenu.Item>Sub Item 1</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item>Sub Item 2</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item>Sub Item 3</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.SubContent>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu.Sub>
|
||||||
|
<DropdownMenu.Arrow />
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
export { docs };
|
||||||
162
packages/ui/src/base/dropdown/index.tsx
Normal file
162
packages/ui/src/base/dropdown/index.tsx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import * as DropdownMenuPrimitives from '@radix-ui/react-dropdown-menu';
|
||||||
|
import { styled, css } from 'styled-components';
|
||||||
|
|
||||||
|
const RightSlot = styled.div`
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 20px;
|
||||||
|
color: var(--mauve11);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const content = css`
|
||||||
|
min-width: 220px;
|
||||||
|
background: ${({ theme }) => theme.colors.bg.base100};
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px;
|
||||||
|
box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35),
|
||||||
|
0px 10px 20px -15px rgba(22, 23, 24, 0.2);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const item = css`
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 25px;
|
||||||
|
padding: 0 5px;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 25px;
|
||||||
|
user-select: none;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&[data-state='disabled'] {
|
||||||
|
color: ${({ theme }) => theme.colors.text.disabled};
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-highlighted] {
|
||||||
|
background-color: ${({ theme }) => theme.colors.bg.highlight};
|
||||||
|
color: ${({ theme }) => theme.colors.text.highlight};
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-highlighted] > ${RightSlot} {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-disabled] ${RightSlot} {
|
||||||
|
color: var(--mauve8);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Root = styled(DropdownMenuPrimitives.Root)``;
|
||||||
|
|
||||||
|
const Content = styled(DropdownMenuPrimitives.Content)`
|
||||||
|
${content}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Trigger = styled(DropdownMenuPrimitives.Trigger)`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Portal = styled(DropdownMenuPrimitives.Portal)``;
|
||||||
|
|
||||||
|
const Item = styled(DropdownMenuPrimitives.Item)`
|
||||||
|
${item}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Sub = styled(DropdownMenuPrimitives.Sub)``;
|
||||||
|
|
||||||
|
const SubTrigger = styled(DropdownMenuPrimitives.SubTrigger)`
|
||||||
|
&[data-state='open'] {
|
||||||
|
background-color: ${({ theme }) => theme.colors.bg.highlight100};
|
||||||
|
}
|
||||||
|
|
||||||
|
${item}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Icon = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 25px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SubContent = styled(DropdownMenuPrimitives.SubContent)`
|
||||||
|
${content}
|
||||||
|
min-width: 220px;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px;
|
||||||
|
box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35),
|
||||||
|
0px 10px 20px -15px rgba(22, 23, 24, 0.2);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Separator = styled(DropdownMenuPrimitives.Separator)`
|
||||||
|
height: 1px;
|
||||||
|
background-color: ${({ theme }) => theme.colors.bg.highlight100};
|
||||||
|
margin: 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CheckboxItem = styled(DropdownMenuPrimitives.CheckboxItem)``;
|
||||||
|
|
||||||
|
const ItemIndicator = styled(DropdownMenuPrimitives.ItemIndicator)`
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 25px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Label = styled(DropdownMenuPrimitives.Label)`
|
||||||
|
padding-left: 25px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 25px;
|
||||||
|
color: var(--mauve11);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RadioGroup = styled(DropdownMenuPrimitives.RadioGroup)``;
|
||||||
|
|
||||||
|
const RadioItem = styled(DropdownMenuPrimitives.RadioItem)`
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--violet11);
|
||||||
|
border-radius: 3px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 25px;
|
||||||
|
padding: 0 5px;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 25px;
|
||||||
|
user-select: none;
|
||||||
|
outline: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Arrow = styled(DropdownMenuPrimitives.Arrow)`
|
||||||
|
fill: ${({ theme }) => theme.colors.bg.base100};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DropdownMenu = Object.assign(Root, {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Trigger,
|
||||||
|
Portal,
|
||||||
|
Item,
|
||||||
|
Sub,
|
||||||
|
SubTrigger,
|
||||||
|
SubContent,
|
||||||
|
Separator,
|
||||||
|
CheckboxItem,
|
||||||
|
ItemIndicator,
|
||||||
|
RightSlot,
|
||||||
|
Label,
|
||||||
|
RadioGroup,
|
||||||
|
RadioItem,
|
||||||
|
Arrow,
|
||||||
|
Icon,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { DropdownMenu };
|
||||||
9
packages/ui/src/base/index.ts
Normal file
9
packages/ui/src/base/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export * from './card';
|
||||||
|
export * from './panel';
|
||||||
|
export * from './row';
|
||||||
|
export * from './view';
|
||||||
|
export * from './avatar';
|
||||||
|
export * from './dialog';
|
||||||
|
export * from './list';
|
||||||
|
export * from './dropdown';
|
||||||
|
export * from './button';
|
||||||
15
packages/ui/src/base/list/index.tsx
Normal file
15
packages/ui/src/base/list/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { View } from '../view';
|
||||||
|
|
||||||
|
type ListProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const List = ({ children }: ListProps) => {
|
||||||
|
return (
|
||||||
|
<View $fc $gap="sm" $p="sm">
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { List };
|
||||||
11
packages/ui/src/base/panel/index.module.scss
Normal file
11
packages/ui/src/base/panel/index.module.scss
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.root {
|
||||||
|
@apply bg-white rounded-lg shadow-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@apply text-2xl font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
@apply p-4;
|
||||||
|
}
|
||||||
16
packages/ui/src/base/panel/index.tsx
Normal file
16
packages/ui/src/base/panel/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
type PanelProps = {
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Panel: React.FC<PanelProps> = ({ title, children, className }) => {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<h1>{title}</h1>
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Panel };
|
||||||
13
packages/ui/src/base/row/index.tsx
Normal file
13
packages/ui/src/base/row/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
type RowProps = {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Row: React.FC<RowProps> = ({ title }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold">{title}</h3>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Row };
|
||||||
68
packages/ui/src/base/tabs/index.tsx
Normal file
68
packages/ui/src/base/tabs/index.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import * as TabsPrimities from '@radix-ui/react-tabs';
|
||||||
|
import { FiX } from 'react-icons/fi';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const Root = styled(TabsPrimities.Root)`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const List = styled(TabsPrimities.List)`
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid var(--mauve6);
|
||||||
|
overflow-x: auto;
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.colors.bg.base100};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Trigger = styled(TabsPrimities.Trigger)`
|
||||||
|
font-family: inherit;
|
||||||
|
padding: 0 20px;
|
||||||
|
height: 45px;
|
||||||
|
max-width: 200px;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1;
|
||||||
|
user-select: none;
|
||||||
|
border-right: 1px solid ${({ theme }) => theme.colors.bg.base100};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: ${({ theme }) => theme.colors.bg.highlight};
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-state='active'] {
|
||||||
|
color: ${({ theme }) => theme.colors.bg.highlight};
|
||||||
|
box-shadow: inset 0 -1px 0 0 currentColor, 0 2px 0 0 currentColor;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Content = styled(TabsPrimities.Content)`
|
||||||
|
flex-grow: 1;
|
||||||
|
outline: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CloseWrapper = styled.div``;
|
||||||
|
|
||||||
|
type CloseProps = {
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Close: React.FC<CloseProps> = ({ onClick }) => (
|
||||||
|
<CloseWrapper onClick={onClick}>
|
||||||
|
<FiX />
|
||||||
|
</CloseWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Tabs = Object.assign(Root, {
|
||||||
|
List,
|
||||||
|
Trigger,
|
||||||
|
Content,
|
||||||
|
Close,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { Tabs };
|
||||||
91
packages/ui/src/base/view/index.tsx
Normal file
91
packages/ui/src/base/view/index.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { styled } from 'styled-components';
|
||||||
|
import { Theme } from '../../theme';
|
||||||
|
|
||||||
|
type SizeKey = keyof Theme['space'];
|
||||||
|
type BgKey = keyof Theme['colors']['bg'];
|
||||||
|
type ColorKey = keyof Theme['colors']['text'];
|
||||||
|
|
||||||
|
const getSize = (
|
||||||
|
theme: Theme,
|
||||||
|
sizes: (SizeKey | undefined)[],
|
||||||
|
multi: number = 1,
|
||||||
|
) => {
|
||||||
|
while (sizes.length) {
|
||||||
|
const size = sizes.shift();
|
||||||
|
if (size) {
|
||||||
|
const value = theme.space[size];
|
||||||
|
if (value) {
|
||||||
|
return `${value * multi}${theme.units.space}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '0';
|
||||||
|
};
|
||||||
|
|
||||||
|
const View = styled.div<{
|
||||||
|
$br?: boolean;
|
||||||
|
$bg?: BgKey;
|
||||||
|
$m?: SizeKey;
|
||||||
|
$mt?: SizeKey;
|
||||||
|
$mr?: SizeKey;
|
||||||
|
$mb?: SizeKey;
|
||||||
|
$ml?: SizeKey;
|
||||||
|
$mx?: SizeKey;
|
||||||
|
$my?: SizeKey;
|
||||||
|
$mm?: number;
|
||||||
|
$c?: ColorKey;
|
||||||
|
$p?: SizeKey;
|
||||||
|
$pt?: SizeKey;
|
||||||
|
$pr?: SizeKey;
|
||||||
|
$pb?: SizeKey;
|
||||||
|
$pl?: SizeKey;
|
||||||
|
$px?: SizeKey;
|
||||||
|
$py?: SizeKey;
|
||||||
|
$pm?: number;
|
||||||
|
$fr?: boolean;
|
||||||
|
$fc?: boolean;
|
||||||
|
$u?: boolean;
|
||||||
|
$gap?: SizeKey;
|
||||||
|
$flexWrap?: boolean;
|
||||||
|
$f?: number;
|
||||||
|
$items?: 'center' | 'flex-start' | 'flex-end' | 'stretch' | 'baseline';
|
||||||
|
$justify?:
|
||||||
|
| 'center'
|
||||||
|
| 'flex-start'
|
||||||
|
| 'flex-end'
|
||||||
|
| 'space-between'
|
||||||
|
| 'space-around'
|
||||||
|
| 'space-evenly'
|
||||||
|
| 'stretch';
|
||||||
|
}>`
|
||||||
|
${({ $u }) => $u && 'all: unset;'}
|
||||||
|
${({ $br }) => $br && 'border-radius: 5px;'}
|
||||||
|
${({ $gap, theme }) =>
|
||||||
|
$gap && `gap: ${theme.space[$gap]}${theme.units.space};`}
|
||||||
|
${({ $c, theme }) => $c && `color: ${theme.colors.text[$c]};`}
|
||||||
|
${({ $bg, theme }) => $bg && `background-color: ${theme.colors.bg[$bg]};`}
|
||||||
|
margin-top: ${({ theme, $mt, $my, $m, $mm }) =>
|
||||||
|
getSize(theme, [$mt, $my, $m], $mm)};
|
||||||
|
margin-right: ${({ theme, $mr, $mx, $m, $mm }) =>
|
||||||
|
getSize(theme, [$mr, $mx, $m], $mm)};
|
||||||
|
margin-bottom: ${({ theme, $mb, $my, $m, $mm }) =>
|
||||||
|
getSize(theme, [$mb, $my, $m], $mm)};
|
||||||
|
margin-left: ${({ theme, $ml, $mx, $m, $mm }) =>
|
||||||
|
getSize(theme, [$ml, $mx, $m], $mm)};
|
||||||
|
padding-top: ${({ theme, $pt, $py, $p, $pm }) =>
|
||||||
|
getSize(theme, [$pt, $py, $p], $pm)};
|
||||||
|
padding-right: ${({ theme, $pr, $px, $p, $pm }) =>
|
||||||
|
getSize(theme, [$pr, $px, $p], $pm)};
|
||||||
|
padding-bottom: ${({ theme, $pb, $py, $p, $pm }) =>
|
||||||
|
getSize(theme, [$pb, $py, $p], $pm)};
|
||||||
|
padding-left: ${({ theme, $pl, $px, $p, $pm }) =>
|
||||||
|
getSize(theme, [$pl, $px, $p], $pm)};
|
||||||
|
${({ $fr }) => $fr && 'display: flex;'}
|
||||||
|
${({ $fc }) => $fc && 'flex-direction: column; display: flex;'}
|
||||||
|
${({ $f }) => $f && `flex: ${$f};`}
|
||||||
|
${({ $flexWrap }) => $flexWrap && 'flex-wrap: wrap;'}
|
||||||
|
${({ $items }) => $items && `align-items: ${$items};`}
|
||||||
|
${({ $justify }) => $justify && `justify-content: ${$justify};`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export { View };
|
||||||
35
packages/ui/src/chat/compose/index.tsx
Normal file
35
packages/ui/src/chat/compose/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { styled } from 'styled-components';
|
||||||
|
import { View } from '../../base';
|
||||||
|
|
||||||
|
type ComposeProps = {
|
||||||
|
value: string;
|
||||||
|
onValueChange: (value: string) => void;
|
||||||
|
onSend?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Input = styled(View)`
|
||||||
|
background: ${({ theme }) => theme.colors.bg.highlight100};
|
||||||
|
padding: ${({ theme }) => `${theme.space.sm}${theme.units.space}`};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Send = styled.button`
|
||||||
|
all: unset;
|
||||||
|
background: ${({ theme }) => theme.colors.bg.highlight};
|
||||||
|
padding: ${({ theme }) => `${theme.space.sm}${theme.units.space}`};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Compose: React.FC<ComposeProps> = ({ value, onValueChange, onSend }) => (
|
||||||
|
<View $fr>
|
||||||
|
<Input
|
||||||
|
$f={1}
|
||||||
|
$u
|
||||||
|
placeholder="Type a message..."
|
||||||
|
as="input"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onValueChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
{!!onSend && <Send onClick={onSend}>Send</Send>}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { Compose };
|
||||||
2
packages/ui/src/chat/index.ts
Normal file
2
packages/ui/src/chat/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './message';
|
||||||
|
export * from './compose';
|
||||||
26
packages/ui/src/chat/message/index.stories.tsx
Normal file
26
packages/ui/src/chat/message/index.stories.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { StoryFn, Meta } from '@storybook/react';
|
||||||
|
import { Message } from './index';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Chat/Message',
|
||||||
|
component: Message,
|
||||||
|
} satisfies Meta<typeof Message>;
|
||||||
|
|
||||||
|
type Story = StoryFn<typeof Message>;
|
||||||
|
|
||||||
|
const Normal: Story = {
|
||||||
|
args: {
|
||||||
|
message: {
|
||||||
|
text: 'Hello World',
|
||||||
|
timestamp: new Date('2023-01-01T00:00:00.000Z'),
|
||||||
|
sender: {
|
||||||
|
avatar: 'https://avatars.githubusercontent.com/u/10047061?v=4',
|
||||||
|
name: 'John Doe',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onPress: () => {},
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
export { Normal };
|
||||||
|
export default meta;
|
||||||
71
packages/ui/src/chat/message/index.tsx
Normal file
71
packages/ui/src/chat/message/index.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Avatar, View } from '../../base';
|
||||||
|
import { Typography } from '../../typography';
|
||||||
|
import { formatRelativeTime } from '../../utils/time';
|
||||||
|
|
||||||
|
type MessageProps = {
|
||||||
|
message: {
|
||||||
|
text: React.ReactNode;
|
||||||
|
timestamp: Date;
|
||||||
|
sender: {
|
||||||
|
avatar?: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
onPress?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MessageContainer = styled(View)`
|
||||||
|
background-color: ${({ theme }) => theme.colors.bg.highlight100};
|
||||||
|
margin-bottom: 15px;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ArrowDown = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
bottom: -10px;
|
||||||
|
left: 30px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 10px solid transparent;
|
||||||
|
border-right: 10px solid transparent;
|
||||||
|
border-top: 10px solid ${({ theme }) => theme.colors.bg.highlight100};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Message: React.FC<MessageProps> = ({ message, onPress }) => {
|
||||||
|
const [time, setTime] = useState<string>(
|
||||||
|
formatRelativeTime(message.timestamp),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setTime(formatRelativeTime(message.timestamp));
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<MessageContainer $p="md" onClick={onPress}>
|
||||||
|
{message.text}
|
||||||
|
<Typography variant="tiny">{time}</Typography>
|
||||||
|
<ArrowDown />
|
||||||
|
</MessageContainer>
|
||||||
|
<View $fr $gap="sm" $items="center" $px="md">
|
||||||
|
<Avatar
|
||||||
|
size="sm"
|
||||||
|
url={message.sender.avatar}
|
||||||
|
name={message.sender.name}
|
||||||
|
/>
|
||||||
|
<View>
|
||||||
|
<Typography variant="overline">{message.sender.name}</Typography>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Message };
|
||||||
237
packages/ui/src/github/action/data.json
Normal file
237
packages/ui/src/github/action/data.json
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
{
|
||||||
|
"id": 30433642,
|
||||||
|
"name": "Build",
|
||||||
|
"node_id": "MDEyOldvcmtmbG93IFJ1bjI2OTI4OQ==",
|
||||||
|
"check_suite_id": 42,
|
||||||
|
"check_suite_node_id": "MDEwOkNoZWNrU3VpdGU0Mg==",
|
||||||
|
"head_branch": "main",
|
||||||
|
"head_sha": "acb5820ced9479c074f688cc328bf03f341a511d",
|
||||||
|
"path": ".github/workflows/build.yml@main",
|
||||||
|
"run_number": 562,
|
||||||
|
"event": "push",
|
||||||
|
"display_title": "Update README.md",
|
||||||
|
"status": "queued",
|
||||||
|
"conclusion": null,
|
||||||
|
"workflow_id": 159038,
|
||||||
|
"url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642",
|
||||||
|
"html_url": "https://github.com/octo-org/octo-repo/actions/runs/30433642",
|
||||||
|
"pull_requests": [],
|
||||||
|
"created_at": "2020-01-22T19:33:08Z",
|
||||||
|
"updated_at": "2020-01-22T19:33:08Z",
|
||||||
|
"actor": {
|
||||||
|
"login": "octocat",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VXNlcjE=",
|
||||||
|
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/octocat",
|
||||||
|
"html_url": "https://github.com/octocat",
|
||||||
|
"followers_url": "https://api.github.com/users/octocat/followers",
|
||||||
|
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/octocat/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/octocat/repos",
|
||||||
|
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/octocat/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"run_attempt": 1,
|
||||||
|
"referenced_workflows": [
|
||||||
|
{
|
||||||
|
"path": "octocat/Hello-World/.github/workflows/deploy.yml@main",
|
||||||
|
"sha": "86e8bc9ecf7d38b1ed2d2cfb8eb87ba9b35b01db",
|
||||||
|
"ref": "refs/heads/main"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "octo-org/octo-repo/.github/workflows/report.yml@v2",
|
||||||
|
"sha": "79e9790903e1c3373b1a3e3a941d57405478a232",
|
||||||
|
"ref": "refs/tags/v2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "octo-org/octo-repo/.github/workflows/secure.yml@1595d4b6de6a9e9751fb270a41019ce507d4099e",
|
||||||
|
"sha": "1595d4b6de6a9e9751fb270a41019ce507d4099e"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"run_started_at": "2020-01-22T19:33:08Z",
|
||||||
|
"triggering_actor": {
|
||||||
|
"login": "octocat",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VXNlcjE=",
|
||||||
|
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/octocat",
|
||||||
|
"html_url": "https://github.com/octocat",
|
||||||
|
"followers_url": "https://api.github.com/users/octocat/followers",
|
||||||
|
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/octocat/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/octocat/repos",
|
||||||
|
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/octocat/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"jobs_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/jobs",
|
||||||
|
"logs_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/logs",
|
||||||
|
"check_suite_url": "https://api.github.com/repos/octo-org/octo-repo/check-suites/414944374",
|
||||||
|
"artifacts_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/artifacts",
|
||||||
|
"cancel_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/cancel",
|
||||||
|
"rerun_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/rerun",
|
||||||
|
"previous_attempt_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/attempts/1",
|
||||||
|
"workflow_url": "https://api.github.com/repos/octo-org/octo-repo/actions/workflows/159038",
|
||||||
|
"head_commit": {
|
||||||
|
"id": "acb5820ced9479c074f688cc328bf03f341a511d",
|
||||||
|
"tree_id": "d23f6eedb1e1b9610bbc754ddb5197bfe7271223",
|
||||||
|
"message": "Create linter.yaml",
|
||||||
|
"timestamp": "2020-01-22T19:33:05Z",
|
||||||
|
"author": {
|
||||||
|
"name": "Octo Cat",
|
||||||
|
"email": "octocat@github.com"
|
||||||
|
},
|
||||||
|
"committer": {
|
||||||
|
"name": "GitHub",
|
||||||
|
"email": "noreply@github.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"id": 1296269,
|
||||||
|
"node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5",
|
||||||
|
"name": "Hello-World",
|
||||||
|
"full_name": "octocat/Hello-World",
|
||||||
|
"owner": {
|
||||||
|
"login": "octocat",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VXNlcjE=",
|
||||||
|
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/octocat",
|
||||||
|
"html_url": "https://github.com/octocat",
|
||||||
|
"followers_url": "https://api.github.com/users/octocat/followers",
|
||||||
|
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/octocat/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/octocat/repos",
|
||||||
|
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/octocat/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"private": false,
|
||||||
|
"html_url": "https://github.com/octocat/Hello-World",
|
||||||
|
"description": "This your first repo!",
|
||||||
|
"fork": false,
|
||||||
|
"url": "https://api.github.com/repos/octocat/Hello-World",
|
||||||
|
"archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}",
|
||||||
|
"assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}",
|
||||||
|
"blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}",
|
||||||
|
"branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}",
|
||||||
|
"collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}",
|
||||||
|
"comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}",
|
||||||
|
"commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}",
|
||||||
|
"compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}",
|
||||||
|
"contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}",
|
||||||
|
"contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors",
|
||||||
|
"deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments",
|
||||||
|
"downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads",
|
||||||
|
"events_url": "https://api.github.com/repos/octocat/Hello-World/events",
|
||||||
|
"forks_url": "https://api.github.com/repos/octocat/Hello-World/forks",
|
||||||
|
"git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}",
|
||||||
|
"git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}",
|
||||||
|
"git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}",
|
||||||
|
"git_url": "git:github.com/octocat/Hello-World.git",
|
||||||
|
"issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}",
|
||||||
|
"issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}",
|
||||||
|
"issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}",
|
||||||
|
"keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}",
|
||||||
|
"labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}",
|
||||||
|
"languages_url": "https://api.github.com/repos/octocat/Hello-World/languages",
|
||||||
|
"merges_url": "https://api.github.com/repos/octocat/Hello-World/merges",
|
||||||
|
"milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}",
|
||||||
|
"notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}",
|
||||||
|
"pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}",
|
||||||
|
"releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}",
|
||||||
|
"ssh_url": "git@github.com:octocat/Hello-World.git",
|
||||||
|
"stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers",
|
||||||
|
"statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}",
|
||||||
|
"subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers",
|
||||||
|
"subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription",
|
||||||
|
"tags_url": "https://api.github.com/repos/octocat/Hello-World/tags",
|
||||||
|
"teams_url": "https://api.github.com/repos/octocat/Hello-World/teams",
|
||||||
|
"trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}",
|
||||||
|
"hooks_url": "http://api.github.com/repos/octocat/Hello-World/hooks"
|
||||||
|
},
|
||||||
|
"head_repository": {
|
||||||
|
"id": 217723378,
|
||||||
|
"node_id": "MDEwOlJlcG9zaXRvcnkyMTc3MjMzNzg=",
|
||||||
|
"name": "octo-repo",
|
||||||
|
"full_name": "octo-org/octo-repo",
|
||||||
|
"private": true,
|
||||||
|
"owner": {
|
||||||
|
"login": "octocat",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VXNlcjE=",
|
||||||
|
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/octocat",
|
||||||
|
"html_url": "https://github.com/octocat",
|
||||||
|
"followers_url": "https://api.github.com/users/octocat/followers",
|
||||||
|
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/octocat/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/octocat/repos",
|
||||||
|
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/octocat/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"html_url": "https://github.com/octo-org/octo-repo",
|
||||||
|
"description": null,
|
||||||
|
"fork": false,
|
||||||
|
"url": "https://api.github.com/repos/octo-org/octo-repo",
|
||||||
|
"forks_url": "https://api.github.com/repos/octo-org/octo-repo/forks",
|
||||||
|
"keys_url": "https://api.github.com/repos/octo-org/octo-repo/keys{/key_id}",
|
||||||
|
"collaborators_url": "https://api.github.com/repos/octo-org/octo-repo/collaborators{/collaborator}",
|
||||||
|
"teams_url": "https://api.github.com/repos/octo-org/octo-repo/teams",
|
||||||
|
"hooks_url": "https://api.github.com/repos/octo-org/octo-repo/hooks",
|
||||||
|
"issue_events_url": "https://api.github.com/repos/octo-org/octo-repo/issues/events{/number}",
|
||||||
|
"events_url": "https://api.github.com/repos/octo-org/octo-repo/events",
|
||||||
|
"assignees_url": "https://api.github.com/repos/octo-org/octo-repo/assignees{/user}",
|
||||||
|
"branches_url": "https://api.github.com/repos/octo-org/octo-repo/branches{/branch}",
|
||||||
|
"tags_url": "https://api.github.com/repos/octo-org/octo-repo/tags",
|
||||||
|
"blobs_url": "https://api.github.com/repos/octo-org/octo-repo/git/blobs{/sha}",
|
||||||
|
"git_tags_url": "https://api.github.com/repos/octo-org/octo-repo/git/tags{/sha}",
|
||||||
|
"git_refs_url": "https://api.github.com/repos/octo-org/octo-repo/git/refs{/sha}",
|
||||||
|
"trees_url": "https://api.github.com/repos/octo-org/octo-repo/git/trees{/sha}",
|
||||||
|
"statuses_url": "https://api.github.com/repos/octo-org/octo-repo/statuses/{sha}",
|
||||||
|
"languages_url": "https://api.github.com/repos/octo-org/octo-repo/languages",
|
||||||
|
"stargazers_url": "https://api.github.com/repos/octo-org/octo-repo/stargazers",
|
||||||
|
"contributors_url": "https://api.github.com/repos/octo-org/octo-repo/contributors",
|
||||||
|
"subscribers_url": "https://api.github.com/repos/octo-org/octo-repo/subscribers",
|
||||||
|
"subscription_url": "https://api.github.com/repos/octo-org/octo-repo/subscription",
|
||||||
|
"commits_url": "https://api.github.com/repos/octo-org/octo-repo/commits{/sha}",
|
||||||
|
"git_commits_url": "https://api.github.com/repos/octo-org/octo-repo/git/commits{/sha}",
|
||||||
|
"comments_url": "https://api.github.com/repos/octo-org/octo-repo/comments{/number}",
|
||||||
|
"issue_comment_url": "https://api.github.com/repos/octo-org/octo-repo/issues/comments{/number}",
|
||||||
|
"contents_url": "https://api.github.com/repos/octo-org/octo-repo/contents/{+path}",
|
||||||
|
"compare_url": "https://api.github.com/repos/octo-org/octo-repo/compare/{base}...{head}",
|
||||||
|
"merges_url": "https://api.github.com/repos/octo-org/octo-repo/merges",
|
||||||
|
"archive_url": "https://api.github.com/repos/octo-org/octo-repo/{archive_format}{/ref}",
|
||||||
|
"downloads_url": "https://api.github.com/repos/octo-org/octo-repo/downloads",
|
||||||
|
"issues_url": "https://api.github.com/repos/octo-org/octo-repo/issues{/number}",
|
||||||
|
"pulls_url": "https://api.github.com/repos/octo-org/octo-repo/pulls{/number}",
|
||||||
|
"milestones_url": "https://api.github.com/repos/octo-org/octo-repo/milestones{/number}",
|
||||||
|
"notifications_url": "https://api.github.com/repos/octo-org/octo-repo/notifications{?since,all,participating}",
|
||||||
|
"labels_url": "https://api.github.com/repos/octo-org/octo-repo/labels{/name}",
|
||||||
|
"releases_url": "https://api.github.com/repos/octo-org/octo-repo/releases{/id}",
|
||||||
|
"deployments_url": "https://api.github.com/repos/octo-org/octo-repo/deployments"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
packages/ui/src/github/action/index.stories.tsx
Normal file
20
packages/ui/src/github/action/index.stories.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { StoryFn, Meta } from '@storybook/react';
|
||||||
|
import { Action } from './index';
|
||||||
|
import action from './data.json';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'GitHub/Action',
|
||||||
|
component: Action,
|
||||||
|
} satisfies Meta<typeof Action>;
|
||||||
|
|
||||||
|
type Story = StoryFn<typeof Action>;
|
||||||
|
|
||||||
|
const Normal: Story = {
|
||||||
|
args: {
|
||||||
|
action: action,
|
||||||
|
onPress: () => {},
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
export { Normal };
|
||||||
|
export default meta;
|
||||||
56
packages/ui/src/github/action/index.tsx
Normal file
56
packages/ui/src/github/action/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { GithubTypes } from '@refocus/sdk';
|
||||||
|
import {
|
||||||
|
IoCheckmarkDoneCircleOutline,
|
||||||
|
IoCloseCircleOutline,
|
||||||
|
} from 'react-icons/io5';
|
||||||
|
import { RxTimer } from 'react-icons/rx';
|
||||||
|
import { FiPlayCircle } from 'react-icons/fi';
|
||||||
|
import { GoQuestion } from 'react-icons/go';
|
||||||
|
import { Avatar, Card, View } from '../../base';
|
||||||
|
import { Typography } from '../../typography';
|
||||||
|
|
||||||
|
type ActionProps = {
|
||||||
|
action: GithubTypes.WorkflowRun;
|
||||||
|
onPress?: (action: GithubTypes.WorkflowRun) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIcon = (status: string | null) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'success':
|
||||||
|
return <IoCheckmarkDoneCircleOutline size={48} color="green" />;
|
||||||
|
case 'failure':
|
||||||
|
return <IoCloseCircleOutline size={48} color="red" />;
|
||||||
|
case 'in_progress':
|
||||||
|
return <FiPlayCircle size={48} />;
|
||||||
|
case 'queued':
|
||||||
|
return <RxTimer size={48} />;
|
||||||
|
default:
|
||||||
|
return <GoQuestion size={48} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Action: React.FC<ActionProps> = ({ action, onPress }) => {
|
||||||
|
const onPressHandler = useCallback(() => {
|
||||||
|
onPress?.(action);
|
||||||
|
}, [action, onPress]);
|
||||||
|
return (
|
||||||
|
<Card $fr $items="center" $p="md" $gap="md" onClick={onPressHandler}>
|
||||||
|
<Avatar
|
||||||
|
url={action.actor?.avatar_url}
|
||||||
|
name={action.actor?.name || action.actor?.login}
|
||||||
|
decal={`#${action.run_attempt}`}
|
||||||
|
/>
|
||||||
|
<View $fc $f={1}>
|
||||||
|
<Typography variant="overline">
|
||||||
|
{action.name} - {action.actor?.name || action.actor?.login}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="title">{action.display_title}</Typography>
|
||||||
|
<Typography variant="subtitle">{action.status}</Typography>
|
||||||
|
</View>
|
||||||
|
<View>{getIcon(action.status)}</View>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Action };
|
||||||
5
packages/ui/src/github/index.ts
Normal file
5
packages/ui/src/github/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './notification';
|
||||||
|
export * from './profile';
|
||||||
|
export * from './pull-request';
|
||||||
|
export * from './login';
|
||||||
|
export * from './not-logged-in';
|
||||||
35
packages/ui/src/github/login/index.tsx
Normal file
35
packages/ui/src/github/login/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { GithubLogin as GithubLoginComponent } from '@refocus/sdk';
|
||||||
|
import { SiGithub } from 'react-icons/si';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { Button, Dialog, View } from '../../base';
|
||||||
|
|
||||||
|
const GithubLogin: GithubLoginComponent = ({ setToken, cancel }) => {
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
const save = useCallback(() => {
|
||||||
|
setToken(value);
|
||||||
|
}, [setToken, value]);
|
||||||
|
return (
|
||||||
|
<Dialog open={true}>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay />
|
||||||
|
<Dialog.Content>
|
||||||
|
<View $fc $gap="md">
|
||||||
|
<View
|
||||||
|
as="input"
|
||||||
|
$u
|
||||||
|
placeholder="Personal Access Token"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Dialog.Buttons>
|
||||||
|
<Button icon={<SiGithub />} onClick={save} title="Save" />
|
||||||
|
</Dialog.Buttons>
|
||||||
|
</View>
|
||||||
|
<Dialog.CloseButton onClick={cancel} />
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { GithubLogin };
|
||||||
26
packages/ui/src/github/not-logged-in/index.tsx
Normal file
26
packages/ui/src/github/not-logged-in/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { useGithub, useWidget, useWidgetId } from '@refocus/sdk';
|
||||||
|
import { SiGithub } from 'react-icons/si';
|
||||||
|
import { Button, View } from '../../base';
|
||||||
|
import { Typography } from '../../typography';
|
||||||
|
import { styled } from 'styled-components';
|
||||||
|
|
||||||
|
const Description = styled(Typography)`
|
||||||
|
text-align: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const NotLoggedIn: React.FC = () => {
|
||||||
|
const { login } = useGithub();
|
||||||
|
const type = useWidgetId();
|
||||||
|
const widget = useWidget(type);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View $p="md" $fc $items="center" $gap="md">
|
||||||
|
<Description>
|
||||||
|
You need to be logged in to Github to see {widget?.name}
|
||||||
|
</Description>
|
||||||
|
<Button icon={<SiGithub />} onClick={login} title="Login" />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NotLoggedIn };
|
||||||
22
packages/ui/src/github/notification/index.tsx
Normal file
22
packages/ui/src/github/notification/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Card } from '../../base/card';
|
||||||
|
import { AsyncResponse } from '../../utils/types';
|
||||||
|
import { Octokit } from 'octokit';
|
||||||
|
|
||||||
|
type GithubNotification = {
|
||||||
|
notification: AsyncResponse<
|
||||||
|
Octokit['rest']['activity']['listNotificationsForAuthenticatedUser']
|
||||||
|
>['data'][0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const GithubNotification: React.FC<GithubNotification> = ({ notification }) => {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<div>{notification.repository.full_name}</div>
|
||||||
|
<div>{notification.subject.title}</div>
|
||||||
|
<div>{notification.reason}</div>
|
||||||
|
<div>{notification.updated_at}</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { GithubNotification };
|
||||||
45
packages/ui/src/github/profile/index.stories.tsx
Normal file
45
packages/ui/src/github/profile/index.stories.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import type { StoryFn, Meta } from '@storybook/react';
|
||||||
|
import { Profile } from './index';
|
||||||
|
import { GithubTypes } from '@refocus/sdk';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'GitHub/Profile',
|
||||||
|
component: Profile,
|
||||||
|
} satisfies Meta<typeof Profile>;
|
||||||
|
|
||||||
|
type Story = StoryFn<typeof Profile>;
|
||||||
|
|
||||||
|
type ProfileData = Partial<GithubTypes.Profile>;
|
||||||
|
|
||||||
|
const profile: ProfileData = {
|
||||||
|
name: 'John Doe',
|
||||||
|
avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4',
|
||||||
|
login: 'johndoe',
|
||||||
|
};
|
||||||
|
|
||||||
|
const Normal: Story = {
|
||||||
|
args: {
|
||||||
|
profile,
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const WithoutName: Story = {
|
||||||
|
args: {
|
||||||
|
profile: {
|
||||||
|
...profile,
|
||||||
|
name: undefined,
|
||||||
|
} as GithubTypes.Profile,
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const WithoutImage: Story = {
|
||||||
|
args: {
|
||||||
|
profile: {
|
||||||
|
...profile,
|
||||||
|
avatar_url: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
export { Normal, WithoutName, WithoutImage };
|
||||||
|
export default meta;
|
||||||
33
packages/ui/src/github/profile/index.tsx
Normal file
33
packages/ui/src/github/profile/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { GithubTypes } from '@refocus/sdk';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { Avatar, Card, View } from '../../base';
|
||||||
|
import { Typography } from '../../typography';
|
||||||
|
import { LuGithub } from 'react-icons/lu';
|
||||||
|
|
||||||
|
type ProfileProps = {
|
||||||
|
profile: GithubTypes.Profile;
|
||||||
|
onPress?: (profile: GithubTypes.Profile) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Profile: React.FC<ProfileProps> = ({ profile, onPress }) => {
|
||||||
|
const onPressHandler = useCallback(() => {
|
||||||
|
onPress?.(profile);
|
||||||
|
}, [onPress, profile]);
|
||||||
|
return (
|
||||||
|
<Card $fr $items="center" $gap="md" $p="md" onClick={onPressHandler}>
|
||||||
|
<Avatar
|
||||||
|
decal={<LuGithub />}
|
||||||
|
url={profile.avatar_url}
|
||||||
|
name={profile.name || profile.login}
|
||||||
|
/>
|
||||||
|
<View $fr $fc>
|
||||||
|
<Typography variant="title">{profile.name || profile.login}</Typography>
|
||||||
|
{profile.name && profile.name !== profile.login && (
|
||||||
|
<Typography variant="subtitle">{profile.login}</Typography>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Profile };
|
||||||
536
packages/ui/src/github/pull-request/data.json
Normal file
536
packages/ui/src/github/pull-request/data.json
Normal file
@@ -0,0 +1,536 @@
|
|||||||
|
{
|
||||||
|
"url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDExOlB1bGxSZXF1ZXN0MQ==",
|
||||||
|
"html_url": "https://github.com/octocat/Hello-World/pull/1347",
|
||||||
|
"diff_url": "https://github.com/octocat/Hello-World/pull/1347.diff",
|
||||||
|
"patch_url": "https://github.com/octocat/Hello-World/pull/1347.patch",
|
||||||
|
"issue_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347",
|
||||||
|
"commits_url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347/commits",
|
||||||
|
"review_comments_url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347/comments",
|
||||||
|
"review_comment_url": "https://api.github.com/repos/octocat/Hello-World/pulls/comments{/number}",
|
||||||
|
"comments_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/comments",
|
||||||
|
"statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e",
|
||||||
|
"number": 1347,
|
||||||
|
"state": "open",
|
||||||
|
"locked": true,
|
||||||
|
"title": "Amazing new feature",
|
||||||
|
"user": {
|
||||||
|
"login": "octocat",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VXNlcjE=",
|
||||||
|
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/octocat",
|
||||||
|
"html_url": "https://github.com/octocat",
|
||||||
|
"followers_url": "https://api.github.com/users/octocat/followers",
|
||||||
|
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/octocat/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/octocat/repos",
|
||||||
|
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/octocat/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"body": "Please pull these awesome changes in!",
|
||||||
|
"labels": [
|
||||||
|
{
|
||||||
|
"id": 208045946,
|
||||||
|
"node_id": "MDU6TGFiZWwyMDgwNDU5NDY=",
|
||||||
|
"url": "https://api.github.com/repos/octocat/Hello-World/labels/bug",
|
||||||
|
"name": "bug",
|
||||||
|
"description": "Something isn't working",
|
||||||
|
"color": "f29513",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"milestone": {
|
||||||
|
"url": "https://api.github.com/repos/octocat/Hello-World/milestones/1",
|
||||||
|
"html_url": "https://github.com/octocat/Hello-World/milestones/v1.0",
|
||||||
|
"labels_url": "https://api.github.com/repos/octocat/Hello-World/milestones/1/labels",
|
||||||
|
"id": 1002604,
|
||||||
|
"node_id": "MDk6TWlsZXN0b25lMTAwMjYwNA==",
|
||||||
|
"number": 1,
|
||||||
|
"state": "open",
|
||||||
|
"title": "v1.0",
|
||||||
|
"description": "Tracking milestone for version 1.0",
|
||||||
|
"creator": {
|
||||||
|
"login": "octocat",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VXNlcjE=",
|
||||||
|
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/octocat",
|
||||||
|
"html_url": "https://github.com/octocat",
|
||||||
|
"followers_url": "https://api.github.com/users/octocat/followers",
|
||||||
|
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/octocat/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/octocat/repos",
|
||||||
|
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/octocat/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"open_issues": 4,
|
||||||
|
"closed_issues": 8,
|
||||||
|
"created_at": "2011-04-10T20:09:31Z",
|
||||||
|
"updated_at": "2014-03-03T18:58:10Z",
|
||||||
|
"closed_at": "2013-02-12T13:22:01Z",
|
||||||
|
"due_on": "2012-10-09T23:39:01Z"
|
||||||
|
},
|
||||||
|
"active_lock_reason": "too heated",
|
||||||
|
"created_at": "2011-01-26T19:01:12Z",
|
||||||
|
"updated_at": "2011-01-26T19:01:12Z",
|
||||||
|
"closed_at": "2011-01-26T19:01:12Z",
|
||||||
|
"merged_at": "2011-01-26T19:01:12Z",
|
||||||
|
"merge_commit_sha": "e5bd3914e2e596debea16f433f57875b5b90bcd6",
|
||||||
|
"assignee": {
|
||||||
|
"login": "octocat",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VXNlcjE=",
|
||||||
|
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/octocat",
|
||||||
|
"html_url": "https://github.com/octocat",
|
||||||
|
"followers_url": "https://api.github.com/users/octocat/followers",
|
||||||
|
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/octocat/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/octocat/repos",
|
||||||
|
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/octocat/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"assignees": [
|
||||||
|
{
|
||||||
|
"login": "octocat",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VXNlcjE=",
|
||||||
|
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/octocat",
|
||||||
|
"html_url": "https://github.com/octocat",
|
||||||
|
"followers_url": "https://api.github.com/users/octocat/followers",
|
||||||
|
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/octocat/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/octocat/repos",
|
||||||
|
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/octocat/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "hubot",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VXNlcjE=",
|
||||||
|
"avatar_url": "https://github.com/images/error/hubot_happy.gif",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/hubot",
|
||||||
|
"html_url": "https://github.com/hubot",
|
||||||
|
"followers_url": "https://api.github.com/users/hubot/followers",
|
||||||
|
"following_url": "https://api.github.com/users/hubot/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/hubot/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/hubot/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/hubot/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/hubot/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/hubot/repos",
|
||||||
|
"events_url": "https://api.github.com/users/hubot/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/hubot/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requested_reviewers": [
|
||||||
|
{
|
||||||
|
"login": "other_user",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VXNlcjE=",
|
||||||
|
"avatar_url": "https://github.com/images/error/other_user_happy.gif",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/other_user",
|
||||||
|
"html_url": "https://github.com/other_user",
|
||||||
|
"followers_url": "https://api.github.com/users/other_user/followers",
|
||||||
|
"following_url": "https://api.github.com/users/other_user/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/other_user/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/other_user/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/other_user/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/other_user/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/other_user/repos",
|
||||||
|
"events_url": "https://api.github.com/users/other_user/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/other_user/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requested_teams": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VGVhbTE=",
|
||||||
|
"url": "https://api.github.com/teams/1",
|
||||||
|
"html_url": "https://github.com/orgs/github/teams/justice-league",
|
||||||
|
"name": "Justice League",
|
||||||
|
"slug": "justice-league",
|
||||||
|
"description": "A great team.",
|
||||||
|
"privacy": "closed",
|
||||||
|
"notification_setting": "notifications_enabled",
|
||||||
|
"permission": "admin",
|
||||||
|
"members_url": "https://api.github.com/teams/1/members{/member}",
|
||||||
|
"repositories_url": "https://api.github.com/teams/1/repos"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"head": {
|
||||||
|
"label": "octocat:new-topic",
|
||||||
|
"ref": "new-topic",
|
||||||
|
"sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
|
||||||
|
"user": {
|
||||||
|
"login": "octocat",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VXNlcjE=",
|
||||||
|
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/octocat",
|
||||||
|
"html_url": "https://github.com/octocat",
|
||||||
|
"followers_url": "https://api.github.com/users/octocat/followers",
|
||||||
|
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/octocat/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/octocat/repos",
|
||||||
|
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/octocat/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"repo": {
|
||||||
|
"id": 1296269,
|
||||||
|
"node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5",
|
||||||
|
"name": "Hello-World",
|
||||||
|
"full_name": "octocat/Hello-World",
|
||||||
|
"owner": {
|
||||||
|
"login": "octocat",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VXNlcjE=",
|
||||||
|
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/octocat",
|
||||||
|
"html_url": "https://github.com/octocat",
|
||||||
|
"followers_url": "https://api.github.com/users/octocat/followers",
|
||||||
|
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/octocat/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/octocat/repos",
|
||||||
|
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/octocat/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"private": false,
|
||||||
|
"html_url": "https://github.com/octocat/Hello-World",
|
||||||
|
"description": "This your first repo!",
|
||||||
|
"fork": false,
|
||||||
|
"url": "https://api.github.com/repos/octocat/Hello-World",
|
||||||
|
"archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}",
|
||||||
|
"assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}",
|
||||||
|
"blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}",
|
||||||
|
"branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}",
|
||||||
|
"collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}",
|
||||||
|
"comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}",
|
||||||
|
"commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}",
|
||||||
|
"compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}",
|
||||||
|
"contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}",
|
||||||
|
"contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors",
|
||||||
|
"deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments",
|
||||||
|
"downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads",
|
||||||
|
"events_url": "https://api.github.com/repos/octocat/Hello-World/events",
|
||||||
|
"forks_url": "https://api.github.com/repos/octocat/Hello-World/forks",
|
||||||
|
"git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}",
|
||||||
|
"git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}",
|
||||||
|
"git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}",
|
||||||
|
"git_url": "git:github.com/octocat/Hello-World.git",
|
||||||
|
"issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}",
|
||||||
|
"issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}",
|
||||||
|
"issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}",
|
||||||
|
"keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}",
|
||||||
|
"labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}",
|
||||||
|
"languages_url": "https://api.github.com/repos/octocat/Hello-World/languages",
|
||||||
|
"merges_url": "https://api.github.com/repos/octocat/Hello-World/merges",
|
||||||
|
"milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}",
|
||||||
|
"notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}",
|
||||||
|
"pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}",
|
||||||
|
"releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}",
|
||||||
|
"ssh_url": "git@github.com:octocat/Hello-World.git",
|
||||||
|
"stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers",
|
||||||
|
"statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}",
|
||||||
|
"subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers",
|
||||||
|
"subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription",
|
||||||
|
"tags_url": "https://api.github.com/repos/octocat/Hello-World/tags",
|
||||||
|
"teams_url": "https://api.github.com/repos/octocat/Hello-World/teams",
|
||||||
|
"trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}",
|
||||||
|
"clone_url": "https://github.com/octocat/Hello-World.git",
|
||||||
|
"mirror_url": "git:git.example.com/octocat/Hello-World",
|
||||||
|
"hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks",
|
||||||
|
"svn_url": "https://svn.github.com/octocat/Hello-World",
|
||||||
|
"homepage": "https://github.com",
|
||||||
|
"language": null,
|
||||||
|
"forks_count": 9,
|
||||||
|
"stargazers_count": 80,
|
||||||
|
"watchers_count": 80,
|
||||||
|
"size": 108,
|
||||||
|
"default_branch": "master",
|
||||||
|
"open_issues_count": 0,
|
||||||
|
"topics": [
|
||||||
|
"octocat",
|
||||||
|
"atom",
|
||||||
|
"electron",
|
||||||
|
"api"
|
||||||
|
],
|
||||||
|
"has_issues": true,
|
||||||
|
"has_projects": true,
|
||||||
|
"has_wiki": true,
|
||||||
|
"has_pages": false,
|
||||||
|
"has_downloads": true,
|
||||||
|
"has_discussions": false,
|
||||||
|
"archived": false,
|
||||||
|
"disabled": false,
|
||||||
|
"pushed_at": "2011-01-26T19:06:43Z",
|
||||||
|
"created_at": "2011-01-26T19:01:12Z",
|
||||||
|
"updated_at": "2011-01-26T19:14:43Z",
|
||||||
|
"permissions": {
|
||||||
|
"admin": false,
|
||||||
|
"push": false,
|
||||||
|
"pull": true
|
||||||
|
},
|
||||||
|
"allow_rebase_merge": true,
|
||||||
|
"temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O",
|
||||||
|
"allow_squash_merge": true,
|
||||||
|
"allow_merge_commit": true,
|
||||||
|
"allow_forking": true,
|
||||||
|
"forks": 123,
|
||||||
|
"open_issues": 123,
|
||||||
|
"license": {
|
||||||
|
"key": "mit",
|
||||||
|
"name": "MIT License",
|
||||||
|
"url": "https://api.github.com/licenses/mit",
|
||||||
|
"spdx_id": "MIT",
|
||||||
|
"node_id": "MDc6TGljZW5zZW1pdA=="
|
||||||
|
},
|
||||||
|
"watchers": 123
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"base": {
|
||||||
|
"label": "octocat:master",
|
||||||
|
"ref": "master",
|
||||||
|
"sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
|
||||||
|
"user": {
|
||||||
|
"login": "octocat",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VXNlcjE=",
|
||||||
|
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/octocat",
|
||||||
|
"html_url": "https://github.com/octocat",
|
||||||
|
"followers_url": "https://api.github.com/users/octocat/followers",
|
||||||
|
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/octocat/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/octocat/repos",
|
||||||
|
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/octocat/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"repo": {
|
||||||
|
"id": 1296269,
|
||||||
|
"node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5",
|
||||||
|
"name": "Hello-World",
|
||||||
|
"full_name": "octocat/Hello-World",
|
||||||
|
"owner": {
|
||||||
|
"login": "octocat",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VXNlcjE=",
|
||||||
|
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/octocat",
|
||||||
|
"html_url": "https://github.com/octocat",
|
||||||
|
"followers_url": "https://api.github.com/users/octocat/followers",
|
||||||
|
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/octocat/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/octocat/repos",
|
||||||
|
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/octocat/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"private": false,
|
||||||
|
"html_url": "https://github.com/octocat/Hello-World",
|
||||||
|
"description": "This your first repo!",
|
||||||
|
"fork": false,
|
||||||
|
"url": "https://api.github.com/repos/octocat/Hello-World",
|
||||||
|
"archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}",
|
||||||
|
"assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}",
|
||||||
|
"blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}",
|
||||||
|
"branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}",
|
||||||
|
"collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}",
|
||||||
|
"comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}",
|
||||||
|
"commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}",
|
||||||
|
"compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}",
|
||||||
|
"contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}",
|
||||||
|
"contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors",
|
||||||
|
"deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments",
|
||||||
|
"downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads",
|
||||||
|
"events_url": "https://api.github.com/repos/octocat/Hello-World/events",
|
||||||
|
"forks_url": "https://api.github.com/repos/octocat/Hello-World/forks",
|
||||||
|
"git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}",
|
||||||
|
"git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}",
|
||||||
|
"git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}",
|
||||||
|
"git_url": "git:github.com/octocat/Hello-World.git",
|
||||||
|
"issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}",
|
||||||
|
"issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}",
|
||||||
|
"issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}",
|
||||||
|
"keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}",
|
||||||
|
"labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}",
|
||||||
|
"languages_url": "https://api.github.com/repos/octocat/Hello-World/languages",
|
||||||
|
"merges_url": "https://api.github.com/repos/octocat/Hello-World/merges",
|
||||||
|
"milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}",
|
||||||
|
"notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}",
|
||||||
|
"pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}",
|
||||||
|
"releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}",
|
||||||
|
"ssh_url": "git@github.com:octocat/Hello-World.git",
|
||||||
|
"stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers",
|
||||||
|
"statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}",
|
||||||
|
"subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers",
|
||||||
|
"subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription",
|
||||||
|
"tags_url": "https://api.github.com/repos/octocat/Hello-World/tags",
|
||||||
|
"teams_url": "https://api.github.com/repos/octocat/Hello-World/teams",
|
||||||
|
"trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}",
|
||||||
|
"clone_url": "https://github.com/octocat/Hello-World.git",
|
||||||
|
"mirror_url": "git:git.example.com/octocat/Hello-World",
|
||||||
|
"hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks",
|
||||||
|
"svn_url": "https://svn.github.com/octocat/Hello-World",
|
||||||
|
"homepage": "https://github.com",
|
||||||
|
"language": null,
|
||||||
|
"forks_count": 9,
|
||||||
|
"stargazers_count": 80,
|
||||||
|
"watchers_count": 80,
|
||||||
|
"size": 108,
|
||||||
|
"default_branch": "master",
|
||||||
|
"open_issues_count": 0,
|
||||||
|
"topics": [
|
||||||
|
"octocat",
|
||||||
|
"atom",
|
||||||
|
"electron",
|
||||||
|
"api"
|
||||||
|
],
|
||||||
|
"has_issues": true,
|
||||||
|
"has_projects": true,
|
||||||
|
"has_wiki": true,
|
||||||
|
"has_pages": false,
|
||||||
|
"has_downloads": true,
|
||||||
|
"has_discussions": false,
|
||||||
|
"archived": false,
|
||||||
|
"disabled": false,
|
||||||
|
"pushed_at": "2011-01-26T19:06:43Z",
|
||||||
|
"created_at": "2011-01-26T19:01:12Z",
|
||||||
|
"updated_at": "2011-01-26T19:14:43Z",
|
||||||
|
"permissions": {
|
||||||
|
"admin": false,
|
||||||
|
"push": false,
|
||||||
|
"pull": true
|
||||||
|
},
|
||||||
|
"allow_rebase_merge": true,
|
||||||
|
"temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O",
|
||||||
|
"allow_squash_merge": true,
|
||||||
|
"allow_merge_commit": true,
|
||||||
|
"forks": 123,
|
||||||
|
"open_issues": 123,
|
||||||
|
"license": {
|
||||||
|
"key": "mit",
|
||||||
|
"name": "MIT License",
|
||||||
|
"url": "https://api.github.com/licenses/mit",
|
||||||
|
"spdx_id": "MIT",
|
||||||
|
"node_id": "MDc6TGljZW5zZW1pdA=="
|
||||||
|
},
|
||||||
|
"watchers": 123
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_links": {
|
||||||
|
"self": {
|
||||||
|
"href": "https://api.github.com/repos/octocat/Hello-World/pulls/1347"
|
||||||
|
},
|
||||||
|
"html": {
|
||||||
|
"href": "https://github.com/octocat/Hello-World/pull/1347"
|
||||||
|
},
|
||||||
|
"issue": {
|
||||||
|
"href": "https://api.github.com/repos/octocat/Hello-World/issues/1347"
|
||||||
|
},
|
||||||
|
"comments": {
|
||||||
|
"href": "https://api.github.com/repos/octocat/Hello-World/issues/1347/comments"
|
||||||
|
},
|
||||||
|
"review_comments": {
|
||||||
|
"href": "https://api.github.com/repos/octocat/Hello-World/pulls/1347/comments"
|
||||||
|
},
|
||||||
|
"review_comment": {
|
||||||
|
"href": "https://api.github.com/repos/octocat/Hello-World/pulls/comments{/number}"
|
||||||
|
},
|
||||||
|
"commits": {
|
||||||
|
"href": "https://api.github.com/repos/octocat/Hello-World/pulls/1347/commits"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"href": "https://api.github.com/repos/octocat/Hello-World/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"author_association": "OWNER",
|
||||||
|
"auto_merge": null,
|
||||||
|
"draft": false,
|
||||||
|
"merged": false,
|
||||||
|
"mergeable": true,
|
||||||
|
"rebaseable": true,
|
||||||
|
"mergeable_state": "clean",
|
||||||
|
"merged_by": {
|
||||||
|
"login": "octocat",
|
||||||
|
"id": 1,
|
||||||
|
"node_id": "MDQ6VXNlcjE=",
|
||||||
|
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/octocat",
|
||||||
|
"html_url": "https://github.com/octocat",
|
||||||
|
"followers_url": "https://api.github.com/users/octocat/followers",
|
||||||
|
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/octocat/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/octocat/repos",
|
||||||
|
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/octocat/received_events",
|
||||||
|
"type": "User",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"comments": 10,
|
||||||
|
"review_comments": 0,
|
||||||
|
"maintainer_can_modify": true,
|
||||||
|
"commits": 3,
|
||||||
|
"additions": 100,
|
||||||
|
"deletions": 3,
|
||||||
|
"changed_files": 5
|
||||||
|
}
|
||||||
20
packages/ui/src/github/pull-request/index.stories.tsx
Normal file
20
packages/ui/src/github/pull-request/index.stories.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { StoryFn, Meta } from '@storybook/react';
|
||||||
|
import { PullRequest } from './index';
|
||||||
|
import pullRequest from './data.json';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'GitHub/Pull Request',
|
||||||
|
component: PullRequest,
|
||||||
|
} satisfies Meta<typeof PullRequest>;
|
||||||
|
|
||||||
|
type Story = StoryFn<typeof PullRequest>;
|
||||||
|
|
||||||
|
const Normal: Story = {
|
||||||
|
args: {
|
||||||
|
pullRequest: pullRequest,
|
||||||
|
onPress: () => {},
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
export { Normal };
|
||||||
|
export default meta;
|
||||||
32
packages/ui/src/github/pull-request/index.tsx
Normal file
32
packages/ui/src/github/pull-request/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { GithubTypes } from '@refocus/sdk';
|
||||||
|
import { Avatar, Card, View } from '../../base';
|
||||||
|
import { Typography } from '../../typography';
|
||||||
|
|
||||||
|
type PullRequestProps = {
|
||||||
|
pullRequest: GithubTypes.PullRequest;
|
||||||
|
onPress?: (oullRequest: GithubTypes.PullRequest) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PullRequest: React.FC<PullRequestProps> = ({ pullRequest, onPress }) => {
|
||||||
|
const onPressHandler = useCallback(() => {
|
||||||
|
onPress?.(pullRequest);
|
||||||
|
}, [pullRequest, onPress]);
|
||||||
|
return (
|
||||||
|
<Card $fr $items="center" $p="md" $gap="md" onClick={onPressHandler}>
|
||||||
|
<Avatar
|
||||||
|
url={pullRequest.user.avatar_url}
|
||||||
|
decal={pullRequest.state === 'open' ? 'open' : 'closed'}
|
||||||
|
/>
|
||||||
|
<View $fc>
|
||||||
|
<Typography variant="overline">
|
||||||
|
{pullRequest.head.repo?.full_name}
|
||||||
|
{' - '}#{pullRequest.number}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="title">{pullRequest.title}</Typography>
|
||||||
|
</View>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { PullRequest };
|
||||||
9
packages/ui/src/index.ts
Normal file
9
packages/ui/src/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export * from './base';
|
||||||
|
export * as Github from './github';
|
||||||
|
export * as Linear from './linear';
|
||||||
|
export * as Interface from './interface';
|
||||||
|
export * as Chat from './chat';
|
||||||
|
export * as Slack from './slack';
|
||||||
|
export { FocusProvider } from './provider';
|
||||||
|
export { UIProvider } from './theme/provider';
|
||||||
|
export { Typography, styles } from './typography';
|
||||||
84
packages/ui/src/interface/add-from-url/index.tsx
Normal file
84
packages/ui/src/interface/add-from-url/index.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
import { Card, Dialog, View } from '../../base';
|
||||||
|
import { useGetWidgetsFromUrl } from '@refocus/sdk';
|
||||||
|
import { Typography } from '../../typography';
|
||||||
|
|
||||||
|
type AddWidgetFromUrlProps = {
|
||||||
|
onCreate: (name: string, data: any) => void;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Root: React.FC<AddWidgetFromUrlProps> = ({ onCreate, children }) => {
|
||||||
|
const [url, setUrl] = useState('');
|
||||||
|
const [widgets, update] = useGetWidgetsFromUrl();
|
||||||
|
|
||||||
|
const handleSave = useCallback(
|
||||||
|
(id: string, data: any) => {
|
||||||
|
onCreate(id, data);
|
||||||
|
},
|
||||||
|
[onCreate],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const parsed = new URL(url, 'http://example.com');
|
||||||
|
if (parsed.host === 'example.com') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
update(parsed);
|
||||||
|
}, [url, update]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
{children}
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay />
|
||||||
|
<Dialog.Content>
|
||||||
|
<Dialog.CloseButton />
|
||||||
|
<View $fc $gap="sm">
|
||||||
|
<View $fr>
|
||||||
|
<View
|
||||||
|
as="input"
|
||||||
|
$f={1}
|
||||||
|
$u
|
||||||
|
placeholder="URL"
|
||||||
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
|
value={url}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
{widgets.map((widget) => (
|
||||||
|
<View key={widget.id}>
|
||||||
|
<Dialog.Close>
|
||||||
|
<Card
|
||||||
|
$fr
|
||||||
|
$items="center"
|
||||||
|
$gap="md"
|
||||||
|
$p="md"
|
||||||
|
onClick={() => handleSave(widget.id, widget.data)}
|
||||||
|
>
|
||||||
|
<View>
|
||||||
|
<Typography variant="header">{widget.icon}</Typography>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Typography variant="title">{widget.name}</Typography>
|
||||||
|
{widget.description && (
|
||||||
|
<Typography variant="body">
|
||||||
|
{widget.description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Card>
|
||||||
|
</Dialog.Close>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddWidgetFromUrl = Object.assign(Root, {
|
||||||
|
Trigger: Dialog.Trigger,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { AddWidgetFromUrl };
|
||||||
66
packages/ui/src/interface/app/index.tsx
Normal file
66
packages/ui/src/interface/app/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
useAddBoard,
|
||||||
|
useBoards,
|
||||||
|
useRemoveBoard,
|
||||||
|
useSelectBoard,
|
||||||
|
useSelectedBoard,
|
||||||
|
} from '@refocus/sdk';
|
||||||
|
import { IoAddCircleOutline } from 'react-icons/io5';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { View } from '../../base';
|
||||||
|
import { Board } from '../board';
|
||||||
|
import { Tabs } from '../../base/tabs';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
const NotificationBar = styled(View)``;
|
||||||
|
|
||||||
|
const App: React.FC = () => {
|
||||||
|
const boards = useBoards();
|
||||||
|
const selected = useSelectedBoard();
|
||||||
|
const selectBoard = useSelectBoard();
|
||||||
|
const addBoardAction = useAddBoard();
|
||||||
|
const removeBoard = useRemoveBoard();
|
||||||
|
|
||||||
|
const addBoard = useCallback(() => {
|
||||||
|
const name = prompt('Board name?');
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addBoardAction(name);
|
||||||
|
}, [addBoardAction]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<View $f={1}>
|
||||||
|
<Tabs value={selected} onValueChange={selectBoard}>
|
||||||
|
<Tabs.List>
|
||||||
|
{Object.entries(boards).map(([id, board]) => (
|
||||||
|
<Tabs.Trigger key={id} value={id}>
|
||||||
|
{board.name}
|
||||||
|
<Tabs.Close onClick={() => removeBoard(id)} />
|
||||||
|
</Tabs.Trigger>
|
||||||
|
))}
|
||||||
|
<View
|
||||||
|
onClick={addBoard}
|
||||||
|
$fr
|
||||||
|
$justify="center"
|
||||||
|
$items="center"
|
||||||
|
$p="md"
|
||||||
|
$bg="highlight100"
|
||||||
|
>
|
||||||
|
<IoAddCircleOutline size={16} />
|
||||||
|
</View>
|
||||||
|
</Tabs.List>
|
||||||
|
{Object.entries(boards).map(([id, board]) => (
|
||||||
|
<Tabs.Content key={id} value={id}>
|
||||||
|
<Board id={id} board={board} />
|
||||||
|
</Tabs.Content>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</View>
|
||||||
|
<NotificationBar></NotificationBar>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { App };
|
||||||
62
packages/ui/src/interface/board/index.tsx
Normal file
62
packages/ui/src/interface/board/index.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
Board,
|
||||||
|
useAddWidget,
|
||||||
|
useRemoveWidget,
|
||||||
|
useUpdateWidget,
|
||||||
|
} from '@refocus/sdk';
|
||||||
|
import { IoAddCircleOutline } from 'react-icons/io5';
|
||||||
|
import { View } from '../../base';
|
||||||
|
import { Widget } from '../widget';
|
||||||
|
import { AddWidgetFromUrl } from '../add-from-url';
|
||||||
|
import { styled } from 'styled-components';
|
||||||
|
|
||||||
|
type BoardProps = {
|
||||||
|
board: Board;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapper = styled(View)`
|
||||||
|
flex-wrap: wrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ItemWrapper = styled(View)`
|
||||||
|
max-width: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 500px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Board: React.FC<BoardProps> = ({ board, id }) => {
|
||||||
|
const setWidgetData = useUpdateWidget();
|
||||||
|
const removeWidget = useRemoveWidget();
|
||||||
|
const addWidget = useAddWidget();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<View $p="md">
|
||||||
|
<AddWidgetFromUrl onCreate={(type, data) => addWidget(id, type, data)}>
|
||||||
|
<AddWidgetFromUrl.Trigger>
|
||||||
|
<View $fr $items="center" $p="sm" $gap="sm">
|
||||||
|
<IoAddCircleOutline />
|
||||||
|
Add from URL
|
||||||
|
</View>
|
||||||
|
</AddWidgetFromUrl.Trigger>
|
||||||
|
</AddWidgetFromUrl>
|
||||||
|
</View>
|
||||||
|
<Wrapper $fr>
|
||||||
|
{Object.entries(board.widgets).map(([widgetId, widget]) => (
|
||||||
|
<ItemWrapper key={widgetId}>
|
||||||
|
<Widget
|
||||||
|
key={widgetId}
|
||||||
|
id={widget.type}
|
||||||
|
data={widget.data}
|
||||||
|
setData={(data) => setWidgetData(id, widgetId, data)}
|
||||||
|
onRemove={() => removeWidget(id, widgetId)}
|
||||||
|
/>
|
||||||
|
</ItemWrapper>
|
||||||
|
))}
|
||||||
|
</Wrapper>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Board };
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user