}
22 | />
23 | );
24 | };
25 |
26 | export default ExampleSelectable;
27 |
--------------------------------------------------------------------------------
/packages/react-virtual-selection/src/hooks/useSelectable.ts:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useEffect } from 'react';
2 |
3 | import SelectionManger from '../classes/SelectionManager';
4 |
5 | const useSelectable = (type: string, item: () => unknown, deps: unknown[] = []): [boolean, React.MutableRefObject
] => {
6 | const [selected, setSelected] = useState(false);
7 | const selectableRef = useRef(null);
8 |
9 | useEffect(() => {
10 | const items = item();
11 |
12 | SelectionManger.Instance.addToSelectable(selectableRef, type, () => items, setSelected);
13 | return () => SelectionManger.Instance.removeFromSelectable(selectableRef, type);
14 | }, deps);
15 |
16 | return [selected, selectableRef];
17 | };
18 |
19 | export default useSelectable;
20 |
--------------------------------------------------------------------------------
/packages/react-virtual-selection/src/hooks/useSelectionCollector.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | import SelectionManager from '../classes/SelectionManager';
4 |
5 | const useSelectionCollector = (type: string) => {
6 | const [selectionData, setSelectionData] = useState([]);
7 |
8 | SelectionManager.Instance.registerSelectableWatcher(type, setSelectionData);
9 |
10 | return selectionData;
11 | };
12 |
13 | export default useSelectionCollector;
14 |
--------------------------------------------------------------------------------
/packages/react-virtual-selection/src/index.ts:
--------------------------------------------------------------------------------
1 | // -=- Hooks -=-
2 | import useSelectable from './hooks/useSelectable';
3 | import useSelectionCollector from './hooks/useSelectionCollector';
4 | import Selectable from './components/Selectable';
5 | import SelectionManager from './classes/SelectionManager';
6 |
7 | export {
8 | useSelectable,
9 | useSelectionCollector,
10 | Selectable,
11 | SelectionManager,
12 | };
13 |
--------------------------------------------------------------------------------
/packages/react-virtual-selection/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import App from './demo/App';
5 |
6 | ReactDOM.render(, document.getElementById('root'));
7 |
--------------------------------------------------------------------------------
/packages/react-virtual-selection/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ['./src/**/*.{ts,tsx}'],
3 | theme: {
4 | extend: {},
5 | },
6 | plugins: [],
7 | };
8 |
--------------------------------------------------------------------------------
/packages/react-virtual-selection/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "esModuleInterop": true,
9 | "module": "commonjs",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "incremental": true,
13 | "outDir": "dist",
14 | "sourceMap": true,
15 | "jsx": "preserve",
16 | "declaration": true,
17 | "declarationMap": true,
18 | },
19 | "include": ["**/*.ts", "**/*.tsx"],
20 | "exclude": ["node_modules", "dist", "src/index.tsx", "src/demo/**"],
21 | }
--------------------------------------------------------------------------------
/packages/react-virtual-selection/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | // Generated using webpack-cli https://github.com/webpack/webpack-cli
3 |
4 | const path = require('path');
5 | const HtmlWebpackPlugin = require('html-webpack-plugin');
6 |
7 | const isProduction = process.env.NODE_ENV === 'production';
8 |
9 | const stylesHandler = 'style-loader';
10 |
11 | const config = {
12 | entry: {
13 | index: './src/index.tsx',
14 | },
15 | output: {
16 | path: path.resolve(__dirname, 'dist'),
17 | },
18 | devServer: {
19 | open: false,
20 | host: 'localhost',
21 | },
22 | plugins: [
23 | new HtmlWebpackPlugin({
24 | template: 'index.html',
25 | }),
26 |
27 | // Add your plugins here
28 | // Learns more about plugins from https://webpack.js.org/configuration/plugins/
29 | ],
30 | module: {
31 | rules: [
32 | {
33 | test: /\.(ts|tsx)$/i,
34 | loader: 'babel-loader',
35 | exclude: ['/node_modules/'],
36 | },
37 | {
38 | test: /\.css$/i,
39 | use: [stylesHandler, 'css-loader', 'postcss-loader'],
40 | },
41 | {
42 | test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i,
43 | type: 'asset',
44 | },
45 |
46 | // Add your rules for custom modules here
47 | // Learn more about loaders from https://webpack.js.org/loaders/
48 | ],
49 | },
50 | resolve: {
51 | extensions: ['.tsx', '.ts', '.js'],
52 | },
53 | };
54 |
55 | module.exports = () => {
56 | if (isProduction) {
57 | config.mode = 'production';
58 | } else {
59 | config.mode = 'development';
60 | }
61 | return config;
62 | };
63 |
--------------------------------------------------------------------------------
/web/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | node: true,
6 | },
7 | extends: [
8 | 'eslint:recommended',
9 | 'plugin:react/recommended',
10 | 'airbnb',
11 | 'plugin:@typescript-eslint/eslint-recommended',
12 | 'plugin:@typescript-eslint/recommended',
13 | ],
14 | parser: '@typescript-eslint/parser',
15 | parserOptions: {
16 | ecmaFeatures: {
17 | jsx: true,
18 | },
19 | ecmaVersion: 13,
20 | sourceType: 'module',
21 | },
22 | plugins: ['react', '@typescript-eslint'],
23 | rules: {
24 | 'react/jsx-filename-extension': [2, { extensions: ['.js', '.jsx', '.ts', '.tsx'] }],
25 | 'no-use-before-define': 'off',
26 | '@typescript-eslint/no-use-before-define': ['error'],
27 | 'import/order': [
28 | 'error',
29 | {
30 | groups: [['external', 'builtin'], 'internal', ['parent', 'sibling', 'index']],
31 | 'newlines-between': 'always',
32 | },
33 | ],
34 | 'import/extensions': 0,
35 | 'react/function-component-definition': 'off',
36 | 'no-param-reassign': 0,
37 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
38 | 'no-underscore-dangle': ['error', { allow: ['_id'] }],
39 | },
40 | settings: {
41 | 'import/resolver': {
42 | node: {
43 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
44 | },
45 | },
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | # -=- NextJS -=-
2 | **/.next/**
3 |
4 | # -=- Enviornment Variables -=-
5 | **/.env.local
6 |
--------------------------------------------------------------------------------
/web/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20.2.0
2 |
3 | WORKDIR /usr/src/
4 |
5 | # -=- Install Packages -=-
6 | COPY package.json ./
7 | COPY yarn.lock ./
8 | RUN yarn
9 |
10 | # -=- Copy Source Code -=-
11 | COPY . .
12 |
13 | # -=- Expose The Port -=-
14 | EXPOSE 3000
15 |
16 | # -=- Build / Run The Code -=-
17 | CMD [ "yarn", "run", "start" ]
--------------------------------------------------------------------------------
/web/jest.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 | const nextJest = require('next/jest');
3 |
4 | const createJestConfig = nextJest({
5 | dir: './src/',
6 | });
7 |
8 | const jestConfig = {
9 | moduleNameMapper: {
10 | '^@/components/(.*)$': '/components/$1',
11 | '^@/pages/(.*)$': '/pages/$1',
12 | },
13 | testPathIgnorePatterns: [
14 | 'renderWrapper.tsx',
15 | 'setupTests.ts',
16 | ],
17 | testEnvironment: 'jest-environment-jsdom',
18 | automock: false,
19 | resetMocks: false,
20 | setupFilesAfterEnv: [
21 | './src/__tests__/setupTests.ts',
22 | ],
23 | };
24 |
25 | module.exports = createJestConfig(jestConfig);
26 |
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "private": true,
4 | "scripts": {
5 | "dev": "next dev ./src/",
6 | "start": "next build ./src/ && next start ./src/",
7 | "lint": "next lint ./src/"
8 | },
9 | "resolutions": {
10 | "@types/react": "17.0.2",
11 | "@types/react-dom": "17.0.2"
12 | },
13 | "dependencies": {
14 | "@emotion/react": "^11.11.1",
15 | "@emotion/styled": "^11.11.0",
16 | "@mui/icons-material": "^5.14.11",
17 | "@mui/material": "^5.14.11",
18 | "dompurify": "^3.0.5",
19 | "dotenv": "^14.2.0",
20 | "emoji-mart": "^3.0.1",
21 | "express": "^4.17.2",
22 | "katex": "^0.16.4",
23 | "next": "12.0.8",
24 | "react": "17.0.2",
25 | "react-contenteditable": "^3.3.7",
26 | "react-dnd": "^15.1.2",
27 | "react-dnd-html5-backend": "^15.1.3",
28 | "react-dom": "17.0.2",
29 | "react-virtual-selection": "1.1.0",
30 | "supertokens-auth-react": "^0.28.1"
31 | },
32 | "devDependencies": {
33 | "@testing-library/dom": "^8.12.0",
34 | "@testing-library/jest-dom": "^5.16.2",
35 | "@testing-library/react": "^12.1.3",
36 | "@testing-library/user-event": "^13.5.0",
37 | "@types/dompurify": "^2.3.3",
38 | "@types/emoji-mart": "^3.0.9",
39 | "@types/jest": "^27.4.0",
40 | "@types/katex": "^0.16.0",
41 | "@types/node": "17.0.10",
42 | "@types/react": "17.0.38",
43 | "@typescript-eslint/eslint-plugin": "^5.10.0",
44 | "@typescript-eslint/parser": "^5.10.0",
45 | "autoprefixer": "^10.4.2",
46 | "eslint": "^8.7.0",
47 | "eslint-config-airbnb": "^19.0.4",
48 | "eslint-config-next": "12.0.8",
49 | "eslint-plugin-import": "^2.25.4",
50 | "eslint-plugin-jsx-a11y": "^6.5.1",
51 | "eslint-plugin-react": "^7.28.0",
52 | "eslint-plugin-react-hooks": "^4.3.0",
53 | "jest": "^27.5.1",
54 | "jest-fetch-mock": "^3.0.3",
55 | "postcss": "^8.4.5",
56 | "tailwindcss": "^3.0.15",
57 | "typescript": "^4.5.5"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/web/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/web/src/components/LoadingPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Spinner from './Spinner';
4 |
5 | const LoadingPage = () => (
6 |
13 | );
14 |
15 | export default LoadingPage;
16 |
--------------------------------------------------------------------------------
/web/src/components/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Spinner = () => (
4 |
5 | );
6 |
7 | export default Spinner;
8 |
--------------------------------------------------------------------------------
/web/src/components/blocks/BlockHandle.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 |
3 | interface BlockHandleProps {
4 | draggableRef: React.LegacyRef,
5 | }
6 |
7 | const BlockHandle = (props: BlockHandleProps) => {
8 | const menuButtonRef = useRef(null);
9 | const { draggableRef } = props;
10 |
11 | return (
12 |
25 | );
26 | };
27 |
28 | export default BlockHandle;
29 |
--------------------------------------------------------------------------------
/web/src/components/blocks/PageBlock.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import Link from 'next/link';
3 |
4 | import getPageInfo from '../../lib/pages/getPageInfo';
5 |
6 | interface PageBlockProps {
7 | blockID: string;
8 | properties: {
9 | value: string,
10 | }
11 | }
12 |
13 | const PageBlock = (props: PageBlockProps) => {
14 | const { blockID, properties } = props;
15 | const { value } = properties;
16 |
17 | const [currentValue, setCurrentValue] = useState(value);
18 |
19 | useEffect(() => {
20 | (async () => {
21 | const { style } = await getPageInfo(blockID);
22 | const { icon, name } = style;
23 |
24 | setCurrentValue(`${icon} ${name}`);
25 | })();
26 | }, [value]);
27 |
28 | return (
29 |
36 |
42 | [[
43 | {' '}
44 |
45 | {currentValue}
46 |
47 | {' '}
48 | ]]
49 |
50 |
51 | );
52 | };
53 |
54 | export default PageBlock;
55 |
--------------------------------------------------------------------------------
/web/src/components/home/AuthButton.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | import React from 'react';
3 | import {
4 | getAuthorisationURLWithQueryParamsAndSetState,
5 | } from 'supertokens-auth-react/recipe/thirdparty';
6 |
7 | type ValidProviders = 'Google';
8 |
9 | const authButtonClicked = async (company: ValidProviders) => {
10 | const authURL = await getAuthorisationURLWithQueryParamsAndSetState({
11 | providerId: company.toLowerCase(),
12 | authorisationURL: `${process.env.NEXT_PUBLIC_BASE_URL}/auth/callback/${company.toLowerCase()}`,
13 | });
14 |
15 | window.location
16 | .assign(authURL);
17 | };
18 |
19 | const AuthButton = (props: { company: ValidProviders }) => {
20 | const { company } = props;
21 |
22 | return (
23 |
41 | );
42 | };
43 |
44 | export default AuthButton;
45 |
--------------------------------------------------------------------------------
/web/src/components/home/AuthNavBar.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import React from 'react';
3 |
4 | const AuthNavBar = () => (
5 |
15 | );
16 |
17 | export default AuthNavBar;
18 |
--------------------------------------------------------------------------------
/web/src/components/home/Intro.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import React from 'react';
3 | import Image from 'next/image';
4 |
5 | import LaptopScreen from './LaptopScreen';
6 |
7 | const Intro = () => (
8 |
9 |
12 | Organize your academic life,
13 |
14 | effortlessly.
15 |
25 |
26 |
27 |
28 |
29 |
30 | Streamline your study routine with Note Rack: the unified
31 |
32 | hub for all your notes and tasks.
33 |
34 |
35 |
36 |
46 |
47 |
48 |
49 |
54 |
55 |
56 |
57 | );
58 |
59 | export default Intro;
60 |
--------------------------------------------------------------------------------
/web/src/components/home/LaptopScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface LaptopScreenProps {
4 | children: React.ReactNode,
5 | }
6 |
7 | const LaptopScreen = (props: LaptopScreenProps) => {
8 | const { children } = props;
9 |
10 | return (
11 |
12 |
13 | {children}
14 |
15 | );
16 | };
17 |
18 | export default LaptopScreen;
19 |
--------------------------------------------------------------------------------
/web/src/components/home/NavBar.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import React from 'react';
3 |
4 | const NavBar = () => (
5 |
25 | );
26 |
27 | export default NavBar;
28 |
--------------------------------------------------------------------------------
/web/src/components/menus/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface ButtonProps {
4 | style: 'primary' | 'secondary'
5 | label: string,
6 | onClick: () => void,
7 | }
8 |
9 | const Button: React.FC = (props) => {
10 | const {
11 | style,
12 | label,
13 | onClick
14 | } = props;
15 |
16 | const styles: Record = {
17 | primary: 'bg-blue-500 border border-blue-600 rounded text-amber-50',
18 | secondary: 'mr-2 text-amber-50/70'
19 | };
20 |
21 | return (
22 |
28 | );
29 | };
30 |
31 | export default Button;
32 |
--------------------------------------------------------------------------------
/web/src/components/pageCustomization/OptionsButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef } from 'react';
2 | import { useRouter } from 'next/router';
3 | import MoreHoriz from '@mui/icons-material/MoreHoriz';
4 |
5 | import OptionsMenu from '../menus/OptionsMenu/OptionsMenu';
6 |
7 | const ShareButton = () => {
8 | const { page } = useRouter().query;
9 | const [isMenuOpen, setIsMenuOpen] = useState(false);
10 | const buttonRef = useRef(null);
11 |
12 | return (
13 |
16 |
17 |
{
20 | setIsMenuOpen(!isMenuOpen);
21 | }}
22 | >
23 |
26 |
27 |
28 | {
29 | isMenuOpen && (
30 |
35 | )
36 | }
37 |
38 | );
39 | };
40 |
41 | export default ShareButton;
42 |
--------------------------------------------------------------------------------
/web/src/contexts/PageContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | import PageDataInterface from '../lib/types/pageTypes';
4 |
5 | interface PageContextProps {
6 | pageData?: PageDataInterface['message'],
7 | setPageData: React.Dispatch>,
8 | }
9 |
10 | const PageContext = createContext({
11 | setPageData: (_) => {},
12 | });
13 |
14 | export default PageContext;
15 |
--------------------------------------------------------------------------------
/web/src/contexts/PagePermissionsContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | import type { UserPermissions, Permissions } from '../lib/types/pageTypes';
4 |
5 | interface PagePermissionsContextProps {
6 | permissionsOnPage?: UserPermissions,
7 | isMenuOpen: boolean,
8 | setIsMenuOpen: (isMenuOpen: boolean) => void,
9 | isPagePublic: boolean,
10 | setIsPagePublic: (isPagePublic: boolean) => void,
11 | currentPermissions: Permissions,
12 | setCurrentPermissions: (currentPermissions: Permissions) => void,
13 | }
14 |
15 | const PagePermissionContext = createContext({
16 | isMenuOpen: false,
17 | setIsMenuOpen: () => {},
18 | isPagePublic: false,
19 | setIsPagePublic: () => {},
20 | currentPermissions: {},
21 | setCurrentPermissions: () => {},
22 | });
23 |
24 | export default PagePermissionContext;
25 |
--------------------------------------------------------------------------------
/web/src/lib/config/superTokensConfig.ts:
--------------------------------------------------------------------------------
1 | import SessionReact from 'supertokens-auth-react/recipe/session';
2 | import ThirdParty, {
3 | Github,
4 | Google,
5 | } from 'supertokens-auth-react/recipe/thirdparty';
6 | import Router from 'next/router';
7 |
8 | const superTokensConfig = () => ({
9 | appInfo: {
10 | appName: 'Note Rack',
11 | apiDomain: process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:8000',
12 | websiteDomain: process.env.NEXT_PUBLIC_BASE_URL || 'http://127.0.0.1:3000',
13 | apiBasePath: `${process.env.NEXT_PUBLIC_API_URL?.replace(/https?:\/\/.*?\//, '')}/auth` || '/auth',
14 | websiteBasePath: '/auth',
15 | },
16 | recipeList: [
17 | ThirdParty.init({
18 | signInAndUpFeature: {
19 | providers: [
20 | Google.init(),
21 | ],
22 | },
23 | }),
24 | SessionReact.init(),
25 | ],
26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
27 | windowHandler: (oI: any) => ({
28 | ...oI,
29 | location: {
30 | ...oI.location,
31 | setHref: (href: string) => {
32 | Router.push(href);
33 | },
34 | },
35 | }),
36 | });
37 |
38 | export default superTokensConfig;
39 |
--------------------------------------------------------------------------------
/web/src/lib/constants/BlockTypes.ts:
--------------------------------------------------------------------------------
1 | import Icon from '../../components/blocks/Icon';
2 | import Title from '../../components/pageCustomization/Title';
3 | import TextBlock from '../../components/blocks/TextBlock';
4 | import PageBlock from '../../components/blocks/PageBlock';
5 | import MathBlock from '../../components/blocks/MathBlock';
6 |
7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
8 | const BlockTypes = {
9 | // -=- Page Components -=-
10 | 'page-icon': Icon,
11 | 'page-title': Title,
12 |
13 | // -=- Text Based Components -=-
14 | text: TextBlock,
15 | h1: TextBlock,
16 | h2: TextBlock,
17 | h3: TextBlock,
18 | h4: TextBlock,
19 | h5: TextBlock,
20 |
21 | // -=- Semi-Text Based Components -=-
22 | quote: TextBlock,
23 | callout: TextBlock,
24 |
25 | // -=- Inline Page Component -=-
26 | page: PageBlock,
27 |
28 | // -=- Other Components -=-
29 | math: MathBlock
30 | } as const;
31 |
32 | export default BlockTypes;
33 |
--------------------------------------------------------------------------------
/web/src/lib/constants/InlineTextStyles.ts:
--------------------------------------------------------------------------------
1 | import inlineTextKeybinds from "../inlineTextKeybinds";
2 |
3 | const InlineTextStyles: {
4 | [key in (typeof inlineTextKeybinds)[number]['type']]: string
5 | } = {
6 | bold: 'font-bold',
7 | italic: 'italic',
8 | underline: 'border-b-[0.1em] dark:border-amber-50 print:dark:border-black border-black',
9 | strikethrough: 'line-through',
10 | } as const;
11 |
12 | export default InlineTextStyles;
13 |
--------------------------------------------------------------------------------
/web/src/lib/constants/ShareOptions.ts:
--------------------------------------------------------------------------------
1 | const dropdownInfo: {
2 | title: string,
3 | description: string,
4 | permissions: {
5 | admin: boolean,
6 | write: boolean,
7 | read: boolean,
8 | }
9 | }[] = [
10 | {
11 | title: 'Full access',
12 | description: 'Can edit, delete, and share',
13 | permissions: {
14 | admin: true,
15 | write: true,
16 | read: true,
17 | },
18 | },
19 | {
20 | title: 'Edit only',
21 | description: 'Can edit, but not delete or share',
22 | permissions: {
23 | admin: false,
24 | write: true,
25 | read: true,
26 | },
27 | },
28 | {
29 | title: 'View only',
30 | description: 'Cannot edit, delete, or share',
31 | permissions: {
32 | admin: false,
33 | write: false,
34 | read: true,
35 | },
36 | },
37 | ];
38 |
39 | export { dropdownInfo };
40 |
--------------------------------------------------------------------------------
/web/src/lib/constants/TextStyles.ts:
--------------------------------------------------------------------------------
1 | // ~ Styling lookup table for elements
2 | const TextStyles: {[key: string]: string} = {
3 | text: '',
4 | h1: 'text-4xl font-bold',
5 | h2: 'text-3xl font-bold',
6 | h3: 'text-2xl font-bold',
7 | h4: 'text-xl font-bold',
8 | h5: 'text-lg font-bold',
9 | quote: 'border-l-4 pl-3 border-zinc-700 dark:border-amber-50 print:dark:border-zinc-700',
10 | callout: `
11 | p-3
12 | bg-black/5 dark:bg-white/5
13 | print:bg-transparent print:dark:bg-transparent
14 | print:before:content-['test']
15 | print:h-full print:w-full
16 | print:before:h-full print:before:w-full
17 | print:before:border-[999px] print:before:-mt-3 print:before:-ml-3 print:before:border-black/5
18 | relative print:overflow-hidden print:before:absolute
19 | `,
20 | };
21 |
22 | export default TextStyles;
23 |
--------------------------------------------------------------------------------
/web/src/lib/deletePage.ts:
--------------------------------------------------------------------------------
1 | const deletePage = async (page: string) => {
2 | // -=- Request -=-
3 | // ~ Send a DELETE request to the API
4 | const deletePageResp = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/page/modify-page/${page}`, {
5 | method: 'DELETE',
6 | headers: {
7 | 'Content-Type': 'application/json',
8 | },
9 | credentials: 'include',
10 | });
11 |
12 | // -=- Success Handling -=-
13 | // ~ Get the response as JSON
14 | const deletePageRespJSON = await deletePageResp.json();
15 |
16 | // ~ If the response is 200 (Ok), return
17 | if (deletePageResp.status === 200) return;
18 |
19 | // -=- Error Handling -=-
20 | // ~ If the response is not 200 (Ok), throw an error
21 | throw new Error(`Couldn't delete page because: ${deletePageRespJSON.message}`);
22 | };
23 |
24 | export default deletePage;
25 |
--------------------------------------------------------------------------------
/web/src/lib/helpers/caret/getCurrentCaretCoordinates.ts:
--------------------------------------------------------------------------------
1 | import isAfterNewLine from './isAfterNewLine';
2 |
3 | /**
4 | * Get the carets coordinates in the window
5 | * @returns The carets coordinates in the window or undefined if the caret is not in the window
6 | */
7 | const getCurrentCaretCoordinates = (): { x: number, y: number } | undefined => {
8 | const selection = window.getSelection();
9 |
10 | if (!selection) return undefined;
11 |
12 | const range = selection.getRangeAt(0).cloneRange();
13 | range.collapse(true);
14 |
15 | const rect = range.getClientRects()[0];
16 |
17 | if (!rect) return undefined;
18 |
19 | // ~ Hack because if the caret is at the end of a line, the
20 | // rect will be the end of the line
21 | if (isAfterNewLine(range)) {
22 | return {
23 | x: rect.left,
24 | y: rect.top + rect.height,
25 | };
26 | }
27 |
28 | return {
29 | x: rect.left,
30 | y: rect.top,
31 | };
32 | };
33 |
34 | export default getCurrentCaretCoordinates;
35 |
--------------------------------------------------------------------------------
/web/src/lib/helpers/caret/getCursorOffset.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Get the number of characters between the start of the element and the
3 | * cursor
4 | * @param element Element to get the cursor offset for
5 | * @returns Number of characters between the start of the element and the
6 | */
7 | const getCursorOffset = (element: HTMLElement): number => {
8 | // ~ Get the range and selection
9 | const selection = window.getSelection();
10 |
11 | if (!selection) return 0;
12 |
13 | if (selection.rangeCount === 0) return 0;
14 |
15 | const range = selection.getRangeAt(0);
16 |
17 | if (!range) return 0;
18 |
19 | try {
20 | // ~ Clone the range and select the contents of the element
21 | const preCaretRange = range.cloneRange();
22 | preCaretRange.selectNodeContents(element);
23 |
24 | // ~ Set the end of the range to the start of the selection
25 | preCaretRange.setEnd(range.endContainer, range.endOffset);
26 |
27 | // ~ Return the length between the start of the element and the cursor
28 | return preCaretRange.toString().length;
29 | } catch (error) {
30 | // ~ If there is an error, return 0
31 | return 0;
32 | }
33 | };
34 |
35 | export default getCursorOffset;
36 |
--------------------------------------------------------------------------------
/web/src/lib/helpers/caret/isAfterNewLine.ts:
--------------------------------------------------------------------------------
1 | import getLastLineLength from "../getLastLineLength";
2 |
3 | /**
4 | * Check if the caret is after a new line
5 | * @param range The range to check
6 | * @returns Whether the caret is after a new line
7 | */
8 | const isAfterNewLine = (range: Range) => {
9 | const rangeContainer = (
10 | range.startContainer.parentElement
11 | || range.startContainer as HTMLElement
12 | );
13 |
14 | if (
15 | !range.startContainer
16 | || !rangeContainer.textContent
17 | || range.startOffset === 0
18 | ) return false;
19 |
20 | const lengthExcludingLastLine = rangeContainer.textContent.length - getLastLineLength(rangeContainer);
21 |
22 | if (lengthExcludingLastLine === 0) return false;
23 |
24 | return lengthExcludingLastLine === range.startOffset;
25 | };
26 |
27 | export default isAfterNewLine;
28 |
--------------------------------------------------------------------------------
/web/src/lib/helpers/caret/isCaretAtBottom.ts:
--------------------------------------------------------------------------------
1 | import getStyleScale from "../getStyleScale";
2 | import isElementFocused from '../isElementFocused';
3 | import getCurrentCaretCoordinates from "./getCurrentCaretCoordinates";
4 |
5 | const isCaretAtBottom = (element: HTMLElement) => {
6 | // ~ Check if the element is focused
7 | if (!isElementFocused(element)) return false;
8 |
9 | const caretCoordinates = getCurrentCaretCoordinates();
10 |
11 | if (!caretCoordinates) return false;
12 |
13 | const { y } = caretCoordinates;
14 |
15 | // ~ Get the caret position relative to the bottom of the element
16 | const bottomPadding = getStyleScale(element, 'paddingBottom');
17 |
18 | const elementPosition = element.getBoundingClientRect().bottom - bottomPadding;
19 |
20 | let lineHeight = getStyleScale(element, 'lineHeight');
21 | const fontSize = getStyleScale(element, 'fontSize');
22 |
23 | if (Number.isNaN(lineHeight)) lineHeight = 1.2;
24 |
25 | const caretPosition = (elementPosition - y) - lineHeight - fontSize;
26 |
27 | // ~ Check if the caret is at the bottom of the element (within 5px)
28 | return caretPosition < 5;
29 | };
30 |
31 | export default isCaretAtBottom;
32 |
--------------------------------------------------------------------------------
/web/src/lib/helpers/caret/isCaretAtTop.ts:
--------------------------------------------------------------------------------
1 | import getStyleScale from "../getStyleScale";
2 | import isElementFocused from '../isElementFocused';
3 | import getCurrentCaretCoordinates from "./getCurrentCaretCoordinates";
4 |
5 | const isCaretAtTop = (element: HTMLElement) => {
6 | // ~ Check if the element is focused
7 | if (!isElementFocused(element)) return false;
8 |
9 | const caretCoordinates = getCurrentCaretCoordinates();
10 |
11 | if (!caretCoordinates) return false;
12 |
13 | const { y } = caretCoordinates;
14 |
15 | // ~ Get the caret position relative to the element
16 | const topPadding = getStyleScale(element, 'paddingTop');
17 | const elementPosition = element.getBoundingClientRect().top + topPadding;
18 | const caretPosition = y - elementPosition;
19 |
20 | // ~ Check if the caret is at the top of the element (within 5px)
21 | return caretPosition < 5;
22 | };
23 |
24 | export default isCaretAtTop;
25 |
--------------------------------------------------------------------------------
/web/src/lib/helpers/findNextBlock.ts:
--------------------------------------------------------------------------------
1 | import type PageDataInterface from "../types/pageTypes";
2 |
3 | /**
4 | * Get the closest block to the element
5 | * @param element The element to start the search from
6 | * @returns The closest block to the element or null if none found
7 | */
8 | export const getClosestBlock = (
9 | element: HTMLElement | null
10 | ): (HTMLElement & { dataset: { blockIndex: string } }) | null => {
11 | if (!element) return null;
12 |
13 | if (element.dataset.blockIndex) {
14 | return element as (HTMLElement & { dataset: { blockIndex: string } });
15 | }
16 |
17 | return getClosestBlock(element.parentElement);
18 | };
19 |
20 | /**
21 | * Find the next block in the editor
22 | * @param element The element to start the search from
23 | * @param iterator The iterator function to use to find the next block
24 | * @param pageData The page data to use to find the next block
25 | * @param editor The editor to search in
26 | * @returns The next block or undefined if none found
27 | */
28 | const findNextBlock = (
29 | element: HTMLElement | null,
30 | iterator: (start: number) => number,
31 | pageData: PageDataInterface['message'],
32 | editor: HTMLElement | null = document.querySelector('.editor')
33 | ): HTMLElement | undefined => {
34 | if (!element || !pageData || !editor) return;
35 |
36 | const block = getClosestBlock(element);
37 |
38 | if (!block) return;
39 |
40 | const blockIndex = parseInt(block.dataset.blockIndex);
41 |
42 | const nextBlockID = pageData.data[iterator(blockIndex)]?._id;
43 |
44 | const nextBlock = editor.querySelector(`#block-${nextBlockID}`) as HTMLElement;
45 |
46 | if (!nextBlock) {
47 | const title = document.getElementById('page-title-text');
48 |
49 | if (!title) return;
50 |
51 | return title as HTMLElement;
52 | }
53 |
54 | if (!nextBlock.dataset.blockIndex) return findNextBlock(nextBlock, iterator, pageData, editor);
55 |
56 | return nextBlock;
57 | }
58 |
59 | export default findNextBlock;
60 |
--------------------------------------------------------------------------------
/web/src/lib/helpers/focusElement.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Focuses an element
3 | * @param element The element to focus
4 | * @param offset The offset to move the cursor to
5 | */
6 | const focusElement = (element: HTMLElement, offset: number = 0) => {
7 | element.focus();
8 |
9 | // ~ Move the cursor to the end of the block unless the only text is a newline
10 | if (element.textContent === '\n') return;
11 |
12 | const range = document.createRange();
13 | const selection = window.getSelection();
14 |
15 | if (!selection) return;
16 |
17 | const iterator = document.createNodeIterator(
18 | element,
19 | // eslint-disable-next-line no-bitwise
20 | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
21 | {
22 | acceptNode: (childNode) => {
23 | if (childNode.nodeName === 'BR') return NodeFilter.FILTER_ACCEPT;
24 | if (childNode.nodeName === '#text') return NodeFilter.FILTER_ACCEPT;
25 | return NodeFilter.FILTER_SKIP;
26 | },
27 | },
28 | );
29 |
30 | while (iterator.nextNode()) {
31 | const node = iterator.referenceNode;
32 |
33 | const length = node.nodeName === '#text'
34 | ? node.textContent?.length || 0
35 | : 1;
36 |
37 | offset -= length;
38 |
39 | if (offset <= 0) {
40 | const index = Math.max(Math.min(offset + length, length), 0);
41 |
42 | if (node.textContent?.at(index - 1) === '\n') {
43 | range.setStart(node, Math.max(index - 1, 0));
44 | } else {
45 | range.setStart(node, index);
46 | }
47 |
48 | break;
49 | }
50 | }
51 |
52 | if (offset > 0) {
53 | range.setStart(iterator.referenceNode, iterator.referenceNode.textContent?.length || 0);
54 | }
55 |
56 | selection.removeAllRanges();
57 | selection.addRange(range);
58 | };
59 |
60 | export default focusElement;
61 |
--------------------------------------------------------------------------------
/web/src/lib/helpers/getCompletion.ts:
--------------------------------------------------------------------------------
1 | import crypto from 'crypto';
2 |
3 | /**
4 | * Get the completion for a block
5 | * @returns The completion
6 | */
7 | const getCompletion = async (
8 | blockIndex: number,
9 | ): Promise => (
10 | new Promise((resolve, reject) => {
11 | const eventID = crypto.randomBytes(12).toString('hex');
12 |
13 | document.dispatchEvent(
14 | new CustomEvent('completionRequest', {
15 | detail: {
16 | index: blockIndex,
17 | eventID,
18 | },
19 | })
20 | );
21 |
22 | const handleCompletion = (event: CustomEvent<{ completion: string, eventID: string }>) => {
23 | if (eventID !== event.detail.eventID) return;
24 |
25 | document.removeEventListener('completion', handleCompletion as EventListener);
26 |
27 | resolve(event.detail.completion);
28 | };
29 |
30 | setTimeout(() => {
31 | document.removeEventListener('completion', handleCompletion as EventListener);
32 | reject('Failed to get completion in time');
33 | }, 1000);
34 |
35 | document.addEventListener('completion', handleCompletion as EventListener);
36 | })
37 | );
38 |
39 | export default getCompletion;
40 |
--------------------------------------------------------------------------------
/web/src/lib/helpers/getOffsetCoordinates.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Get the x and y coordinates of a cursor at a given offset
3 | * @param offset Offset of the cursor
4 | * @returns X and y coordinates of the cursor
5 | */
6 | const getOffsetCoordinates = (element: HTMLElement, offset: number): { x: number, y: number } => {
7 | const range = document.createRange();
8 |
9 | const iterator = document.createNodeIterator(
10 | element,
11 | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
12 | {
13 | acceptNode: (childNode) => {
14 | if (childNode.nodeName === 'BR') return NodeFilter.FILTER_ACCEPT;
15 | if (childNode.nodeName === '#text') return NodeFilter.FILTER_ACCEPT;
16 | return NodeFilter.FILTER_SKIP;
17 | },
18 | },
19 | );
20 | if (!iterator.referenceNode) return { x: 0, y: 0 };
21 |
22 | while (iterator.nextNode()) {
23 | const textNode = iterator.referenceNode;
24 |
25 | if (!textNode) continue;
26 |
27 | if (textNode.textContent?.length! >= offset) {
28 | range.setStart(textNode, offset);
29 | range.collapse(true);
30 | break;
31 | }
32 |
33 | offset -= textNode.textContent?.length!;
34 | }
35 |
36 | const rect = range.getClientRects()[0];
37 |
38 | if (!rect) return { x: 0, y: 0 };
39 |
40 | return {
41 | x: rect.left,
42 | y: rect.top,
43 | };
44 | };
45 |
46 | export default getOffsetCoordinates;
47 |
--------------------------------------------------------------------------------
/web/src/lib/helpers/getStringDistance.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Get the distance between two strings
3 | * @param a
4 | * @param b
5 | * @returns Distance between the two strings
6 | */
7 | const getStringDistance = (a: string, b: string): number => {
8 | if (a.length === 0) return b.length;
9 |
10 | if (b.length === 0) return a.length;
11 |
12 | const matrix = [];
13 |
14 | // ~ Increment along the first column of each row
15 | let i;
16 | for (i = 0; i <= b.length; i++) {
17 | matrix[i] = [i];
18 | }
19 |
20 | // ~ Increment each column in the first row
21 | let j;
22 | for (j = 0; j <= a.length; j++) {
23 | matrix[0][j] = j;
24 | }
25 |
26 | // ~ Fill in the rest of the matrix
27 | for (i = 1; i <= b.length; i++) {
28 | for (j = 1; j <= a.length; j++) {
29 | if (b.charAt(i - 1) === a.charAt(j - 1)) {
30 | matrix[i][j] = matrix[i - 1][j - 1];
31 | } else {
32 | matrix[i][j] = Math.min(
33 | matrix[i - 1][j - 1], // ~ substitution
34 | matrix[i][j - 1], // ~ insertion
35 | matrix[i - 1][j], // ~ deletion
36 | ) + 1;
37 | }
38 | }
39 | }
40 |
41 | return matrix[b.length][a.length];
42 | };
43 |
44 | export default getStringDistance;
45 |
--------------------------------------------------------------------------------
/web/src/lib/helpers/getStyleScale.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | /**
4 | * Get the scale of a style property of an element in pixels
5 | * @param element The element to get the style scale from
6 | * @param style The css style property to get the scale from
7 | * @returns The scale of the style property in pixels
8 | */
9 | const getStyleScale = (
10 | element: HTMLElement,
11 | style: keyof React.CSSProperties
12 | ) => (
13 | +window
14 | .getComputedStyle(element)
15 | .getPropertyValue(style.replace(/([A-Z])/g, '-$1'))
16 | .toLowerCase()
17 | .replace('px', '')
18 | );
19 |
20 | export default getStyleScale;
21 |
--------------------------------------------------------------------------------
/web/src/lib/helpers/inlineBlocks/findNodesInRange.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Find the text nodes that contain a given range
3 | * @param element The element to search in
4 | * @param range The range to search for
5 | * @returns An object containing the nodes and the start offset of the nodes
6 | */
7 | const findNodesInRange = (
8 | element: HTMLElement,
9 | range: {
10 | start: number;
11 | end: number;
12 | }
13 | ): {
14 | nodes: Node[];
15 | startOffset: number;
16 | } => {
17 | let startOffset = 0;
18 | let endOffset = 0;
19 | const treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null);
20 |
21 | let blocksContainingRegex: Node[] = [];
22 |
23 | // ~ Find the text nodes that contains the regex
24 | while (treeWalker.nextNode()) {
25 | const length = treeWalker.currentNode.textContent?.length || 0;
26 |
27 | endOffset += length;
28 |
29 | if (endOffset >= range.start) {
30 | blocksContainingRegex.push(treeWalker.currentNode)
31 |
32 | // ~ All of the text nodes the regex could be in have been found
33 | if (endOffset > range.end) break;
34 |
35 | continue;
36 | }
37 |
38 | startOffset += length;
39 | }
40 |
41 | return {
42 | nodes: blocksContainingRegex,
43 | startOffset,
44 | }
45 | };
46 |
47 | export default findNodesInRange;
48 |
--------------------------------------------------------------------------------
/web/src/lib/helpers/isElementFocused.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Checks if the given element is focused or contains the focused element
3 | * @param element The element to check
4 | * @returns Whether the element is focused or contains the focused element
5 | */
6 | const isElementFocused = (element: HTMLElement) => (
7 | document.activeElement === element
8 | || element.contains(document.activeElement)
9 | );
10 |
11 | export default isElementFocused;
--------------------------------------------------------------------------------
/web/src/lib/helpers/saveBlock.ts:
--------------------------------------------------------------------------------
1 | import InlineTextStyles from "../constants/InlineTextStyles";
2 | import type { EditableText } from "../types/blockTypes";
3 |
4 | /**
5 | * Walk up the tree to get the full text style of the node
6 | * @param node The node to get the full text style of
7 | * @param topNode The top node to stop at
8 | * @returns The full text style of the node
9 | */
10 | const getFullTextStyle = (node: Node, topNode: HTMLElement) => {
11 | let currentNode = node;
12 | let style: string[] = [];
13 |
14 | while (currentNode.parentElement && currentNode.parentElement !== topNode) {
15 | currentNode = currentNode.parentElement;
16 |
17 | const type = (currentNode as HTMLElement).getAttribute('data-inline-type');
18 |
19 | if (!type) continue;
20 |
21 | style.push(...JSON.parse(type));
22 | }
23 |
24 | return style as (keyof typeof InlineTextStyles)[];
25 | };
26 |
27 | /**
28 | * Get the representation of the block to save
29 | * @param element The element to save
30 | * @param completionText The completion text to remove
31 | * @returns The value and style of the block
32 | */
33 | const saveBlock = (
34 | element: HTMLDivElement,
35 | completionText: string | null = null,
36 | ) => {
37 | const style: EditableText['properties']['style'] = [];
38 |
39 | const treeWalker = document.createTreeWalker(
40 | element,
41 | NodeFilter.SHOW_TEXT,
42 | null
43 | );
44 |
45 | let length = 0;
46 |
47 | while (treeWalker.nextNode()) {
48 | const node = treeWalker.currentNode;
49 |
50 | if (!node.textContent) continue;
51 |
52 | length += node.textContent.length;
53 |
54 | const type = getFullTextStyle(node, element);
55 |
56 | if (!type.length) continue;
57 |
58 | style.push({
59 | type,
60 | start: length - node.textContent.length,
61 | end: length,
62 | });
63 | }
64 |
65 | // ~ Remove the completion text from the end of the block
66 | const completionOffset = completionText?.length || 0;
67 | const elementValue = element.innerText.substring(0, element.innerText.length - completionOffset);
68 |
69 | return {
70 | value: elementValue,
71 | style,
72 | };
73 | };
74 |
75 | export default saveBlock;
76 |
--------------------------------------------------------------------------------
/web/src/lib/inlineTextKeybinds.ts:
--------------------------------------------------------------------------------
1 | const inlineTextKeybinds = [
2 | {
3 | keybind: /(\*\*)(.*?)\1/g,
4 | plainTextKeybind: '**',
5 | type: 'bold',
6 | },
7 | {
8 | keybind: /(? {
2 | // -=- Request -=-
3 | // ~ Send a PATCH request to the API to change the expansion state of the page
4 | const pageTreeResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/account/edit-page-tree/${page}`, {
5 | method: 'PATCH',
6 | headers: { 'Content-Type': 'application/json' },
7 | credentials: 'include',
8 | body: JSON.stringify({
9 | 'new-expansion-state': expanded,
10 | }),
11 | });
12 |
13 | // -=- Success Handling -=-
14 | // ~ If the response is 200 (Ok), return
15 | if (pageTreeResponse.status === 200) return;
16 |
17 | // -=- Error Handling -=-
18 | // ~ If the response is not 200 (Ok), throw an error
19 | const pageTree = await pageTreeResponse.json();
20 |
21 | throw new Error(`Couldn't get page info because of: ${pageTree.message}`);
22 | };
23 |
24 | export default editPageTree;
25 |
--------------------------------------------------------------------------------
/web/src/lib/pageTrees/getPageTree.ts:
--------------------------------------------------------------------------------
1 | const getPageTree = async () => {
2 | // -=- Request -=-
3 | // ~ Send a GET request to the API to get the page tree
4 | const pageTreeResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/account/get-page-tree`, {
5 | method: 'GET',
6 | credentials: 'include',
7 | });
8 |
9 | // -=- Success Handling -=-
10 | // ~ Get the response as JSON
11 | const pageTree = await pageTreeResponse.json();
12 |
13 | // ~ If the response is 200 (Ok), return the page tree
14 | if (pageTreeResponse.status === 200) return pageTree.message;
15 |
16 | // -=- Error Handling -=-
17 | // ~ If the response is not 200 (Ok), throw an error
18 | throw new Error(`Couldn't get page info because of: ${pageTree.message}`);
19 | };
20 |
21 | export default getPageTree;
22 |
--------------------------------------------------------------------------------
/web/src/lib/pages/editStyle.ts:
--------------------------------------------------------------------------------
1 | const editStyle = async (style: Record, page: string) => {
2 | // -=- Fetch -=-
3 | // ~ Send a PATCH request to the API to update the page's style
4 | const styleUpdateResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/page/modify-page/${page}`, {
5 | method: 'PATCH',
6 | headers: { 'Content-Type': 'application/json' },
7 | credentials: 'include',
8 | body: JSON.stringify({
9 | style,
10 | }),
11 | });
12 |
13 | // -=- Response -=-
14 | // ~ If the response is not 200, throw an error
15 | const styleUpdateResponseJSON = await styleUpdateResponse.json();
16 |
17 | // ~ If the response is 200 (Ok), return
18 | if (styleUpdateResponse.status === 200) return;
19 |
20 | // -=- Error Handling -=-
21 | // ~ If the response is not 200 (Ok), throw an error
22 | throw new Error(`Couldn't update pages style because: ${styleUpdateResponseJSON.message}`);
23 | };
24 |
25 | export default editStyle;
26 |
--------------------------------------------------------------------------------
/web/src/lib/pages/getPageInfo.ts:
--------------------------------------------------------------------------------
1 | const getPageInfo = async (page: string) => {
2 | // -=- Fetch -=-
3 | // ~ Send a GET request to the API to get the page's info
4 | const pageInfoResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/page/get-page-info/${page}`, {
5 | method: 'GET',
6 | credentials: 'include',
7 | });
8 |
9 | // -=- Response -=-
10 | // ~ Get the response as JSON
11 | const pageInfo = await pageInfoResponse.json();
12 |
13 | // ~ If the response is 200 (Ok), return the page info
14 | if (pageInfoResponse.status === 200) return pageInfo.message;
15 |
16 | // -=- Error Handling -=-
17 | // ~ If the response is not 200 (Ok), throw an error
18 | throw new Error(`Couldn't get page info because of: ${pageInfo.message}`);
19 | };
20 |
21 | export default getPageInfo;
22 |
--------------------------------------------------------------------------------
/web/src/lib/types/blockTypes.d.ts:
--------------------------------------------------------------------------------
1 | import type { Dispatch, SetStateAction } from 'react';
2 |
3 | import BlockTypes from '../constants/BlockTypes';
4 | import type PageDataInterface from './pageTypes';
5 | import InlineTextStyles from '../../lib/constants/InlineTextStyles';
6 |
7 | // -=- Used for the base block -=-
8 | interface BaseBlockProps {
9 | blockType: keyof typeof BlockTypes,
10 | blockID: string,
11 | properties: Record,
12 | page: string,
13 | index: number,
14 | }
15 |
16 | // -=- Used for blocks that can't be deleted and are only controlled by the server -=-
17 | interface PermanentBlock {
18 | page: string,
19 | }
20 |
21 | // -=- Used for for blocks that can't be deleted but can be edited -=-
22 | interface PermanentEditableText extends PermanentBlock {
23 | index: number,
24 | }
25 |
26 | // -=- Used for p through h1 -=-
27 | interface EditableText extends PermanentEditableText {
28 | properties: {
29 | value: string,
30 | style?: {
31 | type: (keyof typeof InlineTextStyles)[],
32 | start: number,
33 | end: number,
34 | }[],
35 | },
36 | type: string,
37 | blockID: string,
38 | setCurrentBlockType: (_type: string) => void,
39 | }
40 |
41 | // -=- Used for check list elements -=-
42 | interface EditableCheckList extends EditableList {
43 | properties: {
44 | value: string,
45 | checked: boolean,
46 | relationship: 'sibling' | 'child',
47 | },
48 | }
49 |
50 | export type {
51 | BaseBlockProps,
52 | PermanentBlock,
53 | PermanentEditableText,
54 | EditableText,
55 | EditableCheckList,
56 | };
57 |
--------------------------------------------------------------------------------
/web/src/lib/types/pageTypes.d.ts:
--------------------------------------------------------------------------------
1 | import BlockTypes from "../constants/BlockTypes"
2 |
3 | interface Block {
4 | _id: string,
5 | blockType: keyof typeof BlockTypes,
6 | properties: Record,
7 | children: Block[]
8 | }
9 |
10 | export interface UserPermissions {
11 | read: boolean,
12 | write: boolean,
13 | admin: boolean,
14 | }
15 |
16 | export interface Permissions {
17 | [key: string]: {
18 | read: boolean,
19 | write: boolean,
20 | admin: boolean,
21 | email: string,
22 | }
23 | }
24 |
25 | interface PageDataInterface {
26 | status: string,
27 | message?: {
28 | style: {
29 | colour: {
30 | r: number,
31 | g: number,
32 | b: number,
33 | }
34 | name: string,
35 | icon: string,
36 | },
37 | data: Block[],
38 | userPermissions: UserPermissions,
39 | permissions?: Permissions,
40 | },
41 | }
42 |
43 | export default PageDataInterface;
44 |
--------------------------------------------------------------------------------
/web/src/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/web/src/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | eslint: {
5 | ignoreDuringBuilds: true,
6 | },
7 | };
8 |
9 | module.exports = nextConfig;
10 |
--------------------------------------------------------------------------------
/web/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-props-no-spreading */
2 | import React from 'react';
3 | import type { AppProps } from 'next/app';
4 | import SuperTokensReact, { SuperTokensWrapper } from 'supertokens-auth-react';
5 |
6 | import '../styles/globals.css';
7 | import '../styles/emojiPicker.css';
8 | import superTokensConfig from '../lib/config/superTokensConfig';
9 |
10 | // -=- Initialization -=-
11 | // ~ If we're in the browser, initialize SuperTokens
12 | if (typeof window !== 'undefined') {
13 | // ~ Initialize SuperTokens
14 | SuperTokensReact.init(superTokensConfig());
15 | }
16 |
17 | // -=- App -=-
18 | // ~ The main app component
19 | const App = ({ Component, pageProps }: AppProps) => (
20 |
21 |
22 |
23 |
24 |
25 | );
26 |
27 | export default App;
28 |
--------------------------------------------------------------------------------
/web/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Html,
3 | Head,
4 | Main,
5 | NextScript,
6 | } from 'next/document';
7 | import React from 'react';
8 |
9 | // -=- Document -=-
10 | // ~ The main document component
11 | const Document = () => (
12 |
13 |
14 | {/* ~ Favicon */}
15 |
16 |
17 | {/* ~ Body w/ dark mode enabled */}
18 |
19 |
20 |
21 |
22 |
23 | );
24 |
25 | export default Document;
26 |
--------------------------------------------------------------------------------
/web/src/pages/auth/callback/[provider].tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { redirectToAuth } from 'supertokens-auth-react';
3 | import { useRouter } from 'next/router';
4 | import { signInAndUp } from 'supertokens-auth-react/recipe/thirdparty';
5 |
6 | import Spinner from '../../../components/Spinner';
7 | import AuthNavBar from '../../../components/home/AuthNavBar';
8 |
9 | const Callback = () => {
10 | const router = useRouter();
11 | const { provider } = router.query;
12 |
13 | useEffect(() => {
14 | (async () => {
15 | try {
16 | const response = await signInAndUp();
17 |
18 | if (response.status === 'OK') {
19 | router.push('/note-rack');
20 |
21 | window.location.assign('/note-rack');
22 | } else {
23 | window.location.assign('/auth?error=Something went wrong');
24 | }
25 | } catch (error) {
26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
27 | if ((error as any).isSuperTokensGeneralError) {
28 | redirectToAuth({
29 | queryParams: {
30 | error: (error as Error).message,
31 | },
32 | });
33 | } else {
34 | window.location.assign('/auth?error=Something went wrong');
35 | }
36 | }
37 | })();
38 | }, [provider]);
39 |
40 | return (
41 |
42 |
45 | {/* Login / Signup Page */}
46 |
47 | {/* Sign Up / Sign In Providers */}
48 |
53 |
54 |
55 | );
56 | };
57 |
58 | export default Callback;
59 |
--------------------------------------------------------------------------------
/web/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import type { GetServerSideProps, NextPage } from 'next';
2 | import React from 'react';
3 |
4 | import NavBar from '../components/home/NavBar';
5 | import Intro from '../components/home/Intro';
6 | import Info from '../components/home/Info';
7 |
8 | const Home: NextPage = () => (
9 |
10 |
11 |
12 |
13 |
14 | );
15 |
16 | const getServerSideProps: GetServerSideProps = async (context) => {
17 | const { req, res } = context;
18 | const { cookies } = req;
19 |
20 | if (cookies.sIRTFrontend && cookies.sIRTFrontend !== '' && cookies.sIRTFrontend !== 'remove') {
21 | res.setHeader('location', '/note-rack');
22 | res.statusCode = 302;
23 | }
24 |
25 | return {
26 | props: {},
27 | };
28 | };
29 |
30 | export { getServerSideProps };
31 |
32 | export default Home;
33 |
--------------------------------------------------------------------------------
/web/src/pages/note-rack/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-empty */
2 | import React, { useEffect } from 'react';
3 | import Router from 'next/router';
4 | import Session from 'supertokens-auth-react/recipe/session';
5 |
6 | import MenuBar from '../../components/MenuBar';
7 |
8 | const NoteRack = () => {
9 | useEffect(() => {
10 | (async () => {
11 | // -=- Verification -=-
12 | // ~ Check if the user is logged in
13 | const session = await Session.doesSessionExist();
14 |
15 | // ~ If the user is not logged in, redirect them to the login page
16 | if (session === false) {
17 | Router.push('/auth');
18 | return;
19 | }
20 |
21 | if (localStorage.getItem('latestPageID') !== null) {
22 | Router.push(`./note-rack/${localStorage.getItem('latestPageID')}`);
23 | return;
24 | };
25 |
26 | // -=- Fetching -=-
27 | // ~ Get the user's home page
28 | const pageID = await fetch(
29 | `${process.env.NEXT_PUBLIC_API_URL}/page/get-home-page`,
30 | {
31 | method: 'POST',
32 | credentials: 'include',
33 | },
34 | );
35 |
36 | // -=- Redirection -=-
37 | // ~ Get the json response from the server
38 | const pageIDJSON = await pageID.json();
39 |
40 | // ~ If the user has a home page, redirect them to it
41 | if (pageIDJSON.status !== 'error') {
42 | Router.push(`./note-rack/${pageIDJSON.message}`);
43 | return;
44 | }
45 |
46 | Router.push('/auth');
47 | })();
48 | }, []);
49 |
50 | // -=- Render -=-
51 | // ~ Return a loading page while the user is being redirected
52 | return (
53 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default NoteRack;
62 |
--------------------------------------------------------------------------------
/web/src/public/blockExamples/Call Out Icon.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/web/src/public/blockExamples/H1 Icon.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/web/src/public/blockExamples/H2 Icon.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/web/src/public/icons/Brain.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/web/src/public/icons/Trash.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/web/src/public/logos/apple.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/web/src/public/logos/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/web/src/public/logos/google.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/web/src/public/promo/Biology-Notes-Example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Eroxl/Note-Rack/1e457868c76256bf8de2390bd09608e3f6927c9e/web/src/public/promo/Biology-Notes-Example.png
--------------------------------------------------------------------------------
/web/src/public/promo/Chat-Example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Eroxl/Note-Rack/1e457868c76256bf8de2390bd09608e3f6927c9e/web/src/public/promo/Chat-Example.png
--------------------------------------------------------------------------------
/web/src/public/promo/Lab-Report-Example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Eroxl/Note-Rack/1e457868c76256bf8de2390bd09608e3f6927c9e/web/src/public/promo/Lab-Report-Example.png
--------------------------------------------------------------------------------
/web/src/public/promo/Math-Example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Eroxl/Note-Rack/1e457868c76256bf8de2390bd09608e3f6927c9e/web/src/public/promo/Math-Example.png
--------------------------------------------------------------------------------
/web/src/public/promo/Notes-Example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Eroxl/Note-Rack/1e457868c76256bf8de2390bd09608e3f6927c9e/web/src/public/promo/Notes-Example.png
--------------------------------------------------------------------------------
/web/src/public/promo/Search-Example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Eroxl/Note-Rack/1e457868c76256bf8de2390bd09608e3f6927c9e/web/src/public/promo/Search-Example.png
--------------------------------------------------------------------------------
/web/src/public/promo/Share-Example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Eroxl/Note-Rack/1e457868c76256bf8de2390bd09608e3f6927c9e/web/src/public/promo/Share-Example.png
--------------------------------------------------------------------------------
/web/src/styles/emojiPicker.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .emoji-mart {
6 | @apply text-zinc-700 dark:text-amber-50 bg-stone-100 dark:bg-neutral-600 border-black border-opacity-5 rounded-md !important;
7 | }
8 |
9 | .emoji-mart-bar {
10 | @apply border-0 border-solid border-black border-opacity-5 !important;
11 | }
12 |
13 | .emoji-mart-anchor {
14 | @apply text-zinc-700/80 dark:text-amber-50/80 !important;
15 | }
16 |
17 | .emoji-mart-anchor-selected {
18 | @apply text-zinc-700 dark:text-amber-50 !important;
19 | }
20 |
21 | .emoji-mart-anchor-bar {
22 | @apply bg-purple-400 transition-all !important;
23 | }
24 |
25 | .emoji-mart-search input {
26 | @apply border-0 border-solid border-black border-opacity-5 dark:text-black !important;
27 | }
28 |
29 | .emoji-mart-category-label span {
30 | @apply bg-stone-100 dark:bg-neutral-600 opacity-90 !important;
31 | }
32 |
33 | .emoji-mart-no-results {
34 | @apply text-zinc-700 dark:text-amber-50 !important;
35 | }
36 |
37 | .emoji-mart-category .emoji-mart-emoji:hover:before {
38 | @apply bg-black dark:bg-white bg-opacity-5 dark:bg-opacity-10 !important;
39 | }
40 |
41 | .emoji-mart-scroll {
42 | -ms-overflow-style: none;
43 | scrollbar-width: none;
44 | }
45 |
46 | .emoji-mart-scroll::-webkit-scrollbar {
47 | display: none;
48 | }
--------------------------------------------------------------------------------
/web/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | editor.body {
6 | @apply w-screen h-screen overflow-x-hidden
7 | }
8 |
9 | .no-scrollbar::-webkit-scrollbar {
10 | display: none;
11 | }
12 |
13 | .no-scrollbar {
14 | -ms-overflow-style: none;
15 | scrollbar-width: none;
16 | }
17 |
18 | .emoji {
19 | display: inline-block;
20 | height: 1em;
21 | width: 1em;
22 | margin: 0 .05em 0 .1em;
23 | vertical-align: -0.1em;
24 | background-repeat: no-repeat;
25 | background-position: center center;
26 | background-size: 1em 1em;
27 | }
28 |
29 | .emoji-mart-preview {
30 | display: none;
31 | }
32 |
33 | .emoji-mart-category .emoji-mart-emoji span {
34 | @apply hover:cursor-pointer
35 | }
36 |
37 | .emoji-mart-anchor-icon {
38 | @apply flex flex-row items-center justify-center
39 | }
40 |
41 | [placeholder]:empty:focus:before {
42 | content: attr(placeholder);
43 | }
44 |
45 | .print-forced-background {
46 | position: relative;
47 | overflow: hidden;
48 | background-color: var(--forced-background-colour);
49 | }
50 |
51 | @media print {
52 | .print-forced-background:before {
53 | content: '';
54 | position: absolute;
55 | top: 0;
56 | right: 0;
57 | left: 0;
58 | bottom: 0;
59 | border: 999px var(--forced-background-colour) solid;
60 | }
61 | }
--------------------------------------------------------------------------------
/web/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true
17 | },
18 | "include": ["src/next-env.d.ts", "**/*.ts", "**/*.tsx"],
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------
/web/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | './src/**/*.{js,ts,jsx,tsx}',
4 | ],
5 | theme: {
6 | extend: {
7 | screens: {
8 | print: {
9 | raw: 'print',
10 | },
11 | },
12 | boxShadow: {
13 | 'under': '0.15rem 0.15rem 0px',
14 | },
15 | },
16 | },
17 | variants: {
18 | },
19 | plugins: [],
20 | important: true,
21 | darkMode: 'class',
22 | };
23 |
--------------------------------------------------------------------------------