├── .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 | Group 8 -------------------------------------------------------------------------------- /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 | 3 | 4 | -------------------------------------------------------------------------------- /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 | 12 | 17 | 26 | 31 | 32 | 33 | 34 | 35 | 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 |
8 |
Admin
9 | 20 |
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 | {/*
React admin template
*/} 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 | {site.title} 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 |
124 |
125 |
126 | {site.title} © 2021 — Published with{" "} 127 | 133 | Ghost 134 | 135 |
136 |
137 | 141 |
142 |
143 |
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 |
13 |
14 |

{cate.name}

15 | {cate.desc &&

{cate.desc}

} 16 | 17 | {cate.tags && cate.tags.length && ( 18 |
19 | {cate.tags.map((tag) => ( 20 | 26 | tag.label 27 | 28 | ))} 29 |
30 | )} 31 |
32 | {cate.image && ( 33 |
34 | {cate.name} 35 |
36 | )} 37 |
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 |
14 |
20 | {post.postTagAssociations && post.postTagAssociations.length && ( 21 |
22 | {post.postTagAssociations.map((postTagAssociation) => ( 23 | 24 | ))} 25 |
26 | )} 27 | {!!post.featured && Featured} 28 |

{post.title}

29 |
30 |
{post.excerpt}
31 |
32 |
33 |
34 | {post.author.name} 39 |
40 | {post.author.name} 41 |
42 |
43 |
{post.publishedDate ? post.publishedDate : ""}
44 |
45 |
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 | {post.title} 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 |
62 | 63 | 64 | 65 | Slug is available 66 | 67 | 68 | 69 | 70 | Slug is available 71 | 72 | 73 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | {categories.map((category, idx) => ( 89 | 90 | 91 | 92 | 93 | 94 | 98 | 99 | ))} 100 | 101 |
No.NameSlugCreated DateAction
{idx + 1}{category.name}{category.slug}{category.createdAt} 95 | 96 | 97 |
102 |
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 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {posts.map((post, idx) => ( 42 | 43 | 44 | 45 | 46 | 47 | 51 | 52 | ))} 53 | 54 |
No.NameSlugCreated DateAction
{idx + 1}{post.title}{post.slug}{post.createdAt} 48 | 49 | 50 |
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 | --------------------------------------------------------------------------------