├── apps ├── .gitkeep ├── bloggo │ ├── public │ │ ├── .gitkeep │ │ ├── facebook.svg │ │ ├── twitter.svg │ │ └── google.svg │ ├── styles │ │ ├── tailwind.css │ │ ├── _fonts.scss │ │ ├── _polyfills.scss │ │ ├── _theme-variables.scss │ │ └── styles.scss │ ├── index.d.ts │ ├── next-env.d.ts │ ├── postcss.config.js │ ├── pages │ │ ├── portal │ │ │ ├── index.tsx │ │ │ ├── [slug].tsx │ │ │ ├── liked-posts.tsx │ │ │ ├── posts.tsx │ │ │ ├── edit-profile.tsx │ │ │ └── create-post.tsx │ │ ├── forgot-password.tsx │ │ ├── 404.tsx │ │ ├── _app.tsx │ │ ├── index.tsx │ │ ├── [username] │ │ │ └── [slug].tsx │ │ └── onboarding.tsx │ ├── .env.example │ ├── jest.config.ts │ ├── tsconfig.spec.json │ ├── tsconfig.json │ ├── next.config.js │ ├── .eslintrc.json │ ├── project.json │ └── tailwind.config.js └── bloggo-e2e │ ├── src │ ├── support │ │ ├── app.po.ts │ │ ├── index.ts │ │ └── commands.ts │ ├── fixtures │ │ └── example.json │ └── integration │ │ └── app.spec.ts │ ├── tsconfig.json │ ├── .eslintrc.json │ ├── cypress.json │ └── project.json ├── libs ├── .gitkeep ├── ui │ ├── src │ │ ├── lib │ │ │ ├── badge │ │ │ │ ├── index.tsx │ │ │ │ ├── badge-list.component.tsx │ │ │ │ └── badge.component.tsx │ │ │ ├── image │ │ │ │ ├── index.tsx │ │ │ │ ├── placeholders │ │ │ │ │ ├── placeholder-large-h.png │ │ │ │ │ ├── placeholder-large.png │ │ │ │ │ ├── placeholder-large-dark-h.png │ │ │ │ │ └── placeholder-large-dark.png │ │ │ │ ├── image-inview-port.util.ts │ │ │ │ └── image-container.component.tsx │ │ │ ├── dark-mode-toggle │ │ │ │ ├── index.tsx │ │ │ │ ├── dark-mode-switch.component.tsx │ │ │ │ └── dark-mode-toggle-container.component.tsx │ │ │ ├── forms │ │ │ │ ├── index.tsx │ │ │ │ ├── form-select.component.tsx │ │ │ │ └── form-feedback.component.tsx │ │ │ ├── navbar │ │ │ │ ├── index.tsx │ │ │ │ ├── navbar.module.scss │ │ │ │ ├── navbar-action-button.component.tsx │ │ │ │ ├── navigation.component.tsx │ │ │ │ └── navbar.component.tsx │ │ │ ├── portal │ │ │ │ ├── index.tsx │ │ │ │ ├── portal-navbar.component.tsx │ │ │ │ └── user-posts-list.component.tsx │ │ │ ├── sliders │ │ │ │ ├── index.tsx │ │ │ │ └── newest-authors-slider.component.tsx │ │ │ ├── buttons │ │ │ │ ├── index.tsx │ │ │ │ ├── share-post-button-container-component.tsx │ │ │ │ ├── bookmark-post-button.component.tsx │ │ │ │ ├── next-prev-buttons.component.tsx │ │ │ │ ├── comment-on-post-button.component.tsx │ │ │ │ ├── button.component.tsx │ │ │ │ └── like-post-button.component.tsx │ │ │ ├── loader.component.tsx │ │ │ ├── post │ │ │ │ ├── index.tsx │ │ │ │ ├── post-thumbnail.component.tsx │ │ │ │ ├── post-content.component.tsx │ │ │ │ ├── post-preview-card-author.component.tsx │ │ │ │ ├── post-preview-card-action-buttons.component.tsx │ │ │ │ ├── post-edit.component.tsx │ │ │ │ ├── post-entry-metadata.component.tsx │ │ │ │ ├── post-preview-card.component.tsx │ │ │ │ ├── post-filter-list-box.component.tsx │ │ │ │ └── latest-posts-preview.component.tsx │ │ │ ├── logo.component.tsx │ │ │ ├── markdown.component.tsx │ │ │ ├── label.component.tsx │ │ │ ├── auth-check.component.tsx │ │ │ ├── page-heading.component.tsx │ │ │ ├── link.component.tsx │ │ │ ├── meta-tags.component.tsx │ │ │ ├── input.component.tsx │ │ │ ├── social-media-links.component.tsx │ │ │ ├── section-heading.component.tsx │ │ │ ├── avatar.component.tsx │ │ │ ├── search-dropdown.component.tsx │ │ │ ├── footer.component.tsx │ │ │ ├── menu-bar.component.tsx │ │ │ ├── app-layout.component.tsx │ │ │ ├── toast.component.tsx │ │ │ ├── cards │ │ │ │ ├── author-card.component.tsx │ │ │ │ ├── small-post-preview-card.component.tsx │ │ │ │ └── large-post-preview-card.component.tsx │ │ │ ├── image-uploader.component.tsx │ │ │ └── header.component.tsx │ │ └── index.ts │ ├── README.md │ ├── .babelrc │ ├── jest.config.ts │ ├── .eslintrc.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── tsconfig.json │ └── project.json └── redux │ ├── src │ ├── lib │ │ ├── layout │ │ │ ├── types.ts │ │ │ └── index.ts │ │ ├── firebase │ │ │ ├── index.ts │ │ │ ├── helpers.ts │ │ │ ├── types.ts │ │ │ ├── auth.ts │ │ │ ├── firebase.ts │ │ │ └── firestore-queries.ts │ │ ├── hooks.ts │ │ ├── user │ │ │ ├── types.ts │ │ │ └── index.ts │ │ └── store.ts │ └── index.ts │ ├── .babelrc │ ├── README.md │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── tsconfig.spec.json │ └── project.json ├── .eslintignore ├── babel.config.json ├── assets └── bloggo.png ├── .prettierignore ├── jest.config.ts ├── firestore.rules ├── .vscode └── extensions.json ├── .prettierrc ├── .editorconfig ├── storage.rules ├── tools └── tsconfig.tools.json ├── tailwind-workspace-preset.js ├── tsconfig.base.json ├── jest.preset.js ├── firebase.json ├── .eslintrc.json ├── firestore.indexes.json ├── LICENSE ├── .gitignore ├── nx.json ├── package.json └── README.md /apps/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/bloggo/public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "babelrcRoots": ["*"] 3 | } 4 | -------------------------------------------------------------------------------- /libs/ui/src/lib/badge/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './badge-list.component'; 2 | -------------------------------------------------------------------------------- /libs/ui/src/lib/image/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './image-container.component'; 2 | -------------------------------------------------------------------------------- /apps/bloggo-e2e/src/support/app.po.ts: -------------------------------------------------------------------------------- 1 | export const getGreeting = () => cy.get('h1'); 2 | -------------------------------------------------------------------------------- /assets/bloggo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msanvarov/bloggo/HEAD/assets/bloggo.png -------------------------------------------------------------------------------- /apps/bloggo/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /libs/redux/src/lib/layout/types.ts: -------------------------------------------------------------------------------- 1 | export interface ILayoutState { 2 | darkMode: boolean; 3 | } 4 | -------------------------------------------------------------------------------- /libs/ui/src/lib/dark-mode-toggle/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './dark-mode-toggle-container.component'; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /libs/ui/src/lib/forms/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './form-feedback.component'; 2 | export * from './form-select.component'; 3 | -------------------------------------------------------------------------------- /libs/ui/src/lib/navbar/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './navbar.component'; 2 | export * from './navbar-action-button.component'; 3 | -------------------------------------------------------------------------------- /libs/ui/src/lib/portal/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './portal-navbar.component'; 2 | export * from './user-posts-list.component'; 3 | -------------------------------------------------------------------------------- /apps/bloggo-e2e/src/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io" 4 | } 5 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | const { getJestProjects } = require('@nx/jest'); 2 | 3 | export default { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /libs/ui/src/lib/sliders/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './most-popular-posts-slider.component'; 2 | export * from './newest-authors-slider.component'; 3 | -------------------------------------------------------------------------------- /libs/ui/src/lib/image/placeholders/placeholder-large-h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msanvarov/bloggo/HEAD/libs/ui/src/lib/image/placeholders/placeholder-large-h.png -------------------------------------------------------------------------------- /libs/ui/src/lib/image/placeholders/placeholder-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msanvarov/bloggo/HEAD/libs/ui/src/lib/image/placeholders/placeholder-large.png -------------------------------------------------------------------------------- /libs/redux/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nrwl/js/babel", 5 | { 6 | "useBuiltIns": "usage" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /libs/ui/src/lib/buttons/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './like-post-button.component'; 2 | export * from './bookmark-post-button.component'; 3 | export * from './button.component'; 4 | -------------------------------------------------------------------------------- /libs/ui/src/lib/image/placeholders/placeholder-large-dark-h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msanvarov/bloggo/HEAD/libs/ui/src/lib/image/placeholders/placeholder-large-dark-h.png -------------------------------------------------------------------------------- /libs/ui/src/lib/image/placeholders/placeholder-large-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msanvarov/bloggo/HEAD/libs/ui/src/lib/image/placeholders/placeholder-large-dark.png -------------------------------------------------------------------------------- /libs/redux/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/store'; 2 | export * from './lib/hooks'; 3 | 4 | export * from './lib/firebase'; 5 | export * from './lib/user'; 6 | export * from './lib/layout'; 7 | -------------------------------------------------------------------------------- /libs/redux/src/lib/firebase/index.ts: -------------------------------------------------------------------------------- 1 | export * from './firebase'; 2 | export * from './auth'; 3 | export * from './firestore-queries'; 4 | export * from './types'; 5 | export * from './helpers'; 6 | -------------------------------------------------------------------------------- /libs/ui/README.md: -------------------------------------------------------------------------------- 1 | # ui 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test ui` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | match /{document=**} { 5 | allow read, write: if true; 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "firsttris.vscode-jest-runner", 6 | "dbaeumer.vscode-eslint" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /apps/bloggo/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | declare module '*.svg' { 3 | const content: any; 4 | export const ReactComponent: any; 5 | export default content; 6 | } 7 | -------------------------------------------------------------------------------- /libs/redux/README.md: -------------------------------------------------------------------------------- 1 | # redux 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test redux` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /libs/ui/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nx/react/babel", 5 | { 6 | "runtime": "automatic", 7 | "useBuiltIns": "usage" 8 | } 9 | ] 10 | ], 11 | "plugins": [] 12 | } 13 | -------------------------------------------------------------------------------- /apps/bloggo/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /libs/ui/src/lib/navbar/navbar.module.scss: -------------------------------------------------------------------------------- 1 | .main-nav { 2 | &.not-on-top { 3 | @apply bg-white dark:bg-neutral-900 backdrop-blur-2xl bg-opacity-70 dark:bg-opacity-60 shadow-sm dark:border-b dark:border-neutral-700; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /apps/bloggo/postcss.config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | module.exports = { 4 | plugins: { 5 | tailwindcss: { 6 | config: join(__dirname, 'tailwind.config.js'), 7 | }, 8 | autoprefixer: {}, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "semi": true, 6 | "importOrder": ["^@bloggo/(.*)$", "^[./]"], 7 | "importOrderSeparation": true, 8 | "importOrderSortSpecifiers": true 9 | } 10 | -------------------------------------------------------------------------------- /libs/redux/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/ui/src/lib/loader.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Loader: React.FC = () => { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /apps/bloggo-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "../../dist/out-tsc", 6 | "allowJs": true, 7 | "types": ["cypress", "node"] 8 | }, 9 | "include": ["src/**/*.ts", "src/**/*.js"] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /libs/redux/src/lib/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; 2 | 3 | import type { AppDispatch, AppState } from './store'; 4 | 5 | export const useAppDispatch = () => useDispatch(); 6 | export const useAppSelector: TypedUseSelectorHook = useSelector; 7 | -------------------------------------------------------------------------------- /libs/ui/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'ui', 4 | preset: '../../jest.preset.js', 5 | transform: { 6 | '^.+\\.[tj]sx?$': 'babel-jest', 7 | }, 8 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 9 | coverageDirectory: '../../coverage/libs/ui', 10 | }; 11 | -------------------------------------------------------------------------------- /storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service firebase.storage { 3 | match /b/{bucket}/o { 4 | match /{allPaths=**} { 5 | allow read: if request.auth!=null; 6 | } 7 | match /uploads/{uid}/{imageHash} { 8 | allow write: if resource == null || request.resource == null 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/bloggo/styles/_fonts.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Lato&display=swap'); 2 | 3 | html body { 4 | @apply font-body antialiased; 5 | } 6 | 7 | h1, 8 | h2, 9 | h3, 10 | h4 { 11 | @apply font-display; 12 | } 13 | 14 | :root { 15 | --font-display: Poppins; 16 | --font-body: Poppins; 17 | } 18 | -------------------------------------------------------------------------------- /apps/bloggo/styles/_polyfills.scss: -------------------------------------------------------------------------------- 1 | /* Hide scrollbar for Chrome, Safari and Opera */ 2 | .hiddenScrollbar::-webkit-scrollbar { 3 | display: none; 4 | } 5 | 6 | /* Hide scrollbar for IE, Edge and Firefox */ 7 | .hiddenScrollbar { 8 | -ms-overflow-style: none; /* IE and Edge */ 9 | scrollbar-width: none; /* Firefox */ 10 | } 11 | -------------------------------------------------------------------------------- /libs/redux/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["**/*.spec.ts", "**/*.test.ts", "jest.config.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/bloggo/pages/portal/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Metatags } from '@bloggo/ui'; 4 | 5 | const PortalIndexPage: React.FC = () => { 6 | return ( 7 | <> 8 | 9 |

Portal Index Page

10 | 11 | ); 12 | }; 13 | 14 | export default PortalIndexPage; 15 | -------------------------------------------------------------------------------- /tailwind-workspace-preset.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: {}, 4 | }, 5 | variants: { 6 | extend: {}, 7 | }, 8 | plugins: [ 9 | require('@tailwindcss/typography'), 10 | require('@tailwindcss/forms'), 11 | require('@tailwindcss/line-clamp'), 12 | require('@tailwindcss/aspect-ratio'), 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /apps/bloggo/pages/forgot-password.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Metatags } from '@bloggo/ui'; 4 | 5 | const ForgotPasswordPage: React.FC = () => { 6 | return ( 7 | <> 8 | 9 |
Forgot Password Page
10 | 11 | ); 12 | }; 13 | 14 | export default ForgotPasswordPage; 15 | -------------------------------------------------------------------------------- /apps/bloggo/.env.example: -------------------------------------------------------------------------------- 1 | # FIREBASE 2 | FIREBASE_API_KEY=AIzaSyDNotinqdiNETddK4e_MA_rNsz1gkYKChw 3 | FIREBASE_AUTH_DOMAIN=bloggo-9fc7d.firebaseapp.com 4 | FIREBASE_PROJECT_ID=bloggo-9fc7d 5 | FIREBASE_STORAGE_BUCKET=bloggo-9fc7d.appspot.com 6 | FIREBASE_MESSAGING_SENDER_ID=424368452854 7 | FIREBASE_APP_ID=1:424368452854:web:361dd4c39219757306e8d7 8 | FIREBASE_MEASUREMENT_ID=G-D2F125TYVK -------------------------------------------------------------------------------- /libs/redux/src/lib/user/types.ts: -------------------------------------------------------------------------------- 1 | export interface IUserState { 2 | user?: Partial | null; 3 | username?: string | null; 4 | } 5 | 6 | export interface IUser { 7 | uid: string; 8 | email: string | null; 9 | emailVerified: boolean; 10 | displayName: string | null; 11 | photoURL: string | null; 12 | createdAt?: string; 13 | lastLoginAt?: string; 14 | } 15 | -------------------------------------------------------------------------------- /libs/redux/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/ui/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@nx/react", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/ui/src/lib/post/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './post-filter-list-box.component'; 2 | export * from './post-preview-card.component'; 3 | export * from './latest-posts-preview.component'; 4 | export * from './post-entry-metadata.component'; 5 | export * from './post-preview-card-action-buttons.component'; 6 | export * from './post-content.component'; 7 | export * from './post-form.component'; 8 | export * from './post-edit.component'; 9 | -------------------------------------------------------------------------------- /apps/bloggo/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'bloggo', 4 | preset: '../../jest.preset.js', 5 | transform: { 6 | '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest', 7 | '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }], 8 | }, 9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 10 | coverageDirectory: '../../coverage/apps/bloggo', 11 | }; 12 | -------------------------------------------------------------------------------- /libs/ui/src/lib/logo.component.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/anchor-is-valid */ 2 | import Link from 'next/link'; 3 | import React from 'react'; 4 | 5 | // TODO: use Next Image component for logo 6 | export const Logo: React.FC = () => { 7 | return ( 8 | 9 | logo 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /apps/bloggo-e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["src/plugins/index.js"], 11 | "rules": { 12 | "@typescript-eslint/no-var-requires": "off", 13 | "no-undef": "off" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /apps/bloggo-e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileServerFolder": ".", 3 | "fixturesFolder": "./src/fixtures", 4 | "integrationFolder": "./src/integration", 5 | "modifyObstructiveCode": false, 6 | "supportFile": "./src/support/index.ts", 7 | "pluginsFile": false, 8 | "video": true, 9 | "videosFolder": "../../dist/cypress/apps/bloggo-e2e/videos", 10 | "screenshotsFolder": "../../dist/cypress/apps/bloggo-e2e/screenshots", 11 | "chromeWebSecurity": false 12 | } 13 | -------------------------------------------------------------------------------- /libs/redux/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'redux', 4 | preset: '../../jest.preset.js', 5 | globals: {}, 6 | testEnvironment: 'node', 7 | transform: { 8 | '^.+\\.[tj]sx?$': [ 9 | 'ts-jest', 10 | { 11 | tsconfig: '/tsconfig.spec.json', 12 | }, 13 | ], 14 | }, 15 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 16 | coverageDirectory: '../../coverage/libs/redux', 17 | }; 18 | -------------------------------------------------------------------------------- /apps/bloggo/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"], 7 | "jsx": "react" 8 | }, 9 | "include": [ 10 | "**/*.test.ts", 11 | "**/*.spec.ts", 12 | "**/*.test.tsx", 13 | "**/*.spec.tsx", 14 | "**/*.test.js", 15 | "**/*.spec.js", 16 | "**/*.test.jsx", 17 | "**/*.spec.jsx", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /libs/redux/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.test.ts", 10 | "**/*.spec.ts", 11 | "**/*.test.tsx", 12 | "**/*.spec.tsx", 13 | "**/*.test.js", 14 | "**/*.spec.js", 15 | "**/*.test.jsx", 16 | "**/*.spec.jsx", 17 | "**/*.d.ts", 18 | "jest.config.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /libs/ui/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.test.ts", 10 | "**/*.spec.ts", 11 | "**/*.test.tsx", 12 | "**/*.spec.tsx", 13 | "**/*.test.js", 14 | "**/*.spec.js", 15 | "**/*.test.jsx", 16 | "**/*.spec.jsx", 17 | "**/*.d.ts", 18 | "jest.config.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /apps/bloggo-e2e/src/integration/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { getGreeting } from '../support/app.po'; 2 | 3 | describe('bloggo', () => { 4 | beforeEach(() => cy.visit('/')); 5 | 6 | it('should display welcome message', () => { 7 | // Custom command example, see `../support/commands.ts` file 8 | cy.login('my-email@something.com', 'myPassword'); 9 | 10 | // Function helper example, see `../support/app.po.ts` file 11 | getGreeting().contains('Welcome bloggo'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /libs/ui/src/lib/markdown.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactMarkdown from 'react-markdown'; 3 | import remarkGfm from 'remark-gfm'; 4 | 5 | type MarkdownProps = { 6 | content: string; 7 | className?: string; 8 | }; 9 | 10 | export const Markdown: React.FC = ({ 11 | className = '', 12 | content, 13 | }) => { 14 | return ( 15 | 16 | {content} 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /libs/ui/src/lib/label.component.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { ReactNode } from 'react'; 3 | 4 | type LabelProps = { 5 | className?: string; 6 | children?: ReactNode; 7 | }; 8 | 9 | export const Label = ({ className = '', children }: LabelProps) => { 10 | return ( 11 | 17 | {children} 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /apps/bloggo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "types": ["node", "jest"], 9 | "strict": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "noEmit": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "incremental": true 15 | }, 16 | "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"], 17 | "exclude": ["node_modules", "jest.config.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /libs/redux/src/lib/layout/index.ts: -------------------------------------------------------------------------------- 1 | import { PayloadAction, createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | darkMode: false, 5 | }; 6 | 7 | const layoutSlice = createSlice({ 8 | name: '@@layout', 9 | initialState, 10 | reducers: { 11 | toggleDarkMode: (state) => ({ darkMode: !state.darkMode }), 12 | setDarkMode: (state, action: PayloadAction) => { 13 | state.darkMode = action.payload; 14 | }, 15 | }, 16 | }); 17 | 18 | export const { setDarkMode, toggleDarkMode } = layoutSlice.actions; 19 | 20 | export const layoutReducer = layoutSlice.reducer; 21 | -------------------------------------------------------------------------------- /apps/bloggo/next.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const withNx = require('@nx/next/plugins/with-nx'); 3 | 4 | /** 5 | * @type {import('@nx/next/plugins/with-nx').WithNxOptions} 6 | **/ 7 | const nextConfig = { 8 | nx: { 9 | // Set this to true if you would like to to use SVGR 10 | // See: https://github.com/gregberge/svgr 11 | svgr: false, 12 | }, 13 | images: { 14 | domains: [ 15 | 'ui-avatars.com', 16 | 'lh3.googleusercontent.com', 17 | 'images.pexels.com', 18 | ], 19 | }, 20 | }; 21 | 22 | module.exports = withNx(nextConfig); 23 | -------------------------------------------------------------------------------- /libs/ui/src/lib/post/post-thumbnail.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { ImageContainer } from '../image'; 4 | 5 | type PostThumbnailProps = { 6 | className?: string; 7 | media: { 8 | type: 'image' | 'video' | 'audio' | 'gallery'; 9 | src: string; 10 | }; 11 | }; 12 | 13 | export const PostThumbnail: React.FC = ({ 14 | className = 'w-full h-full', 15 | media, 16 | }) => { 17 | return ( 18 |
19 | 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /libs/ui/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "files": [ 8 | "../../node_modules/@nx/react/typings/cssmodule.d.ts", 9 | "../../node_modules/@nx/react/typings/image.d.ts" 10 | ], 11 | "exclude": [ 12 | "**/*.spec.ts", 13 | "**/*.test.ts", 14 | "**/*.spec.tsx", 15 | "**/*.test.tsx", 16 | "**/*.spec.js", 17 | "**/*.test.js", 18 | "**/*.spec.jsx", 19 | "**/*.test.jsx", 20 | "jest.config.ts" 21 | ], 22 | "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] 23 | } 24 | -------------------------------------------------------------------------------- /apps/bloggo/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:@nx/react-typescript", 4 | "../../.eslintrc.json", 5 | "next", 6 | "next/core-web-vitals" 7 | ], 8 | "ignorePatterns": ["!**/*"], 9 | "overrides": [ 10 | { 11 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 12 | "rules": { 13 | "@next/next/no-html-link-for-pages": ["error", "apps/bloggo/pages"] 14 | } 15 | }, 16 | { 17 | "files": ["*.ts", "*.tsx"], 18 | "rules": {} 19 | }, 20 | { 21 | "files": ["*.js", "*.jsx"], 22 | "rules": {} 23 | } 24 | ], 25 | "env": { 26 | "jest": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /libs/redux/src/lib/firebase/helpers.ts: -------------------------------------------------------------------------------- 1 | import { DocumentSnapshot, Timestamp } from 'firebase/firestore'; 2 | 3 | /**` 4 | * Converts a firestore document to JSON 5 | * @param {DocumentSnapshot} doc 6 | */ 7 | export const docToJSON = (doc: DocumentSnapshot) => { 8 | const data = doc.data(); 9 | if (data) { 10 | return { 11 | ...data, 12 | // TIMESTAMP is not JSON serializable. Must be converted to milliseconds 13 | createdAt: data.createdAt.toMillis(), 14 | updatedAt: data.updatedAt.toMillis(), 15 | }; 16 | } else { 17 | return null; 18 | } 19 | }; 20 | 21 | export const fromMillis = Timestamp.fromMillis; 22 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2017", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@bloggo/redux": ["libs/redux/src/index.ts"], 19 | "@bloggo/ui": ["libs/ui/src/index.ts"] 20 | } 21 | }, 22 | "exclude": ["node_modules", "tmp"] 23 | } 24 | -------------------------------------------------------------------------------- /libs/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "noImplicitOverride": true, 11 | "noPropertyAccessFromIndexSignature": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true 14 | }, 15 | "files": [], 16 | "include": [], 17 | "references": [ 18 | { 19 | "path": "./tsconfig.lib.json" 20 | }, 21 | { 22 | "path": "./tsconfig.spec.json" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /apps/bloggo-e2e/src/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | -------------------------------------------------------------------------------- /libs/ui/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/ui/src", 5 | "projectType": "library", 6 | "tags": [], 7 | "targets": { 8 | "lint": { 9 | "executor": "@nx/linter:eslint", 10 | "outputs": ["{options.outputFile}"], 11 | "options": { 12 | "lintFilePatterns": ["libs/ui/**/*.{ts,tsx,js,jsx}"] 13 | } 14 | }, 15 | "test": { 16 | "executor": "@nx/jest:jest", 17 | "outputs": ["{workspaceRoot}/coverage/libs/ui"], 18 | "options": { 19 | "jestConfig": "libs/ui/jest.config.ts", 20 | "passWithNoTests": true 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/redux/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/redux/src", 5 | "projectType": "library", 6 | "targets": { 7 | "lint": { 8 | "executor": "@nx/linter:eslint", 9 | "outputs": ["{options.outputFile}"], 10 | "options": { 11 | "lintFilePatterns": ["libs/redux/**/*.ts"] 12 | } 13 | }, 14 | "test": { 15 | "executor": "@nx/jest:jest", 16 | "outputs": ["{workspaceRoot}/coverage/libs/redux"], 17 | "options": { 18 | "jestConfig": "libs/redux/jest.config.ts", 19 | "passWithNoTests": true 20 | } 21 | } 22 | }, 23 | "tags": [] 24 | } 25 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { 4 | ...nxPreset, 5 | /* TODO: Update to latest Jest snapshotFormat 6 | * By default Nx has kept the older style of Jest Snapshot formats 7 | * to prevent breaking of any existing tests with snapshots. 8 | * It's recommend you update to the latest format. 9 | * You can do this by removing snapshotFormat property 10 | * and running tests with --update-snapshot flag. 11 | * Example: "nx affected --targets=test --update-snapshot" 12 | * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format 13 | */ 14 | snapshotFormat: { escapeString: true, printBasicPrototype: true }, 15 | }; 16 | -------------------------------------------------------------------------------- /libs/ui/src/lib/auth-check.component.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-no-useless-fragment */ 2 | import { ReactNode } from 'react'; 3 | 4 | import { AppState, useAppSelector } from '@bloggo/redux'; 5 | 6 | import { Link } from './link.component'; 7 | 8 | type AppCheckProps = { 9 | // managed by Next 10 | fallback?: ReactNode; 11 | children: ReactNode; 12 | }; 13 | 14 | export const AuthCheck = ({ children, fallback }: AppCheckProps) => { 15 | const { user } = useAppSelector((state: AppState) => state.user); 16 | return user ? ( 17 | <>{children} 18 | ) : ( 19 | <>{fallback} || ( 20 | Must be authenticated to access resource. 21 | ) 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /libs/redux/src/lib/user/index.ts: -------------------------------------------------------------------------------- 1 | import { PayloadAction, createSlice } from '@reduxjs/toolkit'; 2 | 3 | import type { IUser, IUserState } from './types'; 4 | 5 | const initialState: IUserState = { 6 | user: undefined, 7 | username: undefined, 8 | }; 9 | 10 | const userSlice = createSlice({ 11 | name: '@@user', 12 | initialState, 13 | reducers: { 14 | setUser: (state, action: PayloadAction) => { 15 | state.user = action.payload; 16 | }, 17 | setUsername: (state, action: PayloadAction) => { 18 | state.username = action.payload; 19 | }, 20 | }, 21 | }); 22 | 23 | export const { setUser, setUsername } = userSlice.actions; 24 | 25 | export const userReducer = userSlice.reducer; 26 | -------------------------------------------------------------------------------- /apps/bloggo/public/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /libs/ui/src/lib/page-heading.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { HTMLAttributes } from 'react'; 2 | 3 | interface PageHeadingProps extends HTMLAttributes { 4 | emoji?: string; 5 | className?: string; 6 | } 7 | 8 | export const PageHeading: React.FC = ({ 9 | className = 'justify-center', 10 | emoji = '', 11 | children, 12 | ...rest 13 | }) => { 14 | return ( 15 |

19 | {emoji && ( 20 | {emoji} 21 | )} 22 | {children || `Heading`} 23 |

24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "hosting": { 7 | "public": "dist/apps/bloggo", 8 | "ignore": [ 9 | "firebase.json", 10 | "**/.*", 11 | "**/node_modules/**" 12 | ] 13 | }, 14 | "storage": { 15 | "rules": "storage.rules" 16 | }, 17 | "emulators": { 18 | "auth": { 19 | "port": 9099 20 | }, 21 | "functions": { 22 | "port": 5001 23 | }, 24 | "firestore": { 25 | "port": 8080 26 | }, 27 | "hosting": { 28 | "port": 5000 29 | }, 30 | "pubsub": { 31 | "port": 8085 32 | }, 33 | "storage": { 34 | "port": 9199 35 | }, 36 | "ui": { 37 | "enabled": true, 38 | "port": 3000 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /libs/ui/src/lib/post/post-content.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactMarkdown from 'react-markdown'; 3 | import remarkGfm from 'remark-gfm'; 4 | 5 | import { Markdown } from '../markdown.component'; 6 | 7 | type PostContentProps = { 8 | content: string; 9 | }; 10 | 11 | // TODO: Add disquis for comments 12 | 13 | export const PostContent: React.FC = ({ content }) => { 14 | return ( 15 |
16 |
20 | 21 |
22 | {/* POST TAGS */} 23 | {/* AUTHOR DATA */} 24 | {/* COMMENT SECTION */} 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /apps/bloggo/pages/portal/[slug].tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import React, { useState } from 'react'; 3 | 4 | import { AppState, useAppSelector } from '@bloggo/redux'; 5 | import { AppLayout, AuthCheck, Metatags, PostEdit } from '@bloggo/ui'; 6 | 7 | const PortalEditPostPage: React.FC = () => { 8 | const router = useRouter(); 9 | const { slug } = router.query; 10 | const { user } = useAppSelector((state: AppState) => state.user); 11 | 12 | return ( 13 | <> 14 | 15 | 16 | 17 |
18 | {user?.uid && } 19 |
20 |
21 |
22 | 23 | ); 24 | }; 25 | 26 | export default PortalEditPostPage; 27 | -------------------------------------------------------------------------------- /apps/bloggo-e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bloggo-e2e", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/bloggo-e2e/src", 5 | "projectType": "application", 6 | "targets": { 7 | "e2e": { 8 | "executor": "@nx/cypress:cypress", 9 | "options": { 10 | "cypressConfig": "apps/bloggo-e2e/cypress.json", 11 | "devServerTarget": "bloggo:serve" 12 | }, 13 | "configurations": { 14 | "production": { 15 | "devServerTarget": "bloggo:serve:production" 16 | } 17 | } 18 | }, 19 | "lint": { 20 | "executor": "@nx/linter:eslint", 21 | "outputs": ["{options.outputFile}"], 22 | "options": { 23 | "lintFilePatterns": ["apps/bloggo-e2e/**/*.{js,ts}"] 24 | } 25 | } 26 | }, 27 | "tags": [], 28 | "implicitDependencies": ["bloggo"] 29 | } 30 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nx/typescript"], 27 | "rules": {} 28 | }, 29 | { 30 | "files": ["*.js", "*.jsx"], 31 | "extends": ["plugin:@nx/javascript"], 32 | "rules": {} 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /libs/ui/src/lib/navbar/navbar-action-button.component.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | 4 | import { Link } from '../link.component'; 5 | 6 | type NavbarActionButtonProps = { 7 | className?: string; 8 | href: string; 9 | icon: React.ReactNode; 10 | }; 11 | export const NavbarActionButton: React.FC = ({ 12 | className, 13 | href, 14 | icon, 15 | }) => { 16 | return ( 17 | 25 | Create post 26 | {icon} 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /libs/ui/src/lib/forms/form-select.component.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from '@headlessui/react'; 2 | import classNames from 'classnames'; 3 | import React, { SelectHTMLAttributes, useState } from 'react'; 4 | 5 | interface FormSelectProps extends SelectHTMLAttributes { 6 | className?: string; 7 | sizeClass?: string; 8 | } 9 | 10 | export const FormSelect: React.FC = ({ 11 | className = '', 12 | sizeClass = 'h-11', 13 | children, 14 | ...rest 15 | }) => { 16 | return ( 17 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /libs/redux/src/lib/firebase/types.ts: -------------------------------------------------------------------------------- 1 | import { FieldValue } from 'firebase/firestore'; 2 | 3 | export interface IFirestoreUserData { 4 | username: string; 5 | displayName: string; 6 | photoURL: string; 7 | createdAt: number; 8 | } 9 | 10 | export interface IFirestoreUsernameData { 11 | username: string; 12 | uid: string; 13 | createdAt: number; 14 | } 15 | 16 | export interface IFirestorePostData { 17 | // author specific data 18 | uid: string; 19 | username: string; 20 | // post specific data 21 | title: string; 22 | slug: string; 23 | content: string; 24 | createdAt: number; 25 | updatedAt: number; 26 | published: boolean; 27 | thumbnail: string; 28 | href: string; 29 | description: string; 30 | likeCount: number; 31 | } 32 | 33 | export interface IFirestorePostPayload 34 | extends Omit { 35 | createdAt: FieldValue; 36 | updatedAt: FieldValue; 37 | } 38 | -------------------------------------------------------------------------------- /libs/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | // file modules 2 | export * from './lib/toast.component'; 3 | export * from './lib/loader.component'; 4 | export * from './lib/avatar.component'; 5 | export * from './lib/toast.component'; 6 | export * from './lib/header.component'; 7 | export * from './lib/page-heading.component'; 8 | export * from './lib/app-layout.component'; 9 | export * from './lib/input.component'; 10 | export * from './lib/link.component'; 11 | export * from './lib/footer.component'; 12 | export * from './lib/meta-tags.component'; 13 | export * from './lib/label.component'; 14 | export * from './lib/auth-check.component'; 15 | // folder modules 16 | export * from './lib/navbar'; 17 | export * from './lib/buttons'; 18 | export * from './lib/forms'; 19 | export * from './lib/dark-mode-toggle'; 20 | export * from './lib/image'; 21 | export * from './lib/post'; 22 | export * from './lib/sliders'; 23 | export * from './lib/badge'; 24 | export * from './lib/portal'; 25 | -------------------------------------------------------------------------------- /libs/ui/src/lib/link.component.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/anchor-is-valid */ 2 | import classNames from 'classnames'; 3 | import NextLink, { LinkProps as NextLinkProps } from 'next/link'; 4 | import { useRouter } from 'next/router'; 5 | import { ReactNode } from 'react'; 6 | 7 | interface LinkProps extends NextLinkProps { 8 | className?: string; 9 | colorClass?: string; 10 | activeClassName?: string; 11 | children?: ReactNode; 12 | } 13 | 14 | export const Link = ({ 15 | className = 'font-medium', 16 | colorClass = 'text-primary-6000 hover:text-primary-800 dark:text-primary-500 dark:hover:text-primary-6000', 17 | activeClassName = '', 18 | children, 19 | ...rest 20 | }: LinkProps) => { 21 | const router = useRouter(); 22 | 23 | return ( 24 | 30 | {children} 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /libs/ui/src/lib/dark-mode-toggle/dark-mode-switch.component.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | import { FiMoon, FiSun } from 'react-icons/fi'; 4 | 5 | type DarkModeSwitchProps = { 6 | className?: string; 7 | isDarkMode: boolean; 8 | onClick: () => void; 9 | }; 10 | 11 | export const DarkModeSwitch: React.FC = ({ 12 | className, 13 | isDarkMode, 14 | onClick, 15 | }) => { 16 | return ( 17 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /libs/ui/src/lib/meta-tags.component.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import React from 'react'; 3 | 4 | type MetatagsProps = { 5 | title?: string; 6 | description?: string; 7 | image?: string; 8 | }; 9 | 10 | export const Metatags: React.FC = ({ 11 | title = 'Bloggo - a blog platform for the modern web', 12 | description = 'A blog platform to write, publish, and share stories.', 13 | image = 'blob:https://vercel.com/c05fcf8a-4a1a-4ede-9a32-af71977cfb7c', 14 | }) => { 15 | return ( 16 | 17 | {title} || Bloggo 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /apps/bloggo/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import React from 'react'; 3 | 4 | import { AppLayout, Button, Metatags } from '@bloggo/ui'; 5 | 6 | const Page404: React.FC = () => { 7 | return ( 8 | <> 9 | 10 | 11 |
12 |
13 |

🪔

14 |

15 | 404 16 |

17 | 18 | Whoops, the page you requested doesn't exists.{' '} 19 | 20 | 23 |
24 |
25 |
26 | 27 | ); 28 | }; 29 | 30 | export default Page404; 31 | -------------------------------------------------------------------------------- /libs/ui/src/lib/badge/badge-list.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Badge, BadgeColor } from './badge.component'; 4 | 5 | // TODO: cleanup the mock data 6 | const categories = [ 7 | { 8 | id: 15, 9 | name: 'Computers', 10 | href: '/archive/the-demo-archive-slug', 11 | thumbnail: 12 | 'https://images.unsplash.com/photo-1532529867795-3c83442c1e5c?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', 13 | count: 26, 14 | color: 'blue', 15 | }, 16 | ].map((category) => ({ ...category, taxonomy: 'category' })); 17 | 18 | type BadgeListProps = { 19 | badgeClassName?: string; 20 | }; 21 | 22 | export const BadgeList: React.FC = ({ badgeClassName }) => { 23 | return ( 24 | <> 25 | {categories.map((category, index) => ( 26 | 33 | ))} 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [ 3 | { 4 | "collectionGroup": "posts", 5 | "queryScope": "COLLECTION_GROUP", 6 | "fields": [ 7 | { 8 | "fieldPath": "published", 9 | "order": "ASCENDING" 10 | }, 11 | { 12 | "fieldPath": "createdAt", 13 | "order": "DESCENDING" 14 | } 15 | ] 16 | }, 17 | { 18 | "collectionGroup": "posts", 19 | "queryScope": "COLLECTION_GROUP", 20 | "fields": [ 21 | { 22 | "fieldPath": "published", 23 | "order": "ASCENDING" 24 | }, 25 | { 26 | "fieldPath": "likeCount", 27 | "order": "DESCENDING" 28 | } 29 | ] 30 | }, 31 | { 32 | "collectionGroup": "posts", 33 | "queryScope": "COLLECTION", 34 | "fields": [ 35 | { 36 | "fieldPath": "published", 37 | "order": "ASCENDING" 38 | }, 39 | { 40 | "fieldPath": "createdAt", 41 | "order": "DESCENDING" 42 | } 43 | ] 44 | } 45 | ], 46 | "fieldOverrides": [] 47 | } 48 | -------------------------------------------------------------------------------- /libs/ui/src/lib/input.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { InputHTMLAttributes, forwardRef } from 'react'; 2 | 3 | export interface InputProps extends InputHTMLAttributes { 4 | sizeClass?: string; 5 | fontClass?: string; 6 | rounded?: string; 7 | } 8 | 9 | export const Input: React.ForwardRefExoticComponent< 10 | InputProps & React.RefAttributes 11 | > = forwardRef( 12 | ( 13 | { 14 | className = '', 15 | sizeClass = 'h-11 px-4 py-3', 16 | fontClass = 'text-sm font-normal', 17 | rounded = 'rounded-full', 18 | children, 19 | type = 'text', 20 | ...args 21 | }, 22 | ref, 23 | ) => { 24 | return ( 25 | 31 | ); 32 | }, 33 | ); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 msanvarov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /libs/ui/src/lib/social-media-links.component.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | import { FiGithub } from 'react-icons/fi'; 4 | 5 | import { ISocialMedia } from './buttons/share-post-button-container-component'; 6 | 7 | type SocialsListProps = { 8 | className?: string; 9 | itemClass?: string; 10 | socials?: ISocialMedia[]; 11 | }; 12 | 13 | const appSocials: ISocialMedia[] = [ 14 | { id: 'Github', name: 'Github', icon: , href: '#' }, 15 | ]; 16 | 17 | export const SocialMediaLinks: React.FC = ({ 18 | className, 19 | itemClass = 'block', 20 | socials = appSocials, 21 | }) => { 22 | return ( 23 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /apps/bloggo/styles/_theme-variables.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --c-primary-50: 239, 246, 255; 3 | --c-primary-100: 219, 234, 254; 4 | --c-primary-200: 191, 219, 254; 5 | --c-primary-300: 147, 197, 253; 6 | --c-primary-400: 96, 165, 250; 7 | --c-primary-500: 59, 130, 246; 8 | --c-primary-600: 37, 99, 235; 9 | --c-primary-700: 29, 78, 216; 10 | --c-primary-800: 30, 64, 175; 11 | --c-primary-900: 30, 58, 138; 12 | // SECONDARY COLOR 13 | --c-secondary-50: 240, 253, 250; 14 | --c-secondary-100: 204, 251, 241; 15 | --c-secondary-200: 153, 246, 228; 16 | --c-secondary-300: 153, 246, 228; 17 | --c-secondary-400: 45, 212, 191; 18 | --c-secondary-500: 20, 184, 166; 19 | --c-secondary-600: 13, 148, 136; 20 | --c-secondary-700: 15, 118, 110; 21 | --c-secondary-800: 17, 94, 89; 22 | --c-secondary-900: 19, 78, 74; 23 | // NEUTRAL COLOR 24 | --c-neutral-50: 248, 250, 252; 25 | --c-neutral-100: 241, 245, 249; 26 | --c-neutral-200: 226, 232, 240; 27 | --c-neutral-300: 203, 213, 225; 28 | --c-neutral-400: 148, 163, 184; 29 | --c-neutral-500: 100, 116, 139; 30 | --c-neutral-600: 71, 85, 105; 31 | --c-neutral-700: 51, 65, 85; 32 | --c-neutral-800: 30, 41, 59; 33 | --c-neutral-900: 15, 23, 42; 34 | } 35 | -------------------------------------------------------------------------------- /libs/ui/src/lib/dark-mode-toggle/dark-mode-toggle-container.component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | import { 4 | AppState, 5 | toggleDarkMode, 6 | useAppDispatch, 7 | useAppSelector, 8 | } from '@bloggo/redux'; 9 | 10 | import { DarkModeSwitch } from './dark-mode-switch.component'; 11 | 12 | type DarkModeToggleContainerProps = { 13 | className?: string; 14 | }; 15 | 16 | export const DarkModeToggleContainer: React.FC< 17 | DarkModeToggleContainerProps 18 | > = ({ className }) => { 19 | const dispatch = useAppDispatch(); 20 | const darkMode = useAppSelector((state: AppState) => state.layout.darkMode); 21 | useEffect(() => { 22 | const root = document.querySelector('html'); 23 | if (!root) return; 24 | if (darkMode) { 25 | !root.classList.contains('dark') && root.classList.add('dark'); 26 | } else { 27 | root.classList.remove('dark'); 28 | } 29 | }, [darkMode]); 30 | 31 | const toggleDarkModeOnClick = () => { 32 | dispatch(toggleDarkMode()); 33 | }; 34 | return ( 35 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /apps/bloggo/public/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS ### 2 | # General 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | # Thumbnails 8 | ._* 9 | 10 | # Files that might appear in the root of a volume 11 | .DocumentRevisions-V100 12 | .fseventsd 13 | .Spotlight-V100 14 | .TemporaryItems 15 | .Trashes 16 | .VolumeIcon.icns 17 | .com.apple.timemachine.donotpresent 18 | 19 | # Directories potentially created on remote AFP share 20 | .AppleDB 21 | .AppleDesktop 22 | Network Trash Folder 23 | Temporary Items 24 | .apdisk 25 | 26 | ### NextJS ### 27 | .next/ 28 | dist/* 29 | 30 | ### Firebase ### 31 | **/.firebaserc 32 | 33 | ### Firebase Patch ### 34 | .runtimeconfig.json 35 | .firebase/ 36 | 37 | ### React ### 38 | .DS_* 39 | *.log 40 | logs 41 | **/*.backup.* 42 | **/*.back.* 43 | 44 | node_modules 45 | bower_components 46 | psd 47 | thumb 48 | yarn.lock 49 | package-lock.json 50 | 51 | ### VisualStudioCode ### 52 | .vscode/* 53 | !.vscode/settings.json 54 | !.vscode/tasks.json 55 | !.vscode/launch.json 56 | !.vscode/extensions.json 57 | !.vscode/*.code-snippets 58 | 59 | # Local History for Visual Studio Code 60 | .history/ 61 | 62 | # Built Visual Studio Code Extensions 63 | *.vsix 64 | 65 | ### VisualStudioCode Patch ### 66 | .history 67 | .ionide 68 | 69 | # Next.js 70 | .next 71 | -------------------------------------------------------------------------------- /libs/redux/src/lib/firebase/auth.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GoogleAuthProvider, 3 | createUserWithEmailAndPassword, 4 | signInWithEmailAndPassword, 5 | signInWithPopup, 6 | signOut, 7 | updateProfile, 8 | } from 'firebase/auth'; 9 | 10 | import { auth } from './firebase'; 11 | 12 | export const login = async (email: string, password: string) => { 13 | return await signInWithEmailAndPassword(auth, email, password); 14 | }; 15 | 16 | export const loginWithGoogleProvider = async () => { 17 | const provider = new GoogleAuthProvider(); 18 | const authResponse = await signInWithPopup(auth, provider); 19 | const credential = GoogleAuthProvider.credentialFromResult(authResponse); 20 | const token = credential.accessToken; 21 | console.log(token); 22 | return authResponse.user; 23 | }; 24 | 25 | export const register = async ( 26 | username: string, 27 | email: string, 28 | password: string, 29 | ) => { 30 | const { user } = await createUserWithEmailAndPassword(auth, email, password); 31 | 32 | updateProfile(user, { 33 | displayName: username, 34 | photoURL: `https://ui-avatars.com/api/?name=${username}`, 35 | }); 36 | 37 | return user; 38 | }; 39 | 40 | export const logout = async () => { 41 | return await signOut(auth); 42 | }; 43 | -------------------------------------------------------------------------------- /libs/redux/src/lib/firebase/firebase.ts: -------------------------------------------------------------------------------- 1 | import { getApp, initializeApp } from 'firebase/app'; 2 | import { getAuth } from 'firebase/auth'; 3 | import { getFirestore } from 'firebase/firestore'; 4 | import { getStorage } from 'firebase/storage'; 5 | 6 | const firebaseConfig = { 7 | apiKey: 8 | process.env.FIREBASE_API_KEY ?? 'AIzaSyDNotinqdiNETddK4e_MA_rNsz1gkYKChw', 9 | authDomain: 10 | process.env.FIREBASE_AUTH_DOMAIN ?? 'bloggo-9fc7d.firebaseapp.com', 11 | projectId: process.env.FIREBASE_PROJECT_ID ?? 'bloggo-9fc7d', 12 | storageBucket: 13 | process.env.FIREBASE_STORAGE_BUCKET ?? 'bloggo-9fc7d.appspot.com', 14 | messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID ?? '424368452854', 15 | appId: 16 | process.env.FIREBASE_APP_ID ?? '1:424368452854:web:361dd4c39219757306e8d7', 17 | measurementId: process.env.FIREBASE_MEASUREMENT_ID ?? 'G-D2F125TYVK', 18 | }; 19 | 20 | export const initializeFirebase = () => { 21 | try { 22 | return getApp(); 23 | } catch (e) { 24 | return initializeApp(firebaseConfig); 25 | } 26 | }; 27 | 28 | const app = initializeFirebase(); 29 | export const auth = getAuth(app); 30 | export const db = getFirestore(app); 31 | export const storage = getStorage(app); 32 | export const STATE_CHANGED = 'state_changed'; 33 | -------------------------------------------------------------------------------- /apps/bloggo-e2e/src/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-namespace 12 | declare namespace Cypress { 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | interface Chainable { 15 | login(email: string, password: string): void; 16 | } 17 | } 18 | // 19 | // -- This is a parent command -- 20 | Cypress.Commands.add('login', (email, password) => { 21 | console.log('Custom command example: Login', email, password); 22 | }); 23 | // 24 | // -- This is a child command -- 25 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 26 | // 27 | // 28 | // -- This is a dual command -- 29 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 30 | // 31 | // 32 | // -- This will overwrite an existing command -- 33 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 34 | -------------------------------------------------------------------------------- /libs/ui/src/lib/image/image-inview-port.util.ts: -------------------------------------------------------------------------------- 1 | export interface InviewPortType { 2 | distanceFromEnd: number; 3 | callback: () => boolean; 4 | target: HTMLElement; 5 | } 6 | 7 | const checkInViewIntersectionObserver = ({ 8 | target, 9 | distanceFromEnd, 10 | callback, 11 | }: InviewPortType) => { 12 | const _funCallback: IntersectionObserverCallback = ( 13 | entries: IntersectionObserverEntry[], 14 | observer: IntersectionObserver, 15 | ) => { 16 | entries.map((entry: IntersectionObserverEntry) => { 17 | if (entry.isIntersecting) { 18 | // NEED CALLBACK WILL RETURN BOOLEAN ---- IF TRUE WE WILL UNOBSERVER AND FALSE IS NO 19 | const unobserve = callback(); 20 | unobserve && observer.unobserve(entry.target); 21 | } 22 | return true; 23 | }); 24 | }; 25 | 26 | // _checkBrowserSupport----- 27 | if (typeof window.IntersectionObserver === 'undefined') { 28 | console.error( 29 | 'window.IntersectionObserver === undefined! => Your Browser is Notsupport', 30 | ); 31 | return; 32 | } 33 | const options = { 34 | root: null, 35 | rootMargin: `${distanceFromEnd}px 0px`, 36 | threshold: 0, 37 | }; 38 | const observer = new IntersectionObserver(_funCallback, options); 39 | target && observer.observe(target); 40 | }; 41 | 42 | export default checkInViewIntersectionObserver; 43 | -------------------------------------------------------------------------------- /libs/redux/src/lib/store.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from '@reduxjs/toolkit'; 2 | import { setupListeners } from '@reduxjs/toolkit/query/react'; 3 | import { 4 | FLUSH, 5 | PAUSE, 6 | PERSIST, 7 | PURGE, 8 | REGISTER, 9 | REHYDRATE, 10 | persistReducer, 11 | persistStore, 12 | } from 'redux-persist'; 13 | import storage from 'redux-persist/lib/storage'; 14 | 15 | import { layoutReducer } from './layout'; 16 | import { userReducer } from './user'; 17 | 18 | const persistConfig = { 19 | key: 'root', 20 | version: 1, 21 | storage, 22 | blacklist: ['user'], 23 | }; 24 | 25 | const reducer = combineReducers({ 26 | user: userReducer, 27 | layout: layoutReducer, 28 | }); 29 | const persistedReducer = persistReducer(persistConfig, reducer); 30 | 31 | export const store = configureStore({ 32 | reducer: persistedReducer, 33 | middleware: (getDefaultMiddleware) => 34 | getDefaultMiddleware({ 35 | serializableCheck: { 36 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], 37 | }, 38 | }), 39 | // .concat(routerMiddleware(history)) 40 | devTools: true, 41 | }); 42 | 43 | setupListeners(store.dispatch); 44 | 45 | export const persistor = persistStore(store); 46 | // top-level state 47 | export type AppDispatch = typeof store.dispatch; 48 | export type AppState = ReturnType; 49 | -------------------------------------------------------------------------------- /apps/bloggo/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@glidejs/glide/dist/css/glide.core.min.css'; 2 | import { Analytics } from '@vercel/analytics/react'; 3 | import 'moment-timezone'; 4 | import { AppProps } from 'next/app'; 5 | import Head from 'next/head'; 6 | import { useRouter } from 'next/router'; 7 | import { useEffect } from 'react'; 8 | import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'; 9 | import { Provider } from 'react-redux'; 10 | import { PersistGate } from 'redux-persist/integration/react'; 11 | 12 | import { persistor, store } from '@bloggo/redux'; 13 | import { Footer, Header, Loader } from '@bloggo/ui'; 14 | 15 | import '../styles/styles.scss'; 16 | 17 | const CustomApp: React.FC = ({ Component, pageProps }) => { 18 | const router = useRouter(); 19 | 20 | useEffect(() => { 21 | // On router change 22 | window.scrollTo(0, 0); 23 | }, [router.asPath]); 24 | return ( 25 | 26 | } {...{ persistor }}> 27 | 28 | Welcome to bloggo! 29 | 30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | ); 39 | }; 40 | 41 | export default CustomApp; 42 | -------------------------------------------------------------------------------- /apps/bloggo/public/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /libs/ui/src/lib/portal/portal-navbar.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Link } from '../link.component'; 4 | 5 | const pages = [ 6 | { 7 | path: '/portal/create-post', 8 | emoji: '✍', 9 | pageName: 'Create Post', 10 | }, 11 | { 12 | path: '/portal/posts', 13 | emoji: '📕', 14 | pageName: 'Posts', 15 | }, 16 | { 17 | path: '/portal/liked-posts', 18 | emoji: '💖', 19 | pageName: 'Liked Posts', 20 | }, 21 | { 22 | path: '/portal/edit-profile', 23 | emoji: '🛠', 24 | pageName: 'Edit Profile', 25 | }, 26 | ]; 27 | 28 | export const PortalNavbar: React.FC = () => { 29 | return ( 30 |
31 |
    32 | {pages.map(({ path, pageName, emoji }, index) => { 33 | return ( 34 |
  • 35 | 41 | {emoji} 42 | {pageName} 43 | 44 |
  • 45 | ); 46 | })} 47 |
48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /libs/ui/src/lib/section-heading.component.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | import React, { HTMLAttributes, ReactNode } from 'react'; 3 | 4 | import NextPrevButtons from './buttons/next-prev-buttons.component'; 5 | 6 | export interface SectionHeadingProps 7 | extends HTMLAttributes { 8 | fontClass?: string; 9 | desc?: ReactNode; 10 | hasNextPrev?: boolean; 11 | isCenter?: boolean; 12 | } 13 | 14 | export const SectionHeading: React.FC = ({ 15 | children, 16 | desc = 'Placeholder description.', 17 | className = 'mb-12 md:mb-16 text-neutral-900 dark:text-neutral-50', 18 | isCenter = false, 19 | hasNextPrev = false, 20 | ...rest 21 | }) => { 22 | return ( 23 |
26 |
31 |

32 | {children || `Section Heading`} 33 |

34 | {desc && ( 35 | 36 | {desc} 37 | 38 | )} 39 |
40 | {hasNextPrev && !isCenter && ( 41 |
42 | 43 |
44 | )} 45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /libs/ui/src/lib/buttons/share-post-button-container-component.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | import { FiFacebook, FiInstagram, FiLinkedin, FiTwitter } from 'react-icons/fi'; 4 | 5 | type SharePostButtonContainerProps = { 6 | className?: string; 7 | itemClass?: string; 8 | }; 9 | 10 | export interface ISocialMedia { 11 | id: string; 12 | name: string; 13 | icon: JSX.Element; 14 | href: string; 15 | } 16 | 17 | // these social media links are for the blog page 18 | const socials: ISocialMedia[] = [ 19 | { id: 'Facebook', name: 'Facebook', icon: , href: '#' }, 20 | { id: 'Twitter', name: 'Twitter', icon: , href: '#' }, 21 | { id: 'Linkedin', name: 'Linkedin', icon: , href: '#' }, 22 | { id: 'Instagram', name: 'Instagram', icon: , href: '#' }, 23 | ]; 24 | 25 | export const SharePostButtonContainer: React.FC< 26 | SharePostButtonContainerProps 27 | > = ({ 28 | className = 'grid gap-[6px]', 29 | itemClass = 'w-7 h-7 text-base hover:bg-neutral-100', 30 | }) => { 31 | return ( 32 |
33 | {socials.map((item, index) => ( 34 | 42 | {item.icon} 43 | 44 | ))} 45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /libs/ui/src/lib/forms/form-feedback.component.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | import { FiXCircle } from 'react-icons/fi'; 4 | 5 | type FormFeedbackProps = { 6 | type: 'warning' | 'info' | 'success' | 'error'; 7 | message: string; 8 | }; 9 | export const FormFeedback: React.FC = ({ 10 | type, 11 | message, 12 | }) => { 13 | return ( 14 |
22 |
23 |
24 |
34 |
35 |

43 | {message} 44 |

45 |
46 |
47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "bloggo", 3 | "affected": { 4 | "defaultBase": "main" 5 | }, 6 | "tasksRunnerOptions": { 7 | "default": { 8 | "runner": "nx-cloud", 9 | "options": { 10 | "cacheableOperations": ["build", "lint", "test", "e2e"], 11 | "accessToken": "ZTNiM2IzYzctNzhlMS00MTA4LTkwYWMtMGRiYjQ4MGM1NTA3fHJlYWQtd3JpdGU=" 12 | } 13 | } 14 | }, 15 | "generators": { 16 | "@nx/react": { 17 | "application": { 18 | "babel": true 19 | } 20 | }, 21 | "@nx/next": { 22 | "application": { 23 | "style": "scss", 24 | "linter": "eslint" 25 | } 26 | } 27 | }, 28 | "defaultProject": "bloggo", 29 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 30 | "targetDefaults": { 31 | "build": { 32 | "dependsOn": ["^build"], 33 | "inputs": ["production", "^production"] 34 | }, 35 | "lint": { 36 | "inputs": ["default", "{workspaceRoot}/.eslintrc.json"] 37 | }, 38 | "e2e": { 39 | "inputs": ["default", "^production"] 40 | }, 41 | "test": { 42 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"] 43 | } 44 | }, 45 | "namedInputs": { 46 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 47 | "sharedGlobals": ["{workspaceRoot}/babel.config.json"], 48 | "production": [ 49 | "default", 50 | "!{projectRoot}/.eslintrc.json", 51 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 52 | "!{projectRoot}/tsconfig.spec.json", 53 | "!{projectRoot}/jest.config.[jt]s", 54 | "!{projectRoot}/src/test-setup.[jt]s" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /libs/ui/src/lib/post/post-preview-card-author.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Moment from 'react-moment'; 3 | 4 | import { IFirestoreUserData } from '@bloggo/redux'; 5 | 6 | import { Avatar } from '../avatar.component'; 7 | import { Link } from '../link.component'; 8 | 9 | type PostPreviewCardAuthorProps = { 10 | className?: string; 11 | author: IFirestoreUserData; 12 | date: number; 13 | hiddenAvatar?: boolean; 14 | size?: 'large' | 'normal'; 15 | }; 16 | 17 | export const PostPreviewCardAuthor: React.FC = ({ 18 | className = 'leading-none', 19 | author, 20 | date, 21 | hiddenAvatar = false, 22 | size = 'normal', 23 | }) => { 24 | return ( 25 |
30 | 34 | {!hiddenAvatar && ( 35 | 43 | )} 44 | 45 | {author.displayName} 46 | 47 | 48 | 49 | 50 | · 51 | 52 | 53 | {date} 54 | 55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /libs/ui/src/lib/buttons/bookmark-post-button.component.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, { useState } from 'react'; 3 | 4 | type BookmarkPostButtonProps = { 5 | containerClassName?: string; 6 | iconClass?: string; 7 | initBookmarked: boolean; 8 | postId: string; 9 | }; 10 | // TODO: redux state 11 | export const BookmarkPostButton: React.FC = ({ 12 | containerClassName = 'h-8 w-8 bg-neutral-50 hover:bg-neutral-100 dark:bg-neutral-800 dark:hover:bg-neutral-700', 13 | postId, 14 | initBookmarked, 15 | }) => { 16 | const [isBookmarkPostButtonClicked, setIsBookmarkPostButtonClicked] = 17 | useState(false); 18 | const isBookmarked = () => { 19 | return isBookmarkPostButtonClicked || initBookmarked; 20 | }; 21 | const handleOnBookmarkButtonClick = () => { 22 | if (isBookmarked()) { 23 | setIsBookmarkPostButtonClicked(false); 24 | } else { 25 | setIsBookmarkPostButtonClicked(true); 26 | } 27 | }; 28 | 29 | return ( 30 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /apps/bloggo/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bloggo", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/bloggo", 5 | "projectType": "application", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/next:build", 9 | "outputs": [ 10 | "{options.outputPath}" 11 | ], 12 | "defaultConfiguration": "production", 13 | "options": { 14 | "outputPath": "dist/apps/bloggo" 15 | }, 16 | "configurations": { 17 | "production": {}, 18 | "development": { 19 | "outputPath": "apps/bloggo" 20 | } 21 | } 22 | }, 23 | "serve": { 24 | "executor": "@nx/next:server", 25 | "options": { 26 | "buildTarget": "bloggo:build", 27 | "dev": true 28 | }, 29 | "configurations": { 30 | "production": { 31 | "buildTarget": "bloggo:build:production", 32 | "dev": false 33 | }, 34 | "development": { 35 | "buildTarget": "bloggo:build:development", 36 | "dev": true 37 | } 38 | }, 39 | "defaultConfiguration": "development" 40 | }, 41 | "export": { 42 | "executor": "@nx/next:export", 43 | "options": { 44 | "buildTarget": "bloggo:build:production" 45 | } 46 | }, 47 | "test": { 48 | "executor": "@nx/jest:jest", 49 | "outputs": [ 50 | "{workspaceRoot}/coverage/apps/bloggo" 51 | ], 52 | "options": { 53 | "jestConfig": "apps/bloggo/jest.config.ts", 54 | "passWithNoTests": true 55 | } 56 | }, 57 | "lint": { 58 | "executor": "@nx/linter:eslint", 59 | "outputs": [ 60 | "{options.outputFile}" 61 | ], 62 | "options": { 63 | "lintFilePatterns": [ 64 | "apps/bloggo/**/*.{ts,tsx,js,jsx}" 65 | ] 66 | } 67 | } 68 | }, 69 | "tags": [] 70 | } 71 | -------------------------------------------------------------------------------- /libs/ui/src/lib/avatar.component.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import React from 'react'; 3 | 4 | type AvatarProps = { 5 | containerClassName?: string; 6 | sizeClass?: string; 7 | radius?: string; 8 | src?: string; 9 | userName?: string; 10 | }; 11 | 12 | const avatarColors = [ 13 | '#ffdd00', 14 | '#fbb034', 15 | '#ff4c4c', 16 | '#c1d82f', 17 | '#f48924', 18 | '#7ac143', 19 | '#30c39e', 20 | '#06BCAE', 21 | '#0695BC', 22 | '#037ef3', 23 | '#146eb4', 24 | '#8e43e7', 25 | '#ea1d5d', 26 | '#fc636b', 27 | '#ff6319', 28 | '#e01f3d', 29 | '#a0ac48', 30 | '#00d1b2', 31 | '#472f92', 32 | '#388ed1', 33 | '#a6192e', 34 | '#4a8594', 35 | '#7B9FAB', 36 | '#1393BD', 37 | '#5E13BD', 38 | '#E208A7', 39 | ]; 40 | 41 | export const Avatar: React.FC = ({ 42 | containerClassName = 'ring-1 ring-white dark:ring-neutral-900', 43 | sizeClass = 'h-6 w-6 text-sm', 44 | radius = 'rounded-md', 45 | src, 46 | userName, 47 | }) => { 48 | const url = src || ''; 49 | const name = userName || 'Anonymous'; 50 | const _setBgColor = (name: string) => { 51 | const backgroundIndex = Math.floor( 52 | name.charCodeAt(0) % avatarColors.length, 53 | ); 54 | return avatarColors[backgroundIndex]; 55 | }; 56 | 57 | return ( 58 |
62 | {url && ( 63 | {name} 69 | )} 70 | {name[0]} 71 |
72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /apps/bloggo/pages/portal/liked-posts.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { AppLayout, AuthCheck, Metatags, PortalNavbar } from '@bloggo/ui'; 4 | 5 | const LikedPostsPage: React.FC = () => { 6 | return ( 7 | <> 8 | 9 | 14 | 15 |
16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 | 25 | 26 | 29 | 30 | 31 | {/* {uid && } */} 32 |
27 | Post 28 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | 42 | ); 43 | }; 44 | 45 | export default LikedPostsPage; 46 | -------------------------------------------------------------------------------- /libs/ui/src/lib/search-dropdown.component.tsx: -------------------------------------------------------------------------------- 1 | import { Popover, Transition } from '@headlessui/react'; 2 | import React, { Fragment, useRef } from 'react'; 3 | import { FiSearch } from 'react-icons/fi'; 4 | 5 | import { Input } from './input.component'; 6 | 7 | export const SearchDropdown: React.FC = () => { 8 | const inputElRef = useRef(null); 9 | 10 | return ( 11 | 12 | {({ open }) => { 13 | if (open) { 14 | setTimeout(() => { 15 | inputElRef.current?.focus(); 16 | }, 100); 17 | } 18 | 19 | return ( 20 | <> 21 | 22 | 23 | 24 | 25 | 35 | 39 |
40 | 45 | 46 |
47 |
48 |
49 | 50 | ); 51 | }} 52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /libs/ui/src/lib/buttons/next-prev-buttons.component.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | import React from 'react'; 3 | import { FiChevronLeft, FiChevronRight } from 'react-icons/fi'; 4 | 5 | export interface NextPrevButtonsProps { 6 | containerClassName?: string; 7 | currentPage?: number; 8 | totalPage?: number; 9 | btnClassName?: string; 10 | onClickNext?: () => void; 11 | onClickPrev?: () => void; 12 | onlyNext?: boolean; 13 | onlyPrev?: boolean; 14 | } 15 | const NextPrevButtons: React.FC = ({ 16 | containerClassName = '', 17 | onClickNext = () => {}, 18 | onClickPrev = () => {}, 19 | btnClassName = 'w-10 h-10', 20 | onlyNext = false, 21 | onlyPrev = false, 22 | }) => { 23 | return ( 24 |
29 | {!onlyNext && ( 30 | 40 | )} 41 | {!onlyPrev && ( 42 | 50 | )} 51 |
52 | ); 53 | }; 54 | 55 | export default NextPrevButtons; 56 | -------------------------------------------------------------------------------- /libs/ui/src/lib/footer.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Logo } from './logo.component'; 4 | import { SocialMediaLinks } from './social-media-links.component'; 5 | 6 | const footerNavigationOptions = [ 7 | { 8 | id: '1', 9 | title: 'Explore', 10 | menus: [ 11 | { href: '/login', label: 'Login' }, 12 | { href: '/register', label: 'Register' }, 13 | { href: '/forgot-password', label: 'Forgot Password' }, 14 | ], 15 | }, 16 | ]; 17 | 18 | export const Footer: React.FC = () => { 19 | return ( 20 |
21 |
22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | {footerNavigationOptions.map((menu, index) => ( 31 |
32 |

33 | {menu.title} 34 |

35 | 48 |
49 | ))} 50 |
51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /libs/ui/src/lib/post/post-preview-card-action-buttons.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { AppState, useAppSelector } from '@bloggo/redux'; 4 | 5 | import { BookmarkPostButton, LikePostButton } from '../buttons'; 6 | import { CommentOnPostButton } from '../buttons/comment-on-post-button.component'; 7 | 8 | type PostPreviewCardActionButtonsProps = { 9 | className?: string; 10 | itemClass?: string; 11 | likes: number; 12 | postId: string; 13 | href: string; 14 | isBookmarked: boolean; 15 | commentCount: number; 16 | hiddenCommentOnMobile?: boolean; 17 | onClickLike?: (id: string) => void; 18 | classBgIcon?: string; 19 | }; 20 | export const PostPreviewCardActionButtons: React.FC< 21 | PostPreviewCardActionButtonsProps 22 | > = ({ 23 | className = '', 24 | itemClass = 'px-3 h-8 text-xs', 25 | hiddenCommentOnMobile = true, 26 | isBookmarked, 27 | likes, 28 | postId, 29 | href, 30 | commentCount, 31 | classBgIcon, 32 | // eslint-disable-next-line @typescript-eslint/no-empty-function 33 | onClickLike = () => {}, 34 | }) => { 35 | const { user } = useAppSelector((state: AppState) => state.user); 36 | return ( 37 | <> 38 |
39 | {user?.uid && ( 40 | 44 | )} 45 | 46 | 53 |
54 |
57 | 6 min read 58 | {/* TODO: fix postId to have the slug */} 59 | 64 |
65 | 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /libs/ui/src/lib/navbar/navigation.component.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/anchor-is-valid */ 2 | import { nanoid } from '@reduxjs/toolkit'; 3 | import { entries, groupBy, map } from 'lodash'; 4 | import React, { Fragment } from 'react'; 5 | 6 | import { AppState, useAppSelector } from '@bloggo/redux'; 7 | 8 | import { INavEntry, NavigationEntry } from './navigation-entry.component'; 9 | 10 | const generateUUID = () => `nc_${nanoid()}`; 11 | 12 | const dashboardNavigationMenus: INavEntry[] = [ 13 | { 14 | id: generateUUID(), 15 | href: '/portal/posts', 16 | name: 'Posts', 17 | }, 18 | { 19 | id: generateUUID(), 20 | href: '/portal/liked-posts', 21 | name: 'Liked Posts', 22 | }, 23 | { 24 | id: generateUUID(), 25 | href: '/portal/edit-profile', 26 | name: 'Profile', 27 | }, 28 | ]; 29 | 30 | export const navigationOptions: INavEntry[] = [ 31 | // hidden menu options until authentication is completed 32 | { 33 | id: generateUUID(), 34 | href: '/dashboard', 35 | name: 'Dashboard', 36 | type: 'dropdown', 37 | children: dashboardNavigationMenus, 38 | restriction: 'private', 39 | }, 40 | { 41 | id: generateUUID(), 42 | href: 'https://github.com/msanvarov/bloggo', 43 | name: 'Github', 44 | restriction: 'public', 45 | }, 46 | ]; 47 | 48 | export const Navigation: React.FC = () => { 49 | const { user } = useAppSelector((state: AppState) => state.user); 50 | return ( 51 |
    52 | {map( 53 | entries(groupBy(navigationOptions, 'restriction')), 54 | ([restriction, navigationEntries], i) => { 55 | if (restriction === 'private' && !user) { 56 | return null; 57 | } 58 | return ( 59 | 60 | {navigationEntries.map((navigationEntry) => ( 61 | 65 | ))} 66 | 67 | ); 68 | }, 69 | )} 70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /libs/ui/src/lib/post/post-edit.component.tsx: -------------------------------------------------------------------------------- 1 | import { doc } from 'firebase/firestore'; 2 | import React from 'react'; 3 | import { useDocumentDataOnce } from 'react-firebase-hooks/firestore'; 4 | import { Data } from 'react-firebase-hooks/firestore/dist/firestore/types'; 5 | 6 | import { IFirestorePostData, db } from '@bloggo/redux'; 7 | 8 | import { BadgeList } from '../badge'; 9 | import { ImageContainer } from '../image'; 10 | import { PostForm } from './post-form.component'; 11 | 12 | type PostEditProps = { 13 | className?: string; 14 | uid: string; 15 | slug: string | string[]; 16 | }; 17 | 18 | export const PostEdit: React.FC = ({ uid, slug }) => { 19 | // TODO: cleanup 20 | const postRef = doc(db, `users/${uid}/posts/${slug}`); 21 | const [post] = useDocumentDataOnce(postRef); 22 | return post ? ( 23 | <> 24 |
25 |
26 |
27 | 28 |

29 | {post['title']} 30 |

31 | {post['description'] && ( 32 | 33 | {post['description']} 34 | 35 | )} 36 |
37 |
38 |
39 |
40 | 49 |
50 | } 53 | /> 54 |
55 | 56 | ) : ( 57 |

Post couldn't be found...

58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /libs/ui/src/lib/sliders/newest-authors-slider.component.tsx: -------------------------------------------------------------------------------- 1 | import Glide from '@glidejs/glide'; 2 | import { nanoid } from '@reduxjs/toolkit'; 3 | import React, { useEffect } from 'react'; 4 | 5 | import { IFirestoreUsernameData } from '@bloggo/redux'; 6 | 7 | import NextPrevButtons from '../buttons/next-prev-buttons.component'; 8 | import { AuthorCard } from '../cards/author-card.component'; 9 | import { SectionHeading } from '../section-heading.component'; 10 | 11 | type NewestAuthorsSliderProps = { 12 | className?: string; 13 | heading: string; 14 | subHeading: string; 15 | authors: IFirestoreUsernameData[]; 16 | }; 17 | 18 | export const NewestAuthorsSlider: React.FC = ({ 19 | heading, 20 | subHeading, 21 | className = '', 22 | authors, 23 | }) => { 24 | const GLIDE_SELECTOR_CLASS = 'glide_' + nanoid(); 25 | useEffect(() => { 26 | setTimeout(() => { 27 | new Glide(`.${GLIDE_SELECTOR_CLASS}`, { 28 | perView: 5, 29 | gap: 32, 30 | bound: true, 31 | breakpoints: { 32 | 1280: { 33 | perView: 4, 34 | }, 35 | 1023: { 36 | gap: 24, 37 | perView: 3, 38 | }, 39 | 767: { 40 | gap: 20, 41 | perView: 2, 42 | }, 43 | 639: { 44 | gap: 20, 45 | perView: 2, 46 | }, 47 | 500: { 48 | gap: 20, 49 | perView: 1, 50 | }, 51 | }, 52 | }).mount(); 53 | }, 100); 54 | }, []); 55 | return ( 56 |
57 |
58 | 59 | {heading} 60 | 61 |
62 |
    63 | {authors.map((item, index) => ( 64 |
  • 65 | 66 |
  • 67 | ))} 68 |
69 |
70 | 74 |
75 |
76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /libs/ui/src/lib/buttons/comment-on-post-button.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Link } from '../link.component'; 4 | 5 | type CommentOnPostButton = { 6 | className?: string; 7 | href: string; 8 | commentCount?: number; 9 | }; 10 | export const CommentOnPostButton: React.FC = ({ 11 | className = 'flex px-3 h-8 text-xs', 12 | href, 13 | commentCount, 14 | }) => { 15 | return ( 16 | 21 | 22 | 29 | 35 | 41 | 47 | 48 | 49 | 50 | {commentCount} 51 | 52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /libs/ui/src/lib/post/post-entry-metadata.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Moment from 'react-moment'; 3 | 4 | import { Avatar } from '../avatar.component'; 5 | import { Link } from '../link.component'; 6 | 7 | type PostEntryMetadataProps = { 8 | className?: string; 9 | meta: { 10 | author: { 11 | username: string; 12 | }; 13 | date: 14 | | number 15 | | { 16 | seconds: number; 17 | nanoseconds: number; 18 | }; 19 | readingTime: string; 20 | }; 21 | size?: 'large' | 'normal'; 22 | avatarRounded?: string; 23 | }; 24 | 25 | export const PostEntryMetadata: React.FC = ({ 26 | className = 'leading-none', 27 | meta: { date, author, readingTime }, 28 | size = 'normal', 29 | avatarRounded, 30 | }) => { 31 | return ( 32 |
38 | 43 | 53 | 54 |
55 |
56 | 61 | {author.username} 62 | 63 |
64 |
65 | 66 | 67 | {typeof date === 'object' ? date.seconds : date} 68 | 69 | 70 | · 71 | 72 | {readingTime} min read 73 | 74 |
75 |
76 |
77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /libs/ui/src/lib/badge/badge.component.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | 4 | import { Link } from '../link.component'; 5 | 6 | export type BadgeColor = 7 | | 'pink' 8 | | 'green' 9 | | 'yellow' 10 | | 'red' 11 | | 'indigo' 12 | | 'blue' 13 | | 'purple' 14 | | 'gray'; 15 | 16 | type BadgeProps = { 17 | className?: string; 18 | name: React.ReactNode; 19 | color?: BadgeColor; 20 | href?: string; 21 | }; 22 | 23 | export const Badge: React.FC = ({ 24 | className = '', 25 | name, 26 | color = 'blue', 27 | href, 28 | }) => { 29 | const classes = classNames( 30 | 'relative inline-flex px-2.5 py-1 rounded-full font-medium text-xs', 31 | className, 32 | ); 33 | 34 | const getColorClass = (hasHover = true) => { 35 | switch (color) { 36 | case 'pink': 37 | return `text-pink-800 bg-pink-100 ${ 38 | hasHover ? 'hover:bg-pink-200' : '' 39 | }`; 40 | case 'red': 41 | return `text-red-800 bg-red-100 ${hasHover ? 'hover:bg-red-200' : ''}`; 42 | case 'gray': 43 | return `text-gray-800 bg-gray-100 ${ 44 | hasHover ? 'hover:bg-gray-200' : '' 45 | }`; 46 | case 'green': 47 | return `text-green-800 bg-green-100 ${ 48 | hasHover ? 'hover:bg-green-200' : '' 49 | }`; 50 | case 'purple': 51 | return `text-white-800 bg-purple-100 ${ 52 | hasHover ? 'hover:bg-purple-200' : '' 53 | }`; 54 | case 'indigo': 55 | return `text-indigo-800 bg-indigo-100 ${ 56 | hasHover ? 'hover:bg-indigo-200' : '' 57 | }`; 58 | case 'yellow': 59 | return `text-yellow-800 bg-yellow-100 ${ 60 | hasHover ? 'hover:bg-yellow-200' : '' 61 | }`; 62 | case 'blue': 63 | return `text-blue-800 bg-blue-100 ${ 64 | hasHover ? 'hover:bg-blue-200' : '' 65 | }`; 66 | default: 67 | return `text-pink-800 bg-pink-100 ${ 68 | hasHover ? 'hover:bg-pink-200' : '' 69 | }`; 70 | } 71 | }; 72 | 73 | return href ? ( 74 | 82 | {name} 83 | 84 | ) : ( 85 | {name} 86 | ); 87 | }; 88 | -------------------------------------------------------------------------------- /apps/bloggo/pages/portal/posts.tsx: -------------------------------------------------------------------------------- 1 | import { QueryDocumentSnapshot } from 'firebase/firestore'; 2 | import React from 'react'; 3 | 4 | import { AppState, useAppSelector } from '@bloggo/redux'; 5 | import { 6 | AppLayout, 7 | AuthCheck, 8 | Metatags, 9 | PortalNavbar, 10 | UserPostsList, 11 | } from '@bloggo/ui'; 12 | 13 | const PostsPage: React.FC = () => { 14 | const { 15 | user: { uid }, 16 | } = useAppSelector((state: AppState) => state.user); 17 | 18 | return ( 19 | <> 20 | 21 | 26 | 27 |
28 | 29 |
30 |
31 |
32 |
33 |
34 |
35 | 36 | 37 | 38 | 41 | 44 | 45 | 48 | 49 | 50 | {uid && } 51 |
39 | Post 40 | 42 | Status 43 | 46 | Edit 47 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | 61 | ); 62 | }; 63 | 64 | export default PostsPage; 65 | -------------------------------------------------------------------------------- /libs/ui/src/lib/menu-bar.component.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, Transition } from '@headlessui/react'; 2 | import { useRouter } from 'next/router'; 3 | import React, { Fragment, useEffect, useState } from 'react'; 4 | 5 | import { MobileNavbar } from './navbar/mobile-navbar.component'; 6 | 7 | export const MenuBar: React.FC = () => { 8 | const router = useRouter(); 9 | const [showMenuBar, setShowMenuBar] = useState(false); 10 | // Hide menu bar on route change 11 | useEffect(() => { 12 | setShowMenuBar(false); 13 | }, [router.asPath]); 14 | 15 | const handleOpenMenu = () => setShowMenuBar(true); 16 | const handleCloseMenu = () => setShowMenuBar(false); 17 | 18 | return ( 19 | <> 20 | 37 | 38 | 39 | 44 |
45 | 54 |
55 | 56 |
57 |
58 | 59 | 68 | 69 | 70 |
71 |
72 |
73 | 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /libs/ui/src/lib/post/post-preview-card.component.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React from 'react'; 3 | 4 | import { IFirestorePostData, IFirestoreUserData } from '@bloggo/redux'; 5 | 6 | import { BadgeList } from '../badge'; 7 | import { Link } from '../link.component'; 8 | import { PostPreviewCardActionButtons } from './post-preview-card-action-buttons.component'; 9 | import { PostPreviewCardAuthor } from './post-preview-card-author.component'; 10 | import { PostThumbnail } from './post-thumbnail.component'; 11 | 12 | interface PostPreviewCardProps { 13 | className?: string; 14 | author: IFirestoreUserData; 15 | post: IFirestorePostData; 16 | ratio?: string; 17 | isAuthorHidden?: boolean; 18 | } 19 | 20 | export const PostPreviewCard: React.FC = ({ 21 | className = 'h-full', 22 | post, 23 | author, 24 | isAuthorHidden = false, 25 | ratio = 'aspect-w-4 aspect-h-3', 26 | }) => { 27 | return ( 28 |
34 |
37 |
38 | 46 |
47 |
48 | 53 | 54 |
55 | {/* badges */} 56 | 57 |
58 |
59 | 60 |
61 | {!isAuthorHidden ? ( 62 | 63 | ) : ( 64 | {post.createdAt} 65 | )} 66 |

67 | 68 | {post.title} 69 | 70 |

71 |
72 | 82 |
83 |
84 |
85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /libs/ui/src/lib/navbar/navbar.component.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { useRouter } from 'next/router'; 3 | import { FiPenTool, FiSettings } from 'react-icons/fi'; 4 | 5 | import { AppState, logout, useAppSelector } from '@bloggo/redux'; 6 | 7 | import { Button } from '../buttons'; 8 | import { DarkModeToggleContainer } from '../dark-mode-toggle'; 9 | import { Logo } from '../logo.component'; 10 | import { MenuBar } from '../menu-bar.component'; 11 | import { SearchDropdown } from '../search-dropdown.component'; 12 | import { NavbarActionButton } from './navbar-action-button.component'; 13 | import './navbar.module.scss'; 14 | import { Navigation } from './navigation.component'; 15 | 16 | /* eslint-disable-next-line */ 17 | export interface NavbarProps { 18 | isTopOfPage: boolean; 19 | } 20 | 21 | export const Navbar: React.FC = ({ isTopOfPage }) => { 22 | const router = useRouter(); 23 | const { user } = useAppSelector((state: AppState) => state.user); 24 | return ( 25 |
31 |
32 |
33 | 34 | 35 |
36 |
37 |
38 | 39 | 40 | {user ? ( 41 | <> 42 |
70 |
71 | 72 |
73 |
74 |
75 |
76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /apps/bloggo/pages/portal/edit-profile.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { AppState, useAppSelector } from '@bloggo/redux'; 4 | import { 5 | AppLayout, 6 | AuthCheck, 7 | Button, 8 | Input, 9 | Label, 10 | Metatags, 11 | PortalNavbar, 12 | } from '@bloggo/ui'; 13 | 14 | const EditProfilePage: React.FC = () => { 15 | const { username } = useAppSelector((state: AppState) => state.user); 16 | return ( 17 | <> 18 | 19 | 24 | 25 |
26 | {/* NAVBAR */} 27 | 28 |
29 | 30 |
31 |
32 |
37 | 46 | 54 | 62 | 70 | 73 |
74 |
75 |
76 |
77 |
78 |
79 | 80 | ); 81 | }; 82 | 83 | export default EditProfilePage; 84 | -------------------------------------------------------------------------------- /libs/ui/src/lib/app-layout.component.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { getAuth } from 'firebase/auth'; 3 | import { doc, getFirestore, onSnapshot } from 'firebase/firestore'; 4 | import { ReactNode, useEffect } from 'react'; 5 | import { useAuthState } from 'react-firebase-hooks/auth'; 6 | 7 | import { setUser, setUsername, useAppDispatch } from '@bloggo/redux'; 8 | 9 | import { PageHeading } from './page-heading.component'; 10 | 11 | const db = getFirestore(); 12 | const auth = getAuth(); 13 | 14 | type AppLayoutProps = { 15 | className?: string; 16 | heading?: string; 17 | headingEmoji?: string; 18 | subHeading?: string; 19 | // basic layout 20 | basicLayout?: boolean; 21 | children: ReactNode; 22 | }; 23 | 24 | export const AppLayout = ({ 25 | className = '', 26 | heading, 27 | subHeading, 28 | headingEmoji, 29 | children, 30 | basicLayout = false, 31 | }: AppLayoutProps) => { 32 | const dispath = useAppDispatch(); 33 | const [user] = useAuthState(auth); 34 | 35 | useEffect(() => { 36 | // turn off realtime subscription 37 | let unsubscribe; 38 | 39 | if (user) { 40 | dispath( 41 | setUser({ 42 | uid: user.uid, 43 | email: user.email, 44 | emailVerified: user.emailVerified, 45 | displayName: user.displayName, 46 | photoURL: user.photoURL, 47 | createdAt: user.metadata.creationTime, 48 | lastLoginAt: user.metadata.lastSignInTime, 49 | }), 50 | ); 51 | unsubscribe = onSnapshot(doc(db, 'users', user.uid), (doc) => { 52 | const data = doc.data(); 53 | if (data) { 54 | dispath(setUsername(data['username'])); 55 | } 56 | }); 57 | } else { 58 | dispath(setUsername(null)); 59 | dispath(setUser(null)); 60 | } 61 | 62 | return unsubscribe; 63 | }, [user]); 64 | 65 | return basicLayout ? ( 66 | // eslint-disable-next-line react/jsx-no-useless-fragment 67 | <>{children} 68 | ) : ( 69 |
70 | {/* background */} 71 |
76 |
77 | {/* heading */} 78 |
79 | {heading} 80 | {subHeading && ( 81 | 82 | {subHeading} 83 | 84 | )} 85 |
86 | {/* content */} 87 |
88 | {children} 89 |
90 |
91 |
92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /libs/ui/src/lib/buttons/button.component.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/anchor-is-valid */ 2 | import classNames from 'classnames'; 3 | import Link from 'next/link'; 4 | import { ButtonHTMLAttributes, ReactNode } from 'react'; 5 | 6 | type ButtonProps = { 7 | primary?: boolean; 8 | className?: string; 9 | translate?: string; 10 | sizeClass?: string; 11 | fontSize?: string; 12 | loading?: boolean; 13 | disabled?: boolean; 14 | type?: ButtonHTMLAttributes['type']; 15 | href?: string; 16 | targetBlank?: boolean; 17 | onClick?: () => void; 18 | children?: ReactNode; 19 | }; 20 | 21 | export const Button = ({ 22 | primary, 23 | className = 'text-neutral-700 dark:text-neutral-200', 24 | translate = '', 25 | sizeClass = 'px-4 py-3 sm:px-6', 26 | fontSize = 'text-sm sm:text-base font-medium', 27 | disabled = false, 28 | href, 29 | children, 30 | targetBlank, 31 | type, 32 | loading, 33 | // eslint-disable-next-line @typescript-eslint/no-empty-function 34 | onClick = () => {}, 35 | }: ButtonProps) => { 36 | const classes = classNames( 37 | 'relative h-auto inline-flex items-center justify-center rounded-full transition-colors', 38 | fontSize, 39 | sizeClass, 40 | translate, 41 | { 42 | 'disabled:bg-opacity-70 bg-primary-6000 hover:bg-primary-700 text-neutral-50': 43 | primary, 44 | }, 45 | className, 46 | 'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-6000 dark:focus:ring-offset-0', 47 | ); 48 | 49 | if (href) { 50 | const externalLinkRegex = /^http/; 51 | return externalLinkRegex.test(href) ? ( 52 | 61 | {children || `External Link`} 62 | 63 | ) : ( 64 | 65 | {children || `Link`} 66 | 67 | ); 68 | } 69 | return ( 70 | 102 | ); 103 | }; 104 | -------------------------------------------------------------------------------- /libs/ui/src/lib/post/post-filter-list-box.component.tsx: -------------------------------------------------------------------------------- 1 | import { Listbox, Transition } from '@headlessui/react'; 2 | import React, { Fragment, useState } from 'react'; 3 | import { FiCheck, FiChevronDown } from 'react-icons/fi'; 4 | 5 | import { Button } from '../buttons'; 6 | 7 | // TODO: move this to a better place 8 | const FILTERS = [ 9 | { id: 0, name: 'Most Recent' }, 10 | { id: 1, name: 'Most Liked' }, 11 | { id: 2, name: 'Most Discussed' }, 12 | { id: 3, name: 'Most Viewed' }, 13 | ]; 14 | 15 | export const PostFilterListBox: React.FC = () => { 16 | const [selected, setSelected] = useState(FILTERS[0]); 17 | return ( 18 | 19 |
20 | 21 | 33 | 34 | 40 | 41 | {FILTERS.map((item, index) => ( 42 | 45 | `${ 46 | active 47 | ? 'text-primary-700 dark:text-neutral-200 bg-primary-50 dark:bg-neutral-700' 48 | : '' 49 | } cursor-default select-none relative py-2 pl-10 pr-4` 50 | } 51 | value={item} 52 | > 53 | {({ selected }) => ( 54 | <> 55 | 60 | {item.name} 61 | 62 | {selected ? ( 63 | 64 | 66 | ) : null} 67 | 68 | )} 69 | 70 | ))} 71 | 72 | 73 |
74 |
75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /apps/bloggo/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'tailwind'; 2 | 3 | @import 'fonts'; 4 | @import 'polyfills'; 5 | @import 'theme-variables'; 6 | 7 | .root { 8 | overflow: hidden; 9 | } 10 | 11 | .nc-will-change-transform { 12 | will-change: transform; 13 | } 14 | .nc-will-change-top { 15 | will-change: top; 16 | } 17 | 18 | .nc-card-title { 19 | @apply transition-colors hover:text-primary-900 dark:hover:text-primary-300 duration-75; 20 | } 21 | 22 | .nc-PostCardCommentBtn, 23 | .bookmark-post-button, 24 | .like-post-button, 25 | .nc-CommentCardLikeReply > button { 26 | &:hover { 27 | span { 28 | color: inherit; 29 | } 30 | } 31 | } 32 | 33 | .react-tabs__tab--selected { 34 | button { 35 | @apply bg-primary-500 text-secondary-50; 36 | } 37 | } 38 | 39 | .nc-box-has-hover { 40 | @apply bg-white rounded-3xl border border-neutral-200 border-opacity-70 hover:bg-neutral-50 hover:shadow-xl hover:border-transparent transition-shadow; 41 | } 42 | 43 | .nc-dark-box-bg-has-hover { 44 | @apply dark:bg-neutral-800 dark:bg-opacity-30 dark:border dark:border-neutral-700 dark:hover:bg-neutral-800 dark:hover:shadow-2xl; 45 | } 46 | 47 | .nc-dark-box-bg { 48 | @apply dark:bg-neutral-800 dark:bg-opacity-30 dark:border dark:border-neutral-700 dark:shadow-2xl; 49 | } 50 | 51 | .nc-origin-100 { 52 | transform-origin: 100% 50% 0px; 53 | } 54 | .nc-origin-50 { 55 | transform-origin: 50% 50% 0px; 56 | } 57 | 58 | .nc-PostFeaturedMedia { 59 | .glide__bullet--active { 60 | @apply bg-white bg-opacity-100; 61 | } 62 | } 63 | 64 | .lds-ellipsis { 65 | display: inline-block; 66 | position: relative; 67 | width: 80px; 68 | height: 80px; 69 | } 70 | .lds-ellipsis div { 71 | position: absolute; 72 | top: 33px; 73 | width: 13px; 74 | height: 13px; 75 | border-radius: 50%; 76 | background: #fff; 77 | animation-timing-function: cubic-bezier(0, 1, 1, 0); 78 | } 79 | .lds-ellipsis div:nth-child(1) { 80 | left: 8px; 81 | animation: lds-ellipsis1 0.6s infinite; 82 | } 83 | .lds-ellipsis div:nth-child(2) { 84 | left: 8px; 85 | animation: lds-ellipsis2 0.6s infinite; 86 | } 87 | .lds-ellipsis div:nth-child(3) { 88 | left: 32px; 89 | animation: lds-ellipsis2 0.6s infinite; 90 | } 91 | .lds-ellipsis div:nth-child(4) { 92 | left: 56px; 93 | animation: lds-ellipsis3 0.6s infinite; 94 | } 95 | @keyframes lds-ellipsis1 { 96 | 0% { 97 | transform: scale(0); 98 | } 99 | 100% { 100 | transform: scale(1); 101 | } 102 | } 103 | @keyframes lds-ellipsis3 { 104 | 0% { 105 | transform: scale(1); 106 | } 107 | 100% { 108 | transform: scale(0); 109 | } 110 | } 111 | @keyframes lds-ellipsis2 { 112 | 0% { 113 | transform: translate(0, 0); 114 | } 115 | 100% { 116 | transform: translate(24px, 0); 117 | } 118 | } 119 | .nc-animation-spin { 120 | animation: myspin 20s linear infinite; 121 | animation-play-state: paused; 122 | 123 | &.playing { 124 | animation-play-state: running; 125 | } 126 | 127 | @keyframes myspin { 128 | from { 129 | transform: rotate(0deg); 130 | } 131 | to { 132 | transform: rotate(360deg); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /libs/ui/src/lib/toast.component.tsx: -------------------------------------------------------------------------------- 1 | import { Transition } from '@headlessui/react'; 2 | import React, { Fragment } from 'react'; 3 | import { 4 | FiAlertCircle, 5 | FiCheckCircle, 6 | FiInfo, 7 | FiX, 8 | FiXCircle, 9 | } from 'react-icons/fi'; 10 | 11 | export type ToastProps = { 12 | isOpen: boolean; 13 | toggle: () => void; 14 | text: { 15 | heading: string; 16 | body: string; 17 | }; 18 | type?: 'success' | 'error' | 'info' | 'warning'; 19 | }; 20 | 21 | export const Toast: React.FC = ({ 22 | isOpen, 23 | toggle, 24 | text: { heading, body }, 25 | type = 'info', 26 | }) => { 27 | const getToastIcon = (type: 'success' | 'error' | 'info' | 'warning') => { 28 | switch (type) { 29 | case 'success': 30 | return ( 31 |