mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-16 09:36:17 +08:00
Feat/cloud integration (#569)
* Chore/unit test (#538) * chore: add unit test * chore: add github action for unit test * feat: init firebase * chore: add development secrets * fix: rename auth -> data-services * feat: update blocksuite 0.3.0-alpha.4 (#543) * feat: add requests * feat: optimize swr cache * feat: add Authorization * feat: add confirm-invitation page * feat: add account sdk api and proxy * docs: update contributing (#550) * docs: update contributing * Update CONTRIBUTING.md * Update CONTRIBUTING.md Co-authored-by: ShortCipher5 <me@shortcipher.me> * feat: update api * feat: remove babelrc setting * feat: add create workspace ui * feat: choose workspaces * feat: login modal * feat: authorization api * feat: login status * fix: remove unused variables * feat: login button * fix: lint * fix: workspace id * fix: i18n type error Co-authored-by: MingLiang Wang <mingliangwang0o0@gmail.com> Co-authored-by: ShortCipher5 <me@shortcipher.me>
This commit is contained in:
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Custom Tag. Set nightly-latest will publish to development."
|
||||
description: 'Custom Tag. Set nightly-latest will publish to development.'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
@@ -20,6 +20,7 @@ jobs:
|
||||
build:
|
||||
name: Lint and Build
|
||||
runs-on: self-hosted
|
||||
environment: development
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -62,6 +63,14 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
env:
|
||||
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
|
||||
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
|
||||
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
|
||||
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
|
||||
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
|
||||
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
|
||||
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
|
||||
|
||||
- name: Export
|
||||
run: pnpm export
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Playwright Tests
|
||||
name: E2E & Unit Tests
|
||||
on:
|
||||
push:
|
||||
branches: [pathfinder]
|
||||
@@ -33,5 +33,8 @@ jobs:
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
- name: Run E2E tests
|
||||
run: pnpm run test:e2e
|
||||
|
||||
- name: Run Unit tests
|
||||
run: pnpm run test:unit
|
||||
+22
-17
@@ -10,12 +10,17 @@ Use the table of contents icon on the top left corner of this document to get to
|
||||
|
||||
## New contributor guide
|
||||
|
||||
Currently we have two versions of AFFiNE:
|
||||
|
||||
- [AFFiNE Pre-Alpha](https://livedemo.affine.pro/). This version users the branch `master`, however is no longer actively developed and will be archived in the future.
|
||||
- [AFFiNE Alpha](https://pathfinder.affine.pro/). This version uses the 'pathfinder' branch, this is the latest version under active development. We plan to update this to the master branch in the near future.
|
||||
|
||||
To get an overview of the project, read the [README](../README.md). Here are some resources to help you get started with open source contributions:
|
||||
|
||||
- [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github)
|
||||
- [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git)
|
||||
- [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow)
|
||||
- [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests)
|
||||
- [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github)
|
||||
- [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git)
|
||||
- [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow)
|
||||
- [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests)
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -47,15 +52,15 @@ For more information about using a codespace for working on GitHub documentation
|
||||
|
||||
2. Fork the repository.
|
||||
|
||||
- Using GitHub Desktop:
|
||||
- Using GitHub Desktop:
|
||||
|
||||
- [Getting started with GitHub Desktop](https://docs.github.com/en/desktop/installing-and-configuring-github-desktop/getting-started-with-github-desktop) will guide you through setting up Desktop.
|
||||
- Once Desktop is set up, you can use it to [fork the repo](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/cloning-and-forking-repositories-from-github-desktop)!
|
||||
- [Getting started with GitHub Desktop](https://docs.github.com/en/desktop/installing-and-configuring-github-desktop/getting-started-with-github-desktop) will guide you through setting up Desktop.
|
||||
- Once Desktop is set up, you can use it to [fork the repo](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/cloning-and-forking-repositories-from-github-desktop)!
|
||||
|
||||
- Using the command line:
|
||||
- [Fork the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) so that you can make your changes without affecting the original project until you're ready to merge them.
|
||||
- Using the command line:
|
||||
- [Fork the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) so that you can make your changes without affecting the original project until you're ready to merge them.
|
||||
|
||||
3. Install or update to **Node.js v16**. For more information, see [the development guide](contributing/development.md).
|
||||
3. Install or update to **Node.js v16**.
|
||||
|
||||
4. Create a working branch and start with your changes!
|
||||
|
||||
@@ -71,13 +76,13 @@ Once your changes are ready, don't forget to self-review to speed up the review
|
||||
|
||||
When you're finished with the changes, create a pull request, also known as a PR.
|
||||
|
||||
- Fill the "Ready for review" template so that we can review your PR. This template helps reviewers understand your changes as well as the purpose of your pull request.
|
||||
- Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one.
|
||||
- Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge.
|
||||
Once you submit your PR, a Docs team member will review your proposal. We may ask questions or request for additional information.
|
||||
- We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch.
|
||||
- As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations).
|
||||
- If you run into any merge issues, checkout this [git tutorial](https://github.com/skills/resolve-merge-conflicts) to help you resolve merge conflicts and other issues.
|
||||
- Fill the "Ready for review" template so that we can review your PR. This template helps reviewers understand your changes as well as the purpose of your pull request.
|
||||
- Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one.
|
||||
- Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge.
|
||||
Once you submit your PR, a Docs team member will review your proposal. We may ask questions or request for additional information.
|
||||
- We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch.
|
||||
- As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations).
|
||||
- If you run into any merge issues, checkout this [git tutorial](https://github.com/skills/resolve-merge-conflicts) to help you resolve merge conflicts and other issues.
|
||||
|
||||
### Your PR is merged!
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
projects: ['<rootDir>/packages/app'],
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||
},
|
||||
transform: {
|
||||
// '^.+\\.[tj]sx?$' to process js/ts with `ts-jest`
|
||||
// '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest`
|
||||
'^.+\\.tsx?$': [
|
||||
'ts-jest',
|
||||
{
|
||||
useESM: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
+7
-2
@@ -8,15 +8,20 @@
|
||||
"export": "pnpm --filter @pathfinder/app export",
|
||||
"start": "pnpm --filter @pathfinder/app start",
|
||||
"lint": "pnpm --filter @pathfinder/app lint",
|
||||
"test:e2e": "playwright test"
|
||||
"test:e2e": "playwright test",
|
||||
"test:unit": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.3.1",
|
||||
"@playwright/test": "^1.28.1",
|
||||
"@types/node": "18.7.18",
|
||||
"eslint": "8.22.0",
|
||||
"eslint-config-next": "12.3.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"prettier": "^2.7.1"
|
||||
"jest": "^29.3.1",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-jest": "^29.0.3",
|
||||
"typescript": "^4.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
|
||||
/** @type { import('eslint').Linter.Config } */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'next/core-web-vitals',
|
||||
'plugin:@next/next/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
rules: {
|
||||
'prettier/prettier': 'warn',
|
||||
},
|
||||
extends: [
|
||||
'next/core-web-vitals',
|
||||
'plugin:@next/next/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
// 'next/babel',
|
||||
],
|
||||
rules: {
|
||||
'prettier/prettier': 'warn',
|
||||
},
|
||||
|
||||
reportUnusedDisableDirectives: true,
|
||||
reportUnusedDisableDirectives: true,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
const { getGitVersion, getCommitHash } = require('./scripts/gitInfo');
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
@@ -14,6 +13,15 @@ const nextConfig = {
|
||||
VERSION: getGitVersion(),
|
||||
COMMIT_HASH: getCommitHash(),
|
||||
},
|
||||
// XXX not test yet
|
||||
rewrites: async () => {
|
||||
return [
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: 'http://100.77.180.48:11001/api/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"@mui/icons-material": "^5.10.9",
|
||||
"@mui/material": "^5.8.6",
|
||||
"@toeverything/pathfinder-logger": "workspace:@pathfinder/logger@*",
|
||||
"@pathfinder/data-services": "workspace:@pathfinder/data-services@*",
|
||||
"cmdk": "^0.1.20",
|
||||
"css-spring": "^4.1.0",
|
||||
"dayjs": "^1.11.7",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { NotFoundTitle, PageContainer } from './styles';
|
||||
|
||||
export const NotfoundPage = () => {
|
||||
return (
|
||||
<PageContainer>
|
||||
<NotFoundTitle>404 - Page Not Found</NotFoundTitle>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotfoundPage;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { absoluteCenter, displayFlex, styled } from '@/styles';
|
||||
|
||||
export const PageContainer = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '100%',
|
||||
height: 'calc(100vh)',
|
||||
backgroundColor: theme.colors.pageBackground,
|
||||
};
|
||||
});
|
||||
|
||||
export const NotFoundTitle = styled('h1')(({ theme }) => {
|
||||
return {
|
||||
position: 'relative',
|
||||
top: 'calc(50% - 100px)',
|
||||
height: '100px',
|
||||
fontSize: '60px',
|
||||
lineHeight: '100px',
|
||||
color: theme.colors.textColor,
|
||||
textAlign: 'center',
|
||||
};
|
||||
});
|
||||
@@ -22,6 +22,9 @@ import { getWarningMessage, shouldShowWarning } from './utils';
|
||||
import { Menu, MenuItem } from '@/ui/menu';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useConfirm } from '@/providers/confirm-provider';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { SyncIcon } from './sync-icon';
|
||||
|
||||
const PopoverContent = () => {
|
||||
const { editor, mode, setMode } = useEditor();
|
||||
@@ -131,6 +134,7 @@ const HeaderRight = () => {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<SyncIcon />
|
||||
<ThemeModeSwitch />
|
||||
<Menu content={<PopoverContent />} placement="bottom-end">
|
||||
<IconButton>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { CloudUnsyncedIcon, CloudInsyncIcon } from '@blocksuite/icons';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { IconButton } from '@/ui/button';
|
||||
|
||||
export const SyncIcon = () => {
|
||||
const { triggerLoginModal } = useModal();
|
||||
const appState = useAppState();
|
||||
|
||||
return appState.user ? (
|
||||
<IconButton iconSize="middle" disabled>
|
||||
<CloudInsyncIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton iconSize="middle" onClick={triggerLoginModal}>
|
||||
<CloudUnsyncedIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
import { signInWithGoogle, login, setToken } from '@pathfinder/data-services';
|
||||
import { styled } from '@/styles';
|
||||
import { Button } from '@/ui/button';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
import { GoogleIcon, StayLogOutIcon } from './icons';
|
||||
|
||||
export const GoogleLoginButton = () => {
|
||||
const { triggerLoginModal } = useModal();
|
||||
return (
|
||||
<StyledGoogleButton
|
||||
onClick={() => {
|
||||
signInWithGoogle()
|
||||
.then(async user => {
|
||||
const idToken = await user.user.getIdToken();
|
||||
const token = await login({ token: idToken, type: 'Google' });
|
||||
setToken({
|
||||
accessToken: token.token,
|
||||
refreshToken: token.refresh,
|
||||
});
|
||||
triggerLoginModal();
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('sign google error', error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ButtonWrapper>
|
||||
<IconWrapper>
|
||||
<GoogleIcon />
|
||||
</IconWrapper>
|
||||
<TextWrapper>
|
||||
<Title>Continue with Google</Title>
|
||||
<Description>Set up an AFFINE account to sync data</Description>
|
||||
</TextWrapper>
|
||||
</ButtonWrapper>
|
||||
</StyledGoogleButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const StayLogOutButton = () => {
|
||||
return (
|
||||
<StyledStayLogOutButton>
|
||||
<ButtonWrapper>
|
||||
<IconWrapper>
|
||||
<StayLogOutIcon />
|
||||
</IconWrapper>
|
||||
<TextWrapper>
|
||||
<Title>Stay logged out</Title>
|
||||
<Description>All changes are saved locally</Description>
|
||||
</TextWrapper>
|
||||
</ButtonWrapper>
|
||||
</StyledStayLogOutButton>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledGoogleButton = styled(Button)(({ theme }) => {
|
||||
return {
|
||||
width: '361px',
|
||||
height: '56px',
|
||||
padding: '4px',
|
||||
background: '#6880FF',
|
||||
color: '#fff',
|
||||
|
||||
'& > span': {
|
||||
marginLeft: 0,
|
||||
},
|
||||
|
||||
':hover': {
|
||||
background: '#516BF4',
|
||||
color: '#fff',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const StyledStayLogOutButton = styled(Button)(({ theme }) => {
|
||||
return {
|
||||
width: '361px',
|
||||
height: '56px',
|
||||
padding: '4px',
|
||||
|
||||
'& > span': {
|
||||
marginLeft: 0,
|
||||
},
|
||||
|
||||
':hover': {
|
||||
borderColor: '#6880FF',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const ButtonWrapper = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
});
|
||||
|
||||
const IconWrapper = styled('div')({
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
flex: '0 48px',
|
||||
borderRadius: '5px',
|
||||
overflow: 'hidden',
|
||||
marginRight: '12px',
|
||||
});
|
||||
|
||||
const TextWrapper = styled('div')({
|
||||
flex: 1,
|
||||
textAlign: 'left',
|
||||
});
|
||||
|
||||
const Title = styled('h1')(({ theme }) => {
|
||||
return {
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
fontWeight: 500,
|
||||
};
|
||||
});
|
||||
|
||||
const Description = styled('p')(({ theme }) => {
|
||||
return {
|
||||
fontSize: '16px',
|
||||
lineHeight: '22px',
|
||||
fontWeight: 400,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.3055 10.0415H21.5V10H12.5V14H18.1515C17.327 16.3285 15.1115 18 12.5 18C9.1865 18 6.5 15.3135 6.5 12C6.5 8.6865 9.1865 6 12.5 6C14.0295 6 15.421 6.577 16.4805 7.5195L19.309 4.691C17.523 3.0265 15.134 2 12.5 2C6.9775 2 2.5 6.4775 2.5 12C2.5 17.5225 6.9775 22 12.5 22C18.0225 22 22.5 17.5225 22.5 12C22.5 11.3295 22.431 10.675 22.3055 10.0415Z" fill="#FFC107"/>
|
||||
<path d="M3.65234 7.3455L6.93784 9.755C7.82684 7.554 9.97984 6 12.4993 6C14.0288 6 15.4203 6.577 16.4798 7.5195L19.3083 4.691C17.5223 3.0265 15.1333 2 12.4993 2C8.65834 2 5.32734 4.1685 3.65234 7.3455Z" fill="#FF3D00"/>
|
||||
<path d="M12.5002 22.0003C15.0832 22.0003 17.4302 21.0118 19.2047 19.4043L16.1097 16.7853C15.0719 17.5745 13.8039 18.0014 12.5002 18.0003C9.89916 18.0003 7.69066 16.3418 6.85866 14.0273L3.59766 16.5398C5.25266 19.7783 8.61366 22.0003 12.5002 22.0003Z" fill="#4CAF50"/>
|
||||
<path d="M22.3055 10.0415H21.5V10H12.5V14H18.1515C17.7571 15.1082 17.0467 16.0766 16.108 16.7855L16.1095 16.7845L19.2045 19.4035C18.9855 19.6025 22.5 17 22.5 12C22.5 11.3295 22.431 10.675 22.3055 10.0415Z" fill="#1976D2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,42 @@
|
||||
import { CloudUnsyncedIcon } from '@blocksuite/icons';
|
||||
import { styled } from '@/styles';
|
||||
import GoogleSvg from './google.svg';
|
||||
|
||||
export const GoogleIcon = () => {
|
||||
return (
|
||||
<GoogleIconWrapper>
|
||||
<picture>
|
||||
<img src={GoogleSvg.src} alt="Google" />
|
||||
</picture>
|
||||
</GoogleIconWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const GoogleIconWrapper = styled('div')(({ theme }) => ({
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
background: theme.colors.pageBackground,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}));
|
||||
|
||||
export const StayLogOutIcon = () => {
|
||||
return (
|
||||
<StayLogOutWrapper>
|
||||
<CloudUnsyncedIcon />
|
||||
</StayLogOutWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const StayLogOutWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '24px',
|
||||
background: theme.colors.hoverBackground,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ResetIcon } from '@blocksuite/icons';
|
||||
import { styled } from '@/styles';
|
||||
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
|
||||
import { TextButton, Button } from '@/ui/button';
|
||||
import { GoogleLoginButton, StayLogOutButton } from './LoginOptionButton';
|
||||
|
||||
interface LoginModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const LoginModal = ({ open, onClose }: LoginModalProps) => {
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<ModalWrapper width={620} height={334}>
|
||||
<Header>
|
||||
<ModalCloseButton
|
||||
top={6}
|
||||
right={6}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentTitle>Currently not logged in</ContentTitle>
|
||||
<GoogleLoginButton />
|
||||
<StayLogOutButton />
|
||||
</Content>
|
||||
<Footer>
|
||||
<TextButton icon={<StyledResetIcon />}>Clear local data</TextButton>
|
||||
</Footer>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = styled('div')({
|
||||
position: 'relative',
|
||||
height: '44px',
|
||||
});
|
||||
|
||||
const Content = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
});
|
||||
|
||||
const ContentTitle = styled('h1')({
|
||||
fontSize: '20px',
|
||||
lineHeight: '28px',
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
paddingBottom: '16px',
|
||||
});
|
||||
|
||||
const Footer = styled('div')({
|
||||
height: '70px',
|
||||
paddingLeft: '24px',
|
||||
marginTop: '32px',
|
||||
});
|
||||
|
||||
const StyledResetIcon = styled(ResetIcon)({
|
||||
marginRight: '12px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
});
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { InformationIcon, LogOutIcon } from '@blocksuite/icons';
|
||||
import { styled } from '@/styles';
|
||||
import { Divider } from '@/ui/divider';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import { SelectorPopperContainer } from './styles';
|
||||
import {
|
||||
PrivateWorkspaceItem,
|
||||
WorkspaceItem,
|
||||
CreateWorkspaceItem,
|
||||
ListItem,
|
||||
LoginItem,
|
||||
} from './WorkspaceItem';
|
||||
|
||||
const workspaces = [
|
||||
{
|
||||
name: 'Design',
|
||||
icon: '',
|
||||
},
|
||||
{
|
||||
name: 'Operation',
|
||||
icon: '',
|
||||
},
|
||||
{
|
||||
name: 'Something is too long to show in this box',
|
||||
icon: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const SelectorPopperContent = () => {
|
||||
const { user, workspaces } = useAppState();
|
||||
return !user ? (
|
||||
<SelectorPopperContainer placement="bottom-start">
|
||||
<LoginItem />
|
||||
<StyledDivider />
|
||||
<ListItem
|
||||
icon={<InformationIcon />}
|
||||
name="About AFFiNE"
|
||||
onClick={() => console.log('About AFFiNE')}
|
||||
/>
|
||||
</SelectorPopperContainer>
|
||||
) : (
|
||||
<SelectorPopperContainer placement="bottom-start">
|
||||
<PrivateWorkspaceItem />
|
||||
<StyledDivider />
|
||||
<WorkspaceGroupTitle>Workspace</WorkspaceGroupTitle>
|
||||
{workspaces.map(workspace => {
|
||||
return (
|
||||
<WorkspaceItem
|
||||
key={workspace.id}
|
||||
id={workspace.id}
|
||||
name={`workspace-${workspace.id}`}
|
||||
icon={''}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<CreateWorkspaceItem />
|
||||
<StyledDivider />
|
||||
<ListItem
|
||||
icon={<InformationIcon />}
|
||||
name="About AFFiNE"
|
||||
onClick={() => console.log('About AFFiNE')}
|
||||
/>
|
||||
<ListItem
|
||||
icon={<LogOutIcon />}
|
||||
name="Sign out"
|
||||
onClick={() => console.log('Sign out')}
|
||||
/>
|
||||
</SelectorPopperContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledDivider = styled(Divider)({
|
||||
margin: '8px 12px',
|
||||
width: 'calc(100% - 24px)',
|
||||
});
|
||||
|
||||
const WorkspaceGroupTitle = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.iconColor,
|
||||
fontSize: theme.font.sm,
|
||||
lineHeight: '30px',
|
||||
height: '30px',
|
||||
padding: '0 12px',
|
||||
};
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { useState } from 'react';
|
||||
import { AddIcon } from '@blocksuite/icons';
|
||||
import { styled } from '@/styles';
|
||||
import {
|
||||
WorkspaceItemAvatar,
|
||||
WorkspaceItemWrapper,
|
||||
WorkspaceItemContent,
|
||||
} from '../styles';
|
||||
import { WorkspaceCreate } from './workspace-create';
|
||||
|
||||
const name = 'Create new Workspace';
|
||||
|
||||
export const CreateWorkspaceItem = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<WorkspaceItemWrapper onClick={() => setOpen(true)}>
|
||||
<WorkspaceItemAvatar>
|
||||
<AddIcon />
|
||||
</WorkspaceItemAvatar>
|
||||
<WorkspaceItemContent>
|
||||
<Name title={name}>{name}</Name>
|
||||
</WorkspaceItemContent>
|
||||
</WorkspaceItemWrapper>
|
||||
<WorkspaceCreate open={open} onClose={() => setOpen(false)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Name = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.quoteColor,
|
||||
fontSize: theme.font.base,
|
||||
fontWeight: 400,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from './CreateWorkspaceItem';
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from './workspace-create';
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { styled } from '@/styles';
|
||||
|
||||
export const StyledModalWrapper = styled('div')(({ theme }) => {
|
||||
return {
|
||||
position: 'relative',
|
||||
padding: '0px',
|
||||
width: '460px',
|
||||
background: theme.colors.popoverBackground,
|
||||
borderRadius: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeader = styled('div')(({ theme }) => {
|
||||
return {
|
||||
margin: '44px 0px 12px 0px',
|
||||
width: '460px',
|
||||
fontWeight: '600',
|
||||
fontSize: '20px;',
|
||||
textAlign: 'center',
|
||||
color: theme.colors.popoverColor,
|
||||
};
|
||||
});
|
||||
|
||||
// export const StyledModalContent = styled('div')(({ theme }) => {});
|
||||
|
||||
export const StyledTextContent = styled('div')(({ theme }) => {
|
||||
return {
|
||||
margin: 'auto',
|
||||
width: '425px',
|
||||
fontFamily: 'Avenir Next',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
textAlign: 'center',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledInputContent = styled('div')(({ theme }) => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: '40px 0 24px 0',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledButtonContent = styled('div')(({ theme }) => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: '0px 0 32px 0',
|
||||
};
|
||||
});
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { createWorkspace } from '@pathfinder/data-services';
|
||||
import Modal from '@/ui/modal';
|
||||
import Input from '@/ui/input';
|
||||
import { Button } from '@/ui/button';
|
||||
import {
|
||||
StyledModalHeader,
|
||||
StyledTextContent,
|
||||
StyledModalWrapper,
|
||||
StyledInputContent,
|
||||
StyledButtonContent,
|
||||
} from './style';
|
||||
import { useState } from 'react';
|
||||
import { ModalCloseButton } from '@/ui/modal';
|
||||
|
||||
interface WorkspaceCreateProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const WorkspaceCreate = ({ open, onClose }: WorkspaceCreateProps) => {
|
||||
const [workspaceName, setWorkspaceId] = useState<string>('');
|
||||
const handlerInputChange = (workspaceName: string) => {
|
||||
setWorkspaceId(workspaceName);
|
||||
};
|
||||
console.log('workspaceName', workspaceName);
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<StyledModalWrapper>
|
||||
<ModalCloseButton />
|
||||
<StyledModalHeader>Create new Workspace</StyledModalHeader>
|
||||
<StyledTextContent>
|
||||
Workspaces are shared environments where teams can collaborate. After
|
||||
creating a Workspace, you can invite others to join.
|
||||
</StyledTextContent>
|
||||
<StyledInputContent>
|
||||
<Input
|
||||
onChange={handlerInputChange}
|
||||
placeholder="Set a Workspace name"
|
||||
value={workspaceName}
|
||||
></Input>
|
||||
</StyledInputContent>
|
||||
<StyledButtonContent>
|
||||
<Button
|
||||
disabled={!workspaceName.length}
|
||||
style={{ width: '260px' }}
|
||||
onClick={() => {
|
||||
createWorkspace({ name: workspaceName, avatar: '' });
|
||||
}}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</StyledButtonContent>
|
||||
</StyledModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceCreate;
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { styled } from '@/styles';
|
||||
import { WorkspaceItemWrapper, WorkspaceItemContent } from './styles';
|
||||
|
||||
interface ListItemProps {
|
||||
name: string;
|
||||
icon: ReactNode;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const ListItem = ({ name, icon, onClick }: ListItemProps) => {
|
||||
return (
|
||||
<WorkspaceItemWrapper onClick={onClick}>
|
||||
<StyledIconWrapper>{icon}</StyledIconWrapper>
|
||||
<WorkspaceItemContent>
|
||||
<Name title={name}>{name}</Name>
|
||||
</WorkspaceItemContent>
|
||||
</WorkspaceItemWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const Name = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.quoteColor,
|
||||
fontSize: theme.font.sm,
|
||||
fontWeight: 400,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
|
||||
const StyledIconWrapper = styled('div')({
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
fontSize: '20px',
|
||||
});
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
import { styled } from '@/styles';
|
||||
import {
|
||||
WorkspaceItemAvatar,
|
||||
LoginItemWrapper,
|
||||
WorkspaceItemContent,
|
||||
} from './styles';
|
||||
|
||||
export const LoginItem = () => {
|
||||
const { triggerLoginModal } = useModal();
|
||||
return (
|
||||
<LoginItemWrapper onClick={() => triggerLoginModal()}>
|
||||
<WorkspaceItemAvatar alt="AFFiNE" src={''}>
|
||||
A
|
||||
</WorkspaceItemAvatar>
|
||||
<WorkspaceItemContent>
|
||||
<Name title="AFFiNE">AFFiNE</Name>
|
||||
<Description
|
||||
title="Log in to sync with affine"
|
||||
className="login-description"
|
||||
>
|
||||
Log in to sync with affine
|
||||
</Description>
|
||||
</WorkspaceItemContent>
|
||||
</LoginItemWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const Name = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.quoteColor,
|
||||
fontSize: theme.font.base,
|
||||
fontWeight: 500,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
|
||||
const Description = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.iconColor,
|
||||
fontSize: theme.font.sm,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { styled } from '@/styles';
|
||||
import { useAppState } from '@/providers/app-state-provider';
|
||||
import {
|
||||
WorkspaceItemAvatar,
|
||||
PrivateWorkspaceWrapper,
|
||||
WorkspaceItemContent,
|
||||
} from './styles';
|
||||
|
||||
export const PrivateWorkspaceItem = () => {
|
||||
const { user } = useAppState();
|
||||
|
||||
return !user ? null : (
|
||||
<PrivateWorkspaceWrapper>
|
||||
<WorkspaceItemAvatar alt={user.name} src={user.avatar_url}>
|
||||
{user.name.charAt(0)}
|
||||
</WorkspaceItemAvatar>
|
||||
<WorkspaceItemContent>
|
||||
<Name title={user.name}>{user.name}</Name>
|
||||
<Email title={user.email}>{user.email}</Email>
|
||||
</WorkspaceItemContent>
|
||||
</PrivateWorkspaceWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const Name = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.quoteColor,
|
||||
fontSize: theme.font.base,
|
||||
fontWeight: 500,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
|
||||
const Email = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.iconColor,
|
||||
fontSize: theme.font.sm,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { SettingsIcon } from '@blocksuite/icons';
|
||||
import { styled } from '@/styles';
|
||||
import { IconButton } from '@/ui/button';
|
||||
|
||||
export const FooterSetting = () => {
|
||||
return (
|
||||
<Wrapper className="footer-setting">
|
||||
<SettingsIcon />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const Wrapper = styled(IconButton)(({ theme }) => {
|
||||
return {
|
||||
fontSize: '20px',
|
||||
};
|
||||
});
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { UsersIcon } from '@blocksuite/icons';
|
||||
import { styled } from '@/styles';
|
||||
import { IconButton } from '@/ui/button';
|
||||
|
||||
export const FooterUsers = () => {
|
||||
return (
|
||||
<Wrapper className="footer-users">
|
||||
<>
|
||||
<UsersIcon />
|
||||
<Tip>99+</Tip>
|
||||
</>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const Wrapper = styled(IconButton)({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '16px',
|
||||
});
|
||||
|
||||
const Tip = styled('span')({
|
||||
fontSize: '12px',
|
||||
});
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { styled } from '@/styles';
|
||||
import {
|
||||
WorkspaceItemAvatar,
|
||||
WorkspaceItemWrapper,
|
||||
WorkspaceItemContent,
|
||||
} from '../styles';
|
||||
import { FooterSetting } from './FooterSetting';
|
||||
import { FooterUsers } from './FooterUsers';
|
||||
|
||||
interface WorkspaceItemProps {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export const WorkspaceItem = ({ name, icon }: WorkspaceItemProps) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<WorkspaceItemAvatar alt={name} src={icon}>
|
||||
{name.charAt(0)}
|
||||
</WorkspaceItemAvatar>
|
||||
<WorkspaceItemContent>
|
||||
<Name title={name}>{name}</Name>
|
||||
</WorkspaceItemContent>
|
||||
<Footer>
|
||||
<FooterUsers />
|
||||
<FooterSetting />
|
||||
</Footer>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const Name = styled('div')(({ theme }) => {
|
||||
return {
|
||||
color: theme.colors.quoteColor,
|
||||
fontSize: theme.font.sm,
|
||||
fontWeight: 400,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
|
||||
const StyledWrapper = styled(WorkspaceItemWrapper)({
|
||||
'& .footer-setting': {
|
||||
display: 'none',
|
||||
},
|
||||
':hover .footer-users': {
|
||||
display: 'none',
|
||||
},
|
||||
':hover .footer-setting': {
|
||||
display: 'block',
|
||||
},
|
||||
});
|
||||
|
||||
const Footer = styled('div')({
|
||||
width: '42px',
|
||||
flex: '0 42px',
|
||||
fontSize: '20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: '12px',
|
||||
});
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from './WorkspaceItem';
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export * from './PrivateWorkspaceItem';
|
||||
export * from './WorkspaceItem';
|
||||
export * from './CreateWorkspaceItem';
|
||||
export * from './ListItem';
|
||||
export * from './LoginItem';
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import MuiAvatar from '@mui/material/Avatar';
|
||||
import { styled } from '@/styles';
|
||||
|
||||
export const WorkspaceItemWrapper = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '5px',
|
||||
padding: '6px 12px',
|
||||
|
||||
':hover': {
|
||||
color: theme.colors.primaryColor,
|
||||
backgroundColor: theme.colors.hoverBackground,
|
||||
},
|
||||
}));
|
||||
|
||||
export const PrivateWorkspaceWrapper = styled(WorkspaceItemWrapper)({
|
||||
padding: '10px 12px',
|
||||
});
|
||||
|
||||
export const LoginItemWrapper = styled(WorkspaceItemWrapper)(({ theme }) => {
|
||||
return {
|
||||
padding: '10px 12px',
|
||||
|
||||
':hover .login-description': {
|
||||
color: theme.colors.primaryColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const WorkspaceItemAvatar = styled(MuiAvatar)({
|
||||
height: '40px',
|
||||
width: '40px',
|
||||
});
|
||||
|
||||
export const WorkspaceItemContent = styled('div')({
|
||||
minWidth: 0,
|
||||
marginLeft: '12px',
|
||||
flexGrow: 1,
|
||||
});
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { Popper } from '@/ui/popper';
|
||||
import { Avatar, WorkspaceName, SelectorWrapper } from './styles';
|
||||
import { SelectorPopperContent } from './SelectorPopperContent';
|
||||
|
||||
export const WorkspaceSelector = () => {
|
||||
return (
|
||||
<Popper
|
||||
content={<SelectorPopperContent />}
|
||||
zIndex={1000}
|
||||
placement="bottom-start"
|
||||
>
|
||||
<SelectorWrapper>
|
||||
<Avatar alt="Affine" />
|
||||
<WorkspaceName>AFFiNE</WorkspaceName>
|
||||
</SelectorWrapper>
|
||||
</Popper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './WorkspaceSelector';
|
||||
@@ -0,0 +1,38 @@
|
||||
import MuiAvatar from '@mui/material/Avatar';
|
||||
import { styled } from '@/styles';
|
||||
import { StyledPopperContainer } from '@/ui/shared/Container';
|
||||
|
||||
export const SelectorWrapper = styled('div')({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const Avatar = styled(MuiAvatar)({
|
||||
height: '28px',
|
||||
width: '28px',
|
||||
});
|
||||
|
||||
export const WorkspaceName = styled('span')(({ theme }) => {
|
||||
return {
|
||||
marginLeft: '12px',
|
||||
lineHeight: 1,
|
||||
fontSize: '18px',
|
||||
fontWeight: 500,
|
||||
color: theme.colors.iconColor,
|
||||
};
|
||||
});
|
||||
|
||||
export const SelectorPopperContainer = styled(StyledPopperContainer)(
|
||||
({ theme }) => {
|
||||
return {
|
||||
width: '334px',
|
||||
boxShadow: theme.shadow.tooltip,
|
||||
padding: '24px 12px',
|
||||
backgroundColor: theme.colors.pageBackground,
|
||||
fontSize: theme.font.xs,
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
StyledArrowButton,
|
||||
StyledLink,
|
||||
StyledListItem,
|
||||
StyledListItemForWorkspace,
|
||||
StyledNewPageButton,
|
||||
StyledSliderBar,
|
||||
StyledSubListItem,
|
||||
@@ -25,6 +26,7 @@ import { useEditor } from '@/providers/editor-provider';
|
||||
import { useModal } from '@/providers/global-modal-provider';
|
||||
|
||||
import { IconButton } from '@/ui/button';
|
||||
import { WorkspaceSelector } from './WorkspaceSelector';
|
||||
import useLocalStorage from '@/hooks/use-local-storage';
|
||||
import { useTranslation } from '@/libs/i18n';
|
||||
const FavoriteList = ({ showList }: { showList: boolean }) => {
|
||||
@@ -69,6 +71,9 @@ export const WorkSpaceSliderBar = () => {
|
||||
return (
|
||||
<>
|
||||
<StyledSliderBar show={show}>
|
||||
<StyledListItemForWorkspace>
|
||||
<WorkspaceSelector />
|
||||
</StyledListItemForWorkspace>
|
||||
<Tooltip content="Search and quickly jump to a page" placement="right">
|
||||
<StyledListItem
|
||||
onClick={() => {
|
||||
@@ -80,7 +85,7 @@ export const WorkSpaceSliderBar = () => {
|
||||
</Tooltip>
|
||||
<Link href={{ pathname: '/page-list/all' }}>
|
||||
<StyledListItem active={router.pathname === '/page-list/all'}>
|
||||
<AllPagesIcon /> {t('All pages')}
|
||||
<AllPagesIcon /> <span>All pages</span>
|
||||
</StyledListItem>
|
||||
</Link>
|
||||
<StyledListItem active={router.pathname === '/page-list/favorite'}>
|
||||
|
||||
@@ -67,6 +67,10 @@ export const StyledListItem = styled.button<{ active?: boolean }>(
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledListItemForWorkspace = styled(StyledListItem)({
|
||||
height: '52px',
|
||||
});
|
||||
|
||||
export const StyledLink = styled(Link)(({ theme }) => {
|
||||
return {
|
||||
flexGrow: 1,
|
||||
|
||||
@@ -9,64 +9,64 @@ import fr from './fr.json';
|
||||
import bn from './bn.json';
|
||||
|
||||
export const LOCALES = [
|
||||
{
|
||||
id: 1000016008,
|
||||
name: 'English',
|
||||
tag: 'en',
|
||||
originalName: 'English',
|
||||
flagEmoji: '🇬🇧',
|
||||
base: true,
|
||||
completeRate: 1,
|
||||
res: en,
|
||||
},
|
||||
{
|
||||
id: 1000016009,
|
||||
name: 'Simplified Chinese',
|
||||
tag: 'zh-Hans',
|
||||
originalName: '简体中文',
|
||||
flagEmoji: '🇨🇳',
|
||||
base: false,
|
||||
completeRate: 1,
|
||||
res: zh_Hans,
|
||||
},
|
||||
{
|
||||
id: 1000016012,
|
||||
name: 'Traditional Chinese',
|
||||
tag: 'zh-Hant',
|
||||
originalName: '繁體中文',
|
||||
flagEmoji: '🇭🇰',
|
||||
base: false,
|
||||
completeRate: 1,
|
||||
res: zh_Hant,
|
||||
},
|
||||
{
|
||||
id: 1000034005,
|
||||
name: 'Serbian',
|
||||
tag: 'sr',
|
||||
originalName: 'српски',
|
||||
flagEmoji: '🇷🇸',
|
||||
base: false,
|
||||
completeRate: 0.9166666666666666,
|
||||
res: sr,
|
||||
},
|
||||
{
|
||||
id: 1000034008,
|
||||
name: 'French',
|
||||
tag: 'fr',
|
||||
originalName: 'français',
|
||||
flagEmoji: '🇫🇷',
|
||||
base: false,
|
||||
completeRate: 1,
|
||||
res: fr,
|
||||
},
|
||||
{
|
||||
id: 1000034010,
|
||||
name: 'Bangla',
|
||||
tag: 'bn',
|
||||
originalName: 'বাংলা',
|
||||
flagEmoji: '🇧🇩',
|
||||
base: false,
|
||||
completeRate: 0.7083333333333334,
|
||||
res: bn,
|
||||
},
|
||||
{
|
||||
id: 1000016008,
|
||||
name: 'English',
|
||||
tag: 'en',
|
||||
originalName: 'English',
|
||||
flagEmoji: '🇬🇧',
|
||||
base: true,
|
||||
completeRate: 1,
|
||||
res: en,
|
||||
},
|
||||
{
|
||||
id: 1000016009,
|
||||
name: 'Simplified Chinese',
|
||||
tag: 'zh-Hans',
|
||||
originalName: '简体中文',
|
||||
flagEmoji: '🇨🇳',
|
||||
base: false,
|
||||
completeRate: 1,
|
||||
res: zh_Hans,
|
||||
},
|
||||
{
|
||||
id: 1000016012,
|
||||
name: 'Traditional Chinese',
|
||||
tag: 'zh-Hant',
|
||||
originalName: '繁體中文',
|
||||
flagEmoji: '🇭🇰',
|
||||
base: false,
|
||||
completeRate: 1,
|
||||
res: zh_Hant,
|
||||
},
|
||||
{
|
||||
id: 1000034005,
|
||||
name: 'Serbian',
|
||||
tag: 'sr',
|
||||
originalName: 'српски',
|
||||
flagEmoji: '🇷🇸',
|
||||
base: false,
|
||||
completeRate: 0.9166666666666666,
|
||||
res: sr,
|
||||
},
|
||||
{
|
||||
id: 1000034008,
|
||||
name: 'French',
|
||||
tag: 'fr',
|
||||
originalName: 'français',
|
||||
flagEmoji: '🇫🇷',
|
||||
base: false,
|
||||
completeRate: 1,
|
||||
res: fr,
|
||||
},
|
||||
{
|
||||
id: 1000034010,
|
||||
name: 'Bangla',
|
||||
tag: 'bn',
|
||||
originalName: 'বাংলা',
|
||||
flagEmoji: '🇧🇩',
|
||||
base: false,
|
||||
completeRate: 0.7083333333333334,
|
||||
res: bn,
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -44,26 +44,25 @@ import { fetchTolgee } from './request';
|
||||
* ```
|
||||
*/
|
||||
export const getAllProjectLanguages = async (size = 1000) => {
|
||||
const url = `/languages?size=${size}`;
|
||||
const resp = await fetchTolgee(url);
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
_embedded: {
|
||||
languages: {
|
||||
id: number;
|
||||
name: string;
|
||||
tag: string;
|
||||
originalName: string;
|
||||
flagEmoji: string;
|
||||
base: boolean;
|
||||
}[];
|
||||
};
|
||||
page: unknown;
|
||||
} = await resp.json();
|
||||
return json._embedded.languages;
|
||||
const url = `/languages?size=${size}`;
|
||||
const resp = await fetchTolgee(url);
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json: {
|
||||
_embedded: {
|
||||
languages: {
|
||||
id: number;
|
||||
name: string;
|
||||
tag: string;
|
||||
originalName: string;
|
||||
flagEmoji: string;
|
||||
base: boolean;
|
||||
}[];
|
||||
};
|
||||
page: unknown;
|
||||
} = await resp.json();
|
||||
return json._embedded.languages;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -72,13 +71,13 @@ export const getAllProjectLanguages = async (size = 1000) => {
|
||||
* See https://tolgee.io/api#operation/getTranslations_
|
||||
*/
|
||||
export const getTranslations = async () => {
|
||||
const url = '/translations';
|
||||
const resp = await fetchTolgee(url);
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
const url = '/translations';
|
||||
const resp = await fetchTolgee(url);
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -87,25 +86,24 @@ export const getTranslations = async () => {
|
||||
* See https://tolgee.io/api#operation/getAllTranslations_1
|
||||
*/
|
||||
export const getLanguagesTranslations = async <T extends string>(
|
||||
languages: T
|
||||
languages: T
|
||||
) => {
|
||||
const url = `/translations/${languages}`;
|
||||
const resp = await fetchTolgee(url);
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json: { [key in T]?: Record<string, string> } = await resp.json();
|
||||
return json;
|
||||
const url = `/translations/${languages}`;
|
||||
const resp = await fetchTolgee(url);
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json: { [key in T]?: Record<string, string> } = await resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
export const getRemoteTranslations = async (languages: string) => {
|
||||
const translations = await getLanguagesTranslations(languages);
|
||||
if (!(languages in translations)) {
|
||||
return {};
|
||||
}
|
||||
// The assert is safe because we checked above
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return translations[languages]!;
|
||||
const translations = await getLanguagesTranslations(languages);
|
||||
if (!(languages in translations)) {
|
||||
return {};
|
||||
}
|
||||
// The assert is safe because we checked above
|
||||
return translations[languages]!;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -114,19 +112,19 @@ export const getRemoteTranslations = async (languages: string) => {
|
||||
* See https://tolgee.io/api#operation/create_2
|
||||
*/
|
||||
export const createsNewKey = async (
|
||||
key: string,
|
||||
translations: Record<string, string>
|
||||
key: string,
|
||||
translations: Record<string, string>
|
||||
) => {
|
||||
const url = '/translations/keys/create';
|
||||
const resp = await fetchTolgee(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: key, translations }),
|
||||
});
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
const url = '/translations/keys/create';
|
||||
const resp = await fetchTolgee(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: key, translations }),
|
||||
});
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -135,16 +133,16 @@ export const createsNewKey = async (
|
||||
* See https://tolgee.io/api#operation/tagKey_1
|
||||
*/
|
||||
export const addTag = async (keyId: string, tagName: string) => {
|
||||
const url = `/keys/${keyId}/tags`;
|
||||
const resp = await fetchTolgee(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name: tagName }),
|
||||
});
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
const url = `/keys/${keyId}/tags`;
|
||||
const resp = await fetchTolgee(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name: tagName }),
|
||||
});
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -153,21 +151,21 @@ export const addTag = async (keyId: string, tagName: string) => {
|
||||
* See https://tolgee.io/api#operation/tagKey_1
|
||||
*/
|
||||
export const removeTag = async (keyId: string, tagId: number) => {
|
||||
const url = `/keys/${keyId}/tags/${tagId}`;
|
||||
const resp = await fetchTolgee(url, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
const url = `/keys/${keyId}/tags/${tagId}`;
|
||||
const resp = await fetchTolgee(url, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
export const addTagByKey = async (key: string, tag: string) => {
|
||||
// TODO get key id by key name
|
||||
// const keyId =
|
||||
// addTag(keyId, tag);
|
||||
// TODO get key id by key name
|
||||
// const keyId =
|
||||
// addTag(keyId, tag);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -176,11 +174,11 @@ export const addTagByKey = async (key: string, tag: string) => {
|
||||
* See https://tolgee.io/api#operation/export_1
|
||||
*/
|
||||
export const exportResources = async () => {
|
||||
const url = `/export`;
|
||||
const resp = await fetchTolgee(url);
|
||||
const url = `/export`;
|
||||
const resp = await fetchTolgee(url);
|
||||
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
return resp;
|
||||
if (resp.status < 200 || resp.status >= 300) {
|
||||
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-console */
|
||||
// cSpell:ignore Tolgee
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
@@ -9,126 +8,123 @@ import type { TranslationRes } from './utils';
|
||||
const RES_DIR = path.resolve(process.cwd(), 'src', 'resources');
|
||||
|
||||
const countKeys = (obj: TranslationRes) => {
|
||||
let count = 0;
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
count++;
|
||||
} else {
|
||||
count += countKeys(value);
|
||||
}
|
||||
});
|
||||
return count;
|
||||
let count = 0;
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
count++;
|
||||
} else {
|
||||
count += countKeys(value);
|
||||
}
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
const getBaseTranslations = async (baseLanguage: { tag: string }) => {
|
||||
try {
|
||||
const baseTranslationsStr = await fs.readFile(
|
||||
path.resolve(RES_DIR, `${baseLanguage.tag}.json`),
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
const baseTranslations = JSON.parse(baseTranslationsStr);
|
||||
return baseTranslations;
|
||||
} catch (e) {
|
||||
console.error('base language:', JSON.stringify(baseLanguage));
|
||||
console.error('Failed to read base language', e);
|
||||
const translations = await getRemoteTranslations(baseLanguage.tag);
|
||||
await fs.writeFile(
|
||||
path.resolve(RES_DIR, `${baseLanguage.tag}.json`),
|
||||
JSON.stringify(translations, null, 4)
|
||||
);
|
||||
}
|
||||
try {
|
||||
const baseTranslationsStr = await fs.readFile(
|
||||
path.resolve(RES_DIR, `${baseLanguage.tag}.json`),
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
const baseTranslations = JSON.parse(baseTranslationsStr);
|
||||
return baseTranslations;
|
||||
} catch (e) {
|
||||
console.error('base language:', JSON.stringify(baseLanguage));
|
||||
console.error('Failed to read base language', e);
|
||||
const translations = await getRemoteTranslations(baseLanguage.tag);
|
||||
await fs.writeFile(
|
||||
path.resolve(RES_DIR, `${baseLanguage.tag}.json`),
|
||||
JSON.stringify(translations, null, 4)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
console.log('Loading project languages...');
|
||||
const languages = await getAllProjectLanguages();
|
||||
const baseLanguage = languages.find(language => language.base);
|
||||
if (!baseLanguage) {
|
||||
console.error(JSON.stringify(languages));
|
||||
throw new Error('Could not find base language');
|
||||
}
|
||||
console.log(
|
||||
`Loading ${baseLanguage.tag} languages translations as base...`
|
||||
);
|
||||
console.log('Loading project languages...');
|
||||
const languages = await getAllProjectLanguages();
|
||||
const baseLanguage = languages.find(language => language.base);
|
||||
if (!baseLanguage) {
|
||||
console.error(JSON.stringify(languages));
|
||||
throw new Error('Could not find base language');
|
||||
}
|
||||
console.log(`Loading ${baseLanguage.tag} languages translations as base...`);
|
||||
|
||||
const baseTranslations = await getBaseTranslations(baseLanguage);
|
||||
const baseKeyNum = countKeys(baseTranslations);
|
||||
const languagesWithTranslations = await Promise.all(
|
||||
languages.map(async language => {
|
||||
console.log(`Loading ${language.tag} translations...`);
|
||||
const translations = await getRemoteTranslations(language.tag);
|
||||
const keyNum = countKeys(translations);
|
||||
const baseTranslations = await getBaseTranslations(baseLanguage);
|
||||
const baseKeyNum = countKeys(baseTranslations);
|
||||
const languagesWithTranslations = await Promise.all(
|
||||
languages.map(async language => {
|
||||
console.log(`Loading ${language.tag} translations...`);
|
||||
const translations = await getRemoteTranslations(language.tag);
|
||||
const keyNum = countKeys(translations);
|
||||
|
||||
return {
|
||||
...language,
|
||||
translations,
|
||||
completeRate: keyNum / baseKeyNum,
|
||||
};
|
||||
})
|
||||
);
|
||||
return {
|
||||
...language,
|
||||
translations,
|
||||
completeRate: keyNum / baseKeyNum,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const availableLanguages = languagesWithTranslations.filter(
|
||||
language => language.completeRate > 0
|
||||
);
|
||||
const availableLanguages = languagesWithTranslations.filter(
|
||||
language => language.completeRate > 0
|
||||
);
|
||||
|
||||
availableLanguages
|
||||
// skip base language
|
||||
.filter(i => !i.base)
|
||||
.forEach(async language => {
|
||||
await fs.writeFile(
|
||||
path.resolve(RES_DIR, `${language.tag}.json`),
|
||||
JSON.stringify(
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.':
|
||||
'',
|
||||
...language.translations,
|
||||
},
|
||||
null,
|
||||
4
|
||||
) + '\n'
|
||||
);
|
||||
});
|
||||
availableLanguages
|
||||
// skip base language
|
||||
.filter(i => !i.base)
|
||||
.forEach(async language => {
|
||||
await fs.writeFile(
|
||||
path.resolve(RES_DIR, `${language.tag}.json`),
|
||||
JSON.stringify(
|
||||
{
|
||||
'// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.':
|
||||
'',
|
||||
...language.translations,
|
||||
},
|
||||
null,
|
||||
4
|
||||
) + '\n'
|
||||
);
|
||||
});
|
||||
|
||||
console.log('Generating meta data...');
|
||||
const code = `// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
console.log('Generating meta data...');
|
||||
const code = `// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
// Run \`pnpm run download-resources\` to regenerate.
|
||||
// To overwrite this, please overwrite ${path.basename(__filename)}
|
||||
${availableLanguages
|
||||
.map(
|
||||
language =>
|
||||
`import ${language.tag.replaceAll('-', '_')} from './${
|
||||
language.tag
|
||||
}.json'`
|
||||
)
|
||||
.join('\n')}
|
||||
.map(
|
||||
language =>
|
||||
`import ${language.tag.replaceAll('-', '_')} from './${
|
||||
language.tag
|
||||
}.json'`
|
||||
)
|
||||
.join('\n')}
|
||||
|
||||
export const LOCALES = [
|
||||
${availableLanguages
|
||||
.map(({ translations, ...language }) =>
|
||||
JSON.stringify({
|
||||
...language,
|
||||
res: '__RES_PLACEHOLDER',
|
||||
}).replace(
|
||||
'"__RES_PLACEHOLDER"',
|
||||
language.tag.replaceAll('-', '_')
|
||||
)
|
||||
.map(({ translations, ...language }) =>
|
||||
JSON.stringify({
|
||||
...language,
|
||||
res: '__RES_PLACEHOLDER',
|
||||
}).replace(
|
||||
'"__RES_PLACEHOLDER"',
|
||||
language.tag.replaceAll('-', '_')
|
||||
)
|
||||
.join(',\n')}
|
||||
)
|
||||
.join(',\n')}
|
||||
] as const;
|
||||
`;
|
||||
|
||||
await fs.writeFile(
|
||||
path.resolve(RES_DIR, 'index.ts'),
|
||||
format(code, {
|
||||
parser: 'typescript',
|
||||
singleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
tabWidth: 4,
|
||||
arrowParens: 'avoid',
|
||||
})
|
||||
);
|
||||
console.log('Done');
|
||||
await fs.writeFile(
|
||||
path.resolve(RES_DIR, 'index.ts'),
|
||||
format(code, {
|
||||
parser: 'typescript',
|
||||
singleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
tabWidth: 4,
|
||||
arrowParens: 'avoid',
|
||||
})
|
||||
);
|
||||
console.log('Done');
|
||||
};
|
||||
|
||||
main();
|
||||
|
||||
@@ -3,53 +3,53 @@ const TOLGEE_API_KEY = process.env['TOLGEE_API_KEY'];
|
||||
const TOLGEE_API_URL = 'https://i18n.affine.pro';
|
||||
|
||||
if (!TOLGEE_API_KEY) {
|
||||
throw new Error(`Please set "TOLGEE_API_KEY" as environment variable!`);
|
||||
throw new Error(`Please set "TOLGEE_API_KEY" as environment variable!`);
|
||||
}
|
||||
|
||||
const withTolgee = (
|
||||
fetch: typeof globalThis.fetch
|
||||
fetch: typeof globalThis.fetch
|
||||
): typeof globalThis.fetch => {
|
||||
const baseUrl = `${TOLGEE_API_URL}/v2/projects`;
|
||||
const headers = new Headers({
|
||||
'X-API-Key': TOLGEE_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
const baseUrl = `${TOLGEE_API_URL}/v2/projects`;
|
||||
const headers = new Headers({
|
||||
'X-API-Key': TOLGEE_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
const isRequest = (input: RequestInfo | URL): input is Request => {
|
||||
return typeof input === 'object' && !('href' in input);
|
||||
};
|
||||
const isRequest = (input: RequestInfo | URL): input is Request => {
|
||||
return typeof input === 'object' && !('href' in input);
|
||||
};
|
||||
|
||||
return new Proxy(fetch, {
|
||||
apply(
|
||||
target,
|
||||
thisArg: unknown,
|
||||
argArray: Parameters<typeof globalThis.fetch>
|
||||
) {
|
||||
if (isRequest(argArray[0])) {
|
||||
// Request
|
||||
if (!argArray[0].headers) {
|
||||
argArray[0] = {
|
||||
...argArray[0],
|
||||
url: `${baseUrl}${argArray[0].url}`,
|
||||
headers,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// URL or URLLike + ?RequestInit
|
||||
if (typeof argArray[0] === 'string') {
|
||||
argArray[0] = `${baseUrl}${argArray[0]}`;
|
||||
}
|
||||
if (!argArray[1]) {
|
||||
argArray[1] = {};
|
||||
}
|
||||
if (!argArray[1].headers) {
|
||||
argArray[1].headers = headers;
|
||||
}
|
||||
}
|
||||
// console.log('fetch', argArray);
|
||||
return target.apply(thisArg, argArray);
|
||||
},
|
||||
});
|
||||
return new Proxy(fetch, {
|
||||
apply(
|
||||
target,
|
||||
thisArg: unknown,
|
||||
argArray: Parameters<typeof globalThis.fetch>
|
||||
) {
|
||||
if (isRequest(argArray[0])) {
|
||||
// Request
|
||||
if (!argArray[0].headers) {
|
||||
argArray[0] = {
|
||||
...argArray[0],
|
||||
url: `${baseUrl}${argArray[0].url}`,
|
||||
headers,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// URL or URLLike + ?RequestInit
|
||||
if (typeof argArray[0] === 'string') {
|
||||
argArray[0] = `${baseUrl}${argArray[0]}`;
|
||||
}
|
||||
if (!argArray[1]) {
|
||||
argArray[1] = {};
|
||||
}
|
||||
if (!argArray[1].headers) {
|
||||
argArray[1].headers = headers;
|
||||
}
|
||||
}
|
||||
// console.log('fetch', argArray);
|
||||
return target.apply(thisArg, argArray);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchTolgee = withTolgee(globalThis.fetch);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-console */
|
||||
// cSpell:ignore Tolgee
|
||||
import { readFile } from 'fs/promises';
|
||||
import path from 'path';
|
||||
@@ -6,10 +5,10 @@ import { addTagByKey, createsNewKey, getRemoteTranslations } from './api';
|
||||
import type { TranslationRes } from './utils';
|
||||
|
||||
const BASE_JSON_PATH = path.resolve(
|
||||
process.cwd(),
|
||||
'src',
|
||||
'resources',
|
||||
'en.json'
|
||||
process.cwd(),
|
||||
'src',
|
||||
'resources',
|
||||
'en.json'
|
||||
);
|
||||
const BASE_LANGUAGES = 'en' as const;
|
||||
|
||||
@@ -23,142 +22,135 @@ const DEPRECATED_TAG_NAME = 'unused' as const;
|
||||
* ```
|
||||
*/
|
||||
const flatRes = (obj: TranslationRes) => {
|
||||
const getEntries = (o: TranslationRes, prefix = ''): [string, string][] =>
|
||||
Object.entries(o).flatMap<[string, string]>(([k, v]) =>
|
||||
typeof v !== 'string'
|
||||
? getEntries(v, `${prefix}${k}.`)
|
||||
: [[`${prefix}${k}`, v]]
|
||||
);
|
||||
return Object.fromEntries(getEntries(obj));
|
||||
const getEntries = (o: TranslationRes, prefix = ''): [string, string][] =>
|
||||
Object.entries(o).flatMap<[string, string]>(([k, v]) =>
|
||||
typeof v !== 'string'
|
||||
? getEntries(v, `${prefix}${k}.`)
|
||||
: [[`${prefix}${k}`, v]]
|
||||
);
|
||||
return Object.fromEntries(getEntries(obj));
|
||||
};
|
||||
|
||||
const differenceObject = (
|
||||
newObj: Record<string, string>,
|
||||
oldObj: Record<string, string>
|
||||
newObj: Record<string, string>,
|
||||
oldObj: Record<string, string>
|
||||
) => {
|
||||
const add: string[] = [];
|
||||
const remove: string[] = [];
|
||||
const modify: string[] = [];
|
||||
const both: string[] = [];
|
||||
const add: string[] = [];
|
||||
const remove: string[] = [];
|
||||
const modify: string[] = [];
|
||||
const both: string[] = [];
|
||||
|
||||
Object.keys(newObj).forEach(key => {
|
||||
if (!(key in oldObj)) {
|
||||
add.push(key);
|
||||
} else {
|
||||
both.push(key);
|
||||
}
|
||||
});
|
||||
Object.keys(newObj).forEach(key => {
|
||||
if (!(key in oldObj)) {
|
||||
add.push(key);
|
||||
} else {
|
||||
both.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(oldObj).forEach(key => {
|
||||
if (!(key in newObj)) {
|
||||
remove.push(key);
|
||||
}
|
||||
});
|
||||
Object.keys(oldObj).forEach(key => {
|
||||
if (!(key in newObj)) {
|
||||
remove.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
both.forEach(key => {
|
||||
if (!(key in newObj) || !(key in oldObj)) {
|
||||
throw new Error('Unreachable');
|
||||
}
|
||||
const newVal = newObj[key];
|
||||
const oldVal = oldObj[key];
|
||||
if (newVal !== oldVal) {
|
||||
modify.push(key);
|
||||
}
|
||||
});
|
||||
return { add, remove, modify };
|
||||
both.forEach(key => {
|
||||
if (!(key in newObj) || !(key in oldObj)) {
|
||||
throw new Error('Unreachable');
|
||||
}
|
||||
const newVal = newObj[key];
|
||||
const oldVal = oldObj[key];
|
||||
if (newVal !== oldVal) {
|
||||
modify.push(key);
|
||||
}
|
||||
});
|
||||
return { add, remove, modify };
|
||||
};
|
||||
|
||||
function warnDiff(diff: { add: string[]; remove: string[]; modify: string[] }) {
|
||||
if (diff.add.length) {
|
||||
console.log('New keys found:', diff.add.join(', '));
|
||||
//See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message
|
||||
process.env['CI'] &&
|
||||
console.log(
|
||||
`::notice file=${BASE_JSON_PATH},line=1,title=New keys::${diff.add.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (diff.remove.length) {
|
||||
console.warn('[WARN]', 'Unused keys found:', diff.remove.join(', '));
|
||||
process.env['CI'] &&
|
||||
console.warn(
|
||||
`::notice file=${BASE_JSON_PATH},line=1,title=Unused keys::${diff.remove.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (diff.modify.length) {
|
||||
console.warn(
|
||||
'[WARN]',
|
||||
'Inconsistent keys found:',
|
||||
diff.modify.join(', ')
|
||||
);
|
||||
process.env['CI'] &&
|
||||
console.warn(
|
||||
`::warning file=${BASE_JSON_PATH},line=1,title=Inconsistent keys::${diff.modify.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (diff.add.length) {
|
||||
console.log('New keys found:', diff.add.join(', '));
|
||||
//See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message
|
||||
process.env['CI'] &&
|
||||
console.log(
|
||||
`::notice file=${BASE_JSON_PATH},line=1,title=New keys::${diff.add.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (diff.remove.length) {
|
||||
console.warn('[WARN]', 'Unused keys found:', diff.remove.join(', '));
|
||||
process.env['CI'] &&
|
||||
console.warn(
|
||||
`::notice file=${BASE_JSON_PATH},line=1,title=Unused keys::${diff.remove.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (diff.modify.length) {
|
||||
console.warn('[WARN]', 'Inconsistent keys found:', diff.modify.join(', '));
|
||||
process.env['CI'] &&
|
||||
console.warn(
|
||||
`::warning file=${BASE_JSON_PATH},line=1,title=Inconsistent keys::${diff.modify.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
console.log('Loading local base translations...');
|
||||
const baseLocalTranslations = JSON.parse(
|
||||
await readFile(BASE_JSON_PATH, {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
);
|
||||
const flatLocalTranslations = flatRes(baseLocalTranslations);
|
||||
console.log(
|
||||
`Loading local base translations success! Total ${
|
||||
Object.keys(flatLocalTranslations).length
|
||||
} keys`
|
||||
);
|
||||
console.log('Loading local base translations...');
|
||||
const baseLocalTranslations = JSON.parse(
|
||||
await readFile(BASE_JSON_PATH, {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
);
|
||||
const flatLocalTranslations = flatRes(baseLocalTranslations);
|
||||
console.log(
|
||||
`Loading local base translations success! Total ${
|
||||
Object.keys(flatLocalTranslations).length
|
||||
} keys`
|
||||
);
|
||||
|
||||
console.log('Fetch remote base translations...');
|
||||
const baseRemoteTranslations = await getRemoteTranslations(BASE_LANGUAGES);
|
||||
const flatRemoteTranslations = flatRes(baseRemoteTranslations);
|
||||
console.log(
|
||||
`Fetch remote base translations success! Total ${
|
||||
Object.keys(flatRemoteTranslations).length
|
||||
} keys`
|
||||
);
|
||||
console.log('Fetch remote base translations...');
|
||||
const baseRemoteTranslations = await getRemoteTranslations(BASE_LANGUAGES);
|
||||
const flatRemoteTranslations = flatRes(baseRemoteTranslations);
|
||||
console.log(
|
||||
`Fetch remote base translations success! Total ${
|
||||
Object.keys(flatRemoteTranslations).length
|
||||
} keys`
|
||||
);
|
||||
|
||||
const diff = differenceObject(
|
||||
flatLocalTranslations,
|
||||
flatRemoteTranslations
|
||||
);
|
||||
const diff = differenceObject(flatLocalTranslations, flatRemoteTranslations);
|
||||
|
||||
console.log(''); // new line
|
||||
warnDiff(diff);
|
||||
console.log(''); // new line
|
||||
console.log(''); // new line
|
||||
warnDiff(diff);
|
||||
console.log(''); // new line
|
||||
|
||||
if (process.argv.slice(2).includes('--check')) {
|
||||
// check mode
|
||||
return;
|
||||
}
|
||||
if (process.argv.slice(2).includes('--check')) {
|
||||
// check mode
|
||||
return;
|
||||
}
|
||||
|
||||
diff.add.forEach(async key => {
|
||||
const val = flatLocalTranslations[key];
|
||||
console.log(`Creating new key: ${key} -> ${val}`);
|
||||
await createsNewKey(key, { [BASE_LANGUAGES]: val });
|
||||
});
|
||||
diff.add.forEach(async key => {
|
||||
const val = flatLocalTranslations[key];
|
||||
console.log(`Creating new key: ${key} -> ${val}`);
|
||||
await createsNewKey(key, { [BASE_LANGUAGES]: val });
|
||||
});
|
||||
|
||||
// TODO remove unused tags from used keys
|
||||
// TODO remove unused tags from used keys
|
||||
|
||||
diff.remove.forEach(key => {
|
||||
// TODO set unused tag
|
||||
// console.log(`Add ${DEPRECATED_TAG_NAME} to ${key}`);
|
||||
addTagByKey(key, DEPRECATED_TAG_NAME);
|
||||
});
|
||||
diff.remove.forEach(key => {
|
||||
// TODO set unused tag
|
||||
// console.log(`Add ${DEPRECATED_TAG_NAME} to ${key}`);
|
||||
addTagByKey(key, DEPRECATED_TAG_NAME);
|
||||
});
|
||||
|
||||
diff.modify.forEach(key => {
|
||||
// TODO warn different between local and remote base translations
|
||||
});
|
||||
diff.modify.forEach(key => {
|
||||
// TODO warn different between local and remote base translations
|
||||
});
|
||||
|
||||
// TODO send notification
|
||||
// TODO send notification
|
||||
};
|
||||
|
||||
main();
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface TranslationRes {
|
||||
[x: string]: string | TranslationRes;
|
||||
[x: string]: string | TranslationRes;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const AlreadyJoined = () => {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
You are already a member of <code>Workspace name</code>
|
||||
</p>
|
||||
<button>Open Workspace</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
export const Confirm = () => {
|
||||
return (
|
||||
<div>
|
||||
<p>Inviter name</p>
|
||||
<p>
|
||||
invite you to join in <code>Workspace name</code>
|
||||
</p>
|
||||
<button>Join</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export const LinkExpired = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>The current invitation link has expired.</h1>
|
||||
<a href={location.origin}>Back to home</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import { AlreadyJoined } from './AlreadyJoined';
|
||||
import { Confirm } from './Confirm';
|
||||
import { LinkExpired } from './LinkExpired';
|
||||
|
||||
export const ConfirmInvitation = () => {
|
||||
const router = useRouter();
|
||||
// Temporary code. The code should be returned by request.
|
||||
const { code } = router.query;
|
||||
const Component = {
|
||||
'-1': LinkExpired,
|
||||
0: Confirm,
|
||||
1: AlreadyJoined,
|
||||
}[code as string];
|
||||
return (
|
||||
<div>
|
||||
<h1>Confirm Invitation</h1>
|
||||
{Component ? <Component /> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import NotfoundPage from '@/components/404';
|
||||
export default function Custom404() {
|
||||
return <NotfoundPage></NotfoundPage>;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import '../../public/variable.css';
|
||||
import './temporary.css';
|
||||
import { EditorProvider } from '@/providers/editor-provider';
|
||||
import { ModalProvider } from '@/providers/global-modal-provider';
|
||||
import { AppStateProvider } from '@/providers/app-state-provider';
|
||||
import { Logger } from '@toeverything/pathfinder-logger';
|
||||
import { WorkSpaceSliderBar } from '@/components/workspace-slider-bar';
|
||||
import '@fontsource/space-mono';
|
||||
@@ -49,6 +50,7 @@ function MyApp({ Component, pageProps }: AppProps) {
|
||||
<ThemeProvider key="ThemeProvider" />,
|
||||
<ModalProvider key="ModalProvider" />,
|
||||
<ConfirmProvider key="ConfirmProvider" />,
|
||||
<AppStateProvider key="appStateProvider" />,
|
||||
]}
|
||||
>
|
||||
<StyledPage>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// for dynamic route get workspace id maybe path will change
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const Post = () => {
|
||||
const router = useRouter();
|
||||
const { workspace_id } = router.query;
|
||||
|
||||
return (
|
||||
<p
|
||||
style={{
|
||||
height: 'calc(100vh)',
|
||||
color: 'gray',
|
||||
}}
|
||||
>
|
||||
workspace_id: {workspace_id},
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export default Post;
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { NextPage } from 'next';
|
||||
import { ConfirmInvitation } from '../pages-content/ConfirmInvitationPage';
|
||||
|
||||
const ConfirmInvitationPage: NextPage = () => {
|
||||
return <ConfirmInvitation />;
|
||||
};
|
||||
|
||||
export default ConfirmInvitationPage;
|
||||
@@ -0,0 +1,57 @@
|
||||
import { createContext, useMemo, useContext, useState, useEffect } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
authorizationEvent,
|
||||
AccessTokenMessage,
|
||||
getWorkspaces,
|
||||
} from '@pathfinder/data-services';
|
||||
import type { Workspace } from '@pathfinder/data-services';
|
||||
|
||||
interface AppStateValue {
|
||||
user: AccessTokenMessage | null;
|
||||
workspaces: Workspace[];
|
||||
}
|
||||
|
||||
interface AppStateContext extends AppStateValue {
|
||||
setState: (state: AppStateValue) => void;
|
||||
}
|
||||
|
||||
const AppState = createContext<AppStateContext>({
|
||||
user: null,
|
||||
workspaces: [],
|
||||
setState: () => {},
|
||||
});
|
||||
|
||||
export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
|
||||
const [state, setState] = useState<AppStateValue>({
|
||||
user: null,
|
||||
workspaces: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const callback = async (user: AccessTokenMessage | null) => {
|
||||
const workspaces = user ? await getWorkspaces() : [];
|
||||
setState(state => ({ ...state, user: user, workspaces }));
|
||||
};
|
||||
authorizationEvent.onChange(callback);
|
||||
|
||||
return () => {
|
||||
authorizationEvent.removeCallback(callback);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const context = useMemo(
|
||||
() => ({
|
||||
...state,
|
||||
setState,
|
||||
}),
|
||||
[state, setState]
|
||||
);
|
||||
|
||||
return <AppState.Provider value={context}>{children}</AppState.Provider>;
|
||||
};
|
||||
|
||||
export const useAppState = () => {
|
||||
const state = useContext(AppState);
|
||||
return state;
|
||||
};
|
||||
@@ -4,12 +4,14 @@ import ShortcutsModal from '@/components/shortcuts-modal';
|
||||
import ContactModal from '@/components/contact-modal';
|
||||
import QuickSearch from '@/components/quick-search';
|
||||
import { ImportModal } from '@/components/import';
|
||||
import { LoginModal } from '@/components/login-modal';
|
||||
|
||||
type ModalContextValue = {
|
||||
triggerShortcutsModal: () => void;
|
||||
triggerContactModal: () => void;
|
||||
triggerQuickSearchModal: (visible?: boolean) => void;
|
||||
triggerImportModal: () => void;
|
||||
triggerLoginModal: () => void;
|
||||
};
|
||||
type ModalContextProps = PropsWithChildren<{}>;
|
||||
type ModalMap = {
|
||||
@@ -17,6 +19,7 @@ type ModalMap = {
|
||||
shortcuts: boolean;
|
||||
quickSearch: boolean;
|
||||
import: boolean;
|
||||
login: boolean;
|
||||
};
|
||||
|
||||
export const ModalContext = createContext<ModalContextValue>({
|
||||
@@ -24,6 +27,7 @@ export const ModalContext = createContext<ModalContextValue>({
|
||||
triggerContactModal: () => {},
|
||||
triggerQuickSearchModal: (visible?) => {},
|
||||
triggerImportModal: () => {},
|
||||
triggerLoginModal: () => {},
|
||||
});
|
||||
|
||||
export const useModal = () => useContext(ModalContext);
|
||||
@@ -36,6 +40,7 @@ export const ModalProvider = ({
|
||||
shortcuts: false,
|
||||
quickSearch: false,
|
||||
import: false,
|
||||
login: false,
|
||||
});
|
||||
|
||||
const triggerHandler = (key: keyof ModalMap, visible?: boolean) => {
|
||||
@@ -60,6 +65,9 @@ export const ModalProvider = ({
|
||||
triggerImportModal: () => {
|
||||
triggerHandler('import');
|
||||
},
|
||||
triggerLoginModal: () => {
|
||||
triggerHandler('login');
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ContactModal
|
||||
@@ -86,6 +94,12 @@ export const ModalProvider = ({
|
||||
triggerHandler('import', false);
|
||||
}}
|
||||
></ImportModal>
|
||||
<LoginModal
|
||||
open={modalMap.login}
|
||||
onClose={() => {
|
||||
triggerHandler('login', false);
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</ModalContext.Provider>
|
||||
);
|
||||
|
||||
@@ -30,6 +30,7 @@ export const getLightTheme = (
|
||||
linkColor2: '#6880FF',
|
||||
linkVisitedColor: '#ABB8FE',
|
||||
popoverColor: '#4C6275',
|
||||
inputColor: '#4C6275',
|
||||
tooltipColor: '#fff',
|
||||
codeColor: '#517ea6',
|
||||
quoteColor: '#4C6275',
|
||||
@@ -105,6 +106,7 @@ export const getDarkTheme = (
|
||||
linkColor2: '#6880FF',
|
||||
linkVisitedColor: '#505FAB',
|
||||
popoverColor: '#C6CBD9',
|
||||
inputColor: '#C6CBD9',
|
||||
tooltipColor: '#fff',
|
||||
codeColor:
|
||||
editorMode === 'edgeless' ? lightTheme.colors.codeColor : '#BDDBFD',
|
||||
@@ -150,6 +152,7 @@ export const globalThemeVariables: (
|
||||
'--affine-link-visited-color': theme.colors.linkVisitedColor,
|
||||
'--affine-icon-color': theme.colors.iconColor,
|
||||
'--affine-popover-color': theme.colors.popoverColor,
|
||||
'--affine-input-color': theme.colors.inputColor,
|
||||
'--affine-code-color': theme.colors.codeColor,
|
||||
'--affine-quote-color': theme.colors.quoteColor,
|
||||
'--affine-selected-color': theme.colors.selectedColor,
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface AffineTheme {
|
||||
linkVisitedColor: string;
|
||||
iconColor: string;
|
||||
popoverColor: string;
|
||||
inputColor: string;
|
||||
tooltipColor: string;
|
||||
codeColor: string;
|
||||
quoteColor: string;
|
||||
|
||||
@@ -39,7 +39,7 @@ export type IconButtonProps = {
|
||||
| typeof SIZE_NORMAL
|
||||
| [number, number];
|
||||
disabled?: boolean;
|
||||
hoverBackground?: string;
|
||||
hoverBackground?: CSSProperties['background'];
|
||||
hoverColor?: string;
|
||||
hoverStyle?: CSSProperties;
|
||||
children: ReactElement<HTMLAttributes<SVGElement>, 'svg'>;
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './icon-button';
|
||||
export * from './button';
|
||||
export * from './text-button';
|
||||
|
||||
@@ -21,7 +21,7 @@ export const StyledIconButton = styled('button', {
|
||||
height: number;
|
||||
borderRadius: number;
|
||||
disabled?: boolean;
|
||||
hoverBackground?: string;
|
||||
hoverBackground?: CSSProperties['background'];
|
||||
hoverColor?: string;
|
||||
hoverStyle?: CSSProperties;
|
||||
}>(
|
||||
@@ -68,6 +68,70 @@ export const StyledIconButton = styled('button', {
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledTextButton = styled('button', {
|
||||
shouldForwardProp: prop => {
|
||||
return ![
|
||||
'borderRadius',
|
||||
'top',
|
||||
'right',
|
||||
'width',
|
||||
'height',
|
||||
'hoverBackground',
|
||||
'hoverColor',
|
||||
'hoverStyle',
|
||||
].includes(prop);
|
||||
},
|
||||
})<
|
||||
Pick<
|
||||
ButtonProps,
|
||||
| 'size'
|
||||
| 'disabled'
|
||||
| 'hoverBackground'
|
||||
| 'hoverColor'
|
||||
| 'hoverStyle'
|
||||
| 'shape'
|
||||
| 'type'
|
||||
| 'bold'
|
||||
>
|
||||
>(
|
||||
({
|
||||
theme,
|
||||
size = 'default',
|
||||
disabled,
|
||||
hoverBackground,
|
||||
hoverColor,
|
||||
hoverStyle,
|
||||
bold = false,
|
||||
shape = 'default',
|
||||
// TODO: Implement type
|
||||
type = 'default',
|
||||
}) => {
|
||||
const { fontSize, borderRadius, padding, height } = getSize(size);
|
||||
console.log('size', size, height);
|
||||
|
||||
return {
|
||||
height,
|
||||
paddingLeft: padding,
|
||||
paddingRight: padding,
|
||||
...displayInlineFlex('flex-start', 'center'),
|
||||
position: 'relative',
|
||||
...(disabled ? { cursor: 'not-allowed', pointerEvents: 'none' } : {}),
|
||||
transition: 'background .15s',
|
||||
// TODO: Implement circle shape
|
||||
borderRadius: shape === 'default' ? borderRadius : height / 2,
|
||||
fontSize,
|
||||
fontWeight: bold ? '500' : '400',
|
||||
|
||||
':hover': {
|
||||
color: hoverColor ?? theme.colors.primaryColor,
|
||||
background: hoverBackground ?? theme.colors.hoverBackground,
|
||||
...(hoverStyle ?? {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledButton = styled('button', {
|
||||
shouldForwardProp: prop => {
|
||||
return !['hoverBackground', 'hoverColor', 'hoverStyle', 'type'].includes(
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { cloneElement, Children, forwardRef } from 'react';
|
||||
import { StyledTextButton } from './styles';
|
||||
|
||||
import { ButtonProps } from './interface';
|
||||
import { getSize } from './utils';
|
||||
|
||||
export const TextButton = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
size = 'default',
|
||||
disabled = false,
|
||||
hoverBackground,
|
||||
hoverColor,
|
||||
hoverStyle,
|
||||
shape = 'default',
|
||||
icon,
|
||||
type = 'default',
|
||||
children,
|
||||
bold = false,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { iconSize } = getSize(size);
|
||||
|
||||
return (
|
||||
<StyledTextButton
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
size={size}
|
||||
shape={shape}
|
||||
hoverBackground={hoverBackground}
|
||||
hoverColor={hoverColor}
|
||||
hoverStyle={hoverStyle}
|
||||
// @ts-ignore
|
||||
type={type}
|
||||
bold={bold}
|
||||
{...props}
|
||||
>
|
||||
{icon &&
|
||||
cloneElement(Children.only(icon), {
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
className: `affine-button-icon ${icon.props.className ?? ''}`,
|
||||
})}
|
||||
{children && <span>{children}</span>}
|
||||
</StyledTextButton>
|
||||
);
|
||||
}
|
||||
);
|
||||
TextButton.displayName = 'TextButton';
|
||||
|
||||
export default TextButton;
|
||||
@@ -0,0 +1,8 @@
|
||||
import MuiDivider from '@mui/material/Divider';
|
||||
import { styled } from '@/styles';
|
||||
|
||||
export const Divider = styled(MuiDivider)(({ theme }) => {
|
||||
return {
|
||||
borderColor: theme.colors.borderColor,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './input';
|
||||
import { Input } from './input';
|
||||
export default Input;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { InputHTMLAttributes, useState } from 'react';
|
||||
import { StyledInput } from './style';
|
||||
|
||||
type inputProps = {
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
width?: number;
|
||||
maxLength?: number;
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
export const Input = (props: inputProps) => {
|
||||
const {
|
||||
disabled,
|
||||
value: valueProp,
|
||||
placeholder,
|
||||
maxLength,
|
||||
width = 260,
|
||||
onChange,
|
||||
} = props;
|
||||
const [value, setValue] = useState<string>(valueProp || '');
|
||||
const handleChange: InputHTMLAttributes<HTMLInputElement>['onChange'] = e => {
|
||||
setValue(e.target.value);
|
||||
onChange && onChange(e.target.value);
|
||||
};
|
||||
return (
|
||||
<StyledInput
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
width={width}
|
||||
maxLength={maxLength}
|
||||
onChange={handleChange}
|
||||
></StyledInput>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { styled } from '@/styles';
|
||||
|
||||
export const StyledInput = styled('input')<{
|
||||
disabled?: boolean;
|
||||
value?: string;
|
||||
width: number;
|
||||
}>(({ theme, width, disabled }) => {
|
||||
const fontWeight = 400;
|
||||
const fontSize = '16px';
|
||||
return {
|
||||
width: `${width}px`,
|
||||
lineHeight: '22px',
|
||||
padding: '8px 12px',
|
||||
fontWeight,
|
||||
fontSize,
|
||||
color: disabled ? theme.colors.disableColor : theme.colors.inputColor,
|
||||
border: `1px solid`,
|
||||
borderColor: theme.colors.borderColor, // TODO: check out disableColor,
|
||||
backgroundColor: theme.colors.popoverBackground,
|
||||
borderRadius: '10px',
|
||||
'&::placeholder': {
|
||||
fontWeight,
|
||||
fontSize,
|
||||
color: theme.colors.placeHolderColor,
|
||||
},
|
||||
'&:focus': {
|
||||
borderColor: theme.colors.primaryColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { describe, test, expect } from '@jest/globals';
|
||||
import { isMobile } from '../get-is-mobile';
|
||||
|
||||
describe('get-is-mobile', () => {
|
||||
test('get-is-mobile', () => {
|
||||
expect(
|
||||
isMobile(
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,19 @@
|
||||
// Inspire by https://stackoverflow.com/a/11381730/8415727
|
||||
export const getIsMobile = function () {
|
||||
let check = false;
|
||||
(function (a) {
|
||||
if (
|
||||
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(
|
||||
a
|
||||
) ||
|
||||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
|
||||
a.substr(0, 4)
|
||||
)
|
||||
export function isMobile(userAgent: string) {
|
||||
return !!(
|
||||
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(
|
||||
userAgent
|
||||
) ||
|
||||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
|
||||
userAgent.substr(0, 4)
|
||||
)
|
||||
check = true;
|
||||
})(navigator?.userAgent || navigator?.vendor || (window as any)?.opera);
|
||||
return check;
|
||||
);
|
||||
}
|
||||
|
||||
export const getIsMobile = function () {
|
||||
return isMobile(
|
||||
navigator?.userAgent || navigator?.vendor || (window as any)?.opera
|
||||
);
|
||||
};
|
||||
|
||||
export default getIsMobile;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@pathfinder/data-services",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"exports": {
|
||||
"./src/*": "./dist/src/*.js",
|
||||
".": "./dist/src/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --project ./tsconfig.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/toeverything/AFFINE.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.2.0",
|
||||
"encoding": "^0.1.13",
|
||||
"firebase": "^9.13.0",
|
||||
"swr": "^2.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { initializeApp } from 'firebase/app';
|
||||
import {
|
||||
getAuth,
|
||||
createUserWithEmailAndPassword,
|
||||
signInWithEmailAndPassword,
|
||||
GoogleAuthProvider,
|
||||
signInWithPopup,
|
||||
} from 'firebase/auth';
|
||||
import type { User } from 'firebase/auth';
|
||||
|
||||
/**
|
||||
* firebaseConfig reference: https://firebase.google.com/docs/web/setup#add_firebase_to_your_app
|
||||
*/
|
||||
const app = initializeApp({
|
||||
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
|
||||
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
||||
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
||||
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
|
||||
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
||||
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
|
||||
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
|
||||
});
|
||||
|
||||
export const firebaseAuth = getAuth(app);
|
||||
|
||||
const signUp = (email: string, password: string) => {
|
||||
return createUserWithEmailAndPassword(firebaseAuth, email, password);
|
||||
};
|
||||
|
||||
const signIn = (email: string, password: string) => {
|
||||
return signInWithEmailAndPassword(firebaseAuth, email, password);
|
||||
};
|
||||
|
||||
const googleAuthProvider = new GoogleAuthProvider();
|
||||
export const signInWithGoogle = () => {
|
||||
return signInWithPopup(firebaseAuth, googleAuthProvider);
|
||||
};
|
||||
|
||||
export const onAuthStateChanged = (callback: (user: User | null) => void) => {
|
||||
firebaseAuth.onAuthStateChanged(callback);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export { signInWithGoogle, onAuthStateChanged } from './auth';
|
||||
export * from './request';
|
||||
export * from './sdks';
|
||||
@@ -0,0 +1,9 @@
|
||||
export class ServiceError extends Error {
|
||||
public message: string;
|
||||
public code: string;
|
||||
constructor(code: string, message: string) {
|
||||
super(message);
|
||||
this.message = message;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import axios from 'axios';
|
||||
import { setAuthorization } from './request';
|
||||
import { handleResponseError } from './response';
|
||||
|
||||
declare module 'axios' {
|
||||
interface AxiosRequestConfig {
|
||||
/**
|
||||
* If true, request will send with Authorization header.
|
||||
*/
|
||||
withAuthorization?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export const request = axios.create({
|
||||
withAuthorization: true,
|
||||
});
|
||||
|
||||
request.interceptors.request.use(setAuthorization);
|
||||
request.interceptors.response.use(handleResponseError);
|
||||
@@ -0,0 +1,4 @@
|
||||
export { request } from './axios';
|
||||
export { ServiceError } from './ServiceError';
|
||||
export { setToken, authorizationEvent } from './request';
|
||||
export type { AccessTokenMessage } from './request';
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import { login } from '../../../sdks';
|
||||
import { AuthorizationEvent } from './events';
|
||||
import type { AccessTokenMessage } from './types';
|
||||
|
||||
const TOKEN_KEY = 'affine_token';
|
||||
|
||||
export const authorizationEvent = new AuthorizationEvent();
|
||||
authorizationEvent.triggerChange(
|
||||
parseAccessToken(getToken()?.accessToken || '')
|
||||
);
|
||||
|
||||
interface Token {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* set null to clear token
|
||||
*/
|
||||
export function setToken(token: Token | null): void {
|
||||
if (token === null) {
|
||||
window.localStorage.removeItem(TOKEN_KEY);
|
||||
authorizationEvent.triggerChange(null);
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(TOKEN_KEY, JSON.stringify(token));
|
||||
authorizationEvent.triggerChange(parseAccessToken(token.accessToken));
|
||||
}
|
||||
|
||||
function getToken(): { accessToken: string; refreshToken: string } | null {
|
||||
try {
|
||||
return JSON.parse(window.localStorage.getItem(TOKEN_KEY) || '');
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseAccessToken(token: string): AccessTokenMessage | null {
|
||||
try {
|
||||
const message: AccessTokenMessage = JSON.parse(
|
||||
window.atob(token.split('.')[1])
|
||||
);
|
||||
return message;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isAccessTokenExpired(token: string) {
|
||||
const message = parseAccessToken(token);
|
||||
if (!message) {
|
||||
return true;
|
||||
}
|
||||
return Date.now() - message.create_at > message.exp;
|
||||
}
|
||||
|
||||
async function isLoggedIn(): Promise<boolean> {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
return isAccessTokenExpired(token.accessToken);
|
||||
}
|
||||
|
||||
let refreshingToken: ReturnType<typeof login> | undefined;
|
||||
|
||||
export async function setAuthorization(config: AxiosRequestConfig<unknown>) {
|
||||
if (!config.headers) {
|
||||
config.headers = {};
|
||||
}
|
||||
|
||||
if (config.withAuthorization) {
|
||||
let token = getToken();
|
||||
if (!token) {
|
||||
throw new Error('No authorization token.');
|
||||
}
|
||||
if (isAccessTokenExpired(token.accessToken)) {
|
||||
if (!refreshingToken) {
|
||||
refreshingToken = login({
|
||||
type: 'Refresh',
|
||||
token: token.refreshToken,
|
||||
});
|
||||
}
|
||||
const newToken = await refreshingToken;
|
||||
token = {
|
||||
accessToken: newToken.token,
|
||||
refreshToken: newToken.refresh,
|
||||
};
|
||||
setToken(token);
|
||||
refreshingToken = undefined;
|
||||
}
|
||||
if (!config.headers) {
|
||||
config.headers = {};
|
||||
}
|
||||
config.headers['Authorization'] = token.accessToken;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { AccessTokenMessage } from './types';
|
||||
|
||||
type Callback = (user: AccessTokenMessage | null) => void;
|
||||
|
||||
export class AuthorizationEvent {
|
||||
private callbacks: Callback[] = [];
|
||||
private lastState: AccessTokenMessage | null = null;
|
||||
|
||||
/**
|
||||
* Callback will execute when call this function.
|
||||
*/
|
||||
onChange(callback: Callback) {
|
||||
this.callbacks.push(callback);
|
||||
callback(this.lastState);
|
||||
}
|
||||
|
||||
triggerChange(user: AccessTokenMessage | null) {
|
||||
this.lastState = user;
|
||||
this.callbacks.forEach(callback => callback(user));
|
||||
}
|
||||
|
||||
removeCallback(callback: Callback) {
|
||||
const index = this.callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './authorization';
|
||||
export type { AccessTokenMessage } from './types';
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface AccessTokenMessage {
|
||||
create_at: number;
|
||||
exp: number;
|
||||
email: string;
|
||||
id: number;
|
||||
name: string;
|
||||
avatar_url: string;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
setAuthorization,
|
||||
setToken,
|
||||
authorizationEvent,
|
||||
} from './authorization';
|
||||
export type { AccessTokenMessage } from './authorization';
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import { ServiceError } from '../ServiceError';
|
||||
|
||||
export function handleResponseError(response: AxiosResponse<any, any>) {
|
||||
const { data, status } = response;
|
||||
if (status === 200) {
|
||||
if (data.error) {
|
||||
// TODO - common error handling
|
||||
const error = new ServiceError(data.error.message, data.error.code);
|
||||
throw error;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { handleResponseError } from './handle-error';
|
||||
@@ -0,0 +1,90 @@
|
||||
import useSWR, { SWRConfiguration } from 'swr';
|
||||
import { request, ServiceError } from '../request';
|
||||
import type {
|
||||
RequestAcceptInviting,
|
||||
RequestInviteCollaborator,
|
||||
RequestRemoveCollaborator,
|
||||
ResponseInviteCollaborator,
|
||||
} from './types';
|
||||
|
||||
const COLLABORATOR_INVITE_URL = '/api/account/invite';
|
||||
const REMOVE_COLLABORATOR_URL = '/api/account/remove_collaborator';
|
||||
|
||||
async function doInviteCollaborator(
|
||||
url: string,
|
||||
req: RequestInviteCollaborator
|
||||
) {
|
||||
const { data } = await request.post<ResponseInviteCollaborator>(url, req);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function inviteCollaborator(req: RequestInviteCollaborator) {
|
||||
return await doInviteCollaborator(COLLABORATOR_INVITE_URL, req);
|
||||
}
|
||||
|
||||
async function doRemoveCollaborator(
|
||||
url: string,
|
||||
req: RequestRemoveCollaborator
|
||||
) {
|
||||
return request.post(url, req);
|
||||
}
|
||||
|
||||
export async function removeCollaborator(req: RequestRemoveCollaborator) {
|
||||
return await doRemoveCollaborator(REMOVE_COLLABORATOR_URL, req);
|
||||
}
|
||||
|
||||
function doAcceptInviting(url: string, req: RequestAcceptInviting) {
|
||||
return request.post(url, req);
|
||||
}
|
||||
|
||||
export async function acceptInviting(req: RequestAcceptInviting) {
|
||||
return await doAcceptInviting(COLLABORATOR_INVITE_URL, req);
|
||||
}
|
||||
|
||||
export function useInviteCollaborator(
|
||||
req: RequestInviteCollaborator,
|
||||
config?: SWRConfiguration
|
||||
) {
|
||||
const { data, error } = useSWR<ResponseInviteCollaborator, ServiceError>(
|
||||
[COLLABORATOR_INVITE_URL, req],
|
||||
doInviteCollaborator,
|
||||
config
|
||||
);
|
||||
return {
|
||||
data,
|
||||
isLoading: !error && !data,
|
||||
isError: error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useRemoveCollaborator(
|
||||
req: RequestRemoveCollaborator,
|
||||
config?: SWRConfiguration
|
||||
) {
|
||||
const { data, error } = useSWR<unknown, ServiceError>(
|
||||
[REMOVE_COLLABORATOR_URL, req],
|
||||
doRemoveCollaborator,
|
||||
config
|
||||
);
|
||||
return {
|
||||
data,
|
||||
isLoading: !error && !data,
|
||||
isError: error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useAcceptInviting(
|
||||
req: RequestAcceptInviting,
|
||||
config?: SWRConfiguration
|
||||
) {
|
||||
const { data, error } = useSWR<unknown, ServiceError>(
|
||||
[COLLABORATOR_INVITE_URL, req],
|
||||
doAcceptInviting,
|
||||
config
|
||||
);
|
||||
return {
|
||||
data,
|
||||
isLoading: !error && !data,
|
||||
isError: error,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './workspace';
|
||||
export * from './workspace.hook';
|
||||
export * from './user';
|
||||
export * from './user.hook';
|
||||
@@ -0,0 +1,33 @@
|
||||
enum WorkspacePermission {
|
||||
read = 'read',
|
||||
write = 'write',
|
||||
}
|
||||
|
||||
export type RequestInviteCollaborator = {
|
||||
email: string;
|
||||
workspace_id: string;
|
||||
};
|
||||
|
||||
export type ResponseInviteCollaborator = {
|
||||
inviting_code: string;
|
||||
};
|
||||
|
||||
export type RequestRemoveCollaborator = {
|
||||
user_id: string;
|
||||
workspace_id: string;
|
||||
};
|
||||
|
||||
export type ResponseRemoveCollaborator = {
|
||||
workspace_id: string;
|
||||
};
|
||||
|
||||
export type RequestAcceptInviting = {
|
||||
inviting_code: string;
|
||||
};
|
||||
|
||||
export type ResponseAcceptInviting = {
|
||||
workspace_id: string;
|
||||
name: string;
|
||||
avatar_url: string;
|
||||
permissions: WorkspacePermission;
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export type CommonError = { error: { code: string; message: string } };
|
||||
export type MayError = Partial<CommonError>;
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './common';
|
||||
export * from './workspace';
|
||||
export * from './account';
|
||||
@@ -0,0 +1,32 @@
|
||||
enum WorkspaceType {
|
||||
'personal' = 'personal',
|
||||
'team' = 'team',
|
||||
}
|
||||
|
||||
export type Workspace = {
|
||||
id: string;
|
||||
name: string;
|
||||
workspace_type: WorkspaceType;
|
||||
avatar_url?: string;
|
||||
public: boolean;
|
||||
};
|
||||
|
||||
export type ResponseGetWorkspaces = {
|
||||
create: Array<Workspace>;
|
||||
read: Array<Workspace>;
|
||||
write: Array<Workspace>;
|
||||
};
|
||||
|
||||
export type RequestCreateWorkspace = {
|
||||
name: string;
|
||||
avatar_url?: string;
|
||||
workspace_type: WorkspaceType;
|
||||
};
|
||||
|
||||
export type RequestUpdateWorkspace = {
|
||||
workspace_type?: WorkspaceType;
|
||||
avatar_url?: string;
|
||||
public?: boolean;
|
||||
};
|
||||
|
||||
export type ResponseCreateWorkspace = any;
|
||||
@@ -0,0 +1,45 @@
|
||||
import useSWR from 'swr';
|
||||
import type { SWRConfiguration } from 'swr';
|
||||
import { login, getUserByEmail } from './user';
|
||||
import type {
|
||||
LoginParams,
|
||||
LoginResponse,
|
||||
GetUserByEmailParams,
|
||||
User,
|
||||
} from './user';
|
||||
|
||||
export const LOGIN_SWR_KEY = 'user.token';
|
||||
export function useLogin(params: LoginParams, config?: SWRConfiguration) {
|
||||
const { data, error, isValidating, isLoading, mutate } =
|
||||
useSWR<LoginResponse>(
|
||||
[LOGIN_SWR_KEY, params],
|
||||
([_, params]) => login(params),
|
||||
config
|
||||
);
|
||||
|
||||
return {
|
||||
loading: isLoading,
|
||||
data,
|
||||
error,
|
||||
mutate,
|
||||
};
|
||||
}
|
||||
|
||||
export const GET_USER_BY_EMAIL_SWR_TOKEN = 'user.getUserByEmail';
|
||||
export function useGetUserByEmail(
|
||||
params: GetUserByEmailParams,
|
||||
config?: SWRConfiguration
|
||||
) {
|
||||
const { data, error, isLoading, mutate } = useSWR<User | null>(
|
||||
[GET_USER_BY_EMAIL_SWR_TOKEN, params],
|
||||
([_, params]) => getUserByEmail(params),
|
||||
config
|
||||
);
|
||||
|
||||
return {
|
||||
loading: isLoading,
|
||||
data,
|
||||
error,
|
||||
mutate,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { request } from '../request';
|
||||
|
||||
export interface ExchangeToken {
|
||||
type: 'Google';
|
||||
/**
|
||||
* Token from firebase.
|
||||
*/
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface RefreshToken {
|
||||
type: 'Refresh';
|
||||
token: string;
|
||||
}
|
||||
|
||||
export type LoginParams = ExchangeToken | RefreshToken;
|
||||
|
||||
export interface LoginResponse {
|
||||
/**
|
||||
* JWT, expires in a very short time
|
||||
*/
|
||||
token: string;
|
||||
/**
|
||||
* Refresh token
|
||||
*/
|
||||
refresh: string;
|
||||
}
|
||||
|
||||
export async function login(params: LoginParams): Promise<LoginResponse> {
|
||||
const data = await request<LoginResponse>({
|
||||
url: '/api/user/token',
|
||||
method: 'POST',
|
||||
data: params,
|
||||
withAuthorization: false,
|
||||
});
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export interface GetUserByEmailParams {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar_url: string;
|
||||
create_at: string;
|
||||
}
|
||||
|
||||
export async function getUserByEmail(
|
||||
params: GetUserByEmailParams
|
||||
): Promise<User | null> {
|
||||
const data = await request<User | null>({
|
||||
url: '/api/user',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
return data.data;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import useSWR, { SWRConfiguration } from 'swr';
|
||||
import type {
|
||||
MayError,
|
||||
RequestCreateWorkspace,
|
||||
RequestUpdateWorkspace,
|
||||
ResponseCreateWorkspace,
|
||||
ResponseGetWorkspaces,
|
||||
} from './types';
|
||||
import { request, ServiceError } from '../request';
|
||||
|
||||
const WORKSPACE_URL = '/api/workspace';
|
||||
|
||||
async function doGetWorkSpaces(url: string) {
|
||||
const { data } = await request.get<ResponseGetWorkspaces>(url);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getWorkSpaces() {
|
||||
return await doGetWorkSpaces(WORKSPACE_URL);
|
||||
}
|
||||
|
||||
async function doCreateWorkspace(url: string, req: RequestCreateWorkspace) {
|
||||
const { data } = await request.put<ResponseCreateWorkspace>(url, req);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function createWorkspace(req: RequestCreateWorkspace) {
|
||||
doCreateWorkspace(WORKSPACE_URL, req);
|
||||
}
|
||||
|
||||
async function doUpdateWorkspace(url: string, req: RequestUpdateWorkspace) {
|
||||
const { data } = await request.post<MayError>(url, req);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function updateWorkspace(id: string, req: RequestUpdateWorkspace) {
|
||||
return doUpdateWorkspace(`${WORKSPACE_URL}${id}`, req);
|
||||
}
|
||||
|
||||
async function doDeleteWorkspace(url: string) {
|
||||
const { data } = await request.delete<MayError>(url);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function deleteWorkspace(id: string) {
|
||||
return doDeleteWorkspace(`${WORKSPACE_URL}${id}`);
|
||||
}
|
||||
|
||||
export function useGetWorkspaces(config?: SWRConfiguration) {
|
||||
const { data, error } = useSWR<ResponseGetWorkspaces, ServiceError, string>(
|
||||
WORKSPACE_URL,
|
||||
doGetWorkSpaces,
|
||||
config
|
||||
);
|
||||
return {
|
||||
data,
|
||||
isLoading: !data,
|
||||
isError: data && !error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useCreateWorkspace(
|
||||
req: RequestCreateWorkspace,
|
||||
config?: SWRConfiguration
|
||||
) {
|
||||
const { data, error } = useSWR<ResponseGetWorkspaces, ServiceError>(
|
||||
[WORKSPACE_URL, req],
|
||||
doCreateWorkspace,
|
||||
config
|
||||
);
|
||||
return {
|
||||
data,
|
||||
isLoading: !error && !data,
|
||||
isError: error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useUpdateWorkspace(
|
||||
id: string,
|
||||
req: RequestCreateWorkspace,
|
||||
config?: SWRConfiguration
|
||||
) {
|
||||
const { data, error } = useSWR<MayError, ServiceError>(
|
||||
[`${WORKSPACE_URL}${id}`, req],
|
||||
doUpdateWorkspace,
|
||||
config
|
||||
);
|
||||
return {
|
||||
data,
|
||||
isLoading: !error && !data,
|
||||
isError: error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useDeleteWorkspace(id: string, config?: SWRConfiguration) {
|
||||
const { data, error } = useSWR<MayError, ServiceError>(
|
||||
`${WORKSPACE_URL}${id}`,
|
||||
doDeleteWorkspace,
|
||||
config
|
||||
);
|
||||
return {
|
||||
data,
|
||||
isLoading: !error && !data,
|
||||
isError: error,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import useSWR from 'swr';
|
||||
import type { SWRConfiguration } from 'swr';
|
||||
import {
|
||||
getWorkspaceDetail,
|
||||
updateWorkspace,
|
||||
deleteWorkspace,
|
||||
inviteMember,
|
||||
Workspace,
|
||||
} from './workspace';
|
||||
import {
|
||||
GetWorkspaceDetailParams,
|
||||
WorkspaceDetail,
|
||||
UpdateWorkspaceParams,
|
||||
DeleteWorkspaceParams,
|
||||
InviteMemberParams,
|
||||
getWorkspaces,
|
||||
} from './workspace';
|
||||
|
||||
export const GET_WORKSPACE_DETAIL_SWR_TOKEN = 'workspace.getWorkspaceDetail';
|
||||
export function useGetWorkspaceDetail(
|
||||
params: GetWorkspaceDetailParams,
|
||||
config?: SWRConfiguration
|
||||
) {
|
||||
const { data, error, isLoading, mutate } = useSWR<WorkspaceDetail | null>(
|
||||
[GET_WORKSPACE_DETAIL_SWR_TOKEN, params],
|
||||
([_, params]) => getWorkspaceDetail(params),
|
||||
config
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
loading: isLoading,
|
||||
mutate,
|
||||
};
|
||||
}
|
||||
|
||||
export const GET_WORKSPACES_SWR_TOKEN = 'workspace.getWorkspaces';
|
||||
export function useGetWorkspaces(config?: SWRConfiguration) {
|
||||
const { data, error, isLoading } = useSWR<Workspace[]>(
|
||||
[GET_WORKSPACES_SWR_TOKEN],
|
||||
() => getWorkspaces(),
|
||||
config
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
loading: isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
export const UPDATE_WORKSPACE_SWR_TOKEN = 'workspace.updateWorkspace';
|
||||
/**
|
||||
* I don't think a hook needed for update workspace.
|
||||
* If you figure out the scene, please implement this function.
|
||||
*/
|
||||
export function useUpdateWorkspace() {}
|
||||
|
||||
export const DELETE_WORKSPACE_SWR_TOKEN = 'workspace.deleteWorkspace';
|
||||
/**
|
||||
* I don't think a hook needed for delete workspace.
|
||||
* If you figure out the scene, please implement this function.
|
||||
*/
|
||||
export function useDeleteWorkspace() {}
|
||||
|
||||
export const INVITE_MEMBER_SWR_TOKEN = 'workspace.inviteMember';
|
||||
/**
|
||||
* I don't think a hook needed for invite member.
|
||||
* If you figure out the scene, please implement this function.
|
||||
*/
|
||||
export function useInviteMember() {}
|
||||
@@ -0,0 +1,186 @@
|
||||
import { request } from '../request';
|
||||
import { User } from './user';
|
||||
|
||||
export interface GetWorkspaceDetailParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export enum WorkspaceType {
|
||||
Private = 0,
|
||||
Normal = 1,
|
||||
}
|
||||
|
||||
export enum PermissionType {
|
||||
Read = 0,
|
||||
Write = 1,
|
||||
Admin = 2,
|
||||
Owner = 3,
|
||||
}
|
||||
|
||||
export interface Workspace {
|
||||
id: string;
|
||||
type: WorkspaceType;
|
||||
public: boolean;
|
||||
permission_type: PermissionType;
|
||||
create_at: number;
|
||||
}
|
||||
|
||||
export async function getWorkspaces(): Promise<Workspace[]> {
|
||||
const data = await request({
|
||||
url: '/api/workspace',
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export interface WorkspaceDetail extends Workspace {
|
||||
owner: User;
|
||||
member_count: number;
|
||||
}
|
||||
|
||||
export async function getWorkspaceDetail(
|
||||
params: GetWorkspaceDetailParams
|
||||
): Promise<WorkspaceDetail | null> {
|
||||
const data = await request<WorkspaceDetail | null>({
|
||||
url: `/api/workspace/${params.id}`,
|
||||
method: 'PUT',
|
||||
});
|
||||
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export interface Permission {
|
||||
id: number;
|
||||
type: PermissionType;
|
||||
workspace_id: string;
|
||||
accepted: boolean;
|
||||
create_at: number;
|
||||
}
|
||||
|
||||
export interface RegisteredUser extends User {
|
||||
type: 'Registered';
|
||||
}
|
||||
|
||||
export interface UnregisteredUser {
|
||||
type: 'Unregistered';
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface Member extends Permission {
|
||||
user: RegisteredUser | UnregisteredUser;
|
||||
}
|
||||
|
||||
export interface GetWorkspaceMembersParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export async function getWorkspaceMembers(
|
||||
params: GetWorkspaceDetailParams
|
||||
): Promise<Member[]> {
|
||||
const data = await request<Member[]>({
|
||||
url: `/api/workspace/${params.id}/permission`,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export interface CreateWorkspaceParams {
|
||||
name: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
export async function createWorkspace(
|
||||
params: CreateWorkspaceParams
|
||||
): Promise<void> {
|
||||
const data = await request({
|
||||
url: '/api/workspace',
|
||||
method: 'POST',
|
||||
data: params,
|
||||
});
|
||||
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export interface UpdateWorkspaceParams {
|
||||
id: string;
|
||||
public: boolean;
|
||||
}
|
||||
|
||||
export async function updateWorkspace(
|
||||
params: UpdateWorkspaceParams
|
||||
): Promise<void> {
|
||||
const data = await request({
|
||||
url: `/api/workspace/${params.id}`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
public: params.public,
|
||||
},
|
||||
});
|
||||
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export interface DeleteWorkspaceParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export async function deleteWorkspace(
|
||||
params: DeleteWorkspaceParams
|
||||
): Promise<void> {
|
||||
const data = await request({
|
||||
url: `/api/workspace/${params.id}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export interface InviteMemberParams {
|
||||
id: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notice: Only support normal(contrast to private) workspace.
|
||||
*/
|
||||
export async function inviteMember(params: InviteMemberParams): Promise<void> {
|
||||
const data = await request({
|
||||
url: `/api/workspace/${params.id}/permission`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
email: params.email,
|
||||
},
|
||||
});
|
||||
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export interface RemoveMemberParams {
|
||||
permissionId: number;
|
||||
}
|
||||
|
||||
export async function removeMember(params: RemoveMemberParams): Promise<void> {
|
||||
const data = await request({
|
||||
url: `/api/permission/${params.permissionId}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export interface AcceptInvitingParams {
|
||||
invitingCode: string;
|
||||
}
|
||||
|
||||
export async function acceptInviting(
|
||||
params: AcceptInvitingParams
|
||||
): Promise<void> {
|
||||
const data = await request({
|
||||
url: `/api/invite/${params.invitingCode}`,
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
return data.data;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": false,
|
||||
"esModuleInterop": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"experimentalDecorators": true,
|
||||
"declaration": true,
|
||||
"baseUrl": ".",
|
||||
"rootDir": ".",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["next-env.d.ts", "src/**/*.ts", "pages/**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Generated
+2724
-73
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user