├── .babelrc
├── .gitignore
├── README.md
├── index.ts
├── next-env.d.ts
├── next.config.js
├── nodemon.json
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── images
│ ├── blog-cover.jpeg
│ ├── icons
│ │ ├── avatar.svg
│ │ ├── facebook.svg
│ │ ├── rss.svg
│ │ └── twitter.svg
│ ├── logo.png
│ └── post_default_image.png
├── styles
│ ├── admin
│ │ ├── app.css
│ │ └── bootstrap.css
│ └── client
│ │ ├── app.css
│ │ └── reset.css
└── vercel.svg
├── server
├── config
│ └── index.ts
├── models
│ ├── definitions
│ │ ├── BaseModel.ts
│ │ ├── Category.ts
│ │ ├── Post.ts
│ │ ├── PostTagAssociation.ts
│ │ ├── Session.ts
│ │ ├── Tag.ts
│ │ └── User.ts
│ └── index.ts
└── repositories
│ ├── CategoryRepository.ts
│ ├── PostRepository.ts
│ ├── UserRepository.ts
│ └── index.ts
├── src
├── components
│ ├── admin
│ │ ├── Layout
│ │ │ ├── Header
│ │ │ │ ├── header.module.scss
│ │ │ │ ├── images
│ │ │ │ │ ├── PowerIcon.tsx
│ │ │ │ │ └── avatar.jpg
│ │ │ │ └── index.tsx
│ │ │ ├── Sidebar
│ │ │ │ ├── index.tsx
│ │ │ │ └── sidebar.module.scss
│ │ │ ├── index.tsx
│ │ │ └── layout.module.scss
│ │ └── styles
│ │ │ ├── theme.scss
│ │ │ └── variables.scss
│ └── client
│ │ ├── Layout.tsx
│ │ ├── components
│ │ ├── Blogs
│ │ │ ├── CategoryHeader.tsx
│ │ │ ├── PostCard.tsx
│ │ │ ├── PostContent.tsx
│ │ │ ├── Tag.tsx
│ │ │ └── index.tsx
│ │ └── Navigation
│ │ │ └── index.tsx
│ │ └── config
│ │ └── siteConfig.ts
├── d.ts
└── pages
│ ├── _app.tsx
│ ├── about.tsx
│ ├── admin
│ ├── category.tsx
│ ├── index.tsx
│ └── post
│ │ ├── create.tsx
│ │ └── index.tsx
│ ├── api
│ ├── admin
│ │ ├── category.ts
│ │ └── post.ts
│ └── hello.ts
│ ├── index.tsx
│ └── posts
│ └── [slug].tsx
├── tsconfig.json
└── tsconfig.server.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": [
4 | ["@babel/plugin-proposal-decorators", { "legacy": true }],
5 | ["@babel/plugin-proposal-class-properties", { "loose": true }]
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Dependency directories
9 | node_modules/
10 |
11 | # dotenv environment variables file
12 | .env
13 |
14 | # build directory
15 | .next/
16 |
17 | # temp folder
18 | tmp
19 |
20 | .DS_Store
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This example will show you how to create a Nextjs blog with Sequelize in typescript with some features as below:
2 |
3 | - Admin Dasboard to mange categoryies, blogs,…
4 | - Landing page to display your blogs,…
5 |
6 | Server entry point is `/index.ts` in development and `/index.js` in production.
7 | The second directory should be added to `.gitignore`.
8 |
9 | ## How to run
10 |
11 | ```bash
12 | npm i
13 |
14 | npm run dev
15 | ```
16 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | import { createServer } from "http";
2 | import { parse } from "url";
3 | import next from "next";
4 |
5 | import { initDB } from "./server/models";
6 |
7 | const port = parseInt(process.env.PORT || "3000", 10);
8 | const dev = process.env.NODE_ENV !== "production";
9 |
10 | async function bootstap() {
11 | try {
12 | await initDB();
13 |
14 | console.log("Connection has been established successfully.");
15 | } catch (err) {
16 | console.error("Unable to connect to the database:", err);
17 | }
18 |
19 | const app = next({ dev });
20 | const handle = app.getRequestHandler();
21 |
22 | app.prepare().then(() => {
23 | createServer((req, res) => {
24 | const parsedUrl = parse(req.url!, true);
25 | handle(req, res, parsedUrl);
26 | }).listen(port);
27 |
28 | // tslint:disable-next-line:no-console
29 | console.log(
30 | `> Server listening at http://localhost:${port} as ${
31 | dev ? "development" : process.env.NODE_ENV
32 | }`
33 | );
34 | });
35 | }
36 |
37 | bootstap();
38 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | i18n: {
3 | locales: ["vn", "en"],
4 | defaultLocale: "vn",
5 | localeDetection: false,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json index.ts",
4 | "ext": "js ts"
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "nodemon",
5 | "build": "next build && tsc --project tsconfig.server.json",
6 | "start": "cross-env NODE_ENV=production node dist/index.js"
7 | },
8 | "dependencies": {
9 | "@babel/core": "^7.15.0",
10 | "@babel/plugin-proposal-class-properties": "^7.14.5",
11 | "@babel/plugin-proposal-decorators": "^7.14.5",
12 | "bootstrap": "^4.5.0",
13 | "cross-env": "^7.0.2",
14 | "dotenv": "^10.0.0",
15 | "mysql2": "^2.3.0",
16 | "next": "latest",
17 | "react": "^17.0.2",
18 | "react-dom": "^17.0.2",
19 | "react-helmet": "^6.1.0",
20 | "reactstrap": "^8.9.0",
21 | "reflect-metadata": "^0.1.13",
22 | "sass": "^1.37.5",
23 | "sequelize": "^6.6.5",
24 | "sequelize-typescript": "^2.1.0"
25 | },
26 | "devDependencies": {
27 | "@types/node": "^12.20.19",
28 | "@types/react": "^16.9.44",
29 | "@types/react-dom": "^16.9.8",
30 | "@types/react-helmet": "^6.1.2",
31 | "@types/validator": "^13.6.3",
32 | "nodemon": "^2.0.4",
33 | "ts-node": "^8.10.2",
34 | "typescript": "4.0"
35 | },
36 | "license": "MIT"
37 | }
38 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ltienphat1307/nextjs-sequelize-typescript/a663040f7093791c64bb2033c2b87185eb5f2842/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/blog-cover.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ltienphat1307/nextjs-sequelize-typescript/a663040f7093791c64bb2033c2b87185eb5f2842/public/images/blog-cover.jpeg
--------------------------------------------------------------------------------
/public/images/icons/avatar.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/icons/facebook.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/images/icons/rss.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/images/icons/twitter.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ltienphat1307/nextjs-sequelize-typescript/a663040f7093791c64bb2033c2b87185eb5f2842/public/images/logo.png
--------------------------------------------------------------------------------
/public/images/post_default_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ltienphat1307/nextjs-sequelize-typescript/a663040f7093791c64bb2033c2b87185eb5f2842/public/images/post_default_image.png
--------------------------------------------------------------------------------
/public/styles/admin/app.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | height: 100%;
4 | }
5 |
6 | body {
7 | overflow-x: hidden;
8 | color: rgba(244, 244, 245, 0.9);
9 | background: radial-gradient(
10 | farthest-side ellipse at 10% 0,
11 | #333867 20%,
12 | #17193b
13 | );
14 | background-attachment: fixed;
15 | background-size: cover;
16 | background-repeat: no-repeat;
17 | }
18 |
19 | *,
20 | *::before,
21 | *::after {
22 | box-sizing: border-box;
23 | }
24 |
--------------------------------------------------------------------------------
/public/styles/client/app.css:
--------------------------------------------------------------------------------
1 | :root {
2 | /* Colours */
3 | --color-primary: #3eb0ef;
4 | --color-base: #15171a;
5 | --color-secondary: #5b7a81;
6 | --color-border: #c7d5d8;
7 | --color-bg: #f5f5f5;
8 |
9 | /* Fonts */
10 | --font-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
11 | "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
12 | sans-serif;
13 | --font-serif: Georgia, Times, serif;
14 | --font-mono: Menlo, Courier, monospace;
15 | --font-light: 100;
16 | --font-normal: 400;
17 | --font-bold: 700;
18 | --font-heavy: 800;
19 |
20 | /* Sizes */
21 | --height: 4rem;
22 | --margin: 2rem;
23 | --radius: 0.6rem;
24 | }
25 |
26 | /* Defaults
27 | /* ---------------------------------------------------------- */
28 |
29 | html {
30 | overflow-x: hidden;
31 | overflow-y: scroll;
32 | font-size: 62.5%;
33 | background: var(--color-base);
34 |
35 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
36 | }
37 |
38 | body {
39 | overflow-x: hidden;
40 | color: #3c484e;
41 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
42 | Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
43 | font-size: 1.5rem;
44 | line-height: 1.6em;
45 | font-weight: 400;
46 | font-style: normal;
47 | letter-spacing: 0;
48 | text-rendering: optimizeLegibility;
49 | background: #fff;
50 |
51 | -webkit-font-smoothing: antialiased;
52 | -moz-osx-font-smoothing: grayscale;
53 | -moz-font-feature-settings: "liga" on;
54 | }
55 |
56 | ::selection {
57 | text-shadow: none;
58 | background: #cbeafb;
59 | }
60 |
61 | hr {
62 | position: relative;
63 | display: block;
64 | width: 100%;
65 | margin: 1.8em 0 2.4em;
66 | padding: 0;
67 | height: 1px;
68 | border: 0;
69 | border-top: 1px solid #e3e9ed;
70 | }
71 |
72 | audio,
73 | canvas,
74 | iframe,
75 | img,
76 | svg,
77 | video {
78 | vertical-align: middle;
79 | }
80 |
81 | fieldset {
82 | margin: 0;
83 | padding: 0;
84 | border: 0;
85 | }
86 |
87 | textarea {
88 | resize: vertical;
89 | }
90 |
91 | p,
92 | ul,
93 | ol,
94 | dl,
95 | blockquote {
96 | margin: 0 0 1.5em 0;
97 | }
98 |
99 | ol,
100 | ul {
101 | padding-left: 1.3em;
102 | padding-right: 1.5em;
103 | }
104 |
105 | ol ol,
106 | ul ul,
107 | ul ol,
108 | ol ul {
109 | margin: 0.5em 0 1em;
110 | }
111 |
112 | ul {
113 | list-style: disc;
114 | }
115 |
116 | ol {
117 | list-style: decimal;
118 | }
119 |
120 | ul,
121 | ol {
122 | max-width: 100%;
123 | }
124 |
125 | li {
126 | margin: 0.5em 0;
127 | padding-left: 0.3em;
128 | line-height: 1.6em;
129 | }
130 |
131 | dt {
132 | float: left;
133 | margin: 0 20px 0 0;
134 | width: 120px;
135 | font-weight: 500;
136 | text-align: right;
137 | }
138 |
139 | dd {
140 | margin: 0 0 5px 0;
141 | text-align: left;
142 | }
143 |
144 | blockquote {
145 | margin: 0.3em 0 1.8em;
146 | padding: 0 1.6em 0 1.6em;
147 | border-left: #cbeafb 0.5em solid;
148 | }
149 |
150 | blockquote p {
151 | margin: 0.8em 0;
152 | font-size: 1.2em;
153 | font-weight: 300;
154 | }
155 |
156 | blockquote small {
157 | display: inline-block;
158 | margin: 0.8em 0 0.8em 1.5em;
159 | font-size: 0.9em;
160 | opacity: 0.8;
161 | }
162 | /* Quotation marks */
163 | blockquote small:before {
164 | content: "\2014 \00A0";
165 | }
166 |
167 | blockquote cite {
168 | font-weight: bold;
169 | }
170 | blockquote cite a {
171 | font-weight: normal;
172 | }
173 |
174 | a {
175 | color: #26a8ed;
176 | text-decoration: none;
177 | }
178 |
179 | a:hover {
180 | text-decoration: underline;
181 | }
182 |
183 | h1,
184 | h2,
185 | h3,
186 | h4,
187 | h5,
188 | h6 {
189 | margin-top: 0;
190 | color: var(--color-base);
191 | line-height: 1.15;
192 | font-weight: 700;
193 | text-rendering: optimizeLegibility;
194 | }
195 |
196 | h1 {
197 | margin: 0 0 0.5em 0;
198 | font-size: 4rem;
199 | font-weight: 700;
200 | }
201 | @media (max-width: 500px) {
202 | h1 {
203 | font-size: 2rem;
204 | }
205 | }
206 |
207 | h2 {
208 | margin: 1.5em 0 0.5em 0;
209 | font-size: 2rem;
210 | }
211 | @media (max-width: 500px) {
212 | h2 {
213 | font-size: 1.8rem;
214 | }
215 | }
216 |
217 | h3 {
218 | margin: 1.5em 0 0.5em 0;
219 | font-size: 1.8rem;
220 | font-weight: 500;
221 | }
222 | @media (max-width: 500px) {
223 | h3 {
224 | font-size: 1.7rem;
225 | }
226 | }
227 |
228 | h4 {
229 | margin: 1.5em 0 0.5em 0;
230 | font-size: 1.6rem;
231 | font-weight: 500;
232 | }
233 |
234 | h5 {
235 | margin: 1.5em 0 0.5em 0;
236 | font-size: 1.4rem;
237 | font-weight: 500;
238 | }
239 |
240 | h6 {
241 | margin: 1.5em 0 0.5em 0;
242 | font-size: 1.4rem;
243 | font-weight: 500;
244 | }
245 |
246 | /* Layout
247 | /* ---------------------------------------------------------- */
248 |
249 | .viewport {
250 | display: flex;
251 | flex-direction: column;
252 | justify-content: space-between;
253 | min-height: 100vh;
254 | }
255 |
256 | .container {
257 | max-width: 1120px;
258 | margin: 0 auto;
259 | padding: 0 4vw;
260 | }
261 |
262 | .content {
263 | margin: 0 auto;
264 | font-size: 2rem;
265 | line-height: 1.7em;
266 | }
267 |
268 | .content-body {
269 | display: flex;
270 | flex-direction: column;
271 | font-family: var(--font-serif);
272 | }
273 |
274 | .post-full-content {
275 | max-width: 720px;
276 | margin: 0 auto;
277 | background: #fff;
278 | }
279 |
280 | .post-feature-image img {
281 | margin: 0 0 3vw;
282 | width: 100%;
283 | height: 500px;
284 | object-fit: cover;
285 | }
286 |
287 | .content-body h1,
288 | .content-body h2,
289 | .content-body h3,
290 | .content-body h4,
291 | .content-body h5,
292 | .content-body h6 {
293 | font-family: var(--font-sans-serif);
294 | }
295 |
296 | .content-body h1 {
297 | margin: 1em 0 0.5em 0;
298 | font-size: 3.4rem;
299 | font-weight: 700;
300 | }
301 | @media (max-width: 500px) {
302 | .content-body h1 {
303 | font-size: 2.8rem;
304 | }
305 | }
306 |
307 | .content-title {
308 | margin: 0 0 0.8em;
309 | font-size: 5rem;
310 | }
311 | @media (max-width: 500px) {
312 | .content-title {
313 | margin: 0.8em 0;
314 | font-size: 3.4rem;
315 | }
316 | .content {
317 | font-size: 1.8rem;
318 | }
319 | }
320 |
321 | .content-body h2 {
322 | margin: 0.8em 0 0.4em 0;
323 | font-size: 3.2rem;
324 | font-weight: 700;
325 | }
326 | @media (max-width: 500px) {
327 | .content-body h2 {
328 | font-size: 2.6rem;
329 | }
330 | }
331 |
332 | .content-body h3 {
333 | margin: 0.5em 0 0.2em 0;
334 | font-size: 2.8rem;
335 | font-weight: 700;
336 | }
337 | @media (max-width: 500px) {
338 | .content-body h3 {
339 | font-size: 2.2rem;
340 | }
341 | }
342 |
343 | .content-body h4 {
344 | margin: 0.5em 0 0.2em 0;
345 | font-size: 2.4rem;
346 | font-weight: 700;
347 | }
348 | @media (max-width: 500px) {
349 | .content-body h4 {
350 | font-size: 2.2rem;
351 | }
352 | }
353 |
354 | .content-body h5 {
355 | display: block;
356 | margin: 0.5em 0;
357 | padding: 1em 0 1.5em;
358 | border: 0;
359 | font-family: Georgia, serif;
360 | color: var(--color-primary);
361 | font-style: italic;
362 | font-size: 3.2rem;
363 | line-height: 1.35em;
364 | text-align: center;
365 | }
366 |
367 | .content-body h6 {
368 | margin: 0.5em 0 0.2em 0;
369 | font-size: 2rem;
370 | font-weight: 700;
371 | }
372 |
373 | .content-body figure {
374 | margin: 0.4em 0 1.6em;
375 | font-size: 2.8rem;
376 | font-weight: 700;
377 | }
378 |
379 | .content-body pre {
380 | margin: 0.4em 0 1.8em;
381 | font-size: 1.6rem;
382 | line-height: 1.4em;
383 | white-space: pre-wrap;
384 | padding: 20px;
385 | background: var(--color-base);
386 | color: #fff;
387 | border-radius: 12px;
388 | }
389 |
390 | /* Header
391 | /* ---------------------------------------------------------- */
392 |
393 | .site-head {
394 | padding-top: 20px;
395 | padding-bottom: 20px;
396 | color: #fff;
397 | background: var(--color-base);
398 | background-position: center;
399 | background-size: cover;
400 | }
401 |
402 | .site-nav-item {
403 | display: inline-block;
404 | padding: 5px 10px;
405 | color: #fff;
406 | opacity: 0.7;
407 | }
408 |
409 | .site-nav-item:hover {
410 | text-decoration: none;
411 | opacity: 1;
412 | }
413 |
414 | .site-nav-icon {
415 | height: 15px;
416 | margin: -5px 0 0;
417 | }
418 |
419 | .site-logo {
420 | height: 25px;
421 | }
422 |
423 | .site-mast {
424 | display: flex;
425 | align-items: center;
426 | justify-content: space-between;
427 | }
428 |
429 | .site-mast-right {
430 | display: flex;
431 | align-items: center;
432 | }
433 |
434 | .site-mast-right .site-nav-item:last-child {
435 | padding-right: 0;
436 | }
437 |
438 | .site-banner {
439 | max-width: 80%;
440 | margin: 0 auto;
441 | padding: 10vw 0;
442 | text-align: center;
443 | }
444 |
445 | .site-banner-title {
446 | margin: 0;
447 | padding: 0;
448 | color: #fff;
449 | font-size: 4rem;
450 | line-height: 1.3em;
451 | }
452 |
453 | .site-banner-desc {
454 | margin: 5px 0 0 0;
455 | padding: 0;
456 | font-size: 2.4rem;
457 | line-height: 1.3em;
458 | opacity: 0.7;
459 | }
460 |
461 | .site-nav {
462 | display: flex;
463 | align-items: center;
464 | justify-content: space-between;
465 | margin: 15px 0 0 0;
466 | }
467 |
468 | .site-nav-left {
469 | margin: 0 20px 0 -10px;
470 | }
471 |
472 | .site-nav-button {
473 | display: inline-block;
474 | padding: 5px 10px;
475 | border: #fff 1px solid;
476 | color: #fff;
477 | font-size: 1.3rem;
478 | line-height: 1em;
479 | border-radius: var(--radius);
480 | opacity: 0.7;
481 | }
482 |
483 | .site-nav-button:hover {
484 | text-decoration: none;
485 | }
486 |
487 | /* Main
488 | /* ---------------------------------------------------------- */
489 |
490 | .site-main {
491 | padding: 4vw 0;
492 | }
493 |
494 | /* Index
495 | /* ---------------------------------------------------------- */
496 |
497 | .post-feed {
498 | display: grid;
499 | justify-content: space-between;
500 | grid-gap: 30px;
501 | grid-template-columns: 1fr 1fr 1fr;
502 | }
503 |
504 | @media (max-width: 980px) {
505 | .post-feed {
506 | grid-template-columns: 1fr 1fr;
507 | }
508 | }
509 | @media (max-width: 680px) {
510 | .post-feed {
511 | grid-template-columns: 1fr;
512 | }
513 | }
514 |
515 | .post-card {
516 | color: inherit;
517 | text-decoration: none;
518 | }
519 |
520 | .post-card:hover {
521 | text-decoration: none;
522 | }
523 |
524 | .post-card-tags {
525 | margin: 0 0 5px 0;
526 | font-size: 1.4rem;
527 | line-height: 1.15em;
528 | color: var(--color-secondary);
529 | }
530 |
531 | .post-card-title {
532 | margin: 0 0 10px 0;
533 | padding: 0;
534 | }
535 |
536 | .post-card-excerpt {
537 | font-size: 1.6rem;
538 | line-height: 1.55em;
539 | }
540 |
541 | .post-card-image {
542 | margin: 0 0 10px 0;
543 | width: auto;
544 | height: 200px;
545 | background: var(--color-secondary) no-repeat center center;
546 | background-size: cover;
547 | }
548 |
549 | .post-card-footer {
550 | display: flex;
551 | align-items: center;
552 | justify-content: space-between;
553 | margin: 10px 0 0 0;
554 | color: var(--color-secondary);
555 | }
556 |
557 | .post-card-footer-left {
558 | display: flex;
559 | align-items: center;
560 | }
561 |
562 | .post-card-footer-right {
563 | display: flex;
564 | flex-direction: column;
565 | }
566 |
567 | .post-card-avatar {
568 | width: 30px;
569 | height: 30px;
570 | margin: 0 7px 0 0;
571 | border-radius: 100%;
572 | display: flex;
573 | align-items: center;
574 | justify-content: center;
575 | }
576 |
577 | .post-card-avatar .author-profile-image {
578 | display: block;
579 | width: 100%;
580 | background: var(--color-secondary);
581 | border-radius: 100%;
582 | object-fit: cover;
583 | }
584 |
585 | .post-card-avatar .default-avatar {
586 | width: 26px;
587 | }
588 |
589 | /* Tag Archives
590 | /* ---------------------------------------------------------- */
591 |
592 | .tag-header {
593 | max-width: 690px;
594 | margin: 0 0 4vw;
595 | }
596 |
597 | .tag-header h1 {
598 | margin: 0 0 1rem 0;
599 | }
600 |
601 | .tag-header p {
602 | margin: 0;
603 | color: var(--color-secondary);
604 | font-size: 2.2rem;
605 | line-height: 1.3em;
606 | }
607 |
608 | @media (max-width: 500px) {
609 | .tag-header {
610 | border-bottom: var(--color-bg) 1px solid;
611 | padding-bottom: 4vw;
612 | }
613 | .tag-header p {
614 | font-size: 1.7rem;
615 | }
616 | }
617 |
618 | /* Author Archives
619 | /* ---------------------------------------------------------- */
620 |
621 | .author-header {
622 | display: flex;
623 | justify-content: space-between;
624 | margin: 0 0 4vw;
625 | }
626 |
627 | .author-header h1 {
628 | margin: 0 0 1rem 0;
629 | }
630 |
631 | .author-header p {
632 | margin: 0;
633 | color: var(--color-secondary);
634 | font-size: 2.2rem;
635 | line-height: 1.3em;
636 | }
637 |
638 | .author-header-image {
639 | flex: 0 0 auto;
640 | margin: 0 0 0 4vw;
641 | height: 120px;
642 | width: 120px;
643 | border-radius: 100%;
644 | overflow: hidden;
645 | }
646 |
647 | .author-header-meta {
648 | display: flex;
649 | margin: 1rem 0 0 0;
650 | }
651 |
652 | .author-header-item {
653 | display: block;
654 | padding: 2px 10px;
655 | }
656 |
657 | .author-header-item:first-child {
658 | padding-left: 0;
659 | }
660 |
661 | @media (max-width: 500px) {
662 | .author-header {
663 | border-bottom: var(--color-bg) 1px solid;
664 | padding-bottom: 4vw;
665 | }
666 | .author-header p {
667 | font-size: 1.7rem;
668 | }
669 | .author-header-image {
670 | height: 80px;
671 | width: 80px;
672 | }
673 | }
674 |
675 | /* Pagination
676 | /* ---------------------------------------------------------- */
677 |
678 | .pagination {
679 | position: relative;
680 | display: flex;
681 | justify-content: space-between;
682 | align-items: center;
683 | margin: 4vw 0 0;
684 | }
685 |
686 | .pagination a {
687 | display: inline-block;
688 | padding: 10px 15px;
689 | border: var(--color-border) 1px solid;
690 | color: var(--color-secondary);
691 | text-decoration: none;
692 | font-size: 1.4rem;
693 | line-height: 1em;
694 | border-radius: var(--radius);
695 | }
696 |
697 | .pagination-location {
698 | position: absolute;
699 | left: 50%;
700 | width: 100px;
701 | margin-left: -50px;
702 | text-align: center;
703 | color: var(--color-secondary);
704 | font-size: 1.3rem;
705 | }
706 |
707 | /* Footer
708 | /* ---------------------------------------------------------- */
709 |
710 | .site-foot {
711 | padding: 20px 0 40px 0;
712 | color: rgba(255, 255, 255, 0.7);
713 | font-size: 1.3rem;
714 | background: var(--color-base);
715 | }
716 |
717 | .site-foot-nav {
718 | display: flex;
719 | align-items: center;
720 | justify-content: space-between;
721 | }
722 |
723 | .site-foot-nav a {
724 | color: rgba(255, 255, 255, 0.7);
725 | }
726 |
727 | .site-foot-nav a:hover {
728 | text-decoration: none;
729 | color: rgba(255, 255, 255, 1);
730 | }
731 |
732 | .site-foot-nav-right a {
733 | display: inline-block;
734 | padding: 2px 5px;
735 | }
736 |
737 | .site-foot-nav-right a:last-child {
738 | padding-right: 0;
739 | }
740 |
741 | /* Koenig Styles
742 | /* ---------------------------------------------------------- */
743 |
744 | .kg-bookmark-card {
745 | width: 100%;
746 | margin-top: 0;
747 | }
748 |
749 | .kg-bookmark-container {
750 | display: flex;
751 | min-height: 148px;
752 | color: var(--color-base);
753 | font-family: var(--font-sans-serif);
754 | text-decoration: none;
755 | border-radius: 3px;
756 | box-shadow: 0 2px 5px -1px rgba(0, 0, 0, 0.15), 0 0 1px rgba(0, 0, 0, 0.09);
757 | }
758 |
759 | .kg-bookmark-container:hover {
760 | color: var(--color-base);
761 | text-decoration: none;
762 | box-shadow: 0 2px 5px -1px rgba(0, 0, 0, 0.15), 0 0 1px rgba(0, 0, 0, 0.09);
763 | }
764 |
765 | .kg-bookmark-content {
766 | flex-grow: 1;
767 | display: flex;
768 | flex-direction: column;
769 | justify-content: flex-start;
770 | align-items: flex-start;
771 | padding: 20px;
772 | }
773 |
774 | .kg-bookmark-title {
775 | color: color(var(--color-secondary) l(-30%));
776 | font-size: 1.6rem;
777 | line-height: 1.5em;
778 | font-weight: 600;
779 | transition: color 0.2s ease-in-out;
780 | }
781 |
782 | .kg-bookmark-container:hover .kg-bookmark-title {
783 | color: var(--color-primary);
784 | }
785 |
786 | .kg-bookmark-description {
787 | display: -webkit-box;
788 | overflow-y: hidden;
789 | margin-top: 12px;
790 | max-height: 48px;
791 | color: color(var(--color-secondary) l(-10%));
792 | font-size: 1.5rem;
793 | line-height: 1.5em;
794 | font-weight: 400;
795 |
796 | -webkit-line-clamp: 2;
797 | -webkit-box-orient: vertical;
798 | }
799 |
800 | .kg-bookmark-thumbnail {
801 | position: relative;
802 | min-width: 33%;
803 | max-height: 100%;
804 | }
805 |
806 | .kg-bookmark-thumbnail img {
807 | position: absolute;
808 | top: 0;
809 | left: 0;
810 | width: 100%;
811 | height: 100%;
812 | border-radius: 0 3px 3px 0;
813 |
814 | object-fit: cover;
815 | }
816 |
817 | .kg-bookmark-metadata {
818 | display: flex;
819 | flex-wrap: wrap;
820 | align-items: center;
821 | margin-top: 14px;
822 | color: color(var(--color-secondary) l(-10%));
823 | font-size: 1.5rem;
824 | font-weight: 400;
825 | }
826 |
827 | .kg-bookmark-icon {
828 | margin-right: 8px;
829 | width: 22px;
830 | height: 22px;
831 | }
832 |
833 | .kg-bookmark-author {
834 | line-height: 1.5em;
835 | }
836 |
837 | .kg-bookmark-author:after {
838 | content: "•";
839 | margin: 0 6px;
840 | }
841 |
842 | .kg-bookmark-publisher {
843 | overflow: hidden;
844 | max-width: 240px;
845 | line-height: 1.5em;
846 | text-overflow: ellipsis;
847 | white-space: nowrap;
848 | }
849 |
850 | /* Gallery Styles
851 | /* ---------------------------------------------------------- */
852 | .kg-gallery-container {
853 | display: flex;
854 | flex-direction: column;
855 | max-width: 1040px;
856 | width: 100%;
857 | }
858 |
859 | .kg-gallery-row {
860 | display: flex;
861 | flex-direction: row;
862 | justify-content: center;
863 | }
864 |
865 | .kg-gallery-image img {
866 | display: block;
867 | margin: 0;
868 | width: 100%;
869 | height: 100%;
870 | }
871 |
872 | .kg-gallery-row:not(:first-of-type) {
873 | margin: 0.75em 0 0 0;
874 | }
875 |
876 | .kg-gallery-image:not(:first-of-type) {
877 | margin: 0 0 0 0.75em;
878 | }
879 |
880 | .kg-gallery-card + .kg-image-card.kg-width-wide,
881 | .kg-gallery-card + .kg-gallery-card,
882 | .kg-image-card.kg-width-wide + .kg-gallery-card,
883 | .kg-image-card.kg-width-wide + .kg-image-card.kg-width-wide {
884 | margin: -2.25em 0 3em;
885 | }
886 |
887 | .container {
888 | padding: 0 0.5rem;
889 | display: flex;
890 | flex-direction: column;
891 | justify-content: center;
892 | align-items: center;
893 | }
894 |
895 | .main {
896 | padding: 5rem 0;
897 | flex: 1;
898 | display: flex;
899 | flex-direction: column;
900 | justify-content: center;
901 | align-items: center;
902 | }
903 |
904 | .footer {
905 | width: 100%;
906 | height: 100px;
907 | border-top: 1px solid #eaeaea;
908 | display: flex;
909 | justify-content: center;
910 | align-items: center;
911 | }
912 |
913 | .footer a {
914 | display: flex;
915 | justify-content: center;
916 | align-items: center;
917 | flex-grow: 1;
918 | }
919 |
920 | .title a {
921 | color: #0070f3;
922 | text-decoration: none;
923 | }
924 |
925 | .title a:hover,
926 | .title a:focus,
927 | .title a:active {
928 | text-decoration: underline;
929 | }
930 |
931 | .title {
932 | margin: 0;
933 | line-height: 1.15;
934 | font-size: 4rem;
935 | }
936 |
937 | .title,
938 | .description {
939 | text-align: center;
940 | }
941 |
942 | .description {
943 | line-height: 1.5;
944 | font-size: 1.5rem;
945 | }
946 |
947 | .code {
948 | background: #fafafa;
949 | border-radius: 5px;
950 | padding: 0.75rem;
951 | font-size: 1.1rem;
952 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
953 | Bitstream Vera Sans Mono, Courier New, monospace;
954 | }
955 |
956 | .grid {
957 | display: flex;
958 | align-items: center;
959 | justify-content: center;
960 | flex-wrap: wrap;
961 | max-width: 800px;
962 | margin-top: 3rem;
963 | }
964 |
965 | .card {
966 | margin: 1rem;
967 | padding: 1.5rem;
968 | text-align: left;
969 | color: inherit;
970 | text-decoration: none;
971 | border: 1px solid #eaeaea;
972 | border-radius: 10px;
973 | transition: color 0.15s ease, border-color 0.15s ease;
974 | width: 45%;
975 | }
976 |
977 | .card:hover,
978 | .card:focus,
979 | .card:active {
980 | color: #0070f3;
981 | border-color: #0070f3;
982 | }
983 |
984 | .card h2 {
985 | margin: 0 0 1rem 0;
986 | font-size: 1.5rem;
987 | }
988 |
989 | .card p {
990 | margin: 0;
991 | font-size: 1.25rem;
992 | line-height: 1.5;
993 | }
994 |
995 | .logo {
996 | height: 1em;
997 | margin-left: 0.5rem;
998 | }
999 |
1000 | @media (max-width: 600px) {
1001 | .grid {
1002 | width: 100%;
1003 | flex-direction: column;
1004 | }
1005 | }
1006 |
--------------------------------------------------------------------------------
/public/styles/client/reset.css:
--------------------------------------------------------------------------------
1 | /* Reset
2 | /* ---------------------------------------------------------- */
3 |
4 | html,
5 | body,
6 | div,
7 | span,
8 | applet,
9 | object,
10 | iframe,
11 | h1,
12 | h2,
13 | h3,
14 | h4,
15 | h5,
16 | h6,
17 | p,
18 | blockquote,
19 | pre,
20 | a,
21 | abbr,
22 | acronym,
23 | address,
24 | big,
25 | cite,
26 | code,
27 | del,
28 | dfn,
29 | em,
30 | img,
31 | ins,
32 | kbd,
33 | q,
34 | s,
35 | samp,
36 | small,
37 | strike,
38 | strong,
39 | sub,
40 | sup,
41 | tt,
42 | var,
43 | dl,
44 | dt,
45 | dd,
46 | ol,
47 | ul,
48 | li,
49 | fieldset,
50 | form,
51 | label,
52 | legend,
53 | table,
54 | caption,
55 | tbody,
56 | tfoot,
57 | thead,
58 | tr,
59 | th,
60 | td,
61 | article,
62 | aside,
63 | canvas,
64 | details,
65 | embed,
66 | figure,
67 | figcaption,
68 | footer,
69 | header,
70 | hgroup,
71 | menu,
72 | nav,
73 | output,
74 | ruby,
75 | section,
76 | summary,
77 | time,
78 | mark,
79 | audio,
80 | video {
81 | margin: 0;
82 | padding: 0;
83 | border: 0;
84 | font: inherit;
85 | font-size: 100%;
86 | vertical-align: baseline;
87 | }
88 | body {
89 | line-height: 1;
90 | }
91 | ol,
92 | ul {
93 | list-style: none;
94 | padding-left: 0;
95 | margin-bottom: 0;
96 | }
97 | blockquote,
98 | q {
99 | quotes: none;
100 | }
101 | blockquote:before,
102 | blockquote:after,
103 | q:before,
104 | q:after {
105 | content: "";
106 | content: none;
107 | }
108 | table {
109 | border-spacing: 0;
110 | border-collapse: collapse;
111 | }
112 | img {
113 | max-width: 100%;
114 | }
115 | html {
116 | box-sizing: border-box;
117 | font-family: sans-serif;
118 |
119 | -ms-text-size-adjust: 100%;
120 | -webkit-text-size-adjust: 100%;
121 | }
122 | *,
123 | *:before,
124 | *:after {
125 | box-sizing: inherit;
126 | }
127 | a {
128 | background-color: transparent;
129 | }
130 | a:active,
131 | a:hover {
132 | outline: 0;
133 | }
134 | b,
135 | strong {
136 | font-weight: bold;
137 | }
138 | i,
139 | em,
140 | dfn {
141 | font-style: italic;
142 | }
143 | h1 {
144 | margin: 0.67em 0;
145 | font-size: 2em;
146 | }
147 | small {
148 | font-size: 80%;
149 | }
150 | sub,
151 | sup {
152 | position: relative;
153 | font-size: 75%;
154 | line-height: 0;
155 | vertical-align: baseline;
156 | }
157 | sup {
158 | top: -0.5em;
159 | }
160 | sub {
161 | bottom: -0.25em;
162 | }
163 | img {
164 | border: 0;
165 | }
166 | svg:not(:root) {
167 | overflow: hidden;
168 | }
169 | mark {
170 | background-color: #fdffb6;
171 | }
172 | code,
173 | kbd,
174 | pre,
175 | samp {
176 | font-family: monospace, monospace;
177 | font-size: 1em;
178 | }
179 | button,
180 | input,
181 | optgroup,
182 | select,
183 | textarea {
184 | margin: 0;
185 | color: inherit;
186 | font: inherit;
187 | }
188 | button {
189 | overflow: visible;
190 | border: none;
191 | }
192 | button,
193 | select {
194 | text-transform: none;
195 | }
196 | button,
197 | html input[type="button"],
198 | input[type="reset"],
199 | input[type="submit"] {
200 | cursor: pointer;
201 |
202 | -webkit-appearance: button;
203 | }
204 | button[disabled],
205 | html input[disabled] {
206 | cursor: default;
207 | }
208 | button::-moz-focus-inner,
209 | input::-moz-focus-inner {
210 | padding: 0;
211 | border: 0;
212 | }
213 | input {
214 | line-height: normal;
215 | }
216 | input:focus {
217 | outline: none;
218 | }
219 | input[type="checkbox"],
220 | input[type="radio"] {
221 | box-sizing: border-box;
222 | padding: 0;
223 | }
224 | input[type="number"]::-webkit-inner-spin-button,
225 | input[type="number"]::-webkit-outer-spin-button {
226 | height: auto;
227 | }
228 | input[type="search"] {
229 | box-sizing: content-box;
230 |
231 | -webkit-appearance: textfield;
232 | }
233 | input[type="search"]::-webkit-search-cancel-button,
234 | input[type="search"]::-webkit-search-decoration {
235 | -webkit-appearance: none;
236 | }
237 | legend {
238 | padding: 0;
239 | border: 0;
240 | }
241 | textarea {
242 | overflow: auto;
243 | }
244 | table {
245 | border-spacing: 0;
246 | border-collapse: collapse;
247 | }
248 | td,
249 | th {
250 | padding: 0;
251 | }
252 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/config/index.ts:
--------------------------------------------------------------------------------
1 | import dotenv from "dotenv";
2 | dotenv.config();
3 |
4 | export default {
5 | DATABASE_HOST: process.env.DATABASE_HOST,
6 | DATABASE_PORT: process.env.DATABASE_PORT,
7 | DATABASE_USER: process.env.DATABASE_USER,
8 | DATABASE_PASSWORD: process.env.DATABASE_PASSWORD,
9 | DATABASE_NAME: process.env.DATABASE_NAME,
10 | };
11 |
--------------------------------------------------------------------------------
/server/models/definitions/BaseModel.ts:
--------------------------------------------------------------------------------
1 | import { Model } from "sequelize-typescript";
2 |
3 | export class BaseModel extends Model {}
4 |
--------------------------------------------------------------------------------
/server/models/definitions/Category.ts:
--------------------------------------------------------------------------------
1 | import { Table, Column, DataType, HasMany } from "sequelize-typescript";
2 |
3 | import { BaseModel } from "./BaseModel";
4 | import { Post } from "./Post";
5 |
6 | @Table({
7 | timestamps: true,
8 | tableName: "category",
9 | })
10 | export class Category extends BaseModel {
11 | @Column({ type: DataType.STRING, allowNull: false })
12 | public name!: string;
13 |
14 | @Column({ type: DataType.STRING, allowNull: false })
15 | public slug!: string;
16 |
17 | /* Associantions */
18 | @HasMany(() => Post)
19 | public posts!: Post[];
20 | /* End Associantions */
21 | }
22 |
--------------------------------------------------------------------------------
/server/models/definitions/Post.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Table,
3 | Column,
4 | DataType,
5 | AllowNull,
6 | BelongsTo,
7 | ForeignKey,
8 | HasMany,
9 | } from "sequelize-typescript";
10 |
11 | import { BaseModel } from "./BaseModel";
12 | import { User } from "./User";
13 | import { PostTagAssociation } from "./PostTagAssociation";
14 | import { Category } from "./Category";
15 |
16 | @Table({
17 | timestamps: true,
18 | tableName: "post",
19 | })
20 | export class Post extends BaseModel {
21 | @Column({ type: DataType.STRING, allowNull: false })
22 | public title!: string;
23 |
24 | @Column({ type: DataType.STRING, allowNull: false })
25 | public slug!: string;
26 |
27 | @AllowNull
28 | @Column({ type: DataType.STRING })
29 | public featureImage?: string;
30 |
31 | @Column({ type: DataType.STRING, allowNull: false })
32 | public excerpt!: string;
33 |
34 | @Column({ type: DataType.TINYINT, defaultValue: false, allowNull: false })
35 | public featured!: boolean;
36 |
37 | @AllowNull
38 | @Column({ type: DataType.DATE })
39 | public publishedDate?: Date;
40 |
41 | @Column({ type: DataType.TEXT, allowNull: false })
42 | public content!: string;
43 |
44 | /* Associantions */
45 | // Author
46 | @BelongsTo(() => User)
47 | public author!: User;
48 |
49 | @ForeignKey(() => User)
50 | @Column({ type: DataType.INTEGER, allowNull: false })
51 | public authorId!: number;
52 | // End Author
53 |
54 | // Category
55 | @BelongsTo(() => Category)
56 | public category!: Category;
57 |
58 | @ForeignKey(() => Category)
59 | @Column({ type: DataType.INTEGER, allowNull: false })
60 | public categoryId!: number;
61 | // End Category
62 |
63 | @HasMany(() => PostTagAssociation)
64 | public postTagAssociations?: PostTagAssociation[];
65 | /* End Associantions */
66 | }
67 |
--------------------------------------------------------------------------------
/server/models/definitions/PostTagAssociation.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Table,
3 | Column,
4 | BelongsTo,
5 | ForeignKey,
6 | DataType,
7 | } from "sequelize-typescript";
8 |
9 | import { BaseModel } from "./BaseModel";
10 | import { Post } from "./Post";
11 | import { Tag } from "./Tag";
12 |
13 | @Table({
14 | timestamps: false,
15 | tableName: "post_tag_association",
16 | })
17 | export class PostTagAssociation extends BaseModel {
18 | @BelongsTo(() => Post)
19 | public post!: Post;
20 |
21 | @ForeignKey(() => Post)
22 | @Column(DataType.INTEGER)
23 | public postId!: number;
24 |
25 | @BelongsTo(() => Tag)
26 | public tag!: Tag;
27 |
28 | @ForeignKey(() => Tag)
29 | @Column(DataType.INTEGER)
30 | public tagId!: number;
31 | }
32 |
--------------------------------------------------------------------------------
/server/models/definitions/Session.ts:
--------------------------------------------------------------------------------
1 | // import { BaseEntity, Column, Entity, Index, PrimaryColumn } from 'typeorm';
2 | // import { SessionEntity } from './TypeormStore';
3 |
4 | // @Entity({ name: 'Sessions' })
5 | // export class Session extends BaseEntity implements SessionEntity {
6 | // @PrimaryColumn()
7 | // public sid!: string;
8 |
9 | // @Column()
10 | // @Index('IDX_expires_at')
11 | // public expiresAt!: number;
12 |
13 | // @Column({ type: 'longtext' })
14 | // public data!: string;
15 |
16 | // @Column()
17 | // public ttl!: string;
18 |
19 | // @Column()
20 | // public userId!: number;
21 |
22 | // @Column()
23 | // public email!: string;
24 |
25 | // @Column()
26 | // @Index('IDX_expires')
27 | // public expires!: Date;
28 | // }
29 |
--------------------------------------------------------------------------------
/server/models/definitions/Tag.ts:
--------------------------------------------------------------------------------
1 | import { Table, Column, DataType } from "sequelize-typescript";
2 | import { BaseModel } from "./BaseModel";
3 |
4 | @Table({
5 | timestamps: false,
6 | tableName: "tag",
7 | })
8 | export class Tag extends BaseModel {
9 | @Column({ type: DataType.STRING, allowNull: false })
10 | public name!: string;
11 | }
12 |
--------------------------------------------------------------------------------
/server/models/definitions/User.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Table,
3 | Column,
4 | DataType,
5 | AllowNull,
6 | IsEmail,
7 | } from "sequelize-typescript";
8 |
9 | import { BaseModel } from "./BaseModel";
10 |
11 | @Table({
12 | timestamps: false,
13 | tableName: "user",
14 | })
15 | export class User extends BaseModel {
16 | @Column({ type: DataType.STRING, allowNull: false })
17 | public name!: string;
18 |
19 | @IsEmail
20 | @Column({ type: DataType.STRING, allowNull: false })
21 | public email!: string;
22 |
23 | @AllowNull
24 | @Column({ type: DataType.STRING })
25 | public password?: string;
26 | }
27 |
--------------------------------------------------------------------------------
/server/models/index.ts:
--------------------------------------------------------------------------------
1 | import { Sequelize } from "sequelize-typescript";
2 |
3 | import { User } from "./definitions/User";
4 | import { Post } from "./definitions/Post";
5 | import { Tag } from "./definitions/Tag";
6 | import { PostTagAssociation } from "./definitions/PostTagAssociation";
7 | import { Category } from "./definitions/Category";
8 | import config from "../config";
9 |
10 | const sequelize = new Sequelize({
11 | host: config.DATABASE_HOST,
12 | database: config.DATABASE_NAME,
13 | dialect: "mysql",
14 | username: config.DATABASE_USER,
15 | password: config.DATABASE_PASSWORD,
16 | });
17 |
18 | sequelize.addModels([User, Category, Post, Tag, PostTagAssociation]);
19 |
20 | export { User, Post, Tag, PostTagAssociation, Category };
21 |
22 | export const initDB = async () => {
23 | await sequelize.authenticate();
24 | await sequelize.sync({ alter: true });
25 |
26 | await User.findOrCreate({
27 | where: { email: "admin@example.com" },
28 | defaults: { name: "admin", email: "admin@example.com" },
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/server/repositories/CategoryRepository.ts:
--------------------------------------------------------------------------------
1 | import { FindOptions } from "sequelize";
2 | import { Category } from "../models";
3 |
4 | export class CategoryRepository extends Category {
5 | public static async findAllRaw(options?: FindOptions) {
6 | options = options || {};
7 | options.raw = true;
8 |
9 | const data = await Category.findAll(options);
10 | const rawData = data.map((d) => ({
11 | ...d,
12 | createdAt: d.createdAt.toString(),
13 | updatedAt: d.updatedAt.toString(),
14 | }));
15 |
16 | return rawData;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/server/repositories/PostRepository.ts:
--------------------------------------------------------------------------------
1 | import { FindOptions } from "sequelize";
2 | import { Post } from "../models";
3 |
4 | export class PostRepository extends Post {
5 | public static async findAllRaw(options?: FindOptions) {
6 | const data = await Post.findAll(options);
7 | const rawData = data.map((d) => ({
8 | ...d.toJSON(),
9 | createdAt: d.createdAt.toString(),
10 | updatedAt: d.updatedAt.toString(),
11 | }));
12 |
13 | return rawData;
14 | }
15 |
16 | public static async findOneRaw(options?: FindOptions): Promise {
17 | const data = await Post.findOne(options);
18 |
19 | if (!data) {
20 | throw Error("Not Found");
21 | }
22 |
23 | const rawData = {
24 | ...data.toJSON(),
25 | createdAt: data.createdAt.toString(),
26 | updatedAt: data.updatedAt.toString(),
27 | };
28 |
29 | return rawData;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/server/repositories/UserRepository.ts:
--------------------------------------------------------------------------------
1 | import { User } from "../models";
2 |
3 | export class UserRepository extends User {}
4 |
--------------------------------------------------------------------------------
/server/repositories/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./UserRepository";
2 | export * from "./CategoryRepository";
3 | export * from "./PostRepository";
4 |
--------------------------------------------------------------------------------
/src/components/admin/Layout/Header/header.module.scss:
--------------------------------------------------------------------------------
1 | @import "../../styles/theme";
2 |
3 | .header {
4 | height: 64px;
5 | border: none;
6 | font-weight: 500;
7 | padding: 0.9rem 40px;
8 | justify-content: flex-end;
9 | }
10 |
11 | .root {
12 | .nav {
13 | height: 100%;
14 | }
15 | }
16 |
17 | .navItem {
18 | font-size: 1.5rem;
19 | outline: 0;
20 | text-align: center;
21 | padding: 0rem 1rem;
22 |
23 | &:hover,
24 | &:focus {
25 | color: $white !important;
26 | }
27 | }
28 |
29 | .headerIcon {
30 | fill: $icon-color;
31 | }
32 |
33 | .avatar {
34 | display: flex;
35 | align-items: center;
36 | justify-content: center;
37 | overflow: hidden;
38 | height: 40px;
39 | width: 40px;
40 | background: $blue;
41 | font-weight: 600;
42 | font-size: 18px;
43 | margin-right: 10px;
44 | img {
45 | height: 100%;
46 | }
47 | }
48 |
49 | .accountCheck {
50 | color: $icon-color;
51 | font-weight: $font-weight-normal;
52 | font-size: 1rem;
53 | }
54 |
55 | .divider {
56 | display: block;
57 | width: 1px;
58 | margin: 10px 14px;
59 | background: $icon-color;
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/admin/Layout/Header/images/PowerIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const PowerIcon: React.FC<{ className: string }> = (props) => {
4 | return (
5 |
36 | );
37 | };
38 |
39 | export default PowerIcon;
40 |
--------------------------------------------------------------------------------
/src/components/admin/Layout/Header/images/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ltienphat1307/nextjs-sequelize-typescript/a663040f7093791c64bb2033c2b87185eb5f2842/src/components/admin/Layout/Header/images/avatar.jpg
--------------------------------------------------------------------------------
/src/components/admin/Layout/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Image from "Next/image";
3 | import { Navbar, Nav, NavItem, NavLink } from "reactstrap";
4 |
5 | import PowerIcon from "./images/PowerIcon";
6 | import avatar from "./images/avatar.jpg";
7 |
8 | import styles from "./header.module.scss";
9 |
10 | export const Header: React.FC = () => {
11 | return (
12 |
13 |
14 |
34 |
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/src/components/admin/Layout/Sidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import styles from "./sidebar.module.scss";
4 |
5 | export const Sidebar: React.FC = () => {
6 | return (
7 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/admin/Layout/Sidebar/sidebar.module.scss:
--------------------------------------------------------------------------------
1 | @import "../../styles/theme";
2 |
3 | .sidebar {
4 | width: 200px;
5 | position: absolute;
6 | left: 0;
7 | top: 0;
8 | bottom: 0;
9 | overflow-y: auto;
10 | background-color: transparent;
11 | background: transparent;
12 | color: rgba(244, 244, 245, 0.9);
13 | margin-left: 15px;
14 | transition: height 1s;
15 | -webkit-transform: translateX(-200px);
16 | transform: translateX(-200px);
17 | }
18 |
19 | .logo {
20 | margin: 20px 0 55px;
21 | font-size: 18px;
22 | width: 100%;
23 | font-weight: $font-weight-normal;
24 | text-align: center;
25 | text-transform: uppercase;
26 |
27 | a {
28 | color: $icon-color;
29 | padding: 0 5px;
30 | text-decoration: none;
31 | white-space: nowrap;
32 | }
33 | }
34 |
35 | .nav {
36 | padding-bottom: 10px;
37 | overflow-y: auto;
38 | overflow-x: hidden;
39 |
40 | li {
41 | > a {
42 | position: relative;
43 | align-items: center;
44 | padding: 13px 20px 13px 10px;
45 | border-top: 1px solid transparent;
46 | white-space: nowrap;
47 | border-radius: 0.25rem;
48 |
49 | > span {
50 | display: flex;
51 | justify-content: center;
52 | align-items: center;
53 | border-radius: 50%;
54 | width: 32px;
55 | height: 32px;
56 | }
57 | }
58 | }
59 | }
60 |
61 | .headerLink {
62 | overflow-x: hidden;
63 | font-size: 1rem;
64 |
65 | a {
66 | font-size: 14px;
67 | font-weight: $font-weight-thin;
68 | display: flex;
69 | justify-content: left;
70 | color: $icon-color;
71 | text-decoration: none;
72 | cursor: pointer;
73 |
74 | &:hover {
75 | text-decoration: none;
76 | color: inherit;
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/components/admin/Layout/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Head from "next/head";
3 |
4 | import { Header } from "./Header";
5 | import { Sidebar } from "./Sidebar";
6 | import styles from "./layout.module.scss";
7 |
8 | export const LayoutAdmin: React.FC = ({ children }) => {
9 | return (
10 | <>
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
{children}
20 | {/*
*/}
21 |
22 |
23 | >
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/components/admin/Layout/layout.module.scss:
--------------------------------------------------------------------------------
1 | .layoutRoot {
2 | height: 100%;
3 | position: relative;
4 | left: 0;
5 | transition: left 0.3s ease-in-out;
6 | }
7 |
8 | .layoutWrap {
9 | position: relative;
10 | min-height: 100vh;
11 | display: flex;
12 | margin-left: 200px;
13 | flex-direction: column;
14 | left: 0;
15 | right: 0;
16 | transition: left 0.3s ease-in-out;
17 | }
18 |
19 | .layoutContent {
20 | position: relative;
21 | flex-grow: 1;
22 | padding: 25px 40px 60px;
23 | background: white;
24 | }
25 |
26 | .laytFooter {
27 | position: absolute;
28 | bottom: 15px;
29 | color: #c1ccd3;
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/admin/styles/theme.scss:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700,800");
2 | // @import "../../../node_modules/bootstrap/scss/bootstrap";
3 |
4 | @import "variables";
5 |
--------------------------------------------------------------------------------
/src/components/admin/styles/variables.scss:
--------------------------------------------------------------------------------
1 | $icon-color: #c1c3cf;
2 | $icon-blue: #3979f6;
3 | $white: #f4f4f5 !default;
4 | $blue: #2477ff !default;
5 |
6 | $font-weight-normal: 400 !default;
7 | $font-weight-thin: 300 !default;
8 |
--------------------------------------------------------------------------------
/src/components/client/Layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Helmet } from "react-helmet";
3 | import Link from "next/link";
4 | import Head from "next/head";
5 |
6 | import Navigation from "./components/Navigation";
7 | import config from "./config/siteConfig";
8 |
9 | const coverImage = "/images/blog-cover.jpeg";
10 | const logoImage = "/images/logo.png";
11 |
12 | export const Layout = ({ children, bodyClass }: any) => {
13 | const site = {
14 | lang: "vn",
15 | title: "Code3x",
16 | description: "Coding, Keep Coding & Coding Forever",
17 | navigation: [
18 | { label: "Home", url: "/" },
19 | { label: "About", url: "/about" },
20 | { label: "Contact", url: "/contact" },
21 | ],
22 | };
23 |
24 | const twitterUrl = `https://twitter.com/`;
25 | const facebookUrl = `https://www.facebook.com/`;
26 |
27 | return (
28 | <>
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
47 |
48 |
49 |
50 |
51 | <>
52 |

57 | {site.title}
58 | >
59 |
60 |
61 |
99 |
100 |
101 |
102 |
{site.title}
103 |
{site.description}
104 |
105 |
115 |
116 |
117 |
118 |
{children}
119 |
120 |
121 |
122 | {/* The footer at the very bottom of the screen */}
123 |
144 |
145 |
146 | >
147 | );
148 | };
149 |
--------------------------------------------------------------------------------
/src/components/client/components/Blogs/CategoryHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | interface CategoryHeaderProps {
4 | name: string;
5 | desc?: string;
6 | image?: string;
7 | tags?: any[];
8 | }
9 |
10 | export const CategoryHeader = (cate: CategoryHeaderProps) => {
11 | return (
12 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/components/client/components/Blogs/PostCard.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 |
4 | import { Post } from "@server/models";
5 | import { Tag } from "./Tag";
6 |
7 | export const PostCard = ({ post }: { post: Post }) => {
8 | const url = `/posts/${post.slug}/`;
9 |
10 | return (
11 |
12 |
13 |
30 |
31 |
46 |
47 |
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/src/components/client/components/Blogs/PostContent.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Post } from "@server/models";
3 |
4 | export const PostContent = ({ post }: { post: Post }) => {
5 | return (
6 |
7 |
8 | {post.featureImage ? (
9 |
10 |
11 |
12 | ) : null}
13 |
14 | {post.title}
15 |
16 | {/* The main post content */}
17 |
21 |
22 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/components/client/components/Blogs/Tag.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Tag as TagEntity } from "@server/models";
4 |
5 | interface TagProps {
6 | tag: TagEntity;
7 | }
8 |
9 | export const Tag = ({ tag }: TagProps) => {
10 | return {tag.name};
11 | };
12 |
--------------------------------------------------------------------------------
/src/components/client/components/Blogs/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./Tag";
2 | export * from "./CategoryHeader";
3 | export * from "./PostCard";
4 |
--------------------------------------------------------------------------------
/src/components/client/components/Navigation/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Link from "next/link";
3 |
4 | interface NavItem {
5 | label: string;
6 | url: string;
7 | }
8 |
9 | interface NavigationProps {
10 | data: NavItem[];
11 | navClass?: string;
12 | }
13 |
14 | const Navigation = ({ data, navClass }: NavigationProps) => {
15 | navClass = navClass || "site-nav-item";
16 |
17 | return (
18 |
19 | {data.map((navItem: any, i: number) => {
20 | return (
21 |
22 | {navItem.label}
23 |
24 | );
25 | })}
26 |
27 | );
28 | };
29 |
30 | export default Navigation;
31 |
--------------------------------------------------------------------------------
/src/components/client/config/siteConfig.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | siteUrl: `http://localhost:8000`, // Site domain. Do not include a trailing slash!
3 |
4 | postsPerPage: 12, // Number of posts shown on paginated pages (changes this requires sometimes to delete the cache)
5 |
6 | siteTitleMeta: `Ghost Gatsby Starter`, // This allows an alternative site title for meta data for pages.
7 | siteDescriptionMeta: `A starter template to build amazing static websites with Ghost and Gatsby`, // This allows an alternative site description for meta data for pages.
8 |
9 | shareImageWidth: 1000, // Change to the width of your default share image
10 | shareImageHeight: 523, // Change to the height of your default share image
11 |
12 | shortTitle: `Ghost`, // Used for App manifest e.g. Mobile Home Screen
13 | siteIcon: `favicon.png`, // Logo in /static dir used for SEO, RSS, and App manifest
14 | backgroundColor: `#e9e9e9`, // Used for Offline Manifest
15 | themeColor: `#15171A`, // Used for Offline Manifest
16 | };
17 |
--------------------------------------------------------------------------------
/src/d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.css";
2 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import type { AppProps } from "next/app";
2 | import { useRouter } from "next/router";
3 |
4 | import { Layout } from "@src/components/client/Layout";
5 | import { LayoutAdmin } from "@src/components/admin/Layout";
6 |
7 | function MyApp({ Component, pageProps }: AppProps) {
8 | const route = useRouter();
9 | const adminPathRegex = /^\/admin\/*/;
10 |
11 | if (route.pathname == "/admin" || adminPathRegex.test(route.pathname)) {
12 | return (
13 |
14 |
15 |
16 | );
17 | }
18 |
19 | return (
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | export default MyApp;
27 |
--------------------------------------------------------------------------------
/src/pages/about.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function About() {
4 | return About us
;
5 | }
6 |
--------------------------------------------------------------------------------
/src/pages/admin/category.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { NextPageContext } from "next";
3 | import {
4 | Table,
5 | Button,
6 | FormGroup,
7 | Label,
8 | Input,
9 | FormFeedback,
10 | } from "reactstrap";
11 |
12 | import { CategoryRepository } from "@server/repositories";
13 | import { Category } from "@server/models";
14 |
15 | interface CategoryPageProps {
16 | categories: Category[];
17 | }
18 |
19 | interface FormData {
20 | name?: string;
21 | slug?: string;
22 | }
23 |
24 | export async function getServerSideProps(_nextPage: NextPageContext) {
25 | const categories = await CategoryRepository.findAllRaw();
26 |
27 | return { props: { categories } };
28 | }
29 |
30 | export default function CategoryPage(props: CategoryPageProps) {
31 | const [formData, setFormData] = useState({});
32 | const [categories, setCategories] = useState(props.categories);
33 |
34 | async function createCategory() {
35 | try {
36 | const res = await fetch("/api/admin/category", {
37 | method: "post",
38 | body: JSON.stringify(formData),
39 | });
40 |
41 | const data = await res.json();
42 | categories.push(data);
43 |
44 | setCategories([...categories]);
45 | setFormData({});
46 | } catch (e) {
47 | console.error(e);
48 | }
49 | }
50 |
51 | function onChange(e: React.ChangeEvent) {
52 | const value = e.target.value;
53 | const name = e.target.name;
54 | const _formData: any = formData;
55 |
56 | _formData[name] = value;
57 | setFormData({ ..._formData });
58 | }
59 |
60 | return (
61 |
103 | );
104 | }
105 |
--------------------------------------------------------------------------------
/src/pages/admin/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function Index() {
4 | return admin dashboard
;
5 | }
6 |
--------------------------------------------------------------------------------
/src/pages/admin/post/create.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Button, FormGroup, Label, Input } from "reactstrap";
3 | import { useRouter } from "next/router";
4 |
5 | import { CategoryRepository } from "@server/repositories";
6 | import { Category } from "@server/models";
7 |
8 | interface FormData {
9 | title?: string;
10 | slug?: string;
11 | excerpt?: string;
12 | content?: string;
13 | categoryId?: number;
14 | }
15 |
16 | interface Props {
17 | categories: Category[];
18 | }
19 |
20 | export async function getServerSideProps() {
21 | const categories = await CategoryRepository.findAllRaw();
22 |
23 | return { props: { categories } };
24 | }
25 |
26 | export default function CreatePostPage(props: Props) {
27 | const { categories } = props;
28 | const [formData, setFormData] = useState({
29 | categoryId: categories.length ? categories[0].id : null,
30 | });
31 | const router = useRouter();
32 |
33 | function onChange(e: any) {
34 | const value = e.target.value;
35 | const name = e.target.name;
36 | const _formData: any = formData;
37 |
38 | _formData[name] = value;
39 | setFormData({ ..._formData });
40 | }
41 |
42 | async function createPost() {
43 | debugger;
44 | try {
45 | const res = await fetch("/api/admin/post", {
46 | method: "post",
47 | body: JSON.stringify(formData),
48 | });
49 | await res.json();
50 |
51 | router.push("/admin/post");
52 | } catch (e) {
53 | console.error(e);
54 | }
55 | }
56 |
57 | return (
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
79 | {categories.map((category) => (
80 |
83 | ))}
84 |
85 |
86 |
87 |
88 |
94 |
95 |
96 |
99 |
100 |
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/src/pages/admin/post/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { NextPageContext } from "next";
3 | import { Table, Button } from "reactstrap";
4 | import Link from "next/link";
5 |
6 | import { PostRepository } from "@server/repositories";
7 | import { Post } from "@server/models";
8 |
9 | interface PostPageProps {
10 | posts: Post[];
11 | }
12 |
13 | export async function getServerSideProps(_nextPage: NextPageContext) {
14 | const posts = await PostRepository.findAllRaw();
15 |
16 | return { props: { posts } };
17 | }
18 |
19 | export default function CategoryPage(props: PostPageProps) {
20 | const [posts, setPosts] = useState(props.posts);
21 | setPosts;
22 | return (
23 |
24 |
Posts
25 |
30 |
31 |
32 |
33 | No. |
34 | Name |
35 | Slug |
36 | Created Date |
37 | Action |
38 |
39 |
40 |
41 | {posts.map((post, idx) => (
42 |
43 | {idx + 1} |
44 | {post.title} |
45 | {post.slug} |
46 | {post.createdAt} |
47 |
48 |
49 |
50 | |
51 |
52 | ))}
53 |
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/pages/api/admin/category.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 | import { CategoryRepository } from "@server/repositories";
3 |
4 | export default async (req: NextApiRequest, res: NextApiResponse) => {
5 | const data = JSON.parse(req.body);
6 |
7 | const category = await CategoryRepository.create({
8 | name: data.name,
9 | slug: data.slug,
10 | });
11 |
12 | return res.status(200).json(category.toJSON());
13 | };
14 |
--------------------------------------------------------------------------------
/src/pages/api/admin/post.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 | import { PostRepository, UserRepository } from "@server/repositories";
3 | import { Post } from "@server/models";
4 |
5 | export default async (req: NextApiRequest, res: NextApiResponse) => {
6 | const data: Post = JSON.parse(req.body);
7 | const user = await UserRepository.findOne({
8 | where: { email: "admin@example.com" },
9 | });
10 |
11 | if (!user) {
12 | return res.status(500).send("Missing User");
13 | }
14 |
15 | data.authorId = user.id;
16 | const category = await PostRepository.create(data);
17 |
18 | return res.status(200).json(category.toJSON());
19 | };
20 |
--------------------------------------------------------------------------------
/src/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from "next";
3 |
4 | type Data = {
5 | name: string;
6 | };
7 |
8 | export default function handler(
9 | req: NextApiRequest,
10 | res: NextApiResponse
11 | ) {
12 | console.log("HOME API @@@@@@@@@");
13 | res.status(200).json({ name: "John Doe" });
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { PostCard } from "@src/components/client/components/Blogs";
4 | import { PostRepository } from "@server/repositories";
5 | import { Post, User } from "@server/models";
6 |
7 | interface PageProps {
8 | posts: Post[];
9 | }
10 |
11 | export async function getServerSideProps() {
12 | const posts = await PostRepository.findAllRaw({
13 | include: {
14 | model: User,
15 | required: true,
16 | },
17 | });
18 |
19 | return { props: { posts } };
20 | }
21 |
22 | export default function Home(props: PageProps) {
23 | const { posts } = props;
24 |
25 | return (
26 |
27 |
28 | {posts.map((post) => (
29 |
30 | ))}
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/pages/posts/[slug].tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { NextPageContext } from "next";
3 |
4 | import { PostContent } from "@src/components/client/components/Blogs/PostContent";
5 | import { PostRepository } from "@server/repositories";
6 | import { Post } from "@server/models";
7 |
8 | export async function getServerSideProps(nextPage: NextPageContext) {
9 | const slug = nextPage.query.slug as string;
10 | let post: Post;
11 |
12 | try {
13 | post = await PostRepository.findOneRaw({
14 | where: { slug },
15 | });
16 |
17 | return { props: { post } };
18 | } catch (e) {
19 | return {
20 | redirect: {
21 | permanent: false,
22 | destination: "/404",
23 | },
24 | };
25 | }
26 | }
27 |
28 | interface PageProps {
29 | post: Post;
30 | }
31 |
32 | export default function PostContentPage(props: PageProps) {
33 | return ;
34 | }
35 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "jsx": "preserve",
6 | "lib": ["dom", "es2017"],
7 | "moduleResolution": "node",
8 | "strict": true,
9 | "allowJs": true,
10 | "noEmit": true,
11 | "allowSyntheticDefaultImports": true,
12 | "esModuleInterop": true,
13 | "skipLibCheck": true,
14 | "noUnusedLocals": true,
15 | "noUnusedParameters": true,
16 | "isolatedModules": true,
17 | "removeComments": false,
18 | "preserveConstEnums": true,
19 | "sourceMap": true,
20 | "forceConsistentCasingInFileNames": true,
21 | "resolveJsonModule": true,
22 | "experimentalDecorators": true,
23 | "emitDecoratorMetadata": true,
24 | "baseUrl": ".",
25 | "paths": {
26 | "@server/*": ["server/*"],
27 | "@src/*": ["src/*"],
28 | }
29 | },
30 | "exclude": ["dist", ".next", "out", "next.config.js"],
31 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
32 | }
33 |
--------------------------------------------------------------------------------
/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "dist",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "include": ["server/**/*.ts"]
11 | }
12 |
--------------------------------------------------------------------------------