├── .eslintignore
├── .eslintrc.cjs
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
└── favicon-32x32.png
├── src
├── App.tsx
├── assets
│ ├── copy-icon.png
│ ├── loader.svg
│ └── react.svg
├── components
│ ├── Accordion.tsx
│ ├── Button.tsx
│ ├── FileUpload.tsx
│ ├── Hint.tsx
│ ├── MultiChoice.tsx
│ ├── NumberInput.tsx
│ ├── Pagination.tsx
│ ├── RadioInput.tsx
│ ├── Tabs.tsx
│ ├── TextArea.tsx
│ └── TextInput.tsx
├── css
│ ├── blink.module.css
│ ├── fadein.module.css
│ └── index.css
├── data
│ └── glyphs.json
├── global
│ └── types.ts
├── hooks
│ ├── useApp.ts
│ ├── useFileUpload.ts
│ ├── useForm.ts
│ ├── useImportExport.ts
│ ├── useLocalStorage.ts
│ └── useResults.ts
├── layout
│ ├── Footer.tsx
│ ├── Header.tsx
│ └── Layout.tsx
├── main.tsx
├── utils
│ ├── createBookGenerator.ts
│ ├── createStringWrapper.ts
│ └── worker.ts
├── views
│ ├── form
│ │ ├── Form.tsx
│ │ ├── FormInput.tsx
│ │ ├── FormSettings.tsx
│ │ └── FormSettingsAdvanced.tsx
│ ├── tabs
│ │ ├── Output.tsx
│ │ ├── Results.tsx
│ │ └── UnsupportedCharacters.tsx
│ └── top
│ │ └── ImportExport.tsx
└── vite-env.d.ts
├── tailwind.config.js
├── text2book.png
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.eslintignore:
--------------------------------------------------------------------------------
1 | test
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | module.exports = {
4 | root: true,
5 | env: { browser: true, es2020: true },
6 | extends: [
7 | 'eslint:recommended',
8 | 'plugin:@typescript-eslint/recommended',
9 | 'plugin:@typescript-eslint/recommended-requiring-type-checking',
10 | 'plugin:react-hooks/recommended',
11 | ],
12 | parser: '@typescript-eslint/parser',
13 | parserOptions: {
14 | ecmaVersion: 'latest',
15 | sourceType: 'module',
16 | project: true,
17 | tsconfigRootDir: __dirname,
18 | },
19 | plugins: ['react-refresh'],
20 | rules: {
21 | 'react-refresh/only-export-components': [
22 | 'warn',
23 | { allowConstantExport: true },
24 | ],
25 | '@typescript-eslint/no-non-null-assertion': 'off',
26 | semi: 2,
27 | 'no-console': 2,
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "tabWidth": 2,
4 | "printWidth": 90,
5 | "singleQuote": true,
6 | "trailingComma": "es5",
7 | "jsxSingleQuote": true,
8 | "bracketSpacing": true
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 TheWilley
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Text2Book
6 |
7 |
8 |
9 | A tool to convert text to Minecraft books
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Introduction •
19 | Features •
20 | Usage •
21 | License
22 |
23 |
24 | ## Introduction
25 |
26 | Text2Book is a web application that allows users to convert text into Minecraft books by generating commands for command
27 | blocks or text sections which can be manually copied over. This tool is designed to simplify the process of creating
28 | in-game books with custom content for Minecraft players and mapmakers. Instead of manually entering lengthy commands or
29 | taking a wild guess how much text to copy over on each page, Text2Book automates the process, making it quicker and more
30 | user-friendly.
31 |
32 | ## Features
33 |
34 | - **Text and File Input**: Enter the desired text content directly into the application or upload a text file.
35 | - **Command and Text Generation**: Generate the necessary commands or text section to create the Minecraft books with the desired content.
36 | - **File and Text output**: Save generated content as a file or display directly within the app in a list.
37 | - **Export and Import**: Save your progress and import it later to continue working on your Minecraft book.
38 | - **Minecraft Version Selection**: ~~Choose between Java and Bedrock versions to generate the appropriate commands.~~ (Only Java works for now — Bedrock is under development, see [#21](https://github.com/TheWilley/Text2Book/issues/21))
39 | - **Incredible Speed**: Generates entire books in seconds by using the very same algorithm implemented in Minecraft (_all Harry Potter books combined in 3 seconds!_).
40 | - **Copy to Clipboard**: Easily copy the generated commands or text lines to your clipboard for quick in-game implementation.
41 |
42 | ## Usage
43 |
44 | Simply go to the [official webpage](https://thewilley.github.io/Text2Book/) to get started, or run the app yourself by following these steps:
45 |
46 | ```bash
47 | # Clone this repository
48 | $ git clone https://github.com/thewilley/Text2Book.git
49 |
50 | # Go into the repository
51 | $ cd Text2Book
52 |
53 | # Install dependencies
54 | $ npm install
55 |
56 | # Build app
57 | $ npm run build
58 |
59 | # If you want to start the app
60 | $ npm run preview
61 |
62 | # If you want to develop the app
63 | $ npm run dev
64 | ```
65 |
66 | ## License
67 |
68 | Text2Book is licensed under the [MIT License](LICENSE), which allows you to use, modify, and distribute the code for
69 | personal and commercial purposes. However, it comes with no warranties or guarantees. If you use Text2Book, please
70 | provide attribution to the original repository and authors.
71 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Text2Book
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "text2book",
3 | "homepage": "https://thewilley.github.io/Text2Book/",
4 | "author": "TheWilley",
5 | "private": true,
6 | "version": "0.2.1",
7 | "type": "module",
8 | "scripts": {
9 | "dev": "vite",
10 | "build": "tsc && vite build",
11 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
12 | "preview": "vite preview",
13 | "test": "node test/test.js",
14 | "predeploy": "npm run build",
15 | "deploy": "gh-pages -d dist",
16 | "prettier": "prettier src --write"
17 | },
18 | "dependencies": {
19 | "@fortawesome/fontawesome-svg-core": "^6.7.2",
20 | "@fortawesome/free-brands-svg-icons": "^6.7.2",
21 | "@fortawesome/free-regular-svg-icons": "^6.7.2",
22 | "@fortawesome/free-solid-svg-icons": "^6.7.2",
23 | "@fortawesome/react-fontawesome": "^0.2.2",
24 | "classnames": "^2.5.1",
25 | "file-saver": "^2.0.5",
26 | "gh-pages": "^5.0.0",
27 | "github-pages": "^3.0.2",
28 | "glob": "^10.3.10",
29 | "minimist": "^1.2.8",
30 | "nbt": "^0.8.1",
31 | "react": "^18.2.0",
32 | "react-dom": "^18.2.0",
33 | "use-local-storage": "^3.0.0"
34 | },
35 | "devDependencies": {
36 | "@types/file-saver": "^2.0.7",
37 | "@types/react": "^18.2.14",
38 | "@types/react-dom": "^18.2.6",
39 | "@typescript-eslint/eslint-plugin": "^5.61.0",
40 | "@typescript-eslint/parser": "^5.61.0",
41 | "@vitejs/plugin-react": "^4.0.1",
42 | "autoprefixer": "^10.4.14",
43 | "eslint": "^8.44.0",
44 | "eslint-plugin-react-hooks": "^4.6.0",
45 | "eslint-plugin-react-refresh": "^0.4.1",
46 | "postcss": "^8.4.26",
47 | "prettier": "^3.2.4",
48 | "tailwindcss": "^3.3.3",
49 | "typescript": "^5.0.2",
50 | "vite": "^4.4.0"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheWilley/Text2Book/7dc3b60c4050143edc99ac5e1012f51e6431ae9b/public/favicon-32x32.png
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import Layout from './layout/Layout.tsx';
2 | import useApp from './hooks/useApp.ts';
3 | import Form from './views/form/Form.tsx';
4 | import Output from './views/tabs/Output.tsx';
5 | import ImportExport from './views/top/ImportExport.tsx';
6 |
7 | function App() {
8 | const {
9 | results,
10 | loading,
11 | fadeinProps,
12 | timeToGenerate,
13 | outputFormat,
14 | setFadeIn,
15 | showResults,
16 | setOutputFormat,
17 | } = useApp();
18 |
19 | return (
20 |
21 |
22 |
28 |
35 |
36 | );
37 | }
38 |
39 | export default App;
40 |
--------------------------------------------------------------------------------
/src/assets/copy-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheWilley/Text2Book/7dc3b60c4050143edc99ac5e1012f51e6431ae9b/src/assets/copy-icon.png
--------------------------------------------------------------------------------
/src/assets/loader.svg:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Accordion.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useState } from 'react';
2 |
3 | function Accordion(props: {
4 | children: React.ReactNode | React.ReactNode[];
5 | id: string;
6 | label: ReactNode;
7 | }) {
8 | const [open, setOpen] = useState(false);
9 |
10 | return (
11 |
12 | setOpen(!open)}
19 | readOnly
20 | />
21 |
27 | {open && props.children}
28 |
29 | );
30 | }
31 |
32 | export default Accordion;
33 |
--------------------------------------------------------------------------------
/src/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | function Button(props: { id: string; label: ReactNode; onclick: () => void }) {
4 | return (
5 |
6 |
14 |
20 |
21 | );
22 | }
23 |
24 | export default Button;
25 |
--------------------------------------------------------------------------------
/src/components/FileUpload.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import useFileUpload from '../hooks/useFileUpload.ts';
3 |
4 | function FileUpload(props: {
5 | callback: (text: string) => void;
6 | useFileUpload: boolean;
7 | label: ReactNode;
8 | }) {
9 | const { fileName, handleFileChange } = useFileUpload(props.callback);
10 |
11 | return (
12 | <>
13 |
16 |
17 |
25 |
26 | {fileName || 'Choose a file'}
27 |
33 |
34 |
35 | >
36 | );
37 | }
38 |
39 | export default FileUpload;
40 |
--------------------------------------------------------------------------------
/src/components/Hint.tsx:
--------------------------------------------------------------------------------
1 | import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import React from 'react';
4 |
5 | type Props = {
6 | text: string;
7 | position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
8 | padding?: number;
9 | children?: React.ReactNode;
10 | };
11 |
12 | function Hint(props: Props) {
13 | // Set default position to 'top-left' if not provided
14 | const position = props.position || 'top-left';
15 | const padding = 0 + (props.padding || 0);
16 |
17 | const hintStyle = {
18 | position: 'absolute' as const,
19 | ...(position === 'top-left' && { top: padding, left: padding }),
20 | ...(position === 'top-right' && { top: padding, right: padding }),
21 | ...(position === 'bottom-left' && { bottom: padding, left: padding }),
22 | ...(position === 'bottom-right' && { bottom: padding, right: padding }),
23 | };
24 |
25 | // Ensure the parent container has relative positioning
26 | return (
27 |
28 | {props.children}
29 |
30 |
31 |
32 |
33 |
34 |
35 | {props.text}
36 |
37 |
38 |
39 |
40 | );
41 | }
42 |
43 | export default Hint;
44 |
--------------------------------------------------------------------------------
/src/components/MultiChoice.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | function MultiChoice(props: {
4 | name: string;
5 | items: { id: string; label: ReactNode; checked: boolean; callback: () => void }[];
6 | }) {
7 | return (
8 |
13 | {props.items.map((item) => (
14 |
15 |
23 |
29 |
30 | ))}
31 |
32 | );
33 | }
34 |
35 | export default MultiChoice;
36 |
--------------------------------------------------------------------------------
/src/components/NumberInput.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | function NumberInput(props: {
4 | label: ReactNode;
5 | id: string;
6 | placeholder: string;
7 | value: number;
8 | max: number;
9 | min: number;
10 | setter: (value: number) => void;
11 | required?: boolean;
12 | }) {
13 | return (
14 | <>
15 |
18 | props.setter(parseInt(e.target.value))}
27 | required={props.required}
28 | />
29 | >
30 | );
31 | }
32 |
33 | export default NumberInput;
34 |
--------------------------------------------------------------------------------
/src/components/Pagination.tsx:
--------------------------------------------------------------------------------
1 | type Props = {
2 | onPrevious: () => void;
3 | onNext: () => void;
4 | previousDisabled: boolean;
5 | nextDisabled: boolean;
6 | visible: boolean;
7 | page: number;
8 | };
9 |
10 | function Pagination(props: Props) {
11 | return (
12 |
13 |
20 | {props.page}
21 |
28 |
29 | );
30 | }
31 |
32 | export default Pagination;
33 |
--------------------------------------------------------------------------------
/src/components/RadioInput.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | function RadioInput(props: {
4 | id: string;
5 | label: ReactNode;
6 | name?: string;
7 | checked: boolean;
8 | callback: () => void;
9 | }) {
10 | return (
11 |
12 |
20 |
26 |
27 | );
28 | }
29 |
30 | export default RadioInput;
31 |
--------------------------------------------------------------------------------
/src/components/Tabs.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useState } from 'react';
2 |
3 | interface TabItem {
4 | id: string;
5 | label: ReactNode;
6 | element: ReactNode;
7 | }
8 |
9 | function Tabs(props: { name: string; items: TabItem[]; disabled: boolean }) {
10 | // State to track the currently active tab
11 | const [activeId, setActiveId] = useState(props.items[0]?.id);
12 |
13 | return (
14 |
21 | {/* Tab Buttons */}
22 |
23 | {props.items.map((item) => (
24 |
25 | {/* Hidden Radio Button */}
26 | setActiveId(item.id)}
33 | disabled={props.disabled}
34 | />
35 | {/* Label acting as the tab button */}
36 |
42 |
43 | ))}
44 |
45 |
46 | {/* Active Tab Content */}
47 |
48 | {props.items.map((item) => (
49 |
55 | {item.element}
56 |
57 | ))}
58 |
59 |
60 | );
61 | }
62 |
63 | export default Tabs;
64 |
--------------------------------------------------------------------------------
/src/components/TextArea.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | function TextArea(props: {
4 | label: ReactNode;
5 | id: string;
6 | placeholder: string;
7 | value: string;
8 | setter: (value: string) => void;
9 | required?: boolean;
10 | }) {
11 | return (
12 | <>
13 |
16 |