1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-08-07 06:25:21 +02:00

Merge branch 'main' into feature/table-of-content

This commit is contained in:
Taly 2022-07-12 17:47:43 +03:00
commit 1dfc5f0013
15 changed files with 23589 additions and 57 deletions

View file

@ -83,6 +83,21 @@ yarn lint
yarn test yarn test
``` ```
### Setup
You can configure application using configs in <code>/config</code> directory.
| Property | Role |
|----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <code>port</code> | to set port of application |
| <code>database</code> | to name directory with data |
| <code>rcFile</code> | to set destination of codexdocsrc config file |
| <code>uploads</code> | to set destination of directory to save uploads |
| <code>secret</code> | to set secret |
| <code>favicon</code> | to set url or favicon path (favicon need to be in /public directory), like `/myFavicon.png`, to get favicon. Server uploads file by url and saves it to temporary directory. And you can get favicon by /favicon static route of application |
You can configure application using configs in <code>/config</code> directory.
### Authentication ### Authentication
To manage pages you need to authorize (available on `/auth`). To manage pages you need to authorize (available on `/auth`).

View file

@ -3,5 +3,6 @@
"database": ".db", "database": ".db",
"rcFile": "./.codexdocsrc", "rcFile": "./.codexdocsrc",
"uploads": "public/uploads", "uploads": "public/uploads",
"secret": "iamasecretstring" "secret": "iamasecretstring",
"favicon": ""
} }

View file

@ -3,5 +3,6 @@
"database": ".db", "database": ".db",
"rcFile": "./.codexdocsrc", "rcFile": "./.codexdocsrc",
"uploads": "/uploads", "uploads": "/uploads",
"secret": "iamasecretstring" "secret": "iamasecretstring",
"favicon": ""
} }

View file

@ -3,5 +3,6 @@
"database": ".testdb", "database": ".testdb",
"rcFile": "./src/test/.codexdocsrc", "rcFile": "./src/test/.codexdocsrc",
"uploads": "public/uploads_test", "uploads": "public/uploads_test",
"secret": "iamasecretstring" "secret": "iamasecretstring",
"favicon": ""
} }

23343
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -7,23 +7,51 @@ import routes from './routes';
import HttpException from './exceptions/httpException'; import HttpException from './exceptions/httpException';
import * as dotenv from 'dotenv'; import * as dotenv from 'dotenv';
import config from 'config'; import config from 'config';
import os from 'os';
import appConfig from 'config';
import { downloadFavicon, FaviconData } from './utils/downloadFavicon';
dotenv.config(); dotenv.config();
const app = express(); const app = express();
const localConfig = rcParser.getConfiguration(); const localConfig = rcParser.getConfiguration();
// Get url to upload favicon from config
const favicon: string = appConfig.get('favicon');
app.locals.config = localConfig; app.locals.config = localConfig;
// view engine setup // view engine setup
app.set('views', path.join(__dirname, './', 'views')); app.set('views', path.join(__dirname, './', 'views'));
app.set('view engine', 'twig'); app.set('view engine', 'twig');
require('./utils/twig'); require('./utils/twig');
const downloadedFaviconFolder = os.tmpdir();
// Check if favicon is not empty
if (favicon) {
// Upload favicon by url, it's path on server is '/temp/favicon.{format}'
downloadFavicon(favicon, downloadedFaviconFolder).then((res) => {
app.locals.favicon = res;
console.log('Favicon successfully uploaded');
})
.catch( (err) => {
console.log(err);
console.log('Favicon has not uploaded');
});
} else {
console.log('Favicon is empty, using default path');
app.locals.favicon = {
destination: '/favicon.png',
type: 'image/png',
} as FaviconData;
}
app.use(morgan('dev')); app.use(morgan('dev'));
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use(cookieParser()); app.use(cookieParser());
app.use(express.static(path.join(__dirname, '../../public'))); app.use(express.static(path.join(__dirname, '../../public')));
app.use('/uploads', express.static(config.get('uploads'))); app.use('/uploads', express.static(config.get('uploads')));
app.use('/favicon', express.static(downloadedFaviconFolder));
app.use('/', routes); app.use('/', routes);

View file

@ -0,0 +1,85 @@
import path from 'path';
import fs from 'fs';
import fetch from 'node-fetch';
/**
* Uploaded favicon data
*/
export interface FaviconData {
// Uploaded favicon path
destination: string;
// File type
type: string;
}
// Initiate controller for aborting request
const controller = new AbortController();
/**
* Check if string is url
*
* @param str - string to check
*/
function checkIsUrl(str: string): boolean {
const re = new RegExp('https?://');
return re.test(str);
}
/**
* Upload favicon by url, or get it by path
*
* @param destination - url or path of favicon
* @param faviconFolder - folder to save favicon
* @returns { Promise<FaviconData> } - Promise with data about favicon
*/
export async function downloadFavicon(destination: string, faviconFolder: string): Promise<FaviconData> {
// Check of destination is empty
if (!destination) {
throw Error('Favicon destination is empty');
}
// Get file name by destination
const filename = destination.substring(destination.lastIndexOf('/')+1);
// Get file format
const format = filename.split('.')[1];
// Check if string is url
if (!checkIsUrl(destination)) {
return {
destination: `/${filename}`,
type: `image/${format}`,
} as FaviconData;
}
// Create timeout to abort request
const timeoutId = setTimeout(() => {
controller.abort();
console.log('Favicon request has timed out.');
}, 5000);
// Make get request to url
const res = await fetch(destination, { signal: controller.signal });
// Get buffer data from response
const fileData = await res.buffer();
// Clear timeout, if data was got
clearTimeout(timeoutId);
// Get file path in temporary directory
const filePath = path.join(faviconFolder, `favicon.${format}`);
// Save file
await fs.writeFile(filePath, fileData, (err) => {
if (err) {
console.log(err);
}
});
return {
destination: `/favicon/favicon.${format}`,
type: `image/${format}`,
} as FaviconData;
}

View file

@ -7,7 +7,7 @@
<meta property="og:title" content="{{ page.title | striptags }}" /> <meta property="og:title" content="{{ page.title | striptags }}" />
<meta property="article:modified_time" content="{{ (page.body.time / 1000) | date("c") }}" /> <meta property="article:modified_time" content="{{ (page.body.time / 1000) | date("c") }}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="icon" type="image/png" href="/favicon.png"> <link rel="icon" type="{{ favicon.type }}" href="{{ favicon.destination }}">
</head> </head>
<script> <script>
window.config = { window.config = {

View file

@ -13,14 +13,18 @@
} }
</textarea> </textarea>
<header class="writing-header"> <header class="writing-header">
<span class="writing-header__left"> <div class="writing-header__inner-container">
<span>
New Page at the
{% set currentPageId = 0 %} {% set currentPageId = 0 %}
{% if page is not empty %} {% if page is not empty %}
{% set currentPageId = page._id %} {% set currentPageId = page._id %}
{% endif %} {% endif %}
<select name="parent"> <div class="select-wrapper">
{% if parentsChildrenOrdered is not empty %}
<label for="parent">Parent Page</label>
{% else %}
<label for="parent">New Page at the</label>
{% endif %}
<select id="parent" name="parent">
<option value="0">Root</option> <option value="0">Root</option>
{% for _page in pagesAvailableGrouped %} {% for _page in pagesAvailableGrouped %}
{% if _page._id != currentPageId %} {% if _page._id != currentPageId %}
@ -34,24 +38,25 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>
</span> </div>
{% if parentsChildrenOrdered is not empty %} {% if parentsChildrenOrdered is not empty %}
<span> <div class="select-wrapper">
Put Above <label for="above">Put Above</label>
<select name="above"> <select id="above" name="above">
<option value="0">—</option> <option value="0">—</option>
{% for _page in parentsChildrenOrdered %} {% for _page in parentsChildrenOrdered %}
<option value="{{ _page._id }}">{{ _page.title }}</option> <option value="{{ _page._id }}">{{ _page.title }}</option>
{% endfor %} {% endfor %}
</select> </select>
</span> </div>
{% endif %} {% endif %}
</span> {% if page is not empty %}
<div class="uri-input-wrapper">
{% if page is not empty %} <label for="uri-input">Alias</label>
<p><input type="text" class="uri-input" name="uri-input" placeholder="URI (Optional)" value="{{ page.uri }}"></p> <input type="text" id="uri-input" class="uri-input" name="uri-input" placeholder="URI (Optional)" value="{{ page.uri }}">
{% endif %} </div>
{% endif %}
</div>
</header> </header>
<div class="writing-editor"> <div class="writing-editor">
<div id="editorjs"></div> <div id="editorjs"></div>

View file

@ -4,12 +4,14 @@
<title>{{ config.title }}</title> <title>{{ config.title }}</title>
<link rel="stylesheet" href="/dist/main.css" /> <link rel="stylesheet" href="/dist/main.css" />
<link rel="preload" href="{{ config.landingFrameSrc }}" as="document"> <link rel="preload" href="{{ config.landingFrameSrc }}" as="document">
<link rel="icon" type="image/png" href="/favicon.png?v=2"> <link rel="icon" type="{{ favicon.type }}" href="{{ favicon.destination }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta property="og:title" content="{{ config.title }}" /> <meta property="og:title" content="{{ config.title }}" />
<meta property="og:site_name" content="{{ config.title }}" /> <meta property="og:site_name" content="{{ config.title }}" />
<meta name="description" property="og:description" content="{{ config.description }}"> <meta name="description" property="og:description" content="{{ config.description }}">
</head> </head>
<script>
</script>
<body class="landing-body"> <body class="landing-body">
{% include "components/header.twig" %} {% include "components/header.twig" %}
<div class="landing-loader" id="frame-loader"> <div class="landing-loader" id="frame-loader">

View file

@ -1,5 +1,4 @@
.writing-header { .writing-header {
display: flex;
position: sticky; position: sticky;
top: 0; top: 0;
padding: 15px 0; padding: 15px 0;
@ -15,18 +14,32 @@
margin: auto; margin: auto;
} }
&__left { &__inner-container {
margin: auto 0; margin: auto 0;
display: flex;
color: var(--color-text-second); color: var(--color-text-second);
gap: 10px;
& span { @media(--mobile) {
margin-right: 10px; flex-flow: column;
} }
}
select { & > * {
max-width: 100px; flex: 0 1 33.3%;
vertical-align: bottom; }
label {
display: block;
margin-bottom: 6px;
}
select {
@apply --select;
}
input {
@apply --input;
}
} }
} }
@ -38,16 +51,6 @@
} }
} }
.uri-input {
box-sizing: border-box;
width: 100%;
padding: 10px 12px;
border-radius: 3px;
border: 1px solid rgba(201, 201, 204, 0.48);
box-shadow: inset 0 1px 2px 0 rgba(35, 44, 72, 0.06);
outline: none;
}
.writing-editor { .writing-editor {
font-size: 15px; font-size: 15px;
line-height: 1.6; line-height: 1.6;

View file

@ -6,6 +6,8 @@
--color-link-hover: #F3F6F8; --color-link-hover: #F3F6F8;
--color-bg-light: #f8f7fa; --color-bg-light: #f8f7fa;
--color-page-active: #ff1767; --color-page-active: #ff1767;
--color-input-primary: #F3F6F8;
--color-input-border: #477CFF;
--color-button-primary: #3389FF; --color-button-primary: #3389FF;
--color-button-primary-hover: #2E7AE6; --color-button-primary-hover: #2E7AE6;
@ -77,6 +79,49 @@
} }
} }
--select {
position: relative;
padding: 10px 30px 10px 10px;
width: 100%;
border: solid 1px transparent;
border-radius: 8px;
background-color: var(--color-input-primary);
box-sizing: border-box;
appearance: none;
line-height: 18px;
background-image: url("../svg/arrow-down.svg");
background-repeat: no-repeat;
background-position: right 15px center;
&:focus {
border: solid 1px var(--color-input-border);
box-shadow: 0 0 0 3px rgba(18, 155, 255, 0.33);
}
&:focus-visible {
outline: none;
}
}
--input {
padding: 10px;
width: 100%;
line-height: 18px;
border: solid 1px transparent;
border-radius: 8px;
background-color: var(--color-input-primary);
box-sizing: border-box;
&:focus {
border: solid 1px var(--color-input-border);
box-shadow: 0 0 0 3px rgba(18, 155, 255, 0.33);
}
&:focus-visible {
outline: none;
}
}
--squircle { --squircle {
border-radius: 8px; border-radius: 8px;
@ -95,4 +140,4 @@
@custom-media --tablet all and (min-width: 980px) and (max-width: 1050px); @custom-media --tablet all and (min-width: 980px) and (max-width: 1050px);
@custom-media --mobile all and (max-width: 980px); @custom-media --mobile all and (max-width: 980px);
@custom-media --retina all and (-webkit-min-device-pixel-ratio: 1.5); @custom-media --retina all and (-webkit-min-device-pixel-ratio: 1.5);
@custom-media --can-hover all and (hover:hover) @custom-media --can-hover all and (hover:hover);

View file

@ -0,0 +1,3 @@
<svg width="8" height="5" viewBox="0 0 8 5" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.83053 0.193862C6.70562 0.0696944 6.53666 0 6.36053 0C6.18441 0 6.01544 0.0696944 5.89053 0.193862L3.4972 2.55386L1.1372 0.193862C1.01229 0.0696944 0.843323 0 0.667199 0C0.491075 0 0.322107 0.0696944 0.197199 0.193862C0.134713 0.255837 0.0851169 0.329571 0.0512711 0.410811C0.0174253 0.49205 0 0.579187 0 0.667195C0 0.755203 0.0174253 0.84234 0.0512711 0.92358C0.0851169 1.00482 0.134713 1.07855 0.197199 1.14053L3.02387 3.9672C3.08584 4.02968 3.15957 4.07928 3.24081 4.11312C3.32205 4.14697 3.40919 4.16439 3.4972 4.16439C3.58521 4.16439 3.67234 4.14697 3.75358 4.11312C3.83482 4.07928 3.90856 4.02968 3.97053 3.9672L6.83053 1.14053C6.89302 1.07855 6.94261 1.00482 6.97646 0.92358C7.0103 0.84234 7.02773 0.755203 7.02773 0.667195C7.02773 0.579187 7.0103 0.49205 6.97646 0.410811C6.94261 0.329571 6.89302 0.255837 6.83053 0.193862Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 958 B

View file

@ -40,7 +40,7 @@ module.exports = () => {
options: { options: {
// you can specify a publicPath here // you can specify a publicPath here
// by default it use publicPath in webpackOptions.output // by default it use publicPath in webpackOptions.output
publicPath: '../', // publicPath: '../',
}, },
}, },
{ {

View file

@ -1369,9 +1369,9 @@
form-data "^3.0.0" form-data "^3.0.0"
"@types/node@*": "@types/node@*":
version "17.0.23" version "18.0.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.0.tgz#67c7b724e1bcdd7a8821ce0d5ee184d3b4dd525a"
integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw== integrity sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==
"@types/node@^16.4.1": "@types/node@^16.4.1":
version "16.11.26" version "16.11.26"
@ -2212,7 +2212,7 @@ clone-deep@^4.0.1:
clone-response@^1.0.2: clone-response@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= integrity sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==
dependencies: dependencies:
mimic-response "^1.0.0" mimic-response "^1.0.0"
@ -2302,10 +2302,10 @@ component-emitter@^1.2.0:
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
compress-brotli@^1.3.6: compress-brotli@^1.3.8:
version "1.3.6" version "1.3.8"
resolved "https://registry.yarnpkg.com/compress-brotli/-/compress-brotli-1.3.6.tgz#64bd6f21f4f3e9841dbac392f4c29218caf5e9d9" resolved "https://registry.yarnpkg.com/compress-brotli/-/compress-brotli-1.3.8.tgz#0c0a60c97a989145314ec381e84e26682e7b38db"
integrity sha512-au99/GqZtUtiCBliqLFbWlhnCxn+XSYjwZ77q6mKN4La4qOXDoLVPZ50iXr0WmAyMxl8yqoq3Yq4OeQNPPkyeQ== integrity sha512-lVcQsjhxhIXsuupfy9fmZUFtAIdBmXA7EGY6GBdgZ++qkM9zG4YFT8iU7FoBxzryNDMOpD1HIFHUSX4D87oqhQ==
dependencies: dependencies:
"@types/json-buffer" "~3.0.0" "@types/json-buffer" "~3.0.0"
json-buffer "~3.0.1" json-buffer "~3.0.1"
@ -3586,9 +3586,9 @@ gonzales-pe@^4.0.3:
minimist "^1.2.5" minimist "^1.2.5"
got@^11.8.2: got@^11.8.2:
version "11.8.3" version "11.8.5"
resolved "https://registry.yarnpkg.com/got/-/got-11.8.3.tgz#f496c8fdda5d729a90b4905d2b07dbd148170770" resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046"
integrity sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg== integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==
dependencies: dependencies:
"@sindresorhus/is" "^4.0.0" "@sindresorhus/is" "^4.0.0"
"@szmarczak/http-timer" "^4.0.5" "@szmarczak/http-timer" "^4.0.5"
@ -4176,11 +4176,11 @@ jws@^3.2.2:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
keyv@^4.0.0: keyv@^4.0.0:
version "4.2.2" version "4.3.2"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.2.2.tgz#4b6f602c0228ef4d8214c03c520bef469ed6b768" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.3.2.tgz#e839df676a0c7ee594c8835e7c1c83742558e5c2"
integrity sha512-uYS0vKTlBIjNCAUqrjlxmruxOEiZxZIHXyp32sdcGmP+ukFrmWUnE//RcPXJH3Vxrni1H2gsQbjHE0bH7MtMQQ== integrity sha512-kn8WmodVBe12lmHpA6W8OY7SNh6wVR+Z+wZESF4iF5FCazaVXGWOtnbnvX0tMQ1bO+/TmOD9LziuYMvrIIs0xw==
dependencies: dependencies:
compress-brotli "^1.3.6" compress-brotli "^1.3.8"
json-buffer "3.0.1" json-buffer "3.0.1"
kind-of@^6.0.2: kind-of@^6.0.2:
@ -4847,7 +4847,7 @@ on-headers@~1.0.2:
once@^1.3.0, once@^1.3.1, once@^1.4.0: once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
dependencies: dependencies:
wrappy "1" wrappy "1"
@ -6877,7 +6877,7 @@ wrap-ansi@^7.0.0:
wrappy@1: wrappy@1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
write-file-atomic@^2.4.2: write-file-atomic@^2.4.2:
version "2.4.3" version "2.4.3"