Feat/appflowy tauri UI (#1835)

* chore: create folders

* chore: setup taliwindcss (#1742)

* chore: create folders

* chore: setup taliwindcss

---------

Co-authored-by: nathan <nathan@appflowy.io>
Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>

* feat: greater to blockquote

* fix: local variable 'text' isn't used

* feat: #1061 Support markdown to create a blockquote

* fix: #1732 the actions of an image look different than the ones of a code block

* fix: command of double tilde to strikethrough

* feat: callout (#1732)

* feat: add callout plugin

* refactor: add SelectionMenuItem.node factory

makes calloutMenuItem more readable

* feat: add color picker

* feat: add popover to callout

* feat: add emoji to callout

* fix: store tint name

* fix: remove leading underscores

* fix: revert export of editor_entry

* refactor: move color tint names to appflowy_editor

* fix: #1732 only re-insert text node if it's parent is text node too while deleting

* docs: doc comment for SelectionMenuItem.node

* fix: disable callout plugin

should be re-enabled after #1753 is done

* fix: typo

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>

* Feat/http server adapt (#1754)

* integrate board plugin into document (#1675)

* fix: cursor doesn't blink when opening selection menu

* feat: add board plugin

* feat: integrate board plugin into document

* feat: add i10n and fix known bugs

* feat: support jump to board page on document

* feat: disable editor scroll only when the board plugin is selected

* chore: dart fix

* chore: remove unused files

* fix: dart lint

* Feat/database view (#1765)

* chore: rename flowy-database to flowy-sqlite

* refactor: rename flowy-grid to flowy-database

* refactor: rename grid to database

* refactor: rename GridEvent to DatabaseEvent

* refactor: rename grid_id to database_id

* refactor: rename dart code

* fix: #1763 [Bug] Mouse unable to click a certain area

* fix: potential async errors (#1772)

* feat: Skeleton task (#1775)

* chore: change tauri dev npm script

* chore: setup prettier

* chore: add protobuf type

* chore: move test calls to separate component

* chore: serve assets from app_flowy folder

* chore: import poppins font

* chore: install eslint, remove errors

* placeholder components

* chore: import colors from UI kit, footer panel

* chore: reorganise components

* chore: redux toolkit, navigation folders and files, navigation hooks

* fix: on add folder others close

* fix: tauri_dev task

* fix: restore grid notification

* chore: navigation items events (#1784)

* chore: change tauri dev npm script

* chore: setup prettier

* chore: add protobuf type

* chore: move test calls to separate component

* chore: serve assets from app_flowy folder

* chore: import poppins font

* chore: install eslint, remove errors

* placeholder components

* chore: import colors from UI kit, footer panel

* chore: reorganise components

* chore: redux toolkit, navigation folders and files, navigation hooks

* fix: on add folder others close

* fix: tauri_dev task

* fix: restore grid notification

* chore: shared button

* chore: folder/file popup, rename/duplicate/delete items

* chore: new page types popup

* fix: navitem pages padding

* fix: page click mishandle

* fix: folder click mishandle

* chore: add other page types

* fix: stop propagating on button click

* fix: one alt

* fix: renaming change bg

* refactor: brake Navigation Panel into smaller components

* chore: header panel folder

* chore: focus and select all on rename popup

* chore: add classname to popup

* chore: navigation panel resize

* Feat/appflowy tauri (#1831)

* feat:grid view structure

* feat:add store and refactor grid page

* chore: import icons, resize grid items, change grid items style, add field type icons, reorganize grid toolbar

* feat: auth screens(login, signup and confirm-account) ui done

* chore: add tailwind class sorter and formatted all files

* chore: group svgs into single folder

* chore: resolve warnings in svg files

* fix: use exported fieldType enum

* fix: resolve FieldType referances

* chore: auth pages fixes, replace links, replace buttons, svg fixes, navigate between pages, navigate to homepage on main button click

---------

Co-authored-by: ascarbek <ascarbek@gmail.com>

* ci: wanrings

---------

Co-authored-by: Mikias Tilahun Abebe <mikiastilahun@gmail.com>
Co-authored-by: Andreas Bichinger <andreas.bichinger@gmail.com>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
Co-authored-by: Askarbek Zadauly <ascarbek@gmail.com>
This commit is contained in:
Nathan.fooo 2023-02-10 16:26:14 +08:00 committed by GitHub
parent cbd351453d
commit 1ad08ba59d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
129 changed files with 4288 additions and 170 deletions

View File

@ -0,0 +1,46 @@
import 'package:app_flowy/core/grid_notification.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/notification.pb.dart';
import 'package:flowy_infra/notifier.dart';
import 'dart:async';
import 'dart:typed_data';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database/row_entities.pb.dart';
typedef UpdateRowNotifiedValue = Either<RowPB, FlowyError>;
typedef UpdateFieldNotifiedValue = Either<List<FieldPB>, FlowyError>;
class RowListener {
final String rowId;
PublishNotifier<UpdateRowNotifiedValue>? updateRowNotifier =
PublishNotifier();
DatabaseNotificationListener? _listener;
RowListener({required this.rowId});
void start() {
_listener =
DatabaseNotificationListener(objectId: rowId, handler: _handler);
}
void _handler(DatabaseNotification ty, Either<Uint8List, FlowyError> result) {
switch (ty) {
case DatabaseNotification.DidUpdateRow:
result.fold(
(payload) =>
updateRowNotifier?.value = left(RowPB.fromBuffer(payload)),
(error) => updateRowNotifier?.value = right(error),
);
break;
default:
break;
}
}
Future<void> stop() async {
await _listener?.stop();
updateRowNotifier?.dispose();
updateRowNotifier = null;
}
}

View File

@ -118,6 +118,54 @@ class AppService {
}
}
extension AppFlowy on Either {
T? getLeftOrNull<T>() {
if (isLeft()) {
final result = fold<T?>((l) => l, (r) => null);
return result;
}
return null;
}
Future<List<Tuple2<AppPB, List<ViewPB>>>> fetchViews(
ViewLayoutTypePB layoutType) async {
final result = <Tuple2<AppPB, List<ViewPB>>>[];
return FolderEventReadCurrentWorkspace().send().then((value) async {
final workspaces = value.getLeftOrNull<WorkspaceSettingPB>();
if (workspaces != null) {
final apps = workspaces.workspace.apps.items;
for (var app in apps) {
final views = await getViews(appId: app.id).then(
(value) => value
.getLeftOrNull<List<ViewPB>>()
?.where((e) => e.layout == layoutType)
.toList(),
);
if (views != null && views.isNotEmpty) {
result.add(Tuple2(app, views));
}
}
}
return result;
});
}
Future<Either<ViewPB, FlowyError>> getView(
String appID,
String viewID,
) async {
final payload = AppIdPB.create()..value = appID;
return FolderEventReadApp(payload).send().then((result) {
return result.fold(
(app) => left(
app.belongings.items.firstWhere((e) => e.id == viewID),
),
(error) => right(error),
);
});
}
}
extension AppFlowy on Either {
T? getLeftOrNull<T>() {
if (isLeft()) {

View File

@ -0,0 +1,2 @@
/src/services
/src/styles

View File

@ -0,0 +1,54 @@
module.exports = {
env: {
browser: true,
es6: true,
node: true,
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint'],
rules: {
'@typescript-eslint/adjacent-overload-signatures': 'error',
'@typescript-eslint/no-empty-function': 'error',
'@typescript-eslint/no-empty-interface': 'warn',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/no-namespace': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/prefer-for-of': 'warn',
'@typescript-eslint/triple-slash-reference': 'error',
'@typescript-eslint/unified-signatures': 'warn',
'constructor-super': 'error',
eqeqeq: ['error', 'always'],
'no-cond-assign': 'error',
'no-duplicate-case': 'error',
'no-duplicate-imports': 'error',
'no-empty': [
'error',
{
allowEmptyCatch: true,
},
],
'no-invalid-this': 'error',
'no-new-wrappers': 'error',
'no-param-reassign': 'error',
'no-redeclare': 'error',
'no-sequences': 'error',
'no-shadow': [
'error',
{
hoist: 'all',
},
],
'no-throw-literal': 'error',
'no-unsafe-finally': 'error',
'no-unused-labels': 'error',
'no-var': 'warn',
'no-void': 'off',
'prefer-const': 'warn',
},
};

View File

@ -0,0 +1,19 @@
.DS_Store
node_modules
/build
/public
/.svelte-kit
/package
/.vscode
.env
.env.*
!.env.example
# rust and generated ts code
/src-tauri
/src/services
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@ -0,0 +1,20 @@
module.exports = {
arrowParens: 'always',
bracketSpacing: true,
endOfLine: 'lf',
htmlWhitespaceSensitivity: 'css',
insertPragma: false,
jsxBracketSameLine: false,
jsxSingleQuote: true,
printWidth: 121,
plugins: [require('prettier-plugin-tailwindcss')],
proseWrap: 'preserve',
quoteProps: 'as-needed',
requirePragma: false,
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'es5',
useTabs: false,
vueIndentScriptAndStyle: false,
};

View File

@ -7,14 +7,20 @@
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
"format": "prettier --write .",
"tauri:dev": "tauri dev"
},
"dependencies": {
"@reduxjs/toolkit": "^1.9.2",
"@tauri-apps/api": "^1.2.0",
"google-protobuf": "^3.21.2",
"nanoid": "^4.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.8.0",
"react18-input-otp": "^1.1.2",
"redux": "^4.2.1",
"ts-results": "^3.3.0"
},
"devDependencies": {
@ -23,7 +29,15 @@
"@types/node": "^18.7.10",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.49.0",
"@vitejs/plugin-react": "^3.0.0",
"autoprefixer": "^10.4.13",
"eslint": "^8.33.0",
"postcss": "^8.4.21",
"prettier": "^2.8.3",
"prettier-plugin-tailwindcss": "^0.2.2",
"tailwindcss": "^3.2.4",
"typescript": "^4.6.4",
"vite": "^4.0.0"
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -1,7 +0,0 @@
.logo.vite:hover {
filter: drop-shadow(0 0 2em #747bff);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafb);
}

View File

@ -1,43 +1,43 @@
import "./App.css";
import {
UserEventSignIn,
SignInPayloadPB,
} from "../services/backend/events/flowy-user/index";
import { nanoid } from "nanoid";
import { UserNotificationListener } from "./components/user/application/notifications";
import { Routes, Route, BrowserRouter } from 'react-router-dom';
import { TestColors } from './components/TestColors/TestColors';
import TestApiButton from './components/TestApiButton/TestApiButton';
import { Welcome } from './pages/Welcome';
import { Provider } from 'react-redux';
import { store } from './store';
import { DocumentPage } from './pages/DocumentPage';
import { BoardPage } from './pages/BoardPage';
import { GridPage } from './pages/GridPage';
import { LoginPage } from './pages/LoginPage';
import { ProtectedRoutes } from './components/auth/ProtectedRoutes';
import { SignUpPage } from './pages/SignUpPage';
import { ConfirmAccountPage } from './pages/ConfirmAccountPage';
function App() {
async function sendSignInEvent() {
let make_payload = () =>
SignInPayloadPB.fromObject({
email: nanoid(4) + "@gmail.com",
password: "A!@123abc",
name: "abc",
});
const App = () => {
// const location = useLocation();
let listener = await new UserNotificationListener({
onUserSignIn: (userProfile) => {
console.log(userProfile);
}, onProfileUpdate(userProfile) {
console.log(userProfile);
// stop listening the changes
listener.stop();
}});
listener.start();
await UserEventSignIn(make_payload());
}
// console.log(location);
return (
<div className="container">
<h1>Welcome to AppFlowy!</h1>
<button type="button" onClick={() => sendSignInEvent()}>
Test Sign In Event
</button>
</div>
<BrowserRouter>
<Provider store={store}>
<Routes>
<Route path={'/'} element={<ProtectedRoutes />}>
<Route path={'/page/colors'} element={<TestColors />} />
<Route path={'/page/api-test'} element={<TestApiButton />} />
<Route path={'/page/document/:id'} element={<DocumentPage />} />
<Route path={'/page/board/:id'} element={<BoardPage />} />
<Route path={'/page/grid/:id'} element={<GridPage />} />
<Route path={'/'} element={<Welcome />} />
</Route>
<Route path={'/auth/login'} element={<LoginPage />}></Route>
<Route path={'/auth/signUp'} element={<SignUpPage />}></Route>
<Route path={'/auth/confirm-account'} element={<ConfirmAccountPage />}></Route>
<Route path={'*'}>Not Found</Route>
</Routes>
</Provider>
</BrowserRouter>
);
}
};
export default App;

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,137 @@
import {
UserEventSignIn,
SignInPayloadPB,
UserEventGetUserProfile,
UserEventGetUserSetting,
} from '../../../services/backend/events/flowy-user/index';
import { nanoid } from 'nanoid';
import { UserNotificationListener } from '../user/application/notifications';
import {
ColorStylePB,
CreateAppPayloadPB,
CreateWorkspacePayloadPB,
FolderEventCreateApp,
FolderEventCreateView,
FolderEventCreateWorkspace,
FolderEventOpenWorkspace,
FolderEventReadCurrentWorkspace,
WorkspaceIdPB,
} from '../../../services/backend/events/flowy-folder';
import { useEffect, useState } from 'react';
import * as dependency_1 from '../../../services/backend/classes/flowy-folder/app';
const TestApiButton = () => {
const [workspaceId, setWorkspaceId] = useState('');
const [appId, setAppId] = useState('');
useEffect(() => {
if (!workspaceId?.length) return;
void (async () => {
const openWorkspaceResult = await FolderEventOpenWorkspace(
WorkspaceIdPB.fromObject({
value: workspaceId,
})
);
if (openWorkspaceResult.ok) {
const pb = openWorkspaceResult.val;
console.log(pb.toObject());
} else {
throw new Error('open workspace error');
}
const createAppResult = await FolderEventCreateApp(
CreateAppPayloadPB.fromObject({
name: 'APP_1',
desc: 'Application One',
color_style: ColorStylePB.fromObject({ theme_color: 'light' }),
workspace_id: workspaceId,
})
);
if (createAppResult.ok) {
const pb = createAppResult.val;
const obj = pb.toObject();
console.log(obj);
} else {
throw new Error('create app error');
}
})();
}, [workspaceId]);
async function sendSignInEvent() {
let make_payload = () =>
SignInPayloadPB.fromObject({
email: nanoid(4) + '@gmail.com',
password: 'A!@123abc',
name: 'abc',
});
let listener = new UserNotificationListener({
onUserSignIn: (userProfile) => {
console.log(userProfile);
},
onProfileUpdate: (userProfile) => {
console.log(userProfile);
// stop listening the changes
void listener.stop();
},
});
await listener.start();
const signInResult = await UserEventSignIn(make_payload());
if (signInResult.ok) {
const pb = signInResult.val;
console.log(pb.toObject());
} else {
throw new Error('sign in error');
}
const getSettingsResult = await UserEventGetUserSetting();
if (getSettingsResult.ok) {
const pb = getSettingsResult.val;
console.log(pb.toObject());
} else {
throw new Error('get user settings error');
}
const createWorkspaceResult = await FolderEventCreateWorkspace(
CreateWorkspacePayloadPB.fromObject({
name: 'WS_1',
desc: 'Workspace One',
})
);
if (createWorkspaceResult.ok) {
const pb = createWorkspaceResult.val;
console.log(pb.toObject());
const workspace: {
id?: string;
name?: string;
desc?: string;
apps?: ReturnType<typeof dependency_1.RepeatedAppPB.prototype.toObject>;
modified_time?: number;
create_time?: number;
} = pb.toObject();
setWorkspaceId(workspace?.id || '');
} else {
throw new Error('create workspace error');
}
/**/
}
return (
<>
<h1 className='text-3xl'>Welcome to AppFlowy!</h1>
<div>
<button className='rounded-md bg-gray-700 p-4' type='button' onClick={() => sendSignInEvent()}>
Sign in and create sample data
</button>
</div>
</>
);
};
export default TestApiButton;

View File

@ -0,0 +1,44 @@
export const TestColors = () => {
return (
<div>
<h2 className={'mb-4'}>Main</h2>
<div className={'mb-8 flex flex-wrap items-center'}>
<div className={'m-2 h-[100px] w-[100px] bg-main-accent'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-main-hovered'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-main-secondary'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-main-selector'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-main-alert'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-main-warning'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-main-success'}></div>
</div>
<h2 className={'mb-4'}>Tint</h2>
<div className={'mb-8 flex flex-wrap items-center'}>
<div className={'m-2 h-[100px] w-[100px] bg-tint-1'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-tint-2'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-tint-3'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-tint-4'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-tint-5'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-tint-6'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-tint-7'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-tint-8'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-tint-9'}></div>
</div>
<h2 className={'mb-4'}>Shades</h2>
<div className={'mb-8 flex flex-wrap items-center'}>
<div className={'m-2 h-[100px] w-[100px] bg-shade-1'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-shade-2'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-shade-3'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-shade-4'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-shade-5'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-shade-6'}></div>
</div>
<h2 className={'mb-4'}>Surface</h2>
<div className={'mb-8 flex flex-wrap items-center'}>
<div className={'m-2 h-[100px] w-[100px] bg-surface-1'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-surface-2'}></div>
<div className={'m-2 h-[100px] w-[100px] bg-surface-3'}></div>
<div className={'bg-surface-4 m-2 h-[100px] w-[100px]'}></div>
</div>
</div>
);
};

View File

@ -0,0 +1,30 @@
import { useState } from 'react';
const TestFonts = () => {
const [sampleText, setSampleText] = useState('Sample Text');
const onInputChange = (e: any) => {
setSampleText(e.target.value);
};
return (
<div className={'flex h-full w-full flex-col items-center justify-center'}>
<div className={'py-2'}>
<input className={'rounded border border-gray-500 px-2 py-1'} value={sampleText} onChange={onInputChange} />
</div>
<div className={'flex flex-1 flex-col items-center justify-center overflow-auto text-2xl'}>
<div className={'mb-4 font-thin'}>{sampleText} 100 Thin</div>
<div className={'mb-4 font-extralight'}>{sampleText} 200 Extra Light</div>
<div className={'mb-4 font-light'}>{sampleText} 300 Light</div>
<div className={'mb-4 font-normal'}>{sampleText} 400 Regular</div>
<div className={'mb-4 font-medium'}>{sampleText} 500 Medium</div>
<div className={'mb-4 font-semibold'}>{sampleText} 600 Semi Bold</div>
<div className={'mb-4 font-bold'}>{sampleText} 700 Bold</div>
<div className={'mb-4 font-extrabold'}>{sampleText} 800 Extra Bold</div>
<div className={'mb-4 font-black'}>{sampleText} 900 Black</div>
</div>
</div>
);
};
export default TestFonts;

View File

@ -0,0 +1,40 @@
import { MouseEventHandler, MouseEvent, ReactNode, useEffect, useState } from 'react';
export const Button = ({
size = 'primary',
children,
onClick,
}: {
size?: 'primary' | 'medium' | 'small' | 'box-small-transparent';
children: ReactNode;
onClick?: MouseEventHandler<HTMLButtonElement>;
}) => {
const [cls, setCls] = useState('');
useEffect(() => {
switch (size) {
case 'primary':
setCls('w-[340px] h-[48px] flex items-center justify-center rounded-lg bg-main-accent text-white');
break;
case 'medium':
setCls('w-[170px] h-[48px] flex items-center justify-center rounded-lg bg-main-accent text-white');
break;
case 'small':
setCls('w-[68px] h-[32px] flex items-center justify-center rounded-lg bg-main-accent text-white text-xs');
break;
case 'box-small-transparent':
setCls('text-black hover:text-main-accent w-[24px] h-[24px]');
break;
}
}, [size]);
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onClick && onClick(e);
};
return (
<button className={cls} onClick={(e) => handleClick(e)}>
{children}
</button>
);
};

View File

@ -0,0 +1,41 @@
import { MouseEvent, ReactNode, useRef } from 'react';
import useOutsideClick from './useOutsideClick';
export interface IPopupItem {
icon: ReactNode;
title: string;
onClick: () => void;
}
export const Popup = ({
items,
className = '',
onOutsideClick,
}: {
items: IPopupItem[];
className: string;
onOutsideClick?: () => void;
}) => {
const ref = useRef<HTMLDivElement>(null);
useOutsideClick(ref, () => onOutsideClick && onOutsideClick());
const handleClick = (e: MouseEvent, item: IPopupItem) => {
e.stopPropagation();
item.onClick();
};
return (
<div ref={ref} className={`${className} rounded-lg bg-white px-2 py-2 shadow-md`}>
{items.map((item, index) => (
<button
key={index}
className={'flex w-full cursor-pointer items-center rounded-lg px-2 py-2 hover:bg-main-secondary'}
onClick={(e) => handleClick(e, item)}
>
{item.icon}
<span className={'ml-2'}>{item.title}</span>
</button>
))}
</div>
);
};

View File

@ -0,0 +1,21 @@
import { SearchSvg } from './svg/SearchSvg';
import { useState } from 'react';
export const SearchInput = () => {
const [active, setActive] = useState(false);
return (
<div className={`flex items-center rounded-lg p-2 ${active && 'bg-main-selector'}`}>
<i className='mr-2 h-5 w-5'>
<SearchSvg />
</i>
<input
onFocus={() => setActive(true)}
onBlur={() => setActive(false)}
className='w-52 text-sm placeholder-gray-400 focus:placeholder-gray-500'
placeholder='Search'
type='search'
/>
</div>
);
};

View File

@ -0,0 +1,8 @@
export default () => {
return (
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<rect x='11.25' y='6' width='1.5' height='12' rx='0.75' fill='currentColor' />
<rect x='18' y='11.25' width='1.5' height='12' rx='0.75' transform='rotate(90 18 11.25)' fill='currentColor' />
</svg>
);
};

View File

@ -0,0 +1,42 @@
export const AppflowyLogo = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 41 40' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M39.9564 24.0195C38.8098 30.1683 33.7828 35.5321 28.0061 38.5411C27.3005 38.9336 26.4627 39.1516 25.6689 39.1952H37.9279C39.1185 39.1952 39.9564 38.323 39.9564 37.2328V24.0195Z'
fill='#F7931E'
/>
<path
d='M15.4381 12.1576C15.2617 12.2884 15.0853 12.4192 14.9089 12.55C11.9103 14.6432 2.82634 21.3589 0.753788 18.4371C-1.27467 15.6026 0.886079 7.57868 6.08952 3.69755C6.17771 3.61033 6.31 3.56672 6.3982 3.4795C12.0867 -0.48885 16.32 0.078058 18.3926 2.95621C20.3328 5.65992 18.1721 9.93353 15.4381 12.1576Z'
fill='#8427E0'
/>
<path
d='M33.8715 36.098C33.7833 36.1852 33.6951 36.2288 33.5628 36.316C27.8743 40.2844 23.641 39.7175 21.5684 36.8393C19.6282 34.1356 21.7889 29.862 24.5229 27.638C24.6993 27.5072 24.8757 27.3763 25.0521 27.2455C28.0507 25.1959 37.1347 18.4366 39.1631 21.3584C41.2357 24.1929 39.119 32.2169 33.8715 36.098Z'
fill='#FFBD00'
/>
<path
d='M17.9954 38.8459C15.085 40.8955 6.70658 38.6715 2.87014 33.264C2.78195 33.1768 2.69376 33.046 2.64966 32.9588C-1.09858 27.5078 -0.481224 23.4086 2.38508 21.4462C5.20728 19.4838 9.61698 21.7515 11.8218 24.586C11.91 24.7168 11.9982 24.804 12.0864 24.9349C14.159 27.8566 20.9499 36.8399 17.9954 38.8459Z'
fill='#E3006D'
/>
<path
d='M15.4385 12.1576C11.3816 13.9455 2.73857 17.6086 1.45976 14.6432C0.357338 12.1576 2.3858 7.09899 6.08994 3.69755C6.17814 3.61033 6.31043 3.56672 6.39862 3.4795C12.0871 -0.48885 16.3204 0.078058 18.393 2.95621C20.3333 5.65992 18.1725 9.93353 15.4385 12.1576Z'
fill='#9327FF'
/>
<path
d='M37.6624 18.3955C34.8402 20.3579 30.4305 18.0903 28.2257 15.2557C28.1375 15.1249 28.0493 15.0377 27.9611 14.9069C25.8444 11.9415 19.0535 2.95819 21.9639 0.952211C24.8743 -1.09738 33.2968 1.12664 37.1333 6.53407C37.2215 6.6649 37.3096 6.75211 37.3978 6.88294C41.102 12.334 40.5287 16.3895 37.6624 18.3955Z'
fill='#00B5FF'
/>
<path
d='M37.6628 18.3934C34.8406 20.3557 30.4309 18.0881 28.2261 15.2536C26.4181 11.1108 22.9344 2.95603 25.8448 1.73499C28.4906 0.601179 33.9587 2.86881 37.4423 6.88077C41.1024 12.3318 40.5291 16.3874 37.6628 18.3934Z'
fill='#00C8FF'
/>
<path
d='M33.8715 36.0986C33.7833 36.1858 33.6951 36.2294 33.5628 36.3166C27.8743 40.285 23.641 39.7181 21.5684 36.8399C19.6282 34.1362 21.7889 29.8626 24.5229 27.6386C28.5799 25.8506 37.2229 22.1875 38.5017 25.1529C39.6482 27.6386 37.6197 32.6971 33.8715 36.0986Z'
fill='#FFCE00'
/>
<path
d='M14.2031 38.061C11.5572 39.1948 6.08922 36.9708 2.64966 32.9588C-1.09858 27.5078 -0.481224 23.4086 2.38508 21.4462C5.20728 19.4838 9.61698 21.7515 11.8218 24.586C13.6298 28.6852 17.1135 36.8399 14.2031 38.061Z'
fill='#FB006D'
/>
</svg>
);
};

View File

@ -0,0 +1,20 @@
export const BoardSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M12.8 2H3.2C2.53726 2 2 2.55964 2 3.25V5.75C2 6.44036 2.53726 7 3.2 7H12.8C13.4627 7 14 6.44036 14 5.75V3.25C14 2.55964 13.4627 2 12.8 2Z'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M12.8 9H3.2C2.53726 9 2 9.55964 2 10.25V12.75C2 13.4404 2.53726 14 3.2 14H12.8C13.4627 14 14 13.4404 14 12.75V10.25C14 9.55964 13.4627 9 12.8 9Z'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
<circle cx='4.5' cy='4.5' r='0.5' fill='currentColor' />
<circle cx='4.5' cy='11.5' r='0.5' fill='currentColor' />
</svg>
);
};

View File

@ -0,0 +1,13 @@
export const ChecklistTypeSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M6.5 8L8.11538 9.5L13.5 4.5' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
<path
d='M13.5 8C13.5 11.0376 11.0376 13.5 8 13.5C4.96243 13.5 2.5 11.0376 2.5 8C2.5 4.96243 4.96243 2.5 8 2.5C8.81896 2.5 9.59612 2.679 10.2945 3'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
);
};

View File

@ -0,0 +1,18 @@
export const CopySvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M11.9743 6.33301H7.35889C6.79245 6.33301 6.33325 6.7922 6.33325 7.35865V11.974C6.33325 12.5405 6.79245 12.9997 7.35889 12.9997H11.9743C12.5407 12.9997 12.9999 12.5405 12.9999 11.974V7.35865C12.9999 6.7922 12.5407 6.33301 11.9743 6.33301Z'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M4.53846 9.66667H4.02564C3.75362 9.66667 3.49275 9.55861 3.3004 9.36626C3.10806 9.17392 3 8.91304 3 8.64103V4.02564C3 3.75362 3.10806 3.49275 3.3004 3.3004C3.49275 3.10806 3.75362 3 4.02564 3H8.64103C8.91304 3 9.17392 3.10806 9.36626 3.3004C9.55861 3.49275 9.66667 3.75362 9.66667 4.02564V4.53846'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
);
};

View File

@ -0,0 +1,15 @@
export const DateTypeSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M11.8889 3.5H4.11111C3.49746 3.5 3 3.94772 3 4.5V11.5C3 12.0523 3.49746 12.5 4.11111 12.5H11.8889C12.5025 12.5 13 12.0523 13 11.5V4.5C13 3.94772 12.5025 3.5 11.8889 3.5Z'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path d='M10 2.5V4.58181' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
<path d='M6 2.5V4.58181' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
<path d='M3 6.5H13' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
</svg>
);
};

View File

@ -0,0 +1,8 @@
export const Details2Svg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<circle cx='8' cy='6' r='1' fill='currentColor' />
<circle cx='8' cy='10' r='1' fill='currentColor' />
</svg>
);
};

View File

@ -0,0 +1,18 @@
export const DocumentSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M10.5 3H11.5C11.8315 3 12.1495 3.12877 12.3839 3.35798C12.6183 3.58719 12.75 3.89807 12.75 4.22222V12.7778C12.75 13.1019 12.6183 13.4128 12.3839 13.642C12.1495 13.8712 11.8315 14 11.5 14H4.5C4.16848 14 3.85054 13.8712 3.61612 13.642C3.3817 13.4128 3.25 13.1019 3.25 12.7778V4.22222C3.25 3.89807 3.3817 3.58719 3.61612 3.35798C3.85054 3.12877 4.16848 3 4.5 3H5.5'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M9.5 2H6.5C6.22386 2 6 2.22386 6 2.5V3.5C6 3.77614 6.22386 4 6.5 4H9.5C9.77614 4 10 3.77614 10 3.5V2.5C10 2.22386 9.77614 2 9.5 2Z'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
);
};

View File

@ -0,0 +1,13 @@
export const EditSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M8 13H14' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
<path
d='M10.8849 3.36289C11.1173 3.13054 11.4324 3 11.761 3C11.9237 3 12.0848 3.03205 12.2351 3.09431C12.3855 3.15658 12.5221 3.24784 12.6371 3.36289C12.7522 3.47794 12.8434 3.61453 12.9057 3.76485C12.968 3.91517 13 4.07629 13 4.23899C13 4.4017 12.968 4.56281 12.9057 4.71314C12.8434 4.86346 12.7522 5.00004 12.6371 5.11509L5.33627 12.4159L3 13L3.58407 10.6637L10.8849 3.36289Z'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
);
};

View File

@ -0,0 +1,13 @@
export const EyeClosed = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
fill-rule='evenodd'
clip-rule='evenodd'
d='M7.68372 17.3771C8.88359 18.1747 10.3278 18.75 12.0001 18.75C15.7261 18.75 18.32 15.8941 19.6011 14.0863C20.4933 12.8272 20.4933 11.1728 19.6011 9.91375C19.1009 9.208 18.4007 8.34252 17.5112 7.54957L16.4489 8.61191C17.236 9.30133 17.8836 10.0844 18.3772 10.781C18.9013 11.5205 18.9013 12.4795 18.3772 13.219C17.1411 14.9633 14.9396 17.25 12.0001 17.25C10.794 17.25 9.71218 16.865 8.77028 16.2905L7.68372 17.3771ZM7.55137 15.3881L6.48903 16.4504C5.5995 15.6575 4.8993 14.792 4.39916 14.0863C3.50692 12.8272 3.50692 11.1728 4.39916 9.91375C5.68028 8.10595 8.27417 5.25 12.0001 5.25C13.6724 5.25 15.1167 5.82531 16.3165 6.62294L15.23 7.7095C14.2881 7.13497 13.2062 6.75 12.0001 6.75C9.06064 6.75 6.85914 9.03672 5.62301 10.781C5.09897 11.5205 5.09897 12.4795 5.62301 13.219C6.11667 13.9156 6.76428 14.6987 7.55137 15.3881ZM10.4887 14.572C10.9279 14.8431 11.4439 15 12.0002 15C13.641 15 14.932 13.6349 14.932 12C14.932 11.4625 14.7925 10.9542 14.5468 10.5139L13.3964 11.6644C13.4197 11.7717 13.432 11.884 13.432 12C13.432 12.8503 12.7694 13.5 12.0002 13.5C11.868 13.5 11.739 13.4808 11.616 13.4448L10.4887 14.572ZM10.6039 12.3355L9.45347 13.486C9.20788 13.0458 9.06836 12.5375 9.06836 12C9.06836 10.3651 10.3594 9 12.0002 9C12.5564 9 13.0724 9.15686 13.5115 9.42792L12.3842 10.5552C12.2612 10.5192 12.1323 10.5 12.0002 10.5C11.231 10.5 10.5684 11.1497 10.5684 12C10.5684 12.116 10.5807 12.2282 10.6039 12.3355Z'
fill='#333333'
/>
<path d='M17.5 5L5 17.5' stroke='#333333' strokeWidth='1.5' strokeLinecap='round' />
</svg>
);
};

View File

@ -0,0 +1,20 @@
export const EyeOpened = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M5.01097 13.6526C4.30282 12.6533 4.30282 11.3467 5.01097 10.3474C6.26959 8.57133 8.66728 6 12 6C15.3327 6 17.7304 8.57133 18.989 10.3474C19.6972 11.3467 19.6972 12.6533 18.989 13.6526C17.7304 15.4287 15.3327 18 12 18C8.66728 18 6.26959 15.4287 5.01097 13.6526Z'
stroke='#333333'
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M11.9999 14.25C13.2049 14.25 14.1818 13.2426 14.1818 12C14.1818 10.7574 13.2049 9.75 11.9999 9.75C10.7949 9.75 9.81812 10.7574 9.81812 12C9.81812 13.2426 10.7949 14.25 11.9999 14.25Z'
stroke='#333333'
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
);
};

View File

@ -0,0 +1,12 @@
export const FilterSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M12.889 3.80282C13.1395 3.47366 12.9048 3 12.4911 3H3.50889C3.09524 3 2.8605 3.47366 3.11102 3.80282L6.79787 8.64692C6.86412 8.73397 6.9 8.84035 6.9 8.94974V11.4836C6.9 11.6652 6.99845 11.8325 7.15718 11.9207L8.35718 12.5873C8.69044 12.7725 9.1 12.5315 9.1 12.1502V8.94974C9.1 8.84035 9.13588 8.73397 9.20213 8.64692L12.889 3.80282Z'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
);
};

View File

@ -0,0 +1,30 @@
export const GridSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M6 2H3C2.44772 2 2 2.44772 2 3V6C2 6.55228 2.44772 7 3 7H6C6.55228 7 7 6.55228 7 6V3C7 2.44772 6.55228 2 6 2Z'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M13 2H10C9.44772 2 9 2.44772 9 3V6C9 6.55228 9.44772 7 10 7H13C13.5523 7 14 6.55228 14 6V3C14 2.44772 13.5523 2 13 2Z'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M13 9H10C9.44772 9 9 9.44772 9 10V13C9 13.5523 9.44772 14 10 14H13C13.5523 14 14 13.5523 14 13V10C14 9.44772 13.5523 9 13 9Z'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M6 9H3C2.44772 9 2 9.44772 2 10V13C2 13.5523 2.44772 14 3 14H6C6.55228 14 7 13.5523 7 13V10C7 9.44772 6.55228 9 6 9Z'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
);
};

View File

@ -0,0 +1,12 @@
export const MultiSelectTypeSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M6.5 4L12.5 4' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
<path d='M6.5 8H12.5' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
<path d='M6.5 12H12.5' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
<circle cx='4' cy='4' r='0.5' fill='currentColor' />
<circle cx='4' cy='8' r='0.5' fill='currentColor' />
<circle cx='4' cy='12' r='0.5' fill='currentColor' />
</svg>
);
};

View File

@ -0,0 +1,10 @@
export const NumberTypeSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M2.201 6.4H3.001V12H2.081V7.384L0.953 7.704L0.729 6.92L2.201 6.4ZM3.91156 12V11.1L6.35156 8.61C6.9449 8.01667 7.24156 7.50333 7.24156 7.07C7.24156 6.73 7.13823 6.46667 6.93156 6.28C6.73156 6.08667 6.4749 5.99 6.16156 5.99C5.5749 5.99 5.14156 6.28 4.86156 6.86L3.89156 6.29C4.11156 5.82333 4.42156 5.47 4.82156 5.23C5.22156 4.99 5.6649 4.87 6.15156 4.87C6.7649 4.87 7.29156 5.06333 7.73156 5.45C8.17156 5.83667 8.39156 6.36333 8.39156 7.03C8.39156 7.74333 7.9949 8.50333 7.20156 9.31L5.62156 10.89H8.52156V12H3.91156ZM12.9025 7.032C13.5105 7.176 14.0025 7.46 14.3785 7.884C14.7625 8.3 14.9545 8.824 14.9545 9.456C14.9545 10.296 14.6705 10.956 14.1025 11.436C13.5345 11.916 12.8385 12.156 12.0145 12.156C11.3745 12.156 10.7985 12.008 10.2865 11.712C9.78253 11.416 9.41853 10.984 9.19453 10.416L10.3705 9.732C10.6185 10.452 11.1665 10.812 12.0145 10.812C12.4945 10.812 12.8745 10.692 13.1545 10.452C13.4345 10.204 13.5745 9.872 13.5745 9.456C13.5745 9.04 13.4345 8.712 13.1545 8.472C12.8745 8.232 12.4945 8.112 12.0145 8.112H11.7025L11.1505 7.284L12.9625 4.896H9.44653V3.6H14.6065V4.776L12.9025 7.032Z'
fill='currentColor'
/>
</svg>
);
};

View File

@ -0,0 +1,18 @@
export const PropertiesSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M11 2H5C3.89543 2 3 2.89543 3 4V12C3 13.1046 3.89543 14 5 14H11C12.1046 14 13 13.1046 13 12V4C13 2.89543 12.1046 2 11 2Z'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M11 8H5C3.89543 8 3 8.89543 3 10V12C3 13.1046 3.89543 14 5 14H11C12.1046 14 13 13.1046 13 12V10C13 8.89543 12.1046 8 11 8Z'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
);
};

View File

@ -0,0 +1,18 @@
export const SearchSvg = () => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
width='100%'
height='100%'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<circle cx='11' cy='11' r='8'></circle>
<line x1='21' y1='21' x2='16.65' y2='16.65'></line>
</svg>
);
};

View File

@ -0,0 +1,12 @@
export const SettingsSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M10.5221 3.21932C11.4366 2.6913 12.5634 2.6913 13.4779 3.21932L18.8653 6.32972C19.7799 6.85774 20.3433 7.83356 20.3433 8.88959V15.1104C20.3433 16.1664 19.7799 17.1423 18.8653 17.6703L13.4779 20.7807C12.5634 21.3087 11.4366 21.3087 10.5221 20.7807L5.13468 17.6703C4.22012 17.1423 3.65673 16.1664 3.65673 15.1104V8.88959C3.65673 7.83356 4.22012 6.85774 5.13467 6.32972L10.5221 3.21932Z'
stroke='currentColor'
strokeWidth='1.5'
/>
<circle cx='12' cy='12' r='3.75' stroke='currentColor' strokeWidth='1.5' />
</svg>
);
};

View File

@ -0,0 +1,16 @@
export const SingleSelectTypeSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M7.78787 8.78787L6.51213 7.51213C6.32314 7.32314 6.45699 7 6.72426 7H9.27574C9.54301 7 9.67686 7.32314 9.48787 7.51213L8.21213 8.78787C8.09497 8.90503 7.90503 8.90503 7.78787 8.78787Z'
fill='currentColor'
/>
<path
d='M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
);
};

View File

@ -0,0 +1,8 @@
export const SortSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<rect x='2.5' y='3' width='4' height='10' rx='1' stroke='currentColor' />
<rect x='9.5' y='7' width='4' height='6' rx='1' stroke='currentColor' />
</svg>
);
};

View File

@ -0,0 +1,14 @@
export const TextTypeSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M7.15625 11.8359L6.43768 9.85414H2.46662L1.74805 11.8359H0.5L3.7903 3H5.11399L8.4043 11.8359H7.15625ZM2.87003 8.75596H6.03427L4.44584 4.40112L2.87003 8.75596Z'
fill='currentColor'
/>
<path
d='M14.4032 5.52454H15.5V11.8359H14.4032V10.7504C13.8569 11.5835 13.0627 12 12.0206 12C11.1381 12 10.386 11.6802 9.76403 11.0407C9.14211 10.3927 8.83114 9.60589 8.83114 8.68022C8.83114 7.75456 9.14211 6.97195 9.76403 6.3324C10.386 5.68443 11.1381 5.36045 12.0206 5.36045C13.0627 5.36045 13.8569 5.777 14.4032 6.6101V5.52454ZM12.1593 10.9397C12.798 10.9397 13.3317 10.7251 13.7603 10.2959C14.1889 9.85835 14.4032 9.31978 14.4032 8.68022C14.4032 8.04067 14.1889 7.50631 13.7603 7.07714C13.3317 6.63955 12.798 6.42076 12.1593 6.42076C11.5289 6.42076 10.9995 6.63955 10.5708 7.07714C10.1422 7.50631 9.92791 8.04067 9.92791 8.68022C9.92791 9.31978 10.1422 9.85835 10.5708 10.2959C10.9995 10.7251 11.5289 10.9397 12.1593 10.9397Z'
fill='currentColor'
/>
</svg>
);
};

View File

@ -0,0 +1,34 @@
export const TrashSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M4.5 6.59985H6.16667H19.5'
stroke='currentColor'
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M8.6665 6.6V4.8C8.6665 4.32261 8.8421 3.86477 9.15466 3.52721C9.46722 3.18964 9.89114 3 10.3332 3H13.6665C14.1085 3 14.5325 3.18964 14.845 3.52721C15.1576 3.86477 15.3332 4.32261 15.3332 4.8V6.6M17.8332 6.6V19.2C17.8332 19.6774 17.6576 20.1352 17.345 20.4728C17.0325 20.8104 16.6085 21 16.1665 21H7.83317C7.39114 21 6.96722 20.8104 6.65466 20.4728C6.3421 20.1352 6.1665 19.6774 6.1665 19.2V6.6H17.8332Z'
stroke='currentColor'
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M10.3335 11.0999V16.4999'
stroke='currentColor'
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M13.6665 11.0999V16.4999'
stroke='currentColor'
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
);
};

View File

@ -0,0 +1,13 @@
export const UrlTypeSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M13 7.688L8.27223 12.1469C7.69304 12.6931 6.90749 13 6.0884 13C5.26931 13 4.48376 12.6931 3.90457 12.1469C3.32538 11.6006 3 10.8598 3 10.0873C3 9.31474 3.32538 8.57387 3.90457 8.02763L8.63234 3.56875C9.01847 3.20459 9.54216 3 10.0882 3C10.6343 3 11.158 3.20459 11.5441 3.56875C11.9302 3.93291 12.1472 4.42683 12.1472 4.94183C12.1472 5.45684 11.9302 5.95075 11.5441 6.31491L6.8112 10.7738C6.61814 10.9559 6.35629 11.0582 6.08326 11.0582C5.81022 11.0582 5.54838 10.9559 5.35531 10.7738C5.16225 10.5917 5.05379 10.3448 5.05379 10.0873C5.05379 9.82975 5.16225 9.58279 5.35531 9.40071L9.72297 5.28632'
stroke='currentColor'
strokeWidth='0.9989'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
);
};

View File

@ -0,0 +1,28 @@
import { useEffect } from 'react';
export default function useOutsideClick(ref: any, handler: (e: MouseEvent | TouchEvent) => void) {
useEffect(
() => {
const listener = (event: MouseEvent | TouchEvent) => {
// Do nothing if clicking ref's element or descendent elements
if (!ref?.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
},
// Add ref and handler to effect dependencies
// It's worth noting that because passed in handler is a new ...
// ... function on every render that will cause this effect ...
// ... callback/cleanup to run every render. It's not a big deal ...
// ... but to optimize you can wrap handler in useCallback before ...
// ... passing it into this hook.
[ref, handler]
);
}

View File

@ -0,0 +1,27 @@
import { useState } from 'react';
export const useResizer = () => {
const [movementX, setMovementX] = useState(0);
const [movementY, setMovementY] = useState(0);
const onMouseDown = () => {
const onMouseMove = (e: MouseEvent) => {
setMovementX(e.movementX);
setMovementY(e.movementY);
};
const onMouseUp = () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
};
return {
movementX,
movementY,
onMouseDown,
};
};

View File

@ -0,0 +1,32 @@
import { useState } from 'react';
import { currentUserActions } from '../../../redux/current-user/slice';
import { useAppDispatch, useAppSelector } from '../../../store';
import { useNavigate } from 'react-router-dom';
export const useConfirmAccount = () => {
const [otpValues, setOtpValues] = useState('');
const appDispatch = useAppDispatch();
const currentUser = useAppSelector((state) => state.currentUser);
const navigate = useNavigate();
const handleChange = (value: string) => {
console.log({ value });
setOtpValues(value);
};
const onConfirmClick = () => {
appDispatch(
currentUserActions.updateUser({
...currentUser,
isAuthenticated: true,
})
);
navigate('/');
};
return {
otpValues,
handleChange,
onConfirmClick,
};
};

View File

@ -0,0 +1,48 @@
import OtpInput from 'react18-input-otp';
import { AppflowyLogo } from '../../_shared/svg/AppflowyLogo';
import { useConfirmAccount } from './ConfirmAccount.hooks';
import { Button } from '../../_shared/Button';
export const ConfirmAccount = () => {
const { handleChange, otpValues, onConfirmClick } = useConfirmAccount();
return (
<div className='flex h-screen w-full flex-col items-center justify-center gap-12 text-center'>
<div className='flex h-10 w-10 justify-center'>
<AppflowyLogo />
</div>
<div className='flex flex-col gap-2'>
<span className='text-2xl font-semibold '>Enter the code sent to your phone</span>
<div>
<span className='block text-gray-500'>Confirm that this phone belongs to you.</span>
<span className='block text-gray-500'>
Code sent to <span className='text-black'>+86 10 6764 5489</span>
</span>
</div>
</div>
<div className='flex h-24 flex-col gap-4 '>
<div className={'flex-1'}>
<OtpInput
value={otpValues}
onChange={handleChange}
numInputs={5}
isInputNum={true}
separator={<span> </span>}
inputStyle='border border-gray-300 rounded-lg h-full !w-14 font-semibold focus:ring-2 focus:ring-main-accent focus:ring-opacity-50'
containerStyle='h-full w-full flex justify-around gap-2 '
/>
</div>
<a href='#' className='text-xs text-main-accent hover:text-main-hovered'>
<span>Send code again</span>
</a>
</div>
<Button size={'primary'} onClick={() => onConfirmClick()}>
Get Started
</Button>
</div>
);
};

View File

@ -0,0 +1,27 @@
import { useState } from 'react';
import { currentUserActions } from '../../../redux/current-user/slice';
import { useAppDispatch, useAppSelector } from '../../../store';
import { useNavigate } from 'react-router-dom';
export const useLogin = () => {
const [showPassword, setShowPassword] = useState(false);
const appDispatch = useAppDispatch();
const currentUser = useAppSelector((state) => state.currentUser);
const navigate = useNavigate();
function onTogglePassword() {
setShowPassword(!showPassword);
}
function onSignInClick() {
appDispatch(
currentUserActions.updateUser({
...currentUser,
isAuthenticated: true,
})
);
navigate('/');
}
return { showPassword, onTogglePassword, onSignInClick };
};

View File

@ -0,0 +1,63 @@
import { AppflowyLogo } from '../../_shared/svg/AppflowyLogo';
import { EyeClosed } from '../../_shared/svg/EyeClosedSvg';
import { EyeOpened } from '../../_shared/svg/EyeOpenSvg';
import { useLogin } from './Login.hooks';
import { Link } from 'react-router-dom';
import { Button } from '../../_shared/Button';
export const Login = () => {
const { showPassword, onTogglePassword, onSignInClick } = useLogin();
return (
<form onSubmit={(e) => e.preventDefault()} method='POST'>
<div className='flex h-screen w-full flex-col items-center justify-center gap-12 text-center'>
<div className='flex h-10 w-10 justify-center'>
<AppflowyLogo />
</div>
<div>
<span className='text-2xl font-semibold leading-9'>Login to Appflowy</span>
</div>
<div className='flex w-full max-w-[340px] flex-col gap-6 '>
<input type='text' className='input w-full' placeholder='Phone / Email' />
<div className='relative w-full'>
<input type={showPassword ? 'text' : 'password'} className='input w-full !pr-10' placeholder='Password' />
{/* Show password button */}
<button
type='button'
className='absolute right-0 top-0 flex h-full w-12 items-center justify-center '
onClick={onTogglePassword}
>
<span className='h-6 w-6'>{showPassword ? <EyeClosed /> : <EyeOpened />}</span>
</button>
</div>
<div className='flex justify-center'>
{/* Forget password link */}
<Link to={'/auth/confirm-account'}>
<span className='text-xs text-main-accent hover:text-main-hovered'>Forgot password?</span>
</Link>
</div>
</div>
<div className='flex w-full max-w-[340px] flex-col gap-6 '>
<Button size={'primary'} onClick={() => onSignInClick()}>
Login
</Button>
{/* signup link */}
<div className='flex justify-center'>
<span className='text-xs text-gray-400'>
Don't have an account?
<Link to={'/auth/signUp'}>
<span className='text-main-accent hover:text-main-hovered'> Sign up</span>
</Link>
</span>
</div>
</div>
</div>
</form>
);
};

View File

@ -0,0 +1,16 @@
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useAuth } from './auth.hooks';
import { Screen } from '../../components/layout/Screen';
export const ProtectedRoutes = () => {
const location = useLocation();
const { currentUser } = useAuth();
return currentUser.isAuthenticated ? (
<Screen>
<Outlet />
</Screen>
) : (
<Navigate to='/auth/login' replace state={{ from: location }} />
);
};

View File

@ -0,0 +1,32 @@
import { useState } from 'react';
import { useAppDispatch, useAppSelector } from '../../../store';
import { currentUserActions } from '../../../redux/current-user/slice';
import { useNavigate } from 'react-router-dom';
export const useSignUp = () => {
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const appDispatch = useAppDispatch();
const currentUser = useAppSelector((state) => state.currentUser);
const navigate = useNavigate();
function onTogglePassword() {
setShowPassword(!showPassword);
}
function onToggleConfirmPassword() {
setShowConfirmPassword(!showConfirmPassword);
}
function onSignUpClick() {
appDispatch(
currentUserActions.updateUser({
...currentUser,
isAuthenticated: true,
})
);
navigate('/');
}
return { showPassword, onTogglePassword, showConfirmPassword, onToggleConfirmPassword, onSignUpClick };
};

View File

@ -0,0 +1,72 @@
import { AppflowyLogo } from '../../_shared/svg/AppflowyLogo';
import { EyeClosed } from '../../_shared/svg/EyeClosedSvg';
import { EyeOpened } from '../../_shared/svg/EyeOpenSvg';
import { useSignUp } from './SignUp.hooks';
import { Link } from 'react-router-dom';
import { Button } from '../../_shared/Button';
export const SignUp = () => {
const { showPassword, onTogglePassword, showConfirmPassword, onToggleConfirmPassword, onSignUpClick } = useSignUp();
return (
<form method='POST' onSubmit={(e) => e.preventDefault()}>
<div className='flex h-screen w-full flex-col items-center justify-center gap-12 text-center'>
<div className='flex h-10 w-10 justify-center'>
<AppflowyLogo />
</div>
<div>
<span className='text-2xl font-semibold'>Sign up to Appflowy</span>
</div>
<div className='flex w-full max-w-[340px] flex-col gap-6'>
<input type='text' className='input w-full' placeholder='Phone / Email' />
<div className='relative w-full'>
<input type={showPassword ? 'text' : 'password'} className='input w-full !pr-10' placeholder='Password' />
<button
className='absolute right-0 top-0 flex h-full w-12 items-center justify-center '
onClick={onTogglePassword}
type='button'
>
<span className='h-6 w-6'>{showPassword ? <EyeClosed /> : <EyeOpened />}</span>
</button>
</div>
<div className='relative w-full'>
<input
type={showConfirmPassword ? 'text' : 'password'}
className='input w-full !pr-10'
placeholder='Repeat Password'
/>
<button
className='absolute right-0 top-0 flex h-full w-12 items-center justify-center '
onClick={onToggleConfirmPassword}
type='button'
>
<span className='h-6 w-6'>{showConfirmPassword ? <EyeClosed /> : <EyeOpened />}</span>
</button>
</div>
</div>
<div className='flex w-full max-w-[340px] flex-col gap-6 '>
<Button size={'primary'} onClick={() => onSignUpClick()}>
Get Started
</Button>
{/* signup link */}
<div className='flex justify-center'>
<span className='text-xs text-gray-500'>
Already have an account?
<Link to={'/auth/login'}>
<span className=' text-main-accent hover:text-main-hovered'> Sign in</span>
</Link>
</span>
</div>
</div>
</div>
</form>
);
};

View File

@ -0,0 +1,14 @@
import { currentUserActions } from '../../redux/current-user/slice';
import { useAppDispatch, useAppSelector } from '../../store';
export const useAuth = () => {
const dispatch = useAppDispatch();
const currentUser = useAppSelector((state) => state.currentUser);
function logout() {
dispatch(currentUserActions.logout());
}
return { currentUser, logout };
};

View File

@ -0,0 +1 @@
export {}

View File

@ -0,0 +1 @@
export {}

View File

@ -0,0 +1,13 @@
import { Link } from 'react-router-dom';
import AddSvg from '../../_shared/svg/AddSvg';
export const GridAddView = () => {
return (
<button className='flex cursor-pointer items-center rounded-lg p-2 text-sm hover:bg-main-selector'>
<i className='mr-2 h-5 w-5'>
<AddSvg />
</i>
<span>Add View</span>
</button>
);
};

View File

@ -0,0 +1,10 @@
import { useAppSelector } from '../../../store';
export const useGridTableCount = () => {
const { grid } = useAppSelector((state) => state);
const { rows } = grid;
return {
count: rows.length,
};
};

View File

@ -0,0 +1,10 @@
import { useGridTableCount } from './GridTableCount.hooks';
export const GridTableCount = () => {
const { count } = useGridTableCount();
return (
<span>
Count : <span className='font-semibold'>{count}</span>
</span>
);
};

View File

@ -0,0 +1,27 @@
import { nanoid } from 'nanoid';
import { FieldType } from '../../../../services/backend/classes/flowy-database/field_entities';
import { gridActions } from '../../../redux/grid/slice';
import { useAppDispatch, useAppSelector } from '../../../store';
export const useGridTableHeaderHooks = function () {
const dispatch = useAppDispatch();
const grid = useAppSelector((state) => state.grid);
const onAddField = () => {
dispatch(
gridActions.addField({
field: {
fieldId: nanoid(8),
name: 'Name',
fieldOptions: {},
fieldType: FieldType.RichText,
},
})
);
};
return {
fields: grid.fields,
onAddField,
};
};

View File

@ -0,0 +1,53 @@
import AddSvg from '../../_shared/svg/AddSvg';
import { useGridTableHeaderHooks } from './GridTableHeader.hooks';
import { TextTypeSvg } from '../../_shared/svg/TextTypeSvg';
import { NumberTypeSvg } from '../../_shared/svg/NumberTypeSvg';
import { DateTypeSvg } from '../../_shared/svg/DateTypeSvg';
import { SingleSelectTypeSvg } from '../../_shared/svg/SingleSelectTypeSvg';
import { MultiSelectTypeSvg } from '../../_shared/svg/MultiSelectTypeSvg';
import { ChecklistTypeSvg } from '../../_shared/svg/ChecklistTypeSvg';
import { UrlTypeSvg } from '../../_shared/svg/UrlTypeSvg';
import { FieldType } from '../../../../services/backend/classes/flowy-database/field_entities';
export const GridTableHeader = () => {
const { fields, onAddField } = useGridTableHeaderHooks();
return (
<>
<thead>
<tr>
{fields.map((field, i) => {
return (
<th key={field.fieldId} className='m-0 border border-l-0 border-shade-6 p-0'>
<div className={'flex cursor-pointer items-center p-2 hover:bg-main-secondary'}>
<i className={'mr-2 h-5 w-5 text-shade-3'}>
{field.fieldType === FieldType.RichText && <TextTypeSvg></TextTypeSvg>}
{field.fieldType === FieldType.Number && <NumberTypeSvg></NumberTypeSvg>}
{field.fieldType === FieldType.DateTime && <DateTypeSvg></DateTypeSvg>}
{field.fieldType === FieldType.SingleSelect && <SingleSelectTypeSvg></SingleSelectTypeSvg>}
{field.fieldType === FieldType.MultiSelect && <MultiSelectTypeSvg></MultiSelectTypeSvg>}
{field.fieldType === FieldType.Checklist && <ChecklistTypeSvg></ChecklistTypeSvg>}
{field.fieldType === FieldType.URL && <UrlTypeSvg></UrlTypeSvg>}
</i>
<span>{field.name}</span>
</div>
</th>
);
})}
<th className='m-0 w-40 border border-r-0 border-shade-6 p-0'>
<div
className='flex cursor-pointer items-center p-2 text-shade-3 hover:bg-main-secondary hover:text-black'
onClick={onAddField}
>
<i className='mr-2 h-5 w-5'>
<AddSvg />
</i>
<span>New column</span>
</div>
</th>
</tr>
</thead>
</>
);
};

View File

@ -0,0 +1,14 @@
import { gridActions } from '../../../redux/grid/slice';
import { useAppDispatch } from '../../../store';
export const useGridAddRow = () => {
const dispatch = useAppDispatch();
function addRow() {
dispatch(gridActions.addRow());
}
return {
addRow,
};
};

View File

@ -0,0 +1,16 @@
import AddSvg from '../../_shared/svg/AddSvg';
import { useGridAddRow } from './GridAddRow.hooks';
export const GridAddRow = () => {
const { addRow } = useGridAddRow();
return (
<div>
<button className='flex cursor-pointer items-center text-gray-500 hover:text-black' onClick={addRow}>
<i className='mr-2 h-5 w-5'>
<AddSvg />
</i>
<span>New row</span>
</button>
</div>
);
};

View File

@ -0,0 +1,28 @@
import { useState } from 'react';
import { gridActions } from '../../../redux/grid/slice';
import { useAppDispatch, useAppSelector } from '../../../store';
export const useGridTableItemHooks = (
rowItem: { value: string | number; fieldId: string; cellId: string },
rowId: string
) => {
const dispatch = useAppDispatch();
const [value, setValue] = useState(rowItem.value);
function onValueChange(event: React.ChangeEvent<HTMLInputElement>) {
setValue(event.target.value);
}
function onValueBlur() {
dispatch(gridActions.updateRowValue({ rowId: rowId, cellId: rowItem.cellId, value }));
}
const grid = useAppSelector((state) => state.grid);
return {
rows: grid.rows,
onValueChange,
value,
onValueBlur,
};
};

View File

@ -0,0 +1,26 @@
import { useGridTableItemHooks } from './GridTableItem.hooks';
export const GridTableItem = ({
rowItem,
rowId,
}: {
rowItem: {
fieldId: string;
value: string | number;
cellId: string;
};
rowId: string;
}) => {
const { value, onValueChange, onValueBlur } = useGridTableItemHooks(rowItem, rowId);
return (
<div>
<input
className='h-full w-full rounded-lg border border-transparent p-2 hover:border-main-accent'
type='text'
value={value}
onChange={onValueChange}
onBlur={onValueBlur}
/>
</div>
);
};

View File

@ -0,0 +1,9 @@
import { useAppSelector } from '../../../store';
export const useGridTableRowsHooks = () => {
const grid = useAppSelector((state) => state.grid);
return {
rows: grid.rows,
};
};

View File

@ -0,0 +1,25 @@
import { GridTableItem } from './GridTableItem';
import { useGridTableRowsHooks } from './GridTableRows.hooks';
export const GridTableRows = () => {
const { rows } = useGridTableRowsHooks();
return (
<tbody>
{rows.map((row, i) => {
return (
<tr key={row.rowId}>
{row.values.map((value) => {
return (
<td key={value.fieldId} className='m-0 border border-l-0 border-shade-6 p-0'>
<GridTableItem rowItem={value} rowId={row.rowId} />
</td>
);
})}
<td className='m-0 border border-r-0 border-shade-6 p-0'></td>
</tr>
);
})}
</tbody>
);
};

View File

@ -0,0 +1,33 @@
import { useState } from 'react';
import { gridActions } from '../../../redux/grid/slice';
import { useAppDispatch, useAppSelector } from '../../../store';
export const useGridTitleHooks = function () {
const dispatch = useAppDispatch();
const grid = useAppSelector((state) => state.grid);
const [title, setTitle] = useState(grid.title);
const [changingTitle, setChangingTitle] = useState(false);
const onTitleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setTitle(event.target.value);
};
const onTitleBlur = () => {
dispatch(gridActions.updateGridTitle({ title }));
setChangingTitle(false);
};
const onTitleClick = () => {
setChangingTitle(true);
};
return {
title,
onTitleChange,
onTitleBlur,
onTitleClick,
changingTitle,
};
};

View File

@ -0,0 +1,15 @@
import { useGridTitleHooks } from './GridTitle.hooks';
import { SettingsSvg } from '../../_shared/svg/SettingsSvg';
export const GridTitle = () => {
const { title } = useGridTitleHooks();
return (
<div className={'flex items-center text-xl font-semibold'}>
<div>{title}</div>
<button className={'ml-2 h-5 w-5'}>
<SettingsSvg></SettingsSvg>
</button>
</div>
);
};

View File

@ -0,0 +1,12 @@
import { PropertiesSvg } from '../../_shared/svg/PropertiesSvg';
export const GridFieldsButton = () => {
return (
<button className={'flex items-center rounded-lg p-2 text-sm hover:bg-main-selector'}>
<i className={'mr-2 h-5 w-5'}>
<PropertiesSvg></PropertiesSvg>
</i>
<span>Fields</span>
</button>
);
};

View File

@ -0,0 +1,12 @@
import { FilterSvg } from '../../_shared/svg/FilterSvg';
export const GridFilterButton = () => {
return (
<button className={'flex items-center rounded-lg p-2 text-sm hover:bg-main-selector'}>
<i className={'mr-2 h-5 w-5'}>
<FilterSvg></FilterSvg>
</i>
<span>Filter</span>
</button>
);
};

View File

@ -0,0 +1,12 @@
import { SortSvg } from '../../_shared/svg/SortSvg';
export const GridSortButton = () => {
return (
<button className={'flex items-center rounded-lg p-2 text-sm hover:bg-main-selector'}>
<i className={'mr-2 h-5 w-5'}>
<SortSvg></SortSvg>
</i>
<span>Sort</span>
</button>
);
};

View File

@ -0,0 +1,17 @@
import { GridAddView } from '../GridAddView/GridAddView';
import { SearchInput } from '../../_shared/SearchInput';
import { GridSortButton } from './GridSortButton';
import { GridFieldsButton } from './GridFieldsButton';
import { GridFilterButton } from './GridFilterButton';
export const GridToolbar = () => {
return (
<div className='flex shrink-0 items-center gap-4'>
<SearchInput />
<GridAddView />
<GridFilterButton></GridFilterButton>
<GridSortButton></GridSortButton>
<GridFieldsButton></GridFieldsButton>
</div>
);
};

View File

@ -0,0 +1,8 @@
export const AppLogo = () => {
return (
<div className={'mb-2 flex h-[60px] items-center justify-between px-6'}>
<img src={'/images/flowy_logo_with_text.svg'} alt={'logo'} />
<img src={'/images/home/hide_menu.svg'} alt={'hide'} />
</div>
);
};

View File

@ -0,0 +1,12 @@
export const FooterPanel = () => {
return (
<div className={'flex items-center justify-between px-2 py-2'}>
<div className={'text-xs text-shade-4'}>
&copy; 2023 AppFlowy. <a href={'https://github.com/AppFlowy-IO/AppFlowy'}>GitHub</a>
</div>
<div>
<button className={'h-8 w-8 rounded bg-main-secondary text-black'}>?</button>
</div>
</div>
);
};

View File

@ -0,0 +1,19 @@
export const Breadcrumbs = () => {
return (
<div className={'flex items-center'}>
<div className={'mr-4 flex items-center'}>
<button className={'p-1'} onClick={() => history.back()}>
<img src={'/images/home/arrow_left.svg'} />
</button>
<button className={'p-1'}>
<img src={'/images/home/arrow_right.svg'} />
</button>
</div>
<div className={'flex items-center'}>
<span className={'mr-8'}>Getting Started</span>
<span className={'mr-8'}>/</span>
<span className={'mr-8'}>Read Me</span>
</div>
</div>
);
};

View File

@ -0,0 +1,11 @@
import { Breadcrumbs } from './Breadcrumbs';
import { PageOptions } from './PageOptions';
export const HeaderPanel = () => {
return (
<div className={'flex h-[60px] items-center justify-between border-b border-shade-6 px-8'}>
<Breadcrumbs></Breadcrumbs>
<PageOptions></PageOptions>
</div>
);
};

View File

@ -0,0 +1,15 @@
import { Button } from '../../_shared/Button';
export const PageOptions = () => {
return (
<div className={'flex items-center'}>
<Button size={'small'} onClick={() => console.log('share click')}>
Share
</Button>
<button className={'ml-8'}>
<img className={'h-8 w-8'} src={`/images/editor/details.svg`} />
</button>
</div>
);
};

View File

@ -0,0 +1,13 @@
import { ReactNode } from 'react';
import { HeaderPanel } from './HeaderPanel/HeaderPanel';
import { FooterPanel } from './FooterPanel';
export const MainPanel = ({ children }: { children: ReactNode }) => {
return (
<div className={'flex h-full flex-1 flex-col'}>
<HeaderPanel></HeaderPanel>
<div className={'min-h-0 flex-1 overflow-auto'}>{children}</div>
<FooterPanel></FooterPanel>
</div>
);
};

View File

@ -0,0 +1,91 @@
import { foldersActions, IFolder } from '../../../redux/folders/slice';
import { useState } from 'react';
import { useAppDispatch } from '../../../store';
import { nanoid } from 'nanoid';
import { pagesActions } from '../../../redux/pages/slice';
export const useFolderEvents = (folder: IFolder) => {
const appDispatch = useAppDispatch();
const [showPages, setShowPages] = useState(false);
const [showFolderOptions, setShowFolderOptions] = useState(false);
const [showNewPageOptions, setShowNewPageOptions] = useState(false);
const [showRenamePopup, setShowRenamePopup] = useState(false);
const onFolderNameClick = () => {
setShowPages(!showPages);
};
const onFolderOptionsClick = () => {
setShowFolderOptions(!showFolderOptions);
};
const onNewPageClick = () => {
setShowNewPageOptions(!showNewPageOptions);
};
const startFolderRename = () => {
closePopup();
setShowRenamePopup(true);
};
const changeFolderTitle = (newTitle: string) => {
appDispatch(foldersActions.renameFolder({ id: folder.id, newTitle }));
};
const closeRenamePopup = () => {
setShowRenamePopup(false);
};
const deleteFolder = () => {
closePopup();
appDispatch(foldersActions.deleteFolder({ id: folder.id }));
};
const duplicateFolder = () => {
closePopup();
appDispatch(foldersActions.addFolder({ id: nanoid(8), title: folder.title }));
};
const closePopup = () => {
setShowFolderOptions(false);
setShowNewPageOptions(false);
};
const onAddNewDocumentPage = () => {
closePopup();
appDispatch(pagesActions.addPage({ folderId: folder.id, pageType: 'document', title: 'New Page 1', id: nanoid(6) }));
};
const onAddNewBoardPage = () => {
closePopup();
appDispatch(pagesActions.addPage({ folderId: folder.id, pageType: 'board', title: 'New Board 1', id: nanoid(6) }));
};
const onAddNewGridPage = () => {
closePopup();
appDispatch(pagesActions.addPage({ folderId: folder.id, pageType: 'grid', title: 'New Grid 1', id: nanoid(6) }));
};
return {
showPages,
onFolderNameClick,
showFolderOptions,
onFolderOptionsClick,
showNewPageOptions,
onNewPageClick,
showRenamePopup,
startFolderRename,
changeFolderTitle,
closeRenamePopup,
deleteFolder,
duplicateFolder,
onAddNewDocumentPage,
onAddNewBoardPage,
onAddNewGridPage,
closePopup,
};
};

View File

@ -0,0 +1,92 @@
import { Details2Svg } from '../../_shared/svg/Details2Svg';
import AddSvg from '../../_shared/svg/AddSvg';
import { NavItemOptionsPopup } from './NavItemOptionsPopup';
import { NewPagePopup } from './NewPagePopup';
import { IFolder } from '../../../redux/folders/slice';
import { useFolderEvents } from './FolderItem.hooks';
import { IPage } from '../../../redux/pages/slice';
import { PageItem } from './PageItem';
import { Button } from '../../_shared/Button';
import { RenamePopup } from './RenamePopup';
export const FolderItem = ({
folder,
pages,
onPageClick,
}: {
folder: IFolder;
pages: IPage[];
onPageClick: (page: IPage) => void;
}) => {
const {
showPages,
onFolderNameClick,
showFolderOptions,
onFolderOptionsClick,
showNewPageOptions,
onNewPageClick,
showRenamePopup,
startFolderRename,
changeFolderTitle,
closeRenamePopup,
deleteFolder,
duplicateFolder,
onAddNewDocumentPage,
onAddNewBoardPage,
onAddNewGridPage,
closePopup,
} = useFolderEvents(folder);
return (
<div className={'relative my-2'}>
<div
onClick={() => onFolderNameClick()}
className={'flex cursor-pointer items-center justify-between rounded-lg px-4 py-2 hover:bg-surface-2'}
>
<div className={'flex min-w-0 flex-1 items-center'}>
<div className={`mr-2 transition-transform duration-500 ${showPages && 'rotate-180'}`}>
<img className={''} src={'/images/home/drop_down_show.svg'} alt={''} />
</div>
<span className={'min-w-0 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap'}>{folder.title}</span>
</div>
<div className={'relative flex items-center'}>
<Button size={'box-small-transparent'} onClick={() => onFolderOptionsClick()}>
<Details2Svg></Details2Svg>
</Button>
<Button size={'box-small-transparent'} onClick={() => onNewPageClick()}>
<AddSvg></AddSvg>
</Button>
{showFolderOptions && (
<NavItemOptionsPopup
onRenameClick={() => startFolderRename()}
onDeleteClick={() => deleteFolder()}
onDuplicateClick={() => duplicateFolder()}
onClose={() => closePopup()}
></NavItemOptionsPopup>
)}
{showNewPageOptions && (
<NewPagePopup
onDocumentClick={() => onAddNewDocumentPage()}
onBoardClick={() => onAddNewBoardPage()}
onGridClick={() => onAddNewGridPage()}
onClose={() => closePopup()}
></NewPagePopup>
)}
</div>
</div>
{showRenamePopup && (
<RenamePopup
value={folder.title}
onChange={(newTitle) => changeFolderTitle(newTitle)}
onClose={closeRenamePopup}
></RenamePopup>
)}
{showPages &&
pages.map((page, index) => <PageItem key={index} page={page} onPageClick={() => onPageClick(page)}></PageItem>)}
</div>
);
};

View File

@ -0,0 +1,54 @@
import { IPopupItem, Popup } from '../../_shared/Popup';
import { EditSvg } from '../../_shared/svg/EditSvg';
import { TrashSvg } from '../../_shared/svg/TrashSvg';
import { CopySvg } from '../../_shared/svg/CopySvg';
export const NavItemOptionsPopup = ({
onRenameClick,
onDeleteClick,
onDuplicateClick,
onClose,
}: {
onRenameClick: () => void;
onDeleteClick: () => void;
onDuplicateClick: () => void;
onClose?: () => void;
}) => {
const items: IPopupItem[] = [
{
icon: (
<i className={'h-[16px] w-[16px] text-black'}>
<EditSvg></EditSvg>
</i>
),
onClick: onRenameClick,
title: 'Rename',
},
{
icon: (
<i className={'h-[16px] w-[16px] text-black'}>
<TrashSvg></TrashSvg>
</i>
),
onClick: onDeleteClick,
title: 'Delete',
},
{
icon: (
<i className={'h-[16px] w-[16px] text-black'}>
<CopySvg></CopySvg>
</i>
),
onClick: onDuplicateClick,
title: 'Duplicate',
},
];
return (
<Popup
onOutsideClick={() => onClose && onClose()}
items={items}
className={'absolute right-0 top-full z-10'}
></Popup>
);
};

View File

@ -0,0 +1,19 @@
import { useAppSelector } from '../../../store';
import { useNavigate } from 'react-router-dom';
export const useNavigationPanelHooks = function () {
const folders = useAppSelector((state) => state.folders);
const pages = useAppSelector((state) => state.pages);
const width = useAppSelector((state) => state.navigationWidth);
const navigate = useNavigate();
return {
width,
folders,
pages,
navigate,
};
};

View File

@ -0,0 +1,52 @@
import { useNavigationPanelHooks } from './NavigationPanel.hooks';
import { Workspace } from '../Workspace';
import { AppLogo } from '../AppLogo';
import { FolderItem } from './FolderItem';
import { PluginsButton } from './PluginsButton';
import { TrashButton } from './TrashButton';
import { NewFolderButton } from './NewFolderButton';
import { NavigationResizer } from './NavigationResizer';
export const NavigationPanel = () => {
const {
width,
folders,
pages,
navigate,
} = useNavigationPanelHooks();
return (
<>
<div className={'flex flex-col justify-between bg-surface-1 text-sm'} style={{ width: `${width}px` }}>
<div className={'flex flex-col'}>
<AppLogo></AppLogo>
<Workspace></Workspace>
<div className={'flex flex-col px-2'}>
{folders.map((folder, index) => (
<FolderItem
key={index}
folder={folder}
pages={pages.filter((page) => page.folderId === folder.id)}
onPageClick={(page) => navigate(`/page/${page.pageType}/${page.id}`)}
></FolderItem>
))}
</div>
</div>
<div className={'flex flex-col'}>
<div className={'border-b border-shade-6 px-2 pb-4'}>
<PluginsButton></PluginsButton>
<TrashButton></TrashButton>
</div>
<NewFolderButton></NewFolderButton>
</div>
</div>
<NavigationResizer></NavigationResizer>
</>
);
};

View File

@ -0,0 +1,22 @@
import { useResizer } from '../../_shared/useResizer';
import { useAppDispatch, useAppSelector } from '../../../store';
import { useEffect } from 'react';
import { navigationWidthActions } from '../../../redux/navigation-width/slice';
export const NavigationResizer = () => {
const width = useAppSelector((state) => state.navigationWidth);
const appDispatch = useAppDispatch();
const { onMouseDown, movementX } = useResizer();
useEffect(() => {
appDispatch(navigationWidthActions.changeWidth(width + movementX));
}, [movementX]);
return (
<button
className={'fixed z-10 h-full w-[15px] cursor-ew-resize'}
style={{ left: `${width - 8}px`, userSelect: 'none' }}
onMouseDown={onMouseDown}
></button>
);
};

View File

@ -0,0 +1,15 @@
import { useAppDispatch } from '../../../store';
import { foldersActions } from '../../../redux/folders/slice';
import { nanoid } from 'nanoid';
export const useNewFolder = () => {
const appDispatch = useAppDispatch();
const onNewFolder = () => {
appDispatch(foldersActions.addFolder({ id: nanoid(8), title: 'New Folder 1' }));
};
return {
onNewFolder,
};
};

View File

@ -0,0 +1,17 @@
import AddSvg from '../../_shared/svg/AddSvg';
import { useNewFolder } from './NewFolderButton.hooks';
export const NewFolderButton = () => {
const { onNewFolder } = useNewFolder();
return (
<button onClick={() => onNewFolder()} className={'flex h-[50px] w-full items-center px-6 hover:bg-surface-2'}>
<div className={'mr-2 rounded-full bg-main-accent text-white'}>
<div className={'h-[24px] w-[24px] text-white'}>
<AddSvg></AddSvg>
</div>
</div>
<span>New Folder</span>
</button>
);
};

View File

@ -0,0 +1,54 @@
import { IPopupItem, Popup } from '../../_shared/Popup';
import { DocumentSvg } from '../../_shared/svg/DocumentSvg';
import { BoardSvg } from '../../_shared/svg/BoardSvg';
import { GridSvg } from '../../_shared/svg/GridSvg';
export const NewPagePopup = ({
onDocumentClick,
onGridClick,
onBoardClick,
onClose,
}: {
onDocumentClick: () => void;
onGridClick: () => void;
onBoardClick: () => void;
onClose?: () => void;
}) => {
const items: IPopupItem[] = [
{
icon: (
<i className={'h-[16px] w-[16px] text-black'}>
<DocumentSvg></DocumentSvg>
</i>
),
onClick: onDocumentClick,
title: 'Document',
},
{
icon: (
<i className={'h-[16px] w-[16px] text-black'}>
<BoardSvg></BoardSvg>
</i>
),
onClick: onBoardClick,
title: 'Board',
},
{
icon: (
<i className={'h-[16px] w-[16px] text-black'}>
<GridSvg></GridSvg>
</i>
),
onClick: onGridClick,
title: 'Grid',
},
];
return (
<Popup
onOutsideClick={() => onClose && onClose()}
items={items}
className={'absolute right-0 top-full z-10'}
></Popup>
);
};

View File

@ -0,0 +1,55 @@
import { IPage, pagesActions } from '../../../redux/pages/slice';
import { useAppDispatch } from '../../../store';
import { useState } from 'react';
import { nanoid } from 'nanoid';
export const usePageEvents = (page: IPage) => {
const appDispatch = useAppDispatch();
const [showPageOptions, setShowPageOptions] = useState(false);
const [showRenamePopup, setShowRenamePopup] = useState(false);
const onPageOptionsClick = () => {
setShowPageOptions(!showPageOptions);
};
const startPageRename = () => {
setShowRenamePopup(true);
closePopup();
};
const changePageTitle = (newTitle: string) => {
appDispatch(pagesActions.renamePage({ id: page.id, newTitle }));
};
const deletePage = () => {
closePopup();
appDispatch(pagesActions.deletePage({ id: page.id }));
};
const duplicatePage = () => {
closePopup();
appDispatch(
pagesActions.addPage({ id: nanoid(8), pageType: page.pageType, title: page.title, folderId: page.folderId })
);
};
const closePopup = () => {
setShowPageOptions(false);
};
const closeRenamePopup = () => {
setShowRenamePopup(false);
};
return {
showPageOptions,
onPageOptionsClick,
showRenamePopup,
startPageRename,
changePageTitle,
deletePage,
duplicatePage,
closePopup,
closeRenamePopup,
};
};

View File

@ -0,0 +1,61 @@
import { DocumentSvg } from '../../_shared/svg/DocumentSvg';
import { BoardSvg } from '../../_shared/svg/BoardSvg';
import { GridSvg } from '../../_shared/svg/GridSvg';
import { Details2Svg } from '../../_shared/svg/Details2Svg';
import { NavItemOptionsPopup } from './NavItemOptionsPopup';
import { IPage } from '../../../redux/pages/slice';
import { Button } from '../../_shared/Button';
import { usePageEvents } from './PageItem.hooks';
import { RenamePopup } from './RenamePopup';
export const PageItem = ({ page, onPageClick }: { page: IPage; onPageClick: () => void }) => {
const {
showPageOptions,
onPageOptionsClick,
showRenamePopup,
startPageRename,
changePageTitle,
deletePage,
duplicatePage,
closePopup,
closeRenamePopup,
} = usePageEvents(page);
return (
<div className={'relative'}>
<div
onClick={() => onPageClick()}
className={'flex cursor-pointer items-center justify-between rounded-lg py-2 pl-8 pr-4 hover:bg-surface-2 '}
>
<div className={'flex min-w-0 flex-1 items-center'}>
<div className={'ml-1 mr-1 h-[16px] w-[16px]'}>
{page.pageType === 'document' && <DocumentSvg></DocumentSvg>}
{page.pageType === 'board' && <BoardSvg></BoardSvg>}
{page.pageType === 'grid' && <GridSvg></GridSvg>}
</div>
<span className={'ml-2 min-w-0 flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap'}>{page.title}</span>
</div>
<div className={'relative flex items-center'}>
<Button size={'box-small-transparent'} onClick={() => onPageOptionsClick()}>
<Details2Svg></Details2Svg>
</Button>
{showPageOptions && (
<NavItemOptionsPopup
onRenameClick={() => startPageRename()}
onDeleteClick={() => deletePage()}
onDuplicateClick={() => duplicatePage()}
onClose={() => closePopup()}
></NavItemOptionsPopup>
)}
</div>
</div>
{showRenamePopup && (
<RenamePopup
value={page.title}
onChange={(newTitle) => changePageTitle(newTitle)}
onClose={closeRenamePopup}
></RenamePopup>
)}
</div>
);
};

View File

@ -0,0 +1,8 @@
export const PluginsButton = () => {
return (
<button className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-surface-2'}>
<img className={'mr-2 h-[24px] w-[24px]'} src={'/images/home/page.svg'} alt={''} />
<span>Plugins</span>
</button>
);
};

View File

@ -0,0 +1,44 @@
import { useEffect, useRef } from 'react';
import useOutsideClick from '../../_shared/useOutsideClick';
export const RenamePopup = ({
value,
onChange,
onClose,
className = '',
}: {
value: string;
onChange: (newTitle: string) => void;
onClose: () => void;
className?: string;
}) => {
const ref = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
useOutsideClick(ref, () => onClose && onClose());
useEffect(() => {
if (!inputRef || !inputRef.current) return;
const { current: el } = inputRef;
el.focus();
el.selectionStart = 0;
el.selectionEnd = el.value.length;
}, [inputRef]);
return (
<div
ref={ref}
className={
'absolute left-[30px] top-[30px] z-10 flex w-[300px] rounded bg-white py-1 px-1.5 shadow-md ' + className
}
>
<input
ref={inputRef}
className={'border-shades-3 flex-1 rounded border bg-main-selector p-1'}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</div>
);
};

View File

@ -0,0 +1,8 @@
export const TrashButton = () => {
return (
<button className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-surface-2'}>
<img className={'mr-2'} src={'/images/home/trash.svg'} alt={''} />
<span>Trash</span>
</button>
);
};

View File

@ -0,0 +1,12 @@
import React, { ReactNode } from 'react';
import { NavigationPanel } from './NavigationPanel/NavigationPanel';
import { MainPanel } from './MainPanel';
export const Screen = ({ children }: { children: ReactNode }) => {
return (
<div className='flex h-screen w-screen bg-white text-black'>
<NavigationPanel></NavigationPanel>
<MainPanel>{children}</MainPanel>
</div>
);
};

View File

@ -0,0 +1,17 @@
import { useAppSelector } from '../../store';
export const Workspace = () => {
const currentUser = useAppSelector((state) => state.currentUser);
return (
<div className={'flex items-center justify-between px-2 py-2'}>
<button className={'flex items-center pl-4'}>
<img className={'mr-2'} src={'/images/home/person.svg'} alt={'user'} />
<span>{currentUser.displayName}</span>
</button>
<button className={'mr-2 rounded-lg p-2 hover:bg-surface-2'}>
<img src={'/images/home/settings.svg'} alt={'settings'} />
</button>
</div>
);
};

View File

@ -1 +1 @@
export * from "./user_listener";
export * from './user_listener';

Some files were not shown because too many files have changed in this diff Show More