├── remoteconfig.template.json ├── packages ├── core │ ├── src │ │ ├── typings.d.ts │ │ ├── constants │ │ │ └── composer.ts │ │ ├── utils │ │ │ ├── nop.ts │ │ │ ├── logger.ts │ │ │ ├── transitions.tsx │ │ │ └── locale.tsx │ │ ├── components │ │ │ ├── LocaleProvider │ │ │ │ ├── config │ │ │ │ │ └── locales.ts │ │ │ │ ├── hooks │ │ │ │ │ └── useLocale.ts │ │ │ │ ├── contexts │ │ │ │ │ └── LocaleContext.ts │ │ │ │ └── index.tsx │ │ │ ├── PostCard │ │ │ │ ├── components │ │ │ │ │ ├── Timestamp.tsx │ │ │ │ │ └── Content │ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── AuthProvider │ │ │ │ ├── hooks │ │ │ │ │ └── useAuth.ts │ │ │ │ ├── enums │ │ │ │ │ └── SignInProviderId.ts │ │ │ │ ├── contexts │ │ │ │ │ └── AuthContext.ts │ │ │ │ ├── components │ │ │ │ │ └── SignInDialog.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── config │ │ │ │ │ └── signInProviders.ts │ │ │ ├── App │ │ │ │ ├── hooks │ │ │ │ │ └── useConfig.ts │ │ │ │ ├── contexts │ │ │ │ │ └── ConfigContext.ts │ │ │ │ ├── components │ │ │ │ │ ├── SinglePostView.tsx │ │ │ │ │ └── FullView.tsx │ │ │ │ └── index.tsx │ │ │ ├── ServiceSourceProvider │ │ │ │ ├── hooks │ │ │ │ │ ├── useAuthSource.ts │ │ │ │ │ └── useDataSource.ts │ │ │ │ ├── contexts │ │ │ │ │ └── ServiceSourceContext.ts │ │ │ │ └── index.tsx │ │ │ ├── BackofficeApp │ │ │ │ ├── index.tsx │ │ │ │ └── BackofficeDialog.tsx │ │ │ ├── PendingPostList │ │ │ │ └── index.tsx │ │ │ ├── Buttons │ │ │ │ ├── ExpandButton.tsx │ │ │ │ ├── LoadMoreButton.tsx │ │ │ │ ├── SignInButton.tsx │ │ │ │ ├── CancelButton.tsx │ │ │ │ ├── ReplyButton.tsx │ │ │ │ ├── QuickReplyButton.tsx │ │ │ │ ├── SignOutButton.tsx │ │ │ │ ├── LikeButton.tsx │ │ │ │ ├── ManagerButton.tsx │ │ │ │ ├── ReadMoreButton.tsx │ │ │ │ └── ShareButton.tsx │ │ │ ├── PostComposer │ │ │ │ ├── components │ │ │ │ │ └── Composer │ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── PostList │ │ │ │ └── index.tsx │ │ │ └── PendingPostCard │ │ │ │ └── index.tsx │ │ ├── types │ │ │ ├── PostAuthor.ts │ │ │ ├── SourceService.ts │ │ │ ├── User.ts │ │ │ ├── AuthService.ts │ │ │ ├── PendingPost.ts │ │ │ ├── Post.ts │ │ │ └── PostService.ts │ │ ├── enums │ │ │ ├── RequestState.ts │ │ │ └── PostUpdateResult.ts │ │ ├── config │ │ │ └── app.ts │ │ ├── hooks │ │ │ ├── useSizeCheck.ts │ │ │ └── useIntersectionObserver.ts │ │ ├── services │ │ │ ├── auth.ts │ │ │ ├── source.ts │ │ │ └── post.ts │ │ └── index.tsx │ ├── .eslintignore │ ├── .editorconfig │ ├── .prettierrc │ ├── tsconfig.json │ ├── .eslintrc │ ├── package.json │ └── README.md ├── discuzz │ ├── src │ │ ├── typings.d.ts │ │ ├── utils │ │ │ └── darkMode.ts │ │ ├── enums │ │ │ └── Theme.ts │ │ ├── config │ │ │ └── mui.ts │ │ └── index.tsx │ ├── .eslintignore │ ├── .editorconfig │ ├── .prettierrc │ ├── tsconfig.json │ ├── .eslintrc │ ├── package.json │ └── README.md ├── auth-firebase │ ├── src │ │ ├── typings.d.ts │ │ └── index.tsx │ ├── .eslintignore │ ├── .editorconfig │ ├── .prettierrc │ ├── tsconfig.json │ ├── .eslintrc │ ├── README.md │ └── package.json ├── locale-en │ ├── src │ │ ├── typings.d.ts │ │ └── index.tsx │ ├── .eslintignore │ ├── .editorconfig │ ├── .prettierrc │ ├── tsconfig.json │ ├── .eslintrc │ └── package.json ├── locale-vi │ ├── src │ │ ├── typings.d.ts │ │ └── index.tsx │ ├── .eslintignore │ ├── .editorconfig │ ├── .prettierrc │ ├── tsconfig.json │ ├── .eslintrc │ └── package.json ├── composer-markdown │ ├── src │ │ ├── typings.d.ts │ │ └── index.tsx │ ├── .eslintignore │ ├── .editorconfig │ ├── .prettierrc │ ├── tsconfig.json │ ├── .eslintrc │ └── package.json ├── data-firestore │ ├── src │ │ └── typings.d.ts │ ├── .eslintignore │ ├── .editorconfig │ ├── .prettierrc │ ├── tsconfig.json │ ├── .eslintrc │ ├── README.md │ └── package.json └── viewer-markdown │ ├── src │ ├── typings.d.ts │ └── index.tsx │ ├── .eslintignore │ ├── .editorconfig │ ├── .prettierrc │ ├── tsconfig.json │ ├── .eslintrc │ └── package.json ├── src ├── react-app-env.d.ts └── index.tsx ├── .env.development ├── docs ├── img.jpg └── firebase-web-code.png ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json └── index.html ├── storage.rules ├── .editorconfig ├── lerna.json ├── firebase.json ├── .gitignore ├── .env.example ├── tsconfig.json ├── firestore.indexes.json ├── .github └── workflows │ ├── prelease-on-main.yml │ ├── beta-on-develop.yml │ └── release-on-tag.yml ├── package.json ├── firestore.rules.example └── README.md /remoteconfig.template.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /packages/core/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module global { 2 | } 3 | -------------------------------------------------------------------------------- /packages/discuzz/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module global { 2 | } 3 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/auth-firebase/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module global { 2 | } 3 | -------------------------------------------------------------------------------- /packages/locale-en/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module global { 2 | } 3 | -------------------------------------------------------------------------------- /packages/locale-vi/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module global { 2 | } 3 | -------------------------------------------------------------------------------- /packages/composer-markdown/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module global { 2 | } 3 | -------------------------------------------------------------------------------- /packages/data-firestore/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module global { 2 | } 3 | -------------------------------------------------------------------------------- /packages/viewer-markdown/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module global { 2 | } 3 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_SERVICE_CONFIG=$SERVICE_CONFIG 2 | REACT_APP_AUTHS=$AUTHS -------------------------------------------------------------------------------- /docs/img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discuzz-app/discuzz/HEAD/docs/img.jpg -------------------------------------------------------------------------------- /packages/core/.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /packages/discuzz/.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discuzz-app/discuzz/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discuzz-app/discuzz/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discuzz-app/discuzz/HEAD/public/logo512.png -------------------------------------------------------------------------------- /packages/locale-en/.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /packages/locale-vi/.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/auth-firebase/.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /packages/composer-markdown/.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /packages/core/src/constants/composer.ts: -------------------------------------------------------------------------------- 1 | export const EMPTY_SYMBOL = decodeURIComponent('%5C%0A') 2 | -------------------------------------------------------------------------------- /packages/data-firestore/.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /packages/viewer-markdown/.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /docs/firebase-web-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discuzz-app/discuzz/HEAD/docs/firebase-web-code.png -------------------------------------------------------------------------------- /packages/core/src/utils/nop.ts: -------------------------------------------------------------------------------- 1 | export const nop = () => {} 2 | export const asyncNop = async () => {} 3 | -------------------------------------------------------------------------------- /packages/core/src/components/LocaleProvider/config/locales.ts: -------------------------------------------------------------------------------- 1 | export const supportedLocales = [ 2 | 'en', 3 | 'vi' 4 | ] 5 | -------------------------------------------------------------------------------- /packages/core/src/types/PostAuthor.ts: -------------------------------------------------------------------------------- 1 | export type PostAuthor = { 2 | id: string; 3 | name: string | null; 4 | photoUrl?: string; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/discuzz/src/utils/darkMode.ts: -------------------------------------------------------------------------------- 1 | export const prefersDarkMode = global.matchMedia && global.matchMedia('(prefers-color-scheme: dark)').matches 2 | -------------------------------------------------------------------------------- /packages/discuzz/src/enums/Theme.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export enum Theme { 4 | AUTO = 'auto', 5 | DARK = 'dark', 6 | LIGHT = 'light' 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/src/enums/RequestState.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export enum RequestState { 3 | INITIAL = 'initial', 4 | LOADING = 'loading', 5 | SUCCESS = 'success', 6 | FAIL = 'fail' 7 | } -------------------------------------------------------------------------------- /packages/core/src/types/SourceService.ts: -------------------------------------------------------------------------------- 1 | import { ServiceSource } from "components/ServiceSourceProvider"; 2 | 3 | export type Source = (config: { 4 | [key: string]: string 5 | }) => ServiceSource -------------------------------------------------------------------------------- /packages/core/src/config/app.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | baseUrl: '', 3 | richText: false, 4 | padding: 10, 5 | pagination: 10, 6 | viewer: undefined, 7 | composer: undefined 8 | } 9 | -------------------------------------------------------------------------------- /storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service firebase.storage { 3 | match /b/{bucket}/o { 4 | match /{allPaths=**} { 5 | allow read, write: if request.auth!=null; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /packages/core/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import logger from 'loglevel' 2 | 3 | export const trace = logger.trace 4 | export const log = logger.debug 5 | export const warn = logger.warn 6 | export const fatal = logger.error 7 | -------------------------------------------------------------------------------- /packages/discuzz/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /packages/locale-en/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /packages/locale-vi/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /packages/auth-firebase/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /packages/composer-markdown/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /packages/data-firestore/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /packages/viewer-markdown/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /packages/core/src/components/PostCard/components/Timestamp.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@mui/system/styled' 2 | 3 | export const Timestamp = styled('i')(({ theme }) => ({ 4 | fontSize: 13, 5 | color: theme.palette.text.secondary 6 | })) 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = false 6 | 7 | indent_style = space 8 | indent_size = 2 9 | 10 | [*.{js,php,html,css,py,twig,less,scss,jsx,ts,tsx}] 11 | charset = utf-8 12 | 13 | -------------------------------------------------------------------------------- /packages/core/src/components/AuthProvider/hooks/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { AuthContext } from '../contexts/AuthContext' 3 | 4 | export const useAuth = () => { 5 | const auth = useContext(AuthContext) 6 | 7 | return auth 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/types/User.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | isAdmin?: boolean 3 | canSavePost?: boolean 4 | 5 | uid: string 6 | isAnonymous: boolean 7 | emailVerified: boolean 8 | photoURL: string | null 9 | displayName: string | null 10 | }; 11 | -------------------------------------------------------------------------------- /packages/core/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/components/App/hooks/useConfig.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { ConfigContext } from '../contexts/ConfigContext' 3 | 4 | export const useConfig = () => { 5 | const config = useContext(ConfigContext) 6 | 7 | return config 8 | } 9 | -------------------------------------------------------------------------------- /packages/discuzz/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } 11 | -------------------------------------------------------------------------------- /packages/locale-en/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } 11 | -------------------------------------------------------------------------------- /packages/locale-vi/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } 11 | -------------------------------------------------------------------------------- /packages/auth-firebase/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/components/LocaleProvider/hooks/useLocale.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { LocaleContext } from '../contexts/LocaleContext' 3 | 4 | export const useLocale = () => { 5 | const locale = useContext(LocaleContext) 6 | 7 | return locale 8 | } 9 | -------------------------------------------------------------------------------- /packages/data-firestore/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } 11 | -------------------------------------------------------------------------------- /packages/composer-markdown/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } 11 | -------------------------------------------------------------------------------- /packages/viewer-markdown/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/enums/PostUpdateResult.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export enum PostUpdateResult { 3 | INVALID = 'invalid', 4 | UPDATED = 'updated', 5 | PENDING = 'pending' 6 | } 7 | export enum PendingPostUpdateResult { 8 | SUCCESS = 'success', 9 | FAIL = 'fail' 10 | } -------------------------------------------------------------------------------- /packages/viewer-markdown/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Markdown from 'markdown-to-jsx' 3 | import { ContentProps } from '@discuzz/core' 4 | 5 | const Content = ({ children }: ContentProps) => { 6 | return ({children}) 7 | } 8 | export default Content 9 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "1.11.18", 6 | "npmClient": "yarn", 7 | "useWorkspaces": true, 8 | "includeMergedTags": true, 9 | "command": { 10 | "publish": { 11 | "yes": true, 12 | "verifyAccess": false 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/components/AuthProvider/enums/SignInProviderId.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export enum SignInProviderId { 3 | GOOGLE = 'google', 4 | FACEBOOK = 'facebook', 5 | TWITTER = 'twitter', 6 | GITHUB = 'github', 7 | MICROSOFT = 'microsoft', 8 | APPLE = 'apple', 9 | YAHOO = 'yahoo' 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/components/ServiceSourceProvider/hooks/useAuthSource.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { ServiceSourceContext } from '../contexts/ServiceSourceContext' 3 | 4 | export const useAuthSource = () => { 5 | const source = useContext(ServiceSourceContext) 6 | 7 | return source.auth 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/components/ServiceSourceProvider/hooks/useDataSource.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { ServiceSourceContext } from '../contexts/ServiceSourceContext' 3 | 4 | export const useDataSource = () => { 5 | const source = useContext(ServiceSourceContext) 6 | 7 | return source.data 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/types/AuthService.ts: -------------------------------------------------------------------------------- 1 | import { User } from "./User"; 2 | import { SignInProviderId } from "components/AuthProvider"; 3 | 4 | export type CurrentUser = () => User | undefined 5 | export type SignIn = () => (providerId?: SignInProviderId) => Promise 6 | export type SignOut = () => () => Promise 7 | -------------------------------------------------------------------------------- /packages/core/src/components/ServiceSourceProvider/contexts/ServiceSourceContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | import { Auth, Data } from 'services/source' 3 | 4 | export type ServiceSource = { 5 | auth?: Auth; 6 | data?: Data; 7 | }; 8 | export const ServiceSourceContext = createContext({}) 9 | -------------------------------------------------------------------------------- /packages/core/src/types/PendingPost.ts: -------------------------------------------------------------------------------- 1 | import { PostAuthor } from './PostAuthor' 2 | 3 | export type PendingPost = { 4 | id: string; 5 | 6 | paths: string[]; 7 | parentId?: string; 8 | postId?: string; 9 | 10 | url: string; 11 | author: PostAuthor; 12 | contents: string; 13 | 14 | createdAt?: Date; 15 | updatedAt?: Date; 16 | deletedAt?: Date; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/core/src/components/LocaleProvider/contexts/LocaleContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | 3 | export type Locale = { 4 | messages: { 5 | [key: string]: string; 6 | }; 7 | functions: { 8 | [key: string]: (...args: any[]) => void; 9 | }; 10 | }; 11 | export const LocaleContext = createContext({ 12 | messages: {}, 13 | functions: {} 14 | }) 15 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useSizeCheck.ts: -------------------------------------------------------------------------------- 1 | import useMediaQuery from '@mui/material/useMediaQuery' 2 | import { useTheme } from '@mui/material/styles' 3 | import { Breakpoint } from '@mui/material' 4 | 5 | export const useSizeCheck = (size: Breakpoint | number) => { 6 | const theme = useTheme() 7 | const check = useMediaQuery(theme.breakpoints.down(size)) 8 | 9 | return check 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/components/App/contexts/ConfigContext.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'config/app' 2 | import { createContext, FunctionComponent } from 'react' 3 | 4 | export type Config = { 5 | baseUrl?: string; 6 | richText?: boolean; 7 | padding?: number; 8 | pagination?: number; 9 | viewer?: FunctionComponent; 10 | composer?: FunctionComponent; 11 | }; 12 | export const ConfigContext = createContext(config) 13 | -------------------------------------------------------------------------------- /packages/core/src/utils/transitions.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, Ref, forwardRef } from 'react' 2 | 3 | import { TransitionProps } from '@mui/material/transitions' 4 | import Slide from '@mui/material/Slide' 5 | 6 | export const SlideUpTransition = forwardRef(function Transition ( 7 | props: TransitionProps & { 8 | children: ReactElement 9 | }, 10 | ref: Ref 11 | ) { 12 | return 13 | }) 14 | -------------------------------------------------------------------------------- /packages/core/src/components/App/components/SinglePostView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { usePostQuery } from 'services/post' 3 | import { PostCard } from 'components/PostCard' 4 | 5 | type SinglePostViewProps = { 6 | id: string 7 | } 8 | 9 | export const SinglePostView = ({ id }: SinglePostViewProps) => { 10 | const { post } = usePostQuery(id) 11 | 12 | return post 13 | ? ( 14 | 15 | ) 16 | : null 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/components/AuthProvider/contexts/AuthContext.ts: -------------------------------------------------------------------------------- 1 | import { User } from 'types/User' 2 | import { createContext } from 'react' 3 | import { asyncNop, nop } from 'utils/nop' 4 | 5 | export type Auth = { 6 | user?: User; 7 | signIn: () => void; 8 | signOut: () => Promise; 9 | signInAnonymously: () => Promise; 10 | }; 11 | export const AuthContext = createContext({ 12 | signIn: nop, 13 | signOut: asyncNop, 14 | signInAnonymously: () => new Promise(nop) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/core/src/types/Post.ts: -------------------------------------------------------------------------------- 1 | import { PostAuthor } from './PostAuthor' 2 | 3 | export type Post = { 4 | id: string; 5 | 6 | paths: string[]; 7 | parentId: string | null; 8 | 9 | url: string; 10 | author: PostAuthor; 11 | contents: string; 12 | 13 | replied: number; 14 | voted: number; 15 | voters: { 16 | [key: string]: number; 17 | }; 18 | 19 | 20 | approvedAt?: Date; 21 | createdAt?: Date; 22 | updatedAt?: Date; 23 | savedAt?: Date; 24 | deletedAt?: Date; 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /packages/core/src/components/LocaleProvider/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | export { useLocale } from './hooks/useLocale' 3 | export { LocaleContext } from './contexts/LocaleContext' 4 | export type { Locale } from './contexts/LocaleContext' 5 | 6 | type LocaleProviderProps = { 7 | provider: any, 8 | children: JSX.Element 9 | } 10 | 11 | export const LocaleProvider = ({ provider: Provider, children }: LocaleProviderProps) => { 12 | return ( 13 | 14 | {children} 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/components/App/components/FullView.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import { PostList } from 'components/PostList' 3 | import { PostComposer } from 'components/PostComposer' 4 | 5 | type FullViewProps = { 6 | url: string 7 | } 8 | 9 | export const FullView = ({ url }: FullViewProps) => { 10 | return ( 11 | 12 | 13 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/services/auth.ts: -------------------------------------------------------------------------------- 1 | import { useAuthSource } from 'components/ServiceSourceProvider' 2 | import { CurrentUser, SignIn, SignOut } from 'types/AuthService' 3 | 4 | export const useCurrentUser: CurrentUser = () => { 5 | const auth = useAuthSource() 6 | 7 | return auth!.useCurrentUser() 8 | } 9 | 10 | export const useSignIn: SignIn = () => { 11 | const auth = useAuthSource() 12 | 13 | return auth!.useSignIn() 14 | } 15 | export const useSignOut: SignOut = () => { 16 | const auth = useAuthSource() 17 | 18 | return auth!.useSignOut() 19 | } 20 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "hosting": { 7 | "public": "build", 8 | "ignore": [ 9 | "firebase.json", 10 | "**/.*", 11 | "**/node_modules/**" 12 | ], 13 | "rewrites": [ 14 | { 15 | "source": "**", 16 | "destination": "/index.html" 17 | } 18 | ] 19 | }, 20 | "storage": { 21 | "rules": "storage.rules" 22 | }, 23 | "remoteconfig": { 24 | "template": "remoteconfig.template.json" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/components/BackofficeApp/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useAuth } from 'components/AuthProvider' 3 | import { ReviewPostList } from 'components/PendingPostList' 4 | import { usePendingPostQuery } from 'services/post' 5 | 6 | export const BackofficeApp = () => { 7 | const { user } = useAuth() 8 | const { pendingPosts } = usePendingPostQuery() 9 | 10 | return ( 11 | (user && user.isAdmin) 12 | ? ( 13 | 16 | ) 17 | : null 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.node_modules 6 | /.yarn 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | .firebase 28 | .firebaserc 29 | .env 30 | firestore.rules 31 | 32 | /packages/*/node_modules 33 | /packages/*/dist 34 | 35 | /tmp -------------------------------------------------------------------------------- /packages/core/src/utils/locale.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LocaleContext, Locale } from 'components/LocaleProvider' 3 | 4 | export type ProviderProps = { 5 | children: JSX.Element 6 | } 7 | 8 | // eslint-disable-next-line react/display-name 9 | export const createProvider = (value: Locale) => ({ children }: ProviderProps) => { 10 | return ( 11 | 12 | {children} 13 | 14 | ) 15 | } 16 | 17 | export const prefersLocale = (global.navigator && global.navigator.language && global.navigator.language.split('-')[0]) || 'en' 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | 3 | FIREBASE_PROJECT= 4 | FIREBASE_APP= 5 | FIREBASE_KEY= 6 | FIREBASE_AUTH_DOMAIN= 7 | FIREBASE_STORAGE_BUCKET= 8 | FIREBASE_MESSAGING_ID= 9 | FIREBASE_REACAPTCHA= 10 | 11 | PUBLIC_URL=https://.web.app/ 12 | 13 | export SERVICE_CONFIG="{'apiKey':'$FIREBASE_KEY','authDomain':'$FIREBASE_AUTH_DOMAIN','projectId':'$FIREBASE_PROJECT','storageBucket':'$FIREBASE_STORAGE_BUCKET','messagingSenderId':'$FIREBASE_MESSAGING_ID','appId':'$FIREBASE_APP','recaptchaKey':'$FIREBASE_REACAPTCHA'}" 14 | export AUTHS="['google', 'apple', 'facebook', 'github', 'twitter', 'microsoft', 'yahoo']" 15 | export BASE_URL=$PUBLIC_URL -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "baseUrl": "src" 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "allowSyntheticDefaultImports": true, 19 | "baseUrl": "src" 20 | }, 21 | "include": ["src"], 22 | "exclude": ["node_modules", "dist"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/discuzz/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "allowSyntheticDefaultImports": true, 19 | "baseUrl": "src" 20 | }, 21 | "include": ["src"], 22 | "exclude": ["node_modules", "dist", "example"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/auth-firebase/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "allowSyntheticDefaultImports": true, 19 | "baseUrl": "src" 20 | }, 21 | "include": ["src"], 22 | "exclude": ["node_modules", "dist", "example"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/locale-en/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "allowSyntheticDefaultImports": true, 19 | "baseUrl": "src" 20 | }, 21 | "include": ["src"], 22 | "exclude": ["node_modules", "dist", "example"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/locale-vi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "allowSyntheticDefaultImports": true, 19 | "baseUrl": "src" 20 | }, 21 | "include": ["src"], 22 | "exclude": ["node_modules", "dist", "example"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/composer-markdown/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "allowSyntheticDefaultImports": true, 19 | "baseUrl": "src" 20 | }, 21 | "include": ["src"], 22 | "exclude": ["node_modules", "dist", "example"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/data-firestore/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "allowSyntheticDefaultImports": true, 19 | "baseUrl": "src" 20 | }, 21 | "include": ["src"], 22 | "exclude": ["node_modules", "dist", "example"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/viewer-markdown/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "allowSyntheticDefaultImports": true, 19 | "baseUrl": "src" 20 | }, 21 | "include": ["src"], 22 | "exclude": ["node_modules", "dist", "example"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/src/components/PendingPostList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ReviewPostCard } from '../PendingPostCard' 3 | import { TransitionGroup } from 'react-transition-group' 4 | import Slide from '@mui/material/Slide' 5 | import { PendingPost } from 'types/PendingPost' 6 | 7 | type ReviewPostLisProps = { 8 | posts: PendingPost[] 9 | } 10 | 11 | export const ReviewPostList = ({ 12 | posts 13 | }: ReviewPostLisProps 14 | ) => { 15 | return ( 16 | 17 | {posts.map((post: PendingPost) => ( 18 | 19 |
20 | 21 |
22 |
23 | )) 24 | } 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "prettier/standard", 8 | "prettier/react", 9 | "plugin:@typescript-eslint/eslint-recommended" 10 | ], 11 | "env": { 12 | "node": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "ecmaFeatures": { 17 | "legacyDecorators": true, 18 | "jsx": true 19 | } 20 | }, 21 | "settings": { 22 | "react": { 23 | "version": "16" 24 | } 25 | }, 26 | "rules": { 27 | "space-before-function-paren": 0, 28 | "react/prop-types": 0, 29 | "react/jsx-handler-names": 0, 30 | "react/jsx-fragments": 0, 31 | "react/no-unused-prop-types": 0, 32 | "import/export": 0 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/discuzz/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "prettier/standard", 8 | "prettier/react", 9 | "plugin:@typescript-eslint/eslint-recommended" 10 | ], 11 | "env": { 12 | "node": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "ecmaFeatures": { 17 | "legacyDecorators": true, 18 | "jsx": true 19 | } 20 | }, 21 | "settings": { 22 | "react": { 23 | "version": "16" 24 | } 25 | }, 26 | "rules": { 27 | "space-before-function-paren": 0, 28 | "react/prop-types": 0, 29 | "react/jsx-handler-names": 0, 30 | "react/jsx-fragments": 0, 31 | "react/no-unused-prop-types": 0, 32 | "import/export": 0 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/locale-en/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "prettier/standard", 8 | "prettier/react", 9 | "plugin:@typescript-eslint/eslint-recommended" 10 | ], 11 | "env": { 12 | "node": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "ecmaFeatures": { 17 | "legacyDecorators": true, 18 | "jsx": true 19 | } 20 | }, 21 | "settings": { 22 | "react": { 23 | "version": "16" 24 | } 25 | }, 26 | "rules": { 27 | "space-before-function-paren": 0, 28 | "react/prop-types": 0, 29 | "react/jsx-handler-names": 0, 30 | "react/jsx-fragments": 0, 31 | "react/no-unused-prop-types": 0, 32 | "import/export": 0 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/locale-vi/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "prettier/standard", 8 | "prettier/react", 9 | "plugin:@typescript-eslint/eslint-recommended" 10 | ], 11 | "env": { 12 | "node": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "ecmaFeatures": { 17 | "legacyDecorators": true, 18 | "jsx": true 19 | } 20 | }, 21 | "settings": { 22 | "react": { 23 | "version": "16" 24 | } 25 | }, 26 | "rules": { 27 | "space-before-function-paren": 0, 28 | "react/prop-types": 0, 29 | "react/jsx-handler-names": 0, 30 | "react/jsx-fragments": 0, 31 | "react/no-unused-prop-types": 0, 32 | "import/export": 0 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/auth-firebase/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "prettier/standard", 8 | "prettier/react", 9 | "plugin:@typescript-eslint/eslint-recommended" 10 | ], 11 | "env": { 12 | "node": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "ecmaFeatures": { 17 | "legacyDecorators": true, 18 | "jsx": true 19 | } 20 | }, 21 | "settings": { 22 | "react": { 23 | "version": "16" 24 | } 25 | }, 26 | "rules": { 27 | "space-before-function-paren": 0, 28 | "react/prop-types": 0, 29 | "react/jsx-handler-names": 0, 30 | "react/jsx-fragments": 0, 31 | "react/no-unused-prop-types": 0, 32 | "import/export": 0 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/components/PostCard/components/Content/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import { useConfig } from 'components/App' 3 | 4 | 5 | 6 | export type ContentProps = { 7 | children: string 8 | } 9 | 10 | 11 | const DefaultViewer = ({ children }: ContentProps) => { 12 | const content = children.split('\n').reduce((acc: JSX.Element[], item: string, index: number) => { 13 | acc.push({item}) 14 | acc.push(
) 15 | return acc 16 | }, []) 17 | return ( 18 | 19 | {content} 20 | 21 | ) 22 | } 23 | 24 | export const Content = (props: ContentProps) => { 25 | const config = useConfig() 26 | const Viewer = config.viewer || DefaultViewer 27 | 28 | return ( 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /packages/data-firestore/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "prettier/standard", 8 | "prettier/react", 9 | "plugin:@typescript-eslint/eslint-recommended" 10 | ], 11 | "env": { 12 | "node": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "ecmaFeatures": { 17 | "legacyDecorators": true, 18 | "jsx": true 19 | } 20 | }, 21 | "settings": { 22 | "react": { 23 | "version": "16" 24 | } 25 | }, 26 | "rules": { 27 | "space-before-function-paren": 0, 28 | "react/prop-types": 0, 29 | "react/jsx-handler-names": 0, 30 | "react/jsx-fragments": 0, 31 | "react/no-unused-prop-types": 0, 32 | "import/export": 0 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/viewer-markdown/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "prettier/standard", 8 | "prettier/react", 9 | "plugin:@typescript-eslint/eslint-recommended" 10 | ], 11 | "env": { 12 | "node": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "ecmaFeatures": { 17 | "legacyDecorators": true, 18 | "jsx": true 19 | } 20 | }, 21 | "settings": { 22 | "react": { 23 | "version": "16" 24 | } 25 | }, 26 | "rules": { 27 | "space-before-function-paren": 0, 28 | "react/prop-types": 0, 29 | "react/jsx-handler-names": 0, 30 | "react/jsx-fragments": 0, 31 | "react/no-unused-prop-types": 0, 32 | "import/export": 0 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/composer-markdown/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "prettier/standard", 8 | "prettier/react", 9 | "plugin:@typescript-eslint/eslint-recommended" 10 | ], 11 | "env": { 12 | "node": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "ecmaFeatures": { 17 | "legacyDecorators": true, 18 | "jsx": true 19 | } 20 | }, 21 | "settings": { 22 | "react": { 23 | "version": "16" 24 | } 25 | }, 26 | "rules": { 27 | "space-before-function-paren": 0, 28 | "react/prop-types": 0, 29 | "react/jsx-handler-names": 0, 30 | "react/jsx-fragments": 0, 31 | "react/no-unused-prop-types": 0, 32 | "import/export": 0 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/components/Buttons/ExpandButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { styled } from '@mui/material/styles' 3 | import IconButton, { IconButtonProps } from '@mui/material/IconButton' 4 | import KeyboardDoubleArrowDownIcon from '@mui/icons-material/KeyboardDoubleArrowDown' 5 | 6 | interface ExpandMoreProps extends IconButtonProps { 7 | expanded: boolean; 8 | } 9 | 10 | export const ExpandButton = styled((props: ExpandMoreProps) => { 11 | const { expanded, ...other } = props 12 | return ( 13 | 14 | 15 | 16 | ) 17 | })(({ theme, expanded }) => ({ 18 | transform: !expanded ? 'rotate(0deg)' : 'rotate(180deg)', 19 | marginLeft: 'auto', 20 | transition: theme.transitions.create('transform', { 21 | duration: theme.transitions.duration.shortest 22 | }) 23 | })) 24 | -------------------------------------------------------------------------------- /packages/discuzz/src/config/mui.ts: -------------------------------------------------------------------------------- 1 | export const lightTheme = { 2 | palette: { 3 | mode: 'light', 4 | background: { 5 | default: 'rgba(0,0,0,0)' 6 | } 7 | }, 8 | components: { 9 | MuiPaper: { 10 | styleOverrides: { 11 | root: { 12 | backgroundImage: 'none', 13 | border: 1, 14 | borderColor: '#e5e5e5', 15 | borderStyle: 'solid' 16 | } 17 | } 18 | } 19 | } 20 | } 21 | 22 | export const darkTheme = { 23 | palette: { 24 | mode: 'dark', 25 | background: { 26 | default: 'rgba(0,0,0,0)', 27 | paper: '#181A1B' 28 | } 29 | }, 30 | components: { 31 | MuiPaper: { 32 | styleOverrides: { 33 | root: { 34 | backgroundImage: 'none', 35 | border: 1, 36 | borderColor: '#303030', 37 | borderStyle: 'solid' 38 | } 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/core/src/components/App/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ConfigContext, Config } from './contexts/ConfigContext' 3 | import { SinglePostView } from './components/SinglePostView' 4 | import { FullView } from './components/FullView' 5 | export { useConfig } from './hooks/useConfig' 6 | export type { Config } from './contexts/ConfigContext' 7 | 8 | type AppProps = { 9 | url: string, 10 | config: Config 11 | } 12 | 13 | export const App = ({ url, config }: AppProps) => { 14 | const postId = url.indexOf('ft:') === 0 ? url.substr(3) : null 15 | 16 | return ( 17 |
20 | 21 | {postId 22 | ? ( 23 | 24 | ) 25 | : ( 26 | 27 | )} 28 | 29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/components/Buttons/LoadMoreButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useLocale } from 'components/LocaleProvider' 3 | import LoadingButton from '@mui/lab/LoadingButton' 4 | import { useSizeCheck } from 'hooks/useSizeCheck' 5 | import MoreHorizIcon from '@mui/icons-material/MoreHoriz' 6 | 7 | type LoadMoreButtonProps = { 8 | buttonRef: any, 9 | onClick: () => void, 10 | hasMore: boolean, 11 | loading: boolean 12 | } 13 | export const LoadMoreButton = ({ buttonRef, onClick, hasMore, loading }: LoadMoreButtonProps) => { 14 | const { messages } = useLocale() 15 | 16 | const smSize = useSizeCheck('sm') 17 | 18 | return (hasMore) 19 | ? ( 20 | 21 |   {smSize ? null : messages.loadMore} 22 | 23 | ) 24 | : ( 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [ 3 | { 4 | "collectionGroup": "posts", 5 | "queryScope": "COLLECTION", 6 | "fields": [ 7 | { 8 | "fieldPath": "parentId", 9 | "order": "ASCENDING" 10 | }, 11 | { 12 | "fieldPath": "url", 13 | "order": "ASCENDING" 14 | }, 15 | { 16 | "fieldPath": "savedAt", 17 | "order": "DESCENDING" 18 | } 19 | ] 20 | }, 21 | { 22 | "collectionGroup": "posts", 23 | "queryScope": "COLLECTION", 24 | "fields": [ 25 | { 26 | "fieldPath": "parentId", 27 | "order": "ASCENDING" 28 | }, 29 | { 30 | "fieldPath": "url", 31 | "order": "ASCENDING" 32 | }, 33 | { 34 | "fieldPath": "createdAt", 35 | "order": "DESCENDING" 36 | } 37 | ] 38 | } 39 | ], 40 | "fieldOverrides": [] 41 | } 42 | -------------------------------------------------------------------------------- /packages/composer-markdown/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import Editor from 'rich-markdown-editor' 3 | import { ComposerProps } from '@discuzz/core' 4 | 5 | const Composer = ({ onChange, value, placeholder, theme }: ComposerProps) => { 6 | const [editorKey, setEditorKey] = useState(String(Math.random())) 7 | const [currentValue, setCurrentValue] = useState('') 8 | 9 | useEffect(() => { 10 | if (value === '' && currentValue !== '') { 11 | setEditorKey(String(Math.random())) 12 | } 13 | }, [value, currentValue]) 14 | 15 | return ( 16 | { 22 | const newValue = content() 23 | 24 | onChange(newValue) 25 | setCurrentValue(newValue) 26 | }} 27 | /> 28 | ) 29 | } 30 | 31 | export default Composer 32 | -------------------------------------------------------------------------------- /packages/core/src/components/Buttons/SignInButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useAuth } from 'components/AuthProvider' 3 | import { useLocale } from 'components/LocaleProvider' 4 | import Button from '@mui/material/Button' 5 | import LoginIcon from '@mui/icons-material/Login' 6 | import { useSizeCheck } from 'hooks/useSizeCheck' 7 | import { IconButton } from '@mui/material' 8 | 9 | export const SignInButton = () => { 10 | const { messages } = useLocale() 11 | const { user, signIn } = useAuth() 12 | 13 | const smSize = useSizeCheck('sm') 14 | 15 | return (user && user.isAnonymous) 16 | ? ( 17 | smSize 18 | ? ( 19 | signIn()}> 20 | 21 | 22 | ) 23 | : ( 24 | 27 | ) 28 | ) 29 | : null 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/src/components/Buttons/CancelButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEventHandler } from 'react' 2 | import IconButton from '@mui/material/IconButton' 3 | import Button from '@mui/material/Button' 4 | import { useSizeCheck } from 'hooks/useSizeCheck' 5 | import ClearIcon from '@mui/icons-material/Clear' 6 | import { SxProps } from '@mui/material' 7 | 8 | type CancelButtonProps = { 9 | onClick: MouseEventHandler 10 | children: JSX.Element | string 11 | sx: SxProps 12 | } 13 | 14 | export const CancelButton = ({ onClick, children, sx }: CancelButtonProps) => { 15 | const smSize = useSizeCheck('sm') 16 | 17 | return smSize 18 | ? ( 19 | 24 | 25 | 26 | ) 27 | : ( 28 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/components/Buttons/ReplyButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import IconButton from '@mui/material/IconButton' 3 | import { useSizeCheck } from 'hooks/useSizeCheck' 4 | import ReplyIcon from '@mui/icons-material/Reply' 5 | import LoadingButton from '@mui/lab/LoadingButton' 6 | import { useLocale } from 'components/LocaleProvider' 7 | 8 | type ReplyButtonProps = { 9 | loading: boolean 10 | } 11 | 12 | export const ReplyButton = ({ loading }: ReplyButtonProps) => { 13 | const { messages } = useLocale() 14 | const smSize = useSizeCheck('sm') 15 | 16 | return smSize 17 | ? ( 18 | 19 | 20 | 21 | ) 22 | : ( 23 | }> 29 | {messages.replyNow} 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/components/PostComposer/components/Composer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useConfig } from 'components/App' 3 | import { useTheme } from '@mui/material/styles' 4 | import InputBase from '@mui/material/InputBase' 5 | 6 | export type ComposerProps = { 7 | onChange: (newValue: string) => void 8 | value: string 9 | placeholder: string 10 | theme?: string 11 | } 12 | 13 | const DefaultComposer = ({ onChange, value, placeholder, theme }: ComposerProps) => { 14 | return ( 15 | { 25 | onChange(event.target.value) 26 | }} 27 | /> 28 | ) 29 | } 30 | 31 | export const Composer = (props: ComposerProps) => { 32 | const config = useConfig() 33 | const theme = useTheme() 34 | const Composer = config.composer || DefaultComposer 35 | 36 | return ( 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /packages/core/src/components/Buttons/QuickReplyButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEventHandler } from 'react' 2 | import IconButton from '@mui/material/IconButton' 3 | import { useSizeCheck } from 'hooks/useSizeCheck' 4 | import SmsOutlinedIcon from '@mui/icons-material/SmsOutlined' 5 | import { useLocale } from 'components/LocaleProvider' 6 | import Button from '@mui/material/Button' 7 | 8 | 9 | type QuickReplyButtonProps = { 10 | onClick: MouseEventHandler 11 | } 12 | 13 | export const QuickReplyButton = ({ onClick }: QuickReplyButtonProps) => { 14 | const { messages } = useLocale() 15 | const smSize = useSizeCheck('sm') 16 | 17 | const color = '#888' 18 | 19 | return smSize 20 | ? ( 21 | 25 | 26 | 27 | ) 28 | : ( 29 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useIntersectionObserver.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | type IntersectionObserverArgs = { 4 | root?: any 5 | target: any 6 | onIntersect: () => void 7 | threshold?: number 8 | rootMargin?: number 9 | enabled?: boolean 10 | } 11 | 12 | export const useIntersectionObserver = ({ 13 | root, 14 | target, 15 | onIntersect, 16 | threshold = 1.0, 17 | rootMargin = 0, 18 | enabled = true 19 | }: IntersectionObserverArgs) => { 20 | useEffect(() => { 21 | if (!enabled) { 22 | return 23 | } 24 | 25 | const observer = new IntersectionObserver( 26 | (entries) => 27 | entries.forEach((entry) => { 28 | if (entry.isIntersecting) { 29 | onIntersect() 30 | } 31 | }), 32 | { 33 | root: root && root.current, 34 | rootMargin: rootMargin + 'px', 35 | threshold 36 | } 37 | ) 38 | 39 | const element = target && target.current 40 | 41 | if (!element) { 42 | return 43 | } 44 | 45 | observer.observe(element) 46 | 47 | return () => observer.unobserve(element) 48 | }, [target.current, enabled, onIntersect]) 49 | } 50 | -------------------------------------------------------------------------------- /packages/core/src/components/ServiceSourceProvider/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { ServiceSourceContext } from './contexts/ServiceSourceContext' 3 | export { useDataSource } from './hooks/useDataSource' 4 | export { useAuthSource } from './hooks/useAuthSource' 5 | export type { ServiceSource } from './contexts/ServiceSourceContext' 6 | import { Auth, Data } from 'services/source' 7 | 8 | type ServiceSourceProviderProps = { 9 | source: any, 10 | children: JSX.Element, 11 | }; 12 | 13 | export const ServiceSourceProvider = ({ source, children }: ServiceSourceProviderProps) => { 14 | const [auth, setAuth] = useState(undefined) 15 | const [data, setData] = useState(undefined) 16 | 17 | useEffect(() => { 18 | Promise.resolve(source.auth(source.config)) 19 | .then((authObject: Auth) => { 20 | Promise.resolve(source.data(source.config, authObject)) 21 | .then(setData) 22 | 23 | setAuth(authObject) 24 | 25 | }) 26 | }, [source]) 27 | 28 | return (auth && data) ? ( 29 | 33 | {children} 34 | 35 | ) : null 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/services/source.ts: -------------------------------------------------------------------------------- 1 | import { CurrentUser, SignIn, SignOut } from "types/AuthService" 2 | 3 | import { 4 | AddPostCommand, 5 | EditPostCommand, 6 | PostListQuery, 7 | PostQuery, 8 | RemovePostCommand, 9 | TogglePostVoteCommand, 10 | PendingPostListQuery, 11 | EditPendingPostCommand, 12 | RejectPendingPostCommand, 13 | ApprovePendingPostCommand 14 | } from 'types/PostService' 15 | 16 | export type Auth = { 17 | useCurrentUser: CurrentUser 18 | useSignIn: SignIn 19 | useSignOut: SignOut 20 | data: { 21 | [key: string]: any 22 | } 23 | } 24 | 25 | export type Data = { 26 | usePostQuery: PostQuery 27 | usePostListQuery: PostListQuery 28 | useAddPostCommand: AddPostCommand 29 | useEditPostCommand: EditPostCommand 30 | useRemovePostCommand: RemovePostCommand 31 | useTogglePostVoteCommand: TogglePostVoteCommand 32 | usePendingPostQuery: PendingPostListQuery 33 | useEditPendingPostCommand: EditPendingPostCommand 34 | useRejectPendingPostCommand: RejectPendingPostCommand 35 | useApprovePendingPostCommand: ApprovePendingPostCommand 36 | } 37 | 38 | export const loadService = (moduleLoader: () => Promise) => async (...args:any[]) => { 39 | const module = await moduleLoader() 40 | return module.default(...args) 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/src/components/Buttons/SignOutButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useAuth } from 'components/AuthProvider' 3 | import { useLocale } from 'components/LocaleProvider' 4 | import LoadingButton from '@mui/lab/LoadingButton' 5 | import LogoutIcon from '@mui/icons-material/Logout' 6 | 7 | import { useSizeCheck } from 'hooks/useSizeCheck' 8 | import { IconButton } from '@mui/material' 9 | 10 | export const SignOutButton = () => { 11 | const [isSigningOut, toggleSigningOut] = useState(false) 12 | const { messages } = useLocale() 13 | const { user, signOut } = useAuth() 14 | const smSize = useSizeCheck('sm') 15 | 16 | return (user && !user.isAnonymous) 17 | ? ( 18 | smSize 19 | ? ( 20 | 21 | 22 | 23 | ) 24 | : ( 25 | { 27 | toggleSigningOut(true) 28 | await signOut() 29 | setTimeout(() => toggleSigningOut(false), 1000) 30 | }} color="warning" startIcon={}> 31 | {messages.signOut} 32 | 33 | ) 34 | ) 35 | : null 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/components/Buttons/LikeButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEventHandler } from 'react' 2 | import { useSizeCheck } from 'hooks/useSizeCheck' 3 | import Button from '@mui/material/Button' 4 | import { useLocale } from 'components/LocaleProvider' 5 | 6 | 7 | type LikeButtonProps = { 8 | onClick: MouseEventHandler | undefined 9 | disabled: boolean 10 | count: number 11 | liked: boolean 12 | icon: JSX.Element 13 | } 14 | 15 | export const LikeButton = ({ disabled, count, liked, onClick, icon }: LikeButtonProps) => { 16 | const smSize = useSizeCheck('sm') 17 | const { messages } = useLocale() 18 | 19 | const color = liked ? 'success' : '#888' 20 | 21 | return smSize 22 | ? ( 23 | 34 | ) 35 | : ( 36 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /packages/auth-firebase/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | Firebase Authentication for [Discuzz](https://github.com/discuzz-app/discuzz) 5 | 6 | 7 | 8 | Authenticate users by integrating with federated identity providers 9 | 10 |
11 | 12 | 13 | 14 |
15 | Table of contents 16 | 17 | --- 18 | 19 | - [Homepage](#homepage) 20 | - [Supported Identity providers](#supported-identity-providers) 21 | - [Contributing](#contributing) 22 | - [Changelog](#changelog) 23 | - [License](#license) 24 | 25 | --- 26 | 27 |
28 | 29 | ## **Homepage** 30 | 31 | [discuzz.mph.am](https://discuzz.mph.am/) 32 | 33 | ## **Supported Identity providers** 34 | - Google 35 | - Meta (Facebook) 36 | - Twitter 37 | - GitHub 38 | - Yahoo 39 | - Microsoft 40 | - Apple 41 | 42 | **To suggest anything, please join our [Discussion board](https://github.com/discuzz-app/discuzz/discussions).** 43 | 44 | 45 | ## **Contributing** 46 | 47 | Please contribute using [GitHub Flow](https://guides.github.com/introduction/flow). Create a branch, add commits, and then [open a pull request](https://github.com/@discuzz-app/discuzz/compare). 48 | 49 | ## **License** 50 | 51 | This project is licensed under the [GNU General Public License v3.0](https://opensource.org/licenses/gpl-3.0.html) - see the [`LICENSE`](LICENSE) file for details. 52 | -------------------------------------------------------------------------------- /packages/data-firestore/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | Firebase Cloud Firestore for [Discuzz](https://github.com/discuzz-app/discuzz) 5 | 6 | 7 | 8 | Store Discuzz's data to a Cloud Realtime database 9 | 10 |
11 | 12 | 13 | 14 |
15 | Table of contents 16 | 17 | --- 18 | 19 | - [Homepage](#homepage) 20 | - [Rules & Indexes](#rules--indexes) 21 | - [Contributing](#contributing) 22 | - [Changelog](#changelog) 23 | - [License](#license) 24 | 25 | --- 26 | 27 |
28 | 29 | ## **Homepage** 30 | 31 | [discuzz.mph.am](https://discuzz.mph.am/) 32 | 33 | **To suggest anything, please join our [Discussion board](https://github.com/discuzz-app/discuzz/discussions).** 34 | 35 | 36 | ## **Rules & Indexes** 37 | 38 | You can extends the rules from [firestore.rules.example](../../firestore.rules.example), and the indexes from [firestore.indexes.json](../../firestore.indexes.json) 39 | 40 | 41 | ## **Contributing** 42 | 43 | Please contribute using [GitHub Flow](https://guides.github.com/introduction/flow). Create a branch, add commits, and then [open a pull request](https://github.com/@discuzz-app/discuzz/compare). 44 | 45 | ## **License** 46 | 47 | This project is licensed under the [GNU General Public License v3.0](https://opensource.org/licenses/gpl-3.0.html) - see the [`LICENSE`](LICENSE) file for details. 48 | -------------------------------------------------------------------------------- /packages/core/src/types/PostService.ts: -------------------------------------------------------------------------------- 1 | import { PendingPost } from "./PendingPost" 2 | import { Post } from "./Post" 3 | import { RequestState } from 'enums/RequestState' 4 | import { PendingPostUpdateResult, PostUpdateResult } from "enums/PostUpdateResult" 5 | 6 | export type PostQuery = (id: string) => { 7 | post: Post | null, 8 | queryState: RequestState 9 | } 10 | export type PostListQuery = (url: string, pagination: number, parent: Post | null) => { 11 | posts: Post[], 12 | loadMore: () => void, 13 | hasMore: boolean, 14 | queryState: RequestState 15 | } 16 | export type AddPostCommand = () => (contents: string, url?: string, parent?: Post) => Promise 17 | 18 | export type EditPostCommand = () => (post: Post, contents: string) => Promise 19 | 20 | export type RemovePostCommand = () => (post: Post) => Promise 21 | 22 | export type TogglePostVoteCommand = () => (post: Post) => Promise 23 | 24 | export type PendingPostListQuery = () => { 25 | pendingPosts: PendingPost[] 26 | } 27 | export type EditPendingPostCommand = () => (pendingPost: PendingPost, contents: string) => Promise 28 | 29 | export type RejectPendingPostCommand = () => (pendingPost: PendingPost) => Promise 30 | 31 | export type ApprovePendingPostCommand = () => (pendingPost: PendingPost) => Promise 32 | -------------------------------------------------------------------------------- /packages/core/src/components/Buttons/ManagerButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, Fragment } from 'react' 2 | import { useAuth } from 'components/AuthProvider' 3 | import { useLocale } from 'components/LocaleProvider' 4 | import Button from '@mui/material/Button' 5 | import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings' 6 | 7 | import IconButton from '@mui/material/IconButton' 8 | import { useSizeCheck } from 'hooks/useSizeCheck' 9 | 10 | import BackofficeDialog from 'components/BackofficeApp/BackofficeDialog' 11 | // const BackofficeDialog = lazy(() => import('components/BackofficeApp/BackofficeDialog')) 12 | 13 | export const ManagerButton = () => { 14 | const { messages } = useLocale() 15 | const { user } = useAuth() 16 | const [backofficeDialogIsOpen, toggleBackofficeDialog] = useState(false) 17 | const smSize = useSizeCheck('sm') 18 | 19 | if (!user || !user.isAdmin) return null 20 | 21 | return ( 22 | 23 | {smSize 24 | ? ( 25 | toggleBackofficeDialog(true)}> 26 | 27 | 28 | ) 29 | : ( 30 | 35 | )} 36 | 37 | toggleBackofficeDialog(false)} 40 | /> 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/src/components/Buttons/ReadMoreButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEventHandler } from 'react' 2 | import IconButton from '@mui/material/IconButton' 3 | import { useSizeCheck } from 'hooks/useSizeCheck' 4 | import { useLocale } from 'components/LocaleProvider' 5 | import Button from '@mui/material/Button' 6 | import Badge from '@mui/material/Badge' 7 | import CodeIcon from '@mui/icons-material/Code' 8 | import CodeOffIcon from '@mui/icons-material/CodeOff' 9 | 10 | 11 | type ReadMoreButtonProps = { 12 | onClick: MouseEventHandler 13 | expanded: boolean 14 | count: number 15 | } 16 | 17 | export const ReadMoreButton = ({ count, expanded, onClick }: ReadMoreButtonProps) => { 18 | const { messages } = useLocale() 19 | const smSize = useSizeCheck('sm') 20 | 21 | if (count === 0) return null 22 | 23 | const icon = expanded ? : 24 | const color = '#888' 25 | 26 | return ( 27 | 33 | {smSize 34 | ? ( 35 | 38 | {icon} 39 | 40 | ) 41 | : ( 42 | 50 | )} 51 | 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /packages/core/src/components/PostList/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react' 2 | import { Post } from 'types/Post' 3 | import { PostCard } from '../PostCard' 4 | import { TransitionGroup } from 'react-transition-group' 5 | import Slide from '@mui/material/Slide' 6 | import { usePostListQuery } from 'services/post' 7 | import { LoadMoreButton } from 'components/Buttons/LoadMoreButton' 8 | 9 | import { useIntersectionObserver } from 'hooks/useIntersectionObserver' 10 | import { useConfig } from 'components/App' 11 | import { RequestState } from 'enums/RequestState' 12 | 13 | type PostLisProps = { 14 | url: string 15 | parent: Post | null 16 | level: number 17 | } 18 | 19 | export const PostList = ({ url, parent, level }: PostLisProps) => { 20 | const { pagination } = useConfig() 21 | const { posts, loadMore, hasMore, queryState } = usePostListQuery(url, pagination!, parent) 22 | 23 | const loadMoreButtonRef = useRef() 24 | 25 | useIntersectionObserver({ 26 | target: loadMoreButtonRef, 27 | onIntersect: loadMore, 28 | enabled: hasMore 29 | }) 30 | 31 | return ( 32 |
33 | 34 | {posts.map((post: Post) => ( 35 | 36 |
37 | 41 |
42 |
43 | )) 44 | } 45 |
46 | 47 | 48 |
49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /packages/locale-en/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createProvider } from '@discuzz/core' 2 | import locale from 'date-fns/locale/en-US' 3 | import { formatDistance } from 'date-fns' 4 | 5 | const Provider = createProvider({ 6 | messages: { 7 | test: 'Hi', 8 | anonymous: 'Anonymous', 9 | admin: 'Administrator', 10 | signIn: 'Sign in', 11 | signInWith: 'Sign in with', 12 | signOut: 'Sign out', 13 | send: 'Send', 14 | replyHere: 'Reply...', 15 | reply: 'Reply', 16 | replyNow: 'Reply', 17 | deletedAt: 'Deleted', 18 | updatedAt: 'Edited', 19 | cancel: 'Cancel', 20 | close: 'Close', 21 | sureDelete: 'Sure', 22 | save: 'Save', 23 | postHere: 'Write something nice…', 24 | managePendingPosts: 'Post Manager', 25 | edit: 'Edit post', 26 | delete: 'Delete post', 27 | approve: 'Approve post', 28 | postAdded: 'Your post has been added', 29 | postUpdated: 'Post has been updated', 30 | pendingPostUpdated: 'Pending post has been updated', 31 | pendingPostApproved: 'Pending post has been approved', 32 | pendingPostDeleted: 'Pending post has been rejected', 33 | postSubmitted: 'Your post has been submitted for review', 34 | postChangeSubmitted: 'Your change has been submitted for review', 35 | postDeleted: 'Post has been deleted', 36 | readMore: 'Show replies', 37 | readLess: 'Hide replies', 38 | like: 'Like', 39 | likes: 'Likes', 40 | loadMore: 'Load more...', 41 | expandMore: 'Expand', 42 | expandLess: 'Collapse' 43 | }, 44 | functions: { 45 | formatDateDistance: (from: Date, to: Date, options: any = {}) => formatDistance(from, to, { 46 | ...options, 47 | locale 48 | }) 49 | } 50 | }) 51 | 52 | export default Provider 53 | -------------------------------------------------------------------------------- /packages/locale-vi/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createProvider } from '@discuzz/core' 2 | import locale from 'date-fns/locale/vi' 3 | import { formatDistance } from 'date-fns' 4 | 5 | const Provider = createProvider({ 6 | messages: { 7 | test: 'Xin chào', 8 | anonymous: 'Người ẩn danh', 9 | admin: 'Người quản trị', 10 | signIn: 'Đăng nhập', 11 | signInWith: 'Đăng nhập với', 12 | signOut: 'Đăng xuất', 13 | send: 'Gửi', 14 | replyHere: 'Trả lời bình luận...', 15 | reply: 'Trả lời', 16 | replyNow: 'Gửi', 17 | deletedAt: 'Đã xóa', 18 | updatedAt: 'Đã sửa đổi', 19 | cancel: 'Hủy', 20 | close: 'Đóng', 21 | sureDelete: 'Chắc chắn', 22 | save: 'Lưu', 23 | postHere: 'Bình luận của bạn…', 24 | managePendingPosts: 'Quản lý', 25 | edit: 'Sửa nội dung', 26 | delete: 'Xóa nội dung', 27 | approve: 'Phê duyệt nội dung', 28 | postAdded: 'Bình luận đã được đăng', 29 | postUpdated: 'Nội dung đã được cập nhật', 30 | pendingPostUpdated: 'Nội dung đã được cập nhật', 31 | pendingPostApproved: 'Bình luận đã được phê duyệt', 32 | pendingPostDeleted: 'Bình luận đã bị từ chối', 33 | postSubmitted: 'Bình luận đã được gửi chờ phê duyệt', 34 | postChangeSubmitted: 'Nội dung đã được gửi chờ phê duyệt', 35 | postDeleted: 'Bình luận đã bị xóa', 36 | expandMore: 'Xem trả lời', 37 | expandLess: 'Thu gọn', 38 | like: 'Thích', 39 | likes: 'Thích', 40 | loadMore: 'Xem thêm...', 41 | readMore: 'Xem trả lời', 42 | readLess: 'Thu gọn' 43 | }, 44 | functions: { 45 | formatDateDistance: (from: Date, to: Date, options: any = {}) => formatDistance(from, to, { 46 | ...options, 47 | locale 48 | }) 49 | } 50 | }) 51 | 52 | export default Provider 53 | -------------------------------------------------------------------------------- /packages/locale-vi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@discuzz/locale-vi", 3 | "version": "1.11.18", 4 | "description": "Discuzz locale vi", 5 | "author": "MartinPham", 6 | "license": "MIT", 7 | "repository": "discuzz/discuzz", 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "source": "src/index.tsx", 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "scripts": { 15 | "libbuild": "run-s build", 16 | "prebuild": "rm -rf dist", 17 | "build": "microbundle-crl --format cjs", 18 | "start": "microbundle-crl watch --no-compress --format cjs" 19 | }, 20 | "dependencies": { 21 | "@discuzz/core": "^1.11.18" 22 | }, 23 | "peerDependencies": { 24 | "date-fns": "^2.27.0", 25 | "react": "^17.0.0" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^16.7.13", 29 | "@types/react": "^17.0.20", 30 | "@types/react-dom": "^17.0.9", 31 | "@typescript-eslint/eslint-plugin": "^5.8.1", 32 | "@typescript-eslint/parser": "^5.8.1", 33 | "babel-eslint": "^10.0.3", 34 | "cross-env": "^7.0.2", 35 | "eslint": "^7.32.0", 36 | "eslint-config-prettier": "^6.7.0", 37 | "eslint-config-standard": "^16.0.3", 38 | "eslint-config-standard-react": "^9.2.0", 39 | "eslint-plugin-import": "^2.25.3", 40 | "eslint-plugin-node": "^11.1.0", 41 | "eslint-plugin-prettier": "^3.1.1", 42 | "eslint-plugin-promise": "^5.2.0", 43 | "eslint-plugin-react": "^7.28.0", 44 | "eslint-plugin-standard": "^4.0.1", 45 | "microbundle-crl": "^0.13.10", 46 | "npm-run-all": "^4.1.5", 47 | "prettier": "^2.0.4", 48 | "react": "^17.0.0", 49 | "typescript": "^4.4.2" 50 | }, 51 | "files": [ 52 | "dist" 53 | ], 54 | "publishConfig": { 55 | "access": "public" 56 | }, 57 | "gitHead": "b6438b79175583863d895f10403197a300b8a6d9" 58 | } 59 | -------------------------------------------------------------------------------- /packages/locale-en/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@discuzz/locale-en", 3 | "version": "1.11.18", 4 | "description": "Discuzz locale en", 5 | "author": "MartinPham", 6 | "license": "MIT", 7 | "repository": "discuzz/discuzz", 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "source": "src/index.tsx", 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "scripts": { 15 | "libbuild": "run-s build", 16 | "prebuild": "rm -rf dist", 17 | "build": "microbundle-crl --format cjs", 18 | "start": "microbundle-crl watch --no-compress --format cjs" 19 | }, 20 | "dependencies": { 21 | "@discuzz/core": "^1.11.18" 22 | }, 23 | "peerDependencies": { 24 | "date-fns": "^2.27.0", 25 | "react": "^17.0.0" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^16.7.13", 29 | "@types/react": "^17.0.20", 30 | "@types/react-dom": "^17.0.9", 31 | "@typescript-eslint/eslint-plugin": "^5.8.1", 32 | "@typescript-eslint/parser": "^5.8.1", 33 | "babel-eslint": "^10.0.3", 34 | "cross-env": "^7.0.2", 35 | "date-fns": "^2.27.0", 36 | "eslint": "^7.32.0", 37 | "eslint-config-prettier": "^6.7.0", 38 | "eslint-config-standard": "^16.0.3", 39 | "eslint-config-standard-react": "^9.2.0", 40 | "eslint-plugin-import": "^2.25.3", 41 | "eslint-plugin-node": "^11.1.0", 42 | "eslint-plugin-prettier": "^3.1.1", 43 | "eslint-plugin-promise": "^5.2.0", 44 | "eslint-plugin-react": "^7.28.0", 45 | "eslint-plugin-standard": "^4.0.1", 46 | "microbundle-crl": "^0.13.10", 47 | "npm-run-all": "^4.1.5", 48 | "prettier": "^2.0.4", 49 | "react": "^17.0.0", 50 | "typescript": "^4.4.2" 51 | }, 52 | "files": [ 53 | "dist" 54 | ], 55 | "publishConfig": { 56 | "access": "public" 57 | }, 58 | "gitHead": "b6438b79175583863d895f10403197a300b8a6d9" 59 | } 60 | -------------------------------------------------------------------------------- /packages/viewer-markdown/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@discuzz/viewer-markdown", 3 | "version": "1.11.18", 4 | "description": "Discuzz viewer plaintext", 5 | "author": "MartinPham", 6 | "license": "MIT", 7 | "repository": "discuzz/discuzz", 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "source": "src/index.tsx", 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "scripts": { 15 | "libbuild": "run-s build", 16 | "prebuild": "rm -rf dist", 17 | "build": "microbundle-crl --format cjs", 18 | "start": "microbundle-crl watch --no-compress --format cjs" 19 | }, 20 | "dependencies": { 21 | "@discuzz/core": "^1.11.18" 22 | }, 23 | "peerDependencies": { 24 | "markdown-to-jsx": "^7.1.5", 25 | "react": "^17.0.0" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^16.7.13", 29 | "@types/react": "^17.0.20", 30 | "@types/react-dom": "^17.0.9", 31 | "@typescript-eslint/eslint-plugin": "^5.8.1", 32 | "@typescript-eslint/parser": "^5.8.1", 33 | "babel-eslint": "^10.0.3", 34 | "cross-env": "^7.0.2", 35 | "eslint": "^7.32.0", 36 | "eslint-config-prettier": "^6.7.0", 37 | "eslint-config-standard": "^16.0.3", 38 | "eslint-config-standard-react": "^9.2.0", 39 | "eslint-plugin-import": "^2.25.3", 40 | "eslint-plugin-node": "^11.1.0", 41 | "eslint-plugin-prettier": "^3.1.1", 42 | "eslint-plugin-promise": "^5.2.0", 43 | "eslint-plugin-react": "^7.28.0", 44 | "eslint-plugin-standard": "^4.0.1", 45 | "markdown-to-jsx": "^7.1.5", 46 | "microbundle-crl": "^0.13.10", 47 | "npm-run-all": "^4.1.5", 48 | "prettier": "^2.0.4", 49 | "react": "^17.0.0", 50 | "typescript": "^4.4.2" 51 | }, 52 | "files": [ 53 | "dist" 54 | ], 55 | "publishConfig": { 56 | "access": "public" 57 | }, 58 | "gitHead": "b6438b79175583863d895f10403197a300b8a6d9" 59 | } 60 | -------------------------------------------------------------------------------- /packages/auth-firebase/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@discuzz/auth-firebase", 3 | "version": "1.11.18", 4 | "description": "Discuzz auth firebase", 5 | "author": "MartinPham", 6 | "license": "MIT", 7 | "repository": "discuzz/discuzz", 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "source": "src/index.tsx", 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "scripts": { 15 | "libbuild": "run-s build", 16 | "prebuild": "rm -rf dist", 17 | "build": "microbundle-crl --format cjs", 18 | "start": "microbundle-crl watch --no-compress --format cjs" 19 | }, 20 | "dependencies": { 21 | "@discuzz/core": "^1.11.18", 22 | "md5": "^2.3.0" 23 | }, 24 | "peerDependencies": { 25 | "firebase": "^9.6.1", 26 | "react": "^17.0.0" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^16.7.13", 30 | "@types/react": "^17.0.20", 31 | "@types/react-dom": "^17.0.9", 32 | "@typescript-eslint/eslint-plugin": "^5.8.1", 33 | "@typescript-eslint/parser": "^5.8.1", 34 | "babel-eslint": "^10.0.3", 35 | "cross-env": "^7.0.2", 36 | "eslint": "^7.32.0", 37 | "eslint-config-prettier": "^6.7.0", 38 | "eslint-config-standard": "^16.0.3", 39 | "eslint-config-standard-react": "^9.2.0", 40 | "eslint-plugin-import": "^2.25.3", 41 | "eslint-plugin-node": "^11.1.0", 42 | "eslint-plugin-prettier": "^3.1.1", 43 | "eslint-plugin-promise": "^5.2.0", 44 | "eslint-plugin-react": "^7.28.0", 45 | "eslint-plugin-standard": "^4.0.1", 46 | "firebase": "^9.6.1", 47 | "microbundle-crl": "^0.13.10", 48 | "npm-run-all": "^4.1.5", 49 | "prettier": "^2.0.4", 50 | "react": "^17.0.0", 51 | "typescript": "^4.4.2" 52 | }, 53 | "files": [ 54 | "dist" 55 | ], 56 | "publishConfig": { 57 | "access": "public" 58 | }, 59 | "gitHead": "b6438b79175583863d895f10403197a300b8a6d9" 60 | } 61 | -------------------------------------------------------------------------------- /packages/data-firestore/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@discuzz/data-firestore", 3 | "version": "1.11.18", 4 | "description": "Discuzz data firestore", 5 | "author": "MartinPham", 6 | "license": "MIT", 7 | "repository": "discuzz/discuzz", 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "source": "src/index.tsx", 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "scripts": { 15 | "libbuild": "run-s build", 16 | "prebuild": "rm -rf dist", 17 | "build": "microbundle-crl --format cjs", 18 | "start": "microbundle-crl watch --no-compress --format cjs" 19 | }, 20 | "dependencies": { 21 | "@discuzz/core": "^1.11.18", 22 | "immer": "^9.0.7", 23 | "use-immer": "^0.6.0" 24 | }, 25 | "peerDependencies": { 26 | "firebase": "^9.6.1", 27 | "react": "^17.0.0" 28 | }, 29 | "devDependencies": { 30 | "@types/node": "^16.7.13", 31 | "@types/react": "^17.0.20", 32 | "@types/react-dom": "^17.0.9", 33 | "@typescript-eslint/eslint-plugin": "^5.8.1", 34 | "@typescript-eslint/parser": "^5.8.1", 35 | "babel-eslint": "^10.0.3", 36 | "cross-env": "^7.0.2", 37 | "eslint": "^7.32.0", 38 | "eslint-config-prettier": "^6.7.0", 39 | "eslint-config-standard": "^16.0.3", 40 | "eslint-config-standard-react": "^9.2.0", 41 | "eslint-plugin-import": "^2.25.3", 42 | "eslint-plugin-node": "^11.1.0", 43 | "eslint-plugin-prettier": "^3.1.1", 44 | "eslint-plugin-promise": "^5.2.0", 45 | "eslint-plugin-react": "^7.28.0", 46 | "eslint-plugin-standard": "^4.0.1", 47 | "firebase": "^9.6.1", 48 | "microbundle-crl": "^0.13.10", 49 | "npm-run-all": "^4.1.5", 50 | "prettier": "^2.0.4", 51 | "react": "^17.0.0", 52 | "typescript": "^4.4.2" 53 | }, 54 | "files": [ 55 | "dist" 56 | ], 57 | "publishConfig": { 58 | "access": "public" 59 | }, 60 | "gitHead": "b6438b79175583863d895f10403197a300b8a6d9" 61 | } 62 | -------------------------------------------------------------------------------- /packages/composer-markdown/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@discuzz/composer-markdown", 3 | "version": "1.11.18", 4 | "description": "Discuzz composer markdown", 5 | "author": "MartinPham", 6 | "license": "MIT", 7 | "repository": "discuzz/discuzz", 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "source": "src/index.tsx", 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "scripts": { 15 | "libbuild": "run-s build", 16 | "prebuild": "rm -rf dist", 17 | "build": "microbundle-crl --format cjs", 18 | "start": "microbundle-crl watch --no-compress --format cjs" 19 | }, 20 | "dependencies": { 21 | "@discuzz/core": "^1.11.18" 22 | }, 23 | "peerDependencies": { 24 | "react": "^17.0.0", 25 | "react-is": "^17.0.0" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^16.7.13", 29 | "@types/react": "^17.0.20", 30 | "@types/react-dom": "^17.0.9", 31 | "@typescript-eslint/eslint-plugin": "^5.8.1", 32 | "@typescript-eslint/parser": "^5.8.1", 33 | "babel-eslint": "^10.0.3", 34 | "cross-env": "^7.0.2", 35 | "eslint": "^7.32.0", 36 | "eslint-config-prettier": "^6.7.0", 37 | "eslint-config-standard": "^16.0.3", 38 | "eslint-config-standard-react": "^9.2.0", 39 | "eslint-plugin-import": "^2.25.3", 40 | "eslint-plugin-node": "^11.1.0", 41 | "eslint-plugin-prettier": "^3.1.1", 42 | "eslint-plugin-promise": "^5.2.0", 43 | "eslint-plugin-react": "^7.28.0", 44 | "eslint-plugin-standard": "^4.0.1", 45 | "microbundle-crl": "^0.13.10", 46 | "npm-run-all": "^4.1.5", 47 | "prettier": "^2.0.4", 48 | "react": "^17.0.0", 49 | "rich-markdown-editor": "^11.21.3", 50 | "styled-components": "^5.3.3", 51 | "typescript": "^4.4.2" 52 | }, 53 | "publishConfig": { 54 | "access": "public" 55 | }, 56 | "files": [ 57 | "dist" 58 | ], 59 | "gitHead": "b6438b79175583863d895f10403197a300b8a6d9" 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/prelease-on-main.yml: -------------------------------------------------------------------------------- 1 | name: Pre-release on Main 2 | 3 | on: 4 | release: 5 | types: [ prereleased ] 6 | 7 | jobs: 8 | prerelease: 9 | name: Pre-release 10 | runs-on: ubuntu-latest 11 | if: ${{ github.event.release.target_commitish == 'main' }} 12 | 13 | strategy: 14 | matrix: 15 | node-version: [lts/*] 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | 21 | - name: Configure Git 22 | run: | 23 | git config --global user.name $GITHUB_ACTOR 24 | git config --global user.email $GITHUB_ACTOR@users.noreply.github.com 25 | 26 | - name: Setup Node ${{ matrix.node_version }} 27 | uses: actions/setup-node@v2 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | registry-url: 'https://registry.npmjs.org' 31 | 32 | - name: Cache dependencies 33 | uses: actions/cache@v2 34 | with: 35 | path: | 36 | ./node_modules 37 | ./.yarn 38 | key: ${{ runner.os }}-yarn-modules-${{ hashFiles('./yarn.lock') }} 39 | restore-keys: | 40 | ${{ runner.os }}-yarn-modules- 41 | 42 | - name: Install build dependencies 43 | run: | 44 | yarn --prefer-offline --pure-lockfile --cache-folder .yarn --modules-folder node_modules 45 | 46 | - name: Build 47 | run: | 48 | export BASE_URL=$PUBLIC_URL 49 | yarn build 50 | env: 51 | SKIP_PREFLIGHT_CHECK: true 52 | PUBLIC_URL: https://next.discuzz.mph.am/ 53 | SERVICE_CONFIG: ${{ secrets.SERVICE_CONFIG }} 54 | AUTHS: ${{ secrets.AUTHS }} 55 | 56 | - name: Set version 57 | run: | 58 | yarn version:set prerelease --no-git-tag-version --preid next-$GITHUB_SHA && yarn version:commit 59 | 60 | - name: Publish to NPM 61 | run: | 62 | yarn version:release --dist-tag next 63 | env: 64 | NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} -------------------------------------------------------------------------------- /packages/discuzz/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo } from 'react' 2 | import { ThemeProvider, createTheme } from '@mui/material/styles' 3 | import ScopedCssBaseline from '@mui/material/ScopedCssBaseline' 4 | import { 5 | DiscuzzCore, 6 | SignInProviderId, 7 | Config, 8 | ServiceSource 9 | } from '@discuzz/core' 10 | import { prefersDarkMode } from 'utils/darkMode' 11 | import { darkTheme, lightTheme } from 'config/mui' 12 | import { Theme } from 'enums/Theme' 13 | export { Theme } from 'enums/Theme' 14 | export { prefersDarkMode } from 'utils/darkMode' 15 | export { createProvider, Auth, loadService } from '@discuzz/core' 16 | export type { ComposerProps, ContentProps, Config, ServiceSource } from '@discuzz/core' 17 | 18 | import logger, { LogLevelDesc } from 'loglevel' 19 | 20 | export type DiscuzzProps = { 21 | url: string, 22 | service: ServiceSource, 23 | auths: SignInProviderId[], 24 | theme?: Theme | any, 25 | config?: Config, 26 | 27 | locale: any, 28 | logLevel?: LogLevelDesc | any 29 | }; 30 | 31 | export const Discuzz = ({ 32 | url, 33 | service, 34 | auths, 35 | theme = Theme.AUTO, 36 | config, 37 | locale, 38 | logLevel = 'warn' 39 | }: DiscuzzProps) => { 40 | useEffect(() => { 41 | console.log('[@discuzz/discuzz] LogLevel: ' + logLevel) 42 | 43 | logger.setLevel(logLevel) 44 | }, [logLevel]) 45 | 46 | const muiTheme: any = useMemo(() => { 47 | let themeObject = theme 48 | if (typeof themeObject === 'string') { 49 | themeObject = (theme === Theme.AUTO ? prefersDarkMode : (theme === Theme.DARK)) ? darkTheme : lightTheme 50 | } 51 | 52 | return createTheme(themeObject) 53 | }, [theme, prefersDarkMode]) 54 | 55 | 56 | return ( 57 | 58 | 59 | 66 | 67 | 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/beta-on-develop.yml: -------------------------------------------------------------------------------- 1 | name: Beta release on Develop 2 | 3 | on: 4 | release: 5 | types: [ prereleased ] 6 | 7 | jobs: 8 | prerelease: 9 | name: Beta release 10 | runs-on: ubuntu-latest 11 | if: ${{ github.event.release.target_commitish == 'develop' }} 12 | 13 | strategy: 14 | matrix: 15 | node-version: [lts/*] 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | 21 | - name: Configure Git 22 | run: | 23 | git config --global user.name $GITHUB_ACTOR 24 | git config --global user.email $GITHUB_ACTOR@users.noreply.github.com 25 | 26 | - name: Setup Node ${{ matrix.node_version }} 27 | uses: actions/setup-node@v2 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | registry-url: 'https://registry.npmjs.org' 31 | 32 | - name: Cache dependencies 33 | uses: actions/cache@v2 34 | with: 35 | path: | 36 | ./node_modules 37 | ./.yarn 38 | key: ${{ runner.os }}-yarn-modules-${{ hashFiles('./yarn.lock') }} 39 | restore-keys: | 40 | ${{ runner.os }}-yarn-modules- 41 | 42 | - name: Install build dependencies 43 | run: | 44 | yarn --prefer-offline --pure-lockfile --cache-folder .yarn --modules-folder node_modules 45 | 46 | - name: Build 47 | run: | 48 | export BASE_URL=$PUBLIC_URL 49 | yarn build 50 | env: 51 | SKIP_PREFLIGHT_CHECK: true 52 | PUBLIC_URL: https://beta.discuzz.mph.am/ 53 | SERVICE_CONFIG: ${{ secrets.SERVICE_CONFIG }} 54 | AUTHS: ${{ secrets.AUTHS }} 55 | 56 | - name: Set version 57 | run: | 58 | yarn version:set prepatch --no-git-tag-version --preid beta-$GITHUB_SHA && yarn version:commit 59 | 60 | - name: Publish to NPM 61 | run: | 62 | yarn version:release --dist-tag beta 63 | env: 64 | NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} -------------------------------------------------------------------------------- /packages/core/src/services/post.ts: -------------------------------------------------------------------------------- 1 | import { useDataSource } from 'components/ServiceSourceProvider' 2 | import { Post } from 'types/Post' 3 | import { 4 | AddPostCommand, 5 | ApprovePendingPostCommand, 6 | EditPendingPostCommand, 7 | EditPostCommand, 8 | PendingPostListQuery, 9 | PostListQuery, 10 | PostQuery, 11 | RejectPendingPostCommand, 12 | RemovePostCommand, 13 | TogglePostVoteCommand 14 | } from 'types/PostService' 15 | 16 | export const usePostQuery: PostQuery = (id: string) => { 17 | const data = useDataSource() 18 | return data!.usePostQuery(id) 19 | } 20 | 21 | export const usePostListQuery: PostListQuery = (url: string, pagination: number, parent: Post | null) => { 22 | const data = useDataSource() 23 | return data!.usePostListQuery(url, pagination, parent) 24 | } 25 | 26 | export const useAddPostCommand: AddPostCommand = () => { 27 | const data = useDataSource() 28 | return data!.useAddPostCommand() 29 | } 30 | 31 | export const useEditPostCommand: EditPostCommand = () => { 32 | const data = useDataSource() 33 | return data!.useEditPostCommand() 34 | } 35 | 36 | export const useRemovePostCommand: RemovePostCommand = () => { 37 | const data = useDataSource() 38 | return data!.useRemovePostCommand() 39 | } 40 | 41 | export const useTogglePostVoteCommand: TogglePostVoteCommand = () => { 42 | const data = useDataSource() 43 | return data!.useTogglePostVoteCommand() 44 | } 45 | 46 | export const usePendingPostQuery: PendingPostListQuery = () => { 47 | const data = useDataSource() 48 | return data!.usePendingPostQuery() 49 | } 50 | 51 | export const useEditPendingPostCommand: EditPendingPostCommand = () => { 52 | const data = useDataSource() 53 | return data!.useEditPendingPostCommand() 54 | } 55 | export const useRejectPendingPostCommand: RejectPendingPostCommand = () => { 56 | const data = useDataSource() 57 | return data!.useRejectPendingPostCommand() 58 | } 59 | export const useApprovePendingPostCommand: ApprovePendingPostCommand = () => { 60 | const data = useDataSource() 61 | return data!.useApprovePendingPostCommand() 62 | } 63 | -------------------------------------------------------------------------------- /packages/discuzz/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@discuzz/discuzz", 3 | "version": "1.11.18", 4 | "description": "Discuzz component", 5 | "author": "MartinPham", 6 | "license": "MIT", 7 | "repository": "discuzz/discuzz", 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "source": "src/index.tsx", 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "scripts": { 15 | "libbuild": "run-s build", 16 | "prebuild": "rm -rf dist", 17 | "build": "microbundle-crl --format cjs", 18 | "start": "microbundle-crl watch --no-compress --format cjs" 19 | }, 20 | "dependencies": { 21 | "@discuzz/core": "^1.11.18", 22 | "@emotion/react": "^11.7.1", 23 | "@emotion/styled": "^11.6.0", 24 | "@mui/icons-material": "^5.2.5", 25 | "@mui/lab": "^5.0.0-alpha.61", 26 | "@mui/material": "^5.2.5", 27 | "loglevel": "^1.8.0", 28 | "notistack": "^2.0.3", 29 | "react-transition-group": "^4.4.2" 30 | }, 31 | "peerDependencies": { 32 | "react": "^17.0.0" 33 | }, 34 | "devDependencies": { 35 | "@types/node": "^16.7.13", 36 | "@types/react": "^17.0.20", 37 | "@types/react-dom": "^17.0.9", 38 | "@typescript-eslint/eslint-plugin": "^5.8.1", 39 | "@typescript-eslint/parser": "^5.8.1", 40 | "babel-eslint": "^10.0.3", 41 | "cross-env": "^7.0.2", 42 | "eslint": "^7.32.0", 43 | "eslint-config-prettier": "^6.7.0", 44 | "eslint-config-standard": "^16.0.3", 45 | "eslint-config-standard-react": "^9.2.0", 46 | "eslint-plugin-import": "^2.25.3", 47 | "eslint-plugin-node": "^11.1.0", 48 | "eslint-plugin-prettier": "^3.1.1", 49 | "eslint-plugin-promise": "^5.2.0", 50 | "eslint-plugin-react": "^7.28.0", 51 | "eslint-plugin-standard": "^4.0.1", 52 | "microbundle-crl": "^0.13.10", 53 | "npm-run-all": "^4.1.5", 54 | "prettier": "^2.0.4", 55 | "react": "^17.0.0", 56 | "typescript": "^4.4.2" 57 | }, 58 | "files": [ 59 | "dist" 60 | ], 61 | "publishConfig": { 62 | "access": "public" 63 | }, 64 | "gitHead": "b6438b79175583863d895f10403197a300b8a6d9" 65 | } 66 | -------------------------------------------------------------------------------- /packages/core/src/components/BackofficeApp/BackofficeDialog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useTheme } from '@mui/material/styles' 3 | import IconButton from '@mui/material/IconButton' 4 | import useMediaQuery from '@mui/material/useMediaQuery' 5 | import Button from '@mui/material/Button' 6 | import Dialog from '@mui/material/Dialog' 7 | import DialogActions from '@mui/material/DialogActions' 8 | import DialogContent from '@mui/material/DialogContent' 9 | import DialogTitle from '@mui/material/DialogTitle' 10 | import { SlideUpTransition } from 'utils/transitions' 11 | import CloseIcon from '@mui/icons-material/Close' 12 | import { BackofficeApp } from '.' 13 | import { useLocale } from 'components/LocaleProvider' 14 | 15 | type BackofficeDialogProps = { 16 | onClose: () => void, 17 | open: boolean 18 | } 19 | 20 | const BackofficeDialog = ({ onClose, open, ...props }: BackofficeDialogProps) => { 21 | const { messages } = useLocale() 22 | const theme = useTheme() 23 | const fullScreen = useMediaQuery(theme.breakpoints.down('md')) 24 | 25 | return ( 26 | 37 | 38 | {messages.managePendingPosts} 39 | theme.palette.grey[500] 47 | }} 48 | > 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ) 60 | } 61 | 62 | export default BackofficeDialog 63 | -------------------------------------------------------------------------------- /packages/core/src/components/AuthProvider/components/SignInDialog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Avatar from '@mui/material/Avatar' 3 | import List from '@mui/material/List' 4 | import ListItem from '@mui/material/ListItem' 5 | import ListItemAvatar from '@mui/material/ListItemAvatar' 6 | import ListItemText from '@mui/material/ListItemText' 7 | import DialogTitle from '@mui/material/DialogTitle' 8 | import Dialog from '@mui/material/Dialog' 9 | import { providers, SignInProvider } from '../config/signInProviders' 10 | import { SlideUpTransition } from 'utils/transitions' 11 | 12 | import { useLocale } from 'components/LocaleProvider' 13 | import { SignInProviderId } from '../enums/SignInProviderId' 14 | 15 | type SignInDialogProps = { 16 | enabledProviders: SignInProviderId[] 17 | open: boolean 18 | onProviderSelected: (provider?: SignInProvider) => void 19 | } 20 | 21 | export const SignInDialog = ({ enabledProviders, open, onProviderSelected }: SignInDialogProps) => { 22 | const { messages } = useLocale() 23 | const closeDialog = () => { 24 | onProviderSelected(undefined) 25 | } 26 | 27 | const selectProvider = (provider: SignInProvider) => { 28 | onProviderSelected(provider) 29 | } 30 | 31 | return ( 32 | 33 | {messages.signInWith} 34 | 35 | {enabledProviders.map((providerId) => { 36 | const provider: SignInProvider = providers[providerId] 37 | 38 | return ( 39 | selectProvider(provider)} key={providerId}> 40 | 43 | 47 | 48 |
} /> 49 | 50 | ) 51 | })} 52 | 53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /packages/core/src/components/AuthProvider/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState, Fragment } from 'react' 2 | import { AuthContext, Auth } from './contexts/AuthContext' 3 | import { SignInDialog } from './components/SignInDialog' 4 | import { SignInProvider } from './config/signInProviders' 5 | import { useSnackbar } from 'notistack' 6 | import { useCurrentUser, useSignIn, useSignOut } from 'services/auth' 7 | import { SignInProviderId } from './enums/SignInProviderId' 8 | export { useAuth } from './hooks/useAuth' 9 | export type { Auth } from './contexts/AuthContext' 10 | export { SignInProviderId } from './enums/SignInProviderId' 11 | 12 | type AuthProviderProps = { 13 | enabledProviders: SignInProviderId[] 14 | children: JSX.Element, 15 | }; 16 | 17 | export const AuthProvider = ({ enabledProviders, children }: AuthProviderProps) => { 18 | const { enqueueSnackbar } = useSnackbar() 19 | const user = useCurrentUser() 20 | const signIn = useSignIn() 21 | const signOut = useSignOut() 22 | 23 | const [signInDialogIsOpen, toggleSignInDialog] = useState(false) 24 | 25 | const providerValue: Auth = { 26 | user, 27 | signIn: useCallback(() => { 28 | toggleSignInDialog(true) 29 | }, []), 30 | 31 | signOut: async () => signOut(), 32 | signInAnonymously: async () => signIn() 33 | } 34 | 35 | return ( 36 | 37 | 38 | {children} 39 | 40 | 41 | { 45 | if (provider) { 46 | try { 47 | await signIn(provider.id) 48 | } catch (error) { 49 | if (error instanceof Error) { 50 | enqueueSnackbar(error.message, { 51 | variant: 'error' 52 | }) 53 | } else { 54 | console.error('Sign In error', error) 55 | } 56 | } 57 | } 58 | 59 | toggleSignInDialog(false) 60 | }} 61 | /> 62 | 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@discuzz/core", 3 | "version": "1.11.18", 4 | "description": "Discuzz core", 5 | "author": "MartinPham", 6 | "license": "MIT", 7 | "repository": "discuzz/core", 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "source": "src/index.tsx", 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "scripts": { 15 | "libbuild": "run-s build", 16 | "prebuild": "rm -rf dist", 17 | "build": "microbundle-crl --format cjs", 18 | "start": "microbundle-crl watch --no-compress --format cjs" 19 | }, 20 | "peerDependencies": { 21 | "@emotion/react": "^11.7.1", 22 | "@emotion/styled": "^11.6.0", 23 | "@mui/icons-material": "^5.2.5", 24 | "@mui/lab": "^5.0.0-alpha.61", 25 | "@mui/material": "^5.2.5", 26 | "loglevel": "^1.8.0", 27 | "notistack": "^2.0.3", 28 | "react": "^17.0.0", 29 | "react-is": "^17.0.0", 30 | "react-transition-group": "^4.4.2" 31 | }, 32 | "devDependencies": { 33 | "@emotion/react": "^11.7.1", 34 | "@emotion/styled": "^11.6.0", 35 | "@mui/icons-material": "^5.2.5", 36 | "@mui/lab": "^5.0.0-alpha.61", 37 | "@mui/material": "^5.2.5", 38 | "@types/node": "^16.7.13", 39 | "@types/react": "^17.0.20", 40 | "@types/react-dom": "^17.0.9", 41 | "@typescript-eslint/eslint-plugin": "^5.8.1", 42 | "@typescript-eslint/parser": "^5.8.1", 43 | "babel-eslint": "^10.0.3", 44 | "cross-env": "^7.0.2", 45 | "eslint": "^7.32.0", 46 | "eslint-config-prettier": "^6.7.0", 47 | "eslint-config-standard": "^16.0.3", 48 | "eslint-config-standard-react": "^9.2.0", 49 | "eslint-plugin-import": "^2.25.3", 50 | "eslint-plugin-node": "^11.1.0", 51 | "eslint-plugin-prettier": "^3.1.1", 52 | "eslint-plugin-promise": "^5.2.0", 53 | "eslint-plugin-react": "^7.28.0", 54 | "eslint-plugin-standard": "^4.0.1", 55 | "loglevel": "^1.8.0", 56 | "microbundle-crl": "^0.13.10", 57 | "notistack": "^2.0.3", 58 | "npm-run-all": "^4.1.5", 59 | "prettier": "^2.0.4", 60 | "react": "^17.0.0", 61 | "react-transition-group": "^4.4.2", 62 | "typescript": "^4.4.2" 63 | }, 64 | "files": [ 65 | "dist" 66 | ], 67 | "publishConfig": { 68 | "access": "public" 69 | }, 70 | "gitHead": "b6438b79175583863d895f10403197a300b8a6d9" 71 | } 72 | -------------------------------------------------------------------------------- /packages/core/src/index.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-use-before-define 2 | import React from 'react' 3 | 4 | import { App, Config } from 'components/App' 5 | 6 | import { SnackbarProvider } from 'notistack' 7 | 8 | import { ServiceSourceProvider } from 'components/ServiceSourceProvider' 9 | import { AuthProvider, SignInProviderId } from 'components/AuthProvider' 10 | 11 | import { config as appConfig } from 'config/app' 12 | import { LocaleProvider } from 'components/LocaleProvider' 13 | 14 | export { createProvider, prefersLocale } from 'utils/locale' 15 | export type { ProviderProps } from 'utils/locale' 16 | export { SignInProviderId, useAuth } from 'components/AuthProvider' 17 | export type { Config } from 'components/App' 18 | export type { ComposerProps } from 'components/PostComposer' 19 | export type { ContentProps } from 'components/PostCard' 20 | export type { ServiceSource } from 'components/ServiceSourceProvider' 21 | export { trace, log, warn, fatal } from 'utils/logger' 22 | export type { CurrentUser, SignIn, SignOut } from 'types/AuthService' 23 | export type { User } from 'types/User' 24 | export type { Post } from 'types/Post' 25 | export type { PendingPost } from 'types/PendingPost' 26 | export { EMPTY_SYMBOL } from 'constants/composer' 27 | export { PostUpdateResult } from 'enums/PostUpdateResult' 28 | export { PendingPostUpdateResult } from 'enums/PostUpdateResult' 29 | export { RequestState } from 'enums/RequestState' 30 | export type { Auth, Data } from 'services/source' 31 | export { loadService } from 'services/source' 32 | 33 | export type { 34 | AddPostCommand, 35 | EditPostCommand, 36 | PostListQuery, 37 | PostQuery, 38 | RemovePostCommand, 39 | TogglePostVoteCommand, 40 | ApprovePendingPostCommand, 41 | EditPendingPostCommand, 42 | PendingPostListQuery, 43 | RejectPendingPostCommand 44 | } from 'types/PostService' 45 | 46 | export type DiscuzzCoreProps = { 47 | url: string, 48 | service: any, 49 | auths: SignInProviderId[], 50 | config?: Config, 51 | 52 | locale: any 53 | } 54 | 55 | 56 | export const DiscuzzCore = ({ 57 | url, 58 | service, 59 | auths: authEnabledProviders, 60 | config, 61 | locale 62 | }: DiscuzzCoreProps) => { 63 | const configObject: Config = { 64 | ...appConfig, 65 | ...(config || {}) 66 | } 67 | 68 | return ( 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | ) 79 | } 80 | 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discuz", 3 | "version": "1.0.0", 4 | "description": "discuz", 5 | "main": "index.js", 6 | "repository": "", 7 | "author": "martinpham.com", 8 | "license": "", 9 | "private": true, 10 | "workspaces": [ 11 | "packages/*" 12 | ], 13 | "devDependencies": { 14 | "@types/jest": "^27.0.1", 15 | "@types/md5": "^2.3.1", 16 | "@types/node": "^16.7.13", 17 | "@types/react": "^17.0.20", 18 | "@types/react-dom": "^17.0.9", 19 | "@types/styled-components": "^5.1.19", 20 | "@typescript-eslint/eslint-plugin": "^5.8.1", 21 | "@typescript-eslint/parser": "^5.8.1", 22 | "lerna": "^4.0.0", 23 | "source-map-explorer": "^2.5.2", 24 | "npm-run-all": "^4.1.5", 25 | "react-scripts": "5.0.0", 26 | "typescript": "^4.5.4" 27 | }, 28 | "dependencies": { 29 | "@discuzz/discuzz": "*", 30 | "@discuzz/viewer-markdown": "*", 31 | "@discuzz/composer-markdown": "*", 32 | "@discuzz/locale-en": "*", 33 | "@discuzz/locale-vi": "*", 34 | "@discuzz/auth-firebase": "*", 35 | "@discuzz/data-firestore": "*", 36 | "firebase": "^9.6.1", 37 | "date-fns": "^2.27.0", 38 | "markdown-to-jsx": "^7.1.5", 39 | "rich-markdown-editor": "^11.21.3", 40 | "styled-components": "^5.3.3", 41 | "react": "^17.0.2", 42 | "react-dom": "^17.0.2" 43 | }, 44 | "scripts": { 45 | "lerna": "lerna", 46 | "libbuild": "lerna run libbuild --stream", 47 | "prelibstart": "lerna run libbuild --scope @discuzz/core --stream", 48 | "libstart": "lerna run --parallel start --stream", 49 | "appstart": "if [[ -f \"./.env\" ]]; then . ./.env; fi && react-scripts start", 50 | "start": "run-p libstart appstart", 51 | "build": "run-s libbuild && run-s appbuild", 52 | "appbuild": "if [[ -f \"./.env\" ]]; then . ./.env; fi && GENERATE_SOURCEMAP=false react-scripts build && mv build/index.html build/tmp.html && envsubst < build/tmp.html > build/index.html && rm build/tmp.html && cp build/static/js/main.*.js build/static/js/main.js", 53 | "deploy": "run-s build && firebase deploy", 54 | "version:set": "lerna version", 55 | "version:commit": "git add -A && git commit -m \"Automated commit version `node -p 'require(\"./lerna.json\").version'`\"", 56 | "version:release": "lerna publish from-package", 57 | "preanalyze": "run-s libbuild", 58 | "analyze": "react-scripts build && source-map-explorer 'build/static/js/*.js'" 59 | }, 60 | "browserslist": { 61 | "production": [ 62 | ">0.2%", 63 | "not dead", 64 | "not op_mini all" 65 | ], 66 | "development": [ 67 | "last 1 chrome version", 68 | "last 1 firefox version", 69 | "last 1 safari version" 70 | ] 71 | }, 72 | "prettier": { 73 | "singleQuote": true, 74 | "semi": false, 75 | "trailingComma": "none" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | Discuzz 25 | 26 | 27 | 28 | 29 | 30 | 40 | 41 |
42 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /packages/core/src/components/AuthProvider/config/signInProviders.ts: -------------------------------------------------------------------------------- 1 | import { SxProps } from '@mui/material' 2 | import { SignInProviderId } from '../enums/SignInProviderId' 3 | 4 | export type SignInProvider = { 5 | id: SignInProviderId; 6 | name: string; 7 | icon?: { 8 | source: string; 9 | styles: SxProps; 10 | }; 11 | }; 12 | 13 | type SignInProviders = { 14 | // eslint-disable-next-line no-unused-vars 15 | [key in SignInProviderId]: SignInProvider; 16 | }; 17 | 18 | const baseIconStyles: SxProps = { 19 | p: 0.5, 20 | mr: -10, 21 | width: 20, 22 | height: 20 23 | } 24 | 25 | export const providers: SignInProviders = { 26 | [SignInProviderId.GOOGLE]: { 27 | id: SignInProviderId.GOOGLE, 28 | name: 'Google', 29 | icon: { 30 | source: 31 | 'https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg', 32 | styles: { 33 | bgcolor: '#fff', 34 | ...baseIconStyles 35 | } 36 | }, 37 | // implementation: () => new GoogleAuthProvider() 38 | }, 39 | 40 | [SignInProviderId.APPLE]: { 41 | id: SignInProviderId.APPLE, 42 | name: 'Apple', 43 | icon: { 44 | source: 'https://unpkg.com/simple-icons@v6/icons/apple.svg', 45 | styles: { 46 | bgcolor: '#000', 47 | ...baseIconStyles, 48 | img: { 49 | filter: 'invert(1)' 50 | } 51 | } 52 | }, 53 | // implementation: () => { 54 | // const provider = new OAuthProvider('apple.com') 55 | // provider.addScope('name') 56 | // provider.addScope('email') 57 | 58 | // return provider 59 | // } 60 | }, 61 | 62 | [SignInProviderId.FACEBOOK]: { 63 | id: SignInProviderId.FACEBOOK, 64 | name: 'Meta (Facebook)', 65 | icon: { 66 | source: 'https://seeklogo.com/images/M/meta-icon-new-facebook-2021-logo-83520C311D-seeklogo.com.png', 67 | styles: { 68 | bgcolor: '#fff', 69 | ...baseIconStyles, 70 | img: { 71 | height: 'auto' 72 | } 73 | } 74 | }, 75 | // implementation: () => new FacebookAuthProvider() 76 | }, 77 | [SignInProviderId.TWITTER]: { 78 | id: SignInProviderId.TWITTER, 79 | name: 'Twitter', 80 | icon: { 81 | source: 'https://unpkg.com/simple-icons@v6/icons/twitter.svg', 82 | styles: { 83 | bgcolor: '#1DA1F2', 84 | ...baseIconStyles, 85 | img: { 86 | filter: 'invert(1)' 87 | } 88 | } 89 | }, 90 | // implementation: () => new TwitterAuthProvider() 91 | }, 92 | 93 | [SignInProviderId.MICROSOFT]: { 94 | id: SignInProviderId.MICROSOFT, 95 | name: 'Microsoft', 96 | icon: { 97 | source: 98 | 'https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/microsoft.svg', 99 | styles: { 100 | bgcolor: '#2F2F2F', 101 | ...baseIconStyles 102 | } 103 | }, 104 | // implementation: () => new OAuthProvider('microsoft.com') 105 | }, 106 | 107 | [SignInProviderId.YAHOO]: { 108 | id: SignInProviderId.YAHOO, 109 | name: 'Yahoo', 110 | icon: { 111 | source: 'https://unpkg.com/simple-icons@v6/icons/yahoo.svg', 112 | styles: { 113 | bgcolor: '#720e9e', 114 | ...baseIconStyles, 115 | img: { 116 | filter: 'invert(1)' 117 | } 118 | } 119 | }, 120 | // implementation: () => new OAuthProvider('yahoo.com') 121 | }, 122 | 123 | [SignInProviderId.GITHUB]: { 124 | id: SignInProviderId.GITHUB, 125 | name: 'GitHub', 126 | icon: { 127 | source: 'https://unpkg.com/simple-icons@v6/icons/github.svg', 128 | styles: { 129 | bgcolor: '#333', 130 | ...baseIconStyles, 131 | img: { 132 | filter: 'invert(1)' 133 | } 134 | } 135 | }, 136 | // implementation: () => new GithubAuthProvider() 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /.github/workflows/release-on-tag.yml: -------------------------------------------------------------------------------- 1 | name: Release on Tag 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | if: ${{ github.event.base_ref == 'refs/heads/main' && !contains(github.event.ref, '-next.') }} 13 | 14 | strategy: 15 | matrix: 16 | node-version: [lts/*] 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v2 21 | with: 22 | ref: main 23 | 24 | - name: Configure Git 25 | run: | 26 | git config --global user.name $GITHUB_ACTOR 27 | git config --global user.email $GITHUB_ACTOR@users.noreply.github.com 28 | 29 | - name: Setup Node ${{ matrix.node_version }} 30 | uses: actions/setup-node@v2 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | registry-url: 'https://registry.npmjs.org' 34 | 35 | - name: Cache dependencies 36 | uses: actions/cache@v2 37 | with: 38 | path: | 39 | ./node_modules 40 | ./.yarn 41 | key: ${{ runner.os }}-yarn-modules-${{ hashFiles('./yarn.lock') }} 42 | restore-keys: | 43 | ${{ runner.os }}-yarn-modules- 44 | 45 | - name: Install build dependencies 46 | run: | 47 | yarn --prefer-offline --pure-lockfile --cache-folder .yarn --modules-folder node_modules 48 | 49 | - name: Build 50 | run: | 51 | export BASE_URL=$PUBLIC_URL 52 | cp README.md packages/discuzz/README.md 53 | cp README.md packages/core/README.md 54 | yarn build 55 | env: 56 | SKIP_PREFLIGHT_CHECK: true 57 | PUBLIC_URL: https://discuzz.mph.am/ 58 | SERVICE_CONFIG: ${{ secrets.SERVICE_CONFIG }} 59 | AUTHS: ${{ secrets.AUTHS }} 60 | 61 | - name: Set version 62 | run: | 63 | yarn version:set $GITHUB_REF_NAME --no-git-tag-version && ((yarn version:commit && git push origin main) || (echo "Nothing to commit")) 64 | 65 | - name: Publish to NPM 66 | run: | 67 | yarn version:release 68 | env: 69 | NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} 70 | 71 | - name: Merge to develop 72 | run: | 73 | (git checkout -b develop) || (git checkout develop) 74 | (git pull --depth 1 origin develop --rebase -X theirs --allow-unrelated-histories) || (echo "Develop branch synced") 75 | git merge -s recursive -X theirs main --allow-unrelated-histories --no-commit 76 | ((yarn version:commit && git push origin develop) || (echo "Nothing to commit")) 77 | 78 | - name: Synchronize & push into Pages branch 79 | run: | 80 | DEPLOY_BRANCH=pages 81 | GIT_COMMIT=$(git rev-parse --short HEAD) 82 | mkdir RELEASE 83 | mv .git RELEASE/.git 84 | cd RELEASE 85 | echo "Checking out $DEPLOY_BRANCH" 86 | git checkout -b $DEPLOY_BRANCH 87 | git status 88 | git stash && git stash drop 89 | (git pull --depth 1 origin $DEPLOY_BRANCH --rebase -X theirs --allow-unrelated-histories) || (echo "Docs branch synced") 90 | mv ./.git ../.git 91 | cd .. 92 | # Push build into the docs branch 93 | mv build BUILD 94 | echo "Pushing build $GIT_COMMIT to $DEPLOY_BRANCH" 95 | mv .git BUILD/.git 96 | cd BUILD 97 | cp index.html 404.html 98 | echo 'discuzz.mph.am' > 'CNAME' 99 | git status 100 | git add -A 101 | git commit -m "Automated Pages build $GIT_COMMIT" 102 | git push origin $DEPLOY_BRANCH 103 | cd .. 104 | 105 | - name: Create release 106 | uses: "marvinpinto/action-automatic-releases@latest" 107 | continue-on-error: true 108 | with: 109 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 110 | prerelease: false 111 | 112 | -------------------------------------------------------------------------------- /packages/core/src/components/Buttons/ShareButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import Avatar from '@mui/material/Avatar' 3 | import Menu from '@mui/material/Menu' 4 | import MenuItem from '@mui/material/MenuItem' 5 | import IconButton from '@mui/material/IconButton' 6 | import { useSizeCheck } from 'hooks/useSizeCheck' 7 | import ShareIcon from '@mui/icons-material/Share' 8 | import Button from '@mui/material/Button' 9 | import { Post } from 'types/Post' 10 | import { useConfig } from 'components/App' 11 | 12 | type ShareButtonProps = { 13 | post: Post 14 | } 15 | export const ShareButton = ({ post } : ShareButtonProps) => { 16 | const [anchorEl, setAnchorEl] = React.useState(null) 17 | const open = Boolean(anchorEl) 18 | const handleClick = (event: React.MouseEvent) => { 19 | setAnchorEl(event.currentTarget) 20 | } 21 | const handleClose = () => { 22 | setAnchorEl(null) 23 | } 24 | const smSize = useSizeCheck('sm') 25 | const config = useConfig() 26 | 27 | const color = '#888' 28 | 29 | return ( 30 | 31 | {smSize 32 | ? ( 33 | 37 | 38 | 39 | ) 40 | : ( 41 | 50 | )} 51 | 86 | { 87 | global.open(`https://www.facebook.com/sharer.php?t=${post.contents}&u=${config.baseUrl}c/${post.id}`) 88 | }}> 89 |   Facebook 92 | 93 | { 94 | global.open(`https://twitter.com/intent/tweet?text=${post.contents}&url=${config.baseUrl}c/${post.id}`) 95 | }}> 96 |   Twitter 103 | 104 | { 105 | global.open(`https://www.linkedin.com/sharing/share-offsite/?url=${config.baseUrl}c/${post.id}`) 106 | }}> 107 |   LinkedIn 114 | 115 | 116 | 117 | ) 118 | } 119 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Discuzz, Config, prefersDarkMode, loadService } from '@discuzz/discuzz' 4 | 5 | const LocaleProviderEn = lazy(() => import('@discuzz/locale-en')) 6 | const LocaleProviderVi = lazy(() => import('@discuzz/locale-vi')) 7 | 8 | const ComposerMarkdown = lazy(() => import('@discuzz/composer-markdown')) 9 | 10 | const ViewerMarkdown = lazy(() => import('@discuzz/viewer-markdown')) 11 | 12 | const AuthFirebase = loadService(() => import('@discuzz/auth-firebase')) 13 | const DataFirestore = loadService(() => import('@discuzz/data-firestore')) 14 | 15 | export class WebComponent extends HTMLElement { 16 | connectedCallback() { 17 | const service = JSON.parse(this.getAttribute('service')!.replace(/'/g, '"'))! 18 | 19 | const serviceSource: any = { 20 | config: service.config 21 | } 22 | if (service.auth === 'firebase') { 23 | serviceSource.auth = AuthFirebase 24 | } 25 | if (service.data === 'firestore') { 26 | serviceSource.data = DataFirestore 27 | } 28 | 29 | 30 | 31 | const options: any = { 32 | url: this.getAttribute('url') || global.location.href, 33 | service: serviceSource, 34 | auths: JSON.parse(this.getAttribute('auths')!.replace(/'/g, '"'))! 35 | } 36 | 37 | if (this.getAttribute('theme')) { 38 | try { 39 | options.theme = JSON.parse(this.getAttribute('theme')!.replace(/'/g, '"'))! 40 | } catch (err) { 41 | options.theme = this.getAttribute('theme') 42 | } 43 | } 44 | if (this.getAttribute('locale')) { 45 | options.locale = this.getAttribute('locale')! 46 | } 47 | 48 | const config: Config = { 49 | baseUrl: this.getAttribute('baseUrl') || '' 50 | } 51 | 52 | if (this.getAttribute('richText')) { 53 | config.richText = this.getAttribute('richText')! === 'true' 54 | } 55 | if (this.getAttribute('padding')) { 56 | config.padding = Number(this.getAttribute('padding')) 57 | } 58 | if (this.getAttribute('pagination')) { 59 | config.pagination = Number(this.getAttribute('pagination')) 60 | } 61 | 62 | let logLevel: any 63 | if (this.getAttribute('logLevel')) { 64 | logLevel = this.getAttribute('logLevel') 65 | } 66 | 67 | options.config = config 68 | 69 | ReactDOM.render( 70 | ( 71 | ...}> 72 | 85 | 86 | ), 87 | this 88 | ) 89 | } 90 | } 91 | 92 | (global as any).prefersDarkMode = prefersDarkMode 93 | 94 | if (process.env.NODE_ENV === 'development') { 95 | customElements.define('x-discuzz-dev', WebComponent) 96 | 97 | const url = new URL(global.location.toString()) 98 | const pathname = url.pathname.toString() 99 | const ftUrl = pathname.indexOf('/c/') === 0 ? `ft:${pathname.substr(3)}` : '' 100 | const ftLocale = url.searchParams.get('locale') || 'en' 101 | const ftTheme = url.searchParams.get('theme') || 'auto' 102 | 103 | document.getElementById('root')!.innerHTML = ` 104 | 114 | ` 115 | } else if (global.customElements) { 116 | customElements.define('x-discuzz', WebComponent) 117 | } 118 | -------------------------------------------------------------------------------- /packages/core/src/components/PostComposer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState, Fragment } from 'react' 2 | import Card from '@mui/material/Card' 3 | import CardHeader from '@mui/material/CardHeader' 4 | import CardContent from '@mui/material/CardContent' 5 | import CardActions from '@mui/material/CardActions' 6 | import Avatar from '@mui/material/Avatar' 7 | import SendIcon from '@mui/icons-material/Send' 8 | import { useAuth } from 'components/AuthProvider' 9 | 10 | import PersonIcon from '@mui/icons-material/Person' 11 | import { Composer } from './components/Composer' 12 | import { EMPTY_SYMBOL } from 'constants/composer' 13 | import Slide from '@mui/material/Slide' 14 | import LoadingButton from '@mui/lab/LoadingButton' 15 | import { useLocale } from 'components/LocaleProvider' 16 | import { useAddPostCommand } from 'services/post' 17 | import { ManagerButton } from 'components/Buttons/ManagerButton' 18 | import { SignInButton } from 'components/Buttons/SignInButton' 19 | import { SignOutButton } from 'components/Buttons/SignOutButton' 20 | import { useSnackbar } from 'notistack' 21 | import { PostUpdateResult } from 'enums/PostUpdateResult' 22 | import { warn } from 'utils/logger' 23 | export { Composer } from './components/Composer' 24 | export type { ComposerProps } from './components/Composer' 25 | 26 | type PostComposerProps = { 27 | url: string 28 | } 29 | 30 | export const PostComposer = ({ url }: PostComposerProps) => { 31 | const { messages } = useLocale() 32 | const { user } = useAuth() 33 | const { enqueueSnackbar } = useSnackbar() 34 | 35 | const [contents, setContents] = useState('') 36 | const [isPostingPost, togglePostingPost] = useState(false) 37 | 38 | const addPostCommand = useAddPostCommand() 39 | 40 | const addPost = useCallback(async () => { 41 | togglePostingPost(true) 42 | 43 | const newContents = contents 44 | setContents('') 45 | 46 | const result = await addPostCommand( 47 | newContents, url 48 | ) 49 | 50 | if (result === PostUpdateResult.UPDATED) { 51 | enqueueSnackbar(messages.postAdded, { 52 | variant: 'success' 53 | }) 54 | } else if (result === PostUpdateResult.PENDING) { 55 | enqueueSnackbar(messages.postSubmitted, { 56 | variant: 'warning' 57 | }) 58 | } else { 59 | warn('add post returns unexpected result', result) 60 | } 61 | 62 | setTimeout(() => togglePostingPost(false), 1) 63 | }, [ 64 | togglePostingPost, 65 | setContents, 66 | contents, 67 | url, 68 | enqueueSnackbar, 69 | messages 70 | ]) 71 | 72 | return user 73 | ? ( 74 | 75 | 76 | 84 | ) 85 | : ( 86 | 87 | ) 88 | } 89 | action={( 90 | 91 | 92 | 93 | 94 | 95 | )} 96 | sx={{ 97 | bgcolor: 'divider', 98 | pt: 1, 99 | pb: 1 100 | }} 101 | title={user.displayName || messages.anonymous} 102 | /> 103 | 104 | 107 | { 111 | setContents(value) 112 | }} 113 | /> 114 | 115 | 116 | 122 | 123 | }> 127 | {messages.send} 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | ) 136 | : null 137 | } 138 | -------------------------------------------------------------------------------- /packages/auth-firebase/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { FirebaseApp, getApp, initializeApp } from 'firebase/app' 2 | import { getAuth } from 'firebase/auth' 3 | import { 4 | getFirestore 5 | } from 'firebase/firestore' 6 | 7 | import { 8 | IdTokenResult, 9 | onAuthStateChanged, 10 | signInWithPopup, 11 | signOut as firebaseAuthSignOut, 12 | signInAnonymously as firebaseAuthSignInAnonymously, 13 | GoogleAuthProvider, 14 | FacebookAuthProvider, 15 | TwitterAuthProvider, 16 | GithubAuthProvider, 17 | OAuthProvider 18 | } from 'firebase/auth' 19 | import { doc, Firestore, getDoc } from 'firebase/firestore' 20 | import { 21 | log, CurrentUser, SignIn, SignOut, User, SignInProviderId, Auth 22 | } from '@discuzz/core' 23 | import md5 from 'md5' 24 | import { useCallback, useEffect, useState } from 'react' 25 | 26 | const nop = () => { } 27 | const asyncNop = async () => { } 28 | 29 | const createAnonymousUser = () => 30 | ({ 31 | uid: '', 32 | displayName: null, 33 | photoURL: null, 34 | emailVerified: false, 35 | isAnonymous: true, 36 | metadata: {}, 37 | providerData: [], 38 | refreshToken: '', 39 | tenantId: '', 40 | delete: asyncNop, 41 | getIdToken: () => new Promise(nop), 42 | getIdTokenResult: () => new Promise(nop), 43 | reload: asyncNop, 44 | toJSON: () => ({}), 45 | email: '', 46 | phoneNumber: '', 47 | providerId: '' 48 | } as User) 49 | 50 | const decorate = async (user: User, firestore?: Firestore) => { 51 | const decoratedUser: any = user 52 | 53 | if (decoratedUser.uid && decoratedUser.isAnonymous) { 54 | decoratedUser.displayName = null 55 | decoratedUser.photoURL = `https://www.gravatar.com/avatar/${md5( 56 | user.uid 57 | )}?d=identicon` 58 | 59 | try { 60 | await getDoc(doc(firestore!, 'check', 'requireAnonymousModeration')) 61 | decoratedUser.canSavePost = true 62 | log('anon can save post') 63 | } catch (error) { 64 | // console.error('Checking moderation error', error) 65 | } 66 | } else if (decoratedUser.emailVerified) { 67 | try { 68 | await getDoc(doc(firestore!, 'check', 'imAdmin')) 69 | decoratedUser.isAdmin = true 70 | decoratedUser.canSavePost = true 71 | log('admin') 72 | } catch (error) { 73 | // console.error('Checking admin error', error) 74 | try { 75 | await getDoc(doc(firestore!, 'check', 'requireModeration')) 76 | decoratedUser.canSavePost = true 77 | log('verified can save post') 78 | } catch (error) { 79 | // console.error('Checking moderation error', error) 80 | } 81 | } 82 | } 83 | return decoratedUser 84 | } 85 | 86 | 87 | export default (config: { [key: string]: string }): Auth => { 88 | let currentApp: (FirebaseApp | null) = null 89 | const appName = (config.id || 'discuzz') + '_auth' 90 | try { 91 | currentApp = getApp(appName) 92 | } catch (error) { 93 | currentApp = initializeApp(config, appName) 94 | } 95 | 96 | const auth = getAuth(currentApp!) 97 | const firestore = getFirestore(currentApp!) 98 | 99 | const useCurrentUser: CurrentUser = () => { 100 | const [user, setUser] = useState(undefined) 101 | 102 | useEffect(() => { 103 | if (!firestore) return 104 | const unsubscribeAuth = onAuthStateChanged( 105 | auth!, 106 | async (currentUser: any) => { 107 | if (currentUser) { 108 | setUser(await decorate(currentUser, firestore)) 109 | } else { 110 | setUser(createAnonymousUser()) 111 | } 112 | } 113 | ) 114 | 115 | return unsubscribeAuth 116 | }, [auth, firestore]) 117 | 118 | return user 119 | } 120 | 121 | const useSignIn: SignIn = () => { 122 | const signIn = useCallback( 123 | async (providerId?: SignInProviderId) => { 124 | if (providerId === undefined) { 125 | return decorate((await firebaseAuthSignInAnonymously(auth!)).user, firestore) 126 | } else { 127 | let provider = null 128 | switch (providerId) { 129 | case SignInProviderId.GOOGLE: 130 | provider = new GoogleAuthProvider() 131 | break; 132 | case SignInProviderId.FACEBOOK: 133 | provider = new FacebookAuthProvider() 134 | break; 135 | case SignInProviderId.TWITTER: 136 | provider = new TwitterAuthProvider() 137 | break; 138 | case SignInProviderId.GITHUB: 139 | provider = new GithubAuthProvider() 140 | break; 141 | case SignInProviderId.MICROSOFT: 142 | provider = new OAuthProvider('microsoft.com') 143 | break; 144 | case SignInProviderId.APPLE: 145 | provider = new OAuthProvider('apple.com') 146 | provider.addScope('name') 147 | provider.addScope('email') 148 | break; 149 | case SignInProviderId.YAHOO: 150 | provider = new OAuthProvider('yahoo.com') 151 | break; 152 | } 153 | 154 | if (provider === null) { 155 | throw new Error('SIGN_IN_PROVIDER_NOT_AVAILABLE') 156 | } 157 | 158 | return signInWithPopup(auth!, provider) 159 | } 160 | }, 161 | [auth] 162 | ) 163 | 164 | return signIn 165 | } 166 | 167 | const useSignOut: SignOut = () => { 168 | const signOut = useCallback(async () => firebaseAuthSignOut(auth!), [auth]) 169 | 170 | return signOut 171 | } 172 | 173 | return { 174 | useCurrentUser, 175 | useSignIn, 176 | useSignOut, 177 | data: { 178 | app: currentApp 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /firestore.rules.example: -------------------------------------------------------------------------------- 1 | rules_version = '2' 2 | 3 | service cloud.firestore { 4 | match /databases/{database}/documents { 5 | function userIsLoggedIn(auth) { 6 | return auth != null 7 | } 8 | 9 | function userIsVerified(auth) { 10 | return auth.token.email_verified 11 | } 12 | 13 | function userWillBeOwner(auth, data) { 14 | return auth.uid == data.author.id 15 | } 16 | 17 | function userIsOwner(auth, currentData) { 18 | return auth.uid == currentData.author.id 19 | } 20 | 21 | function userIsAdmin(auth) { 22 | return auth.token.firebase.sign_in_provider == "google.com" 23 | && auth.token.email_verified 24 | && auth.token.email == "ADMIN-EMAIL@HOST.COM" 25 | } 26 | 27 | function requireModeration() { 28 | return false 29 | } 30 | 31 | function requireAnonymousModeration() { 32 | return false 33 | } 34 | 35 | function postIsValid(data) { 36 | return data.contents != "" 37 | } 38 | 39 | function postIsNotDeleted(currentData) { 40 | return !("deletedAt" in currentData) 41 | } 42 | 43 | function postContentsIsNotChanged(data, currentData) { 44 | return data.contents == currentData.contents 45 | } 46 | 47 | function postDataIsNotChanged(data, currentData) { 48 | return data.paths == currentData.paths 49 | && data.url == currentData.url 50 | && data.author == currentData.author 51 | && data.parentId == currentData.parentId 52 | && data.approvedAt == currentData.approvedAt 53 | && data.createdAt == currentData.createdAt 54 | } 55 | function postVoteIsNotChanged(data, currentData) { 56 | return data.voted == currentData.voted 57 | && data.voters == currentData.voters 58 | } 59 | 60 | function postIsBeingDeleted(data, currentData) { 61 | return data.contents == "" 62 | && postIsNotDeleted(currentData) 63 | && postDataIsNotChanged(data, currentData) 64 | } 65 | 66 | function postIsBeingEdited(data, currentData) { 67 | return postIsValid(data) 68 | && postIsNotDeleted(currentData) 69 | && postDataIsNotChanged(data, currentData) 70 | && postVoteIsNotChanged(data, currentData) 71 | } 72 | 73 | function postCanBeVoted(data, currentData) { 74 | return "voters" in data 75 | && "voted" in data 76 | && postIsNotDeleted(currentData) 77 | && postDataIsNotChanged(data, currentData) 78 | && postContentsIsNotChanged(data, currentData) 79 | } 80 | 81 | function postIsBeingVoted(auth, data, currentData) { 82 | return postCanBeVoted(data, currentData) 83 | && data.voted - currentData.voted == 1 84 | && (auth.uid in data.voters) 85 | && !(auth.uid in currentData.voters) 86 | } 87 | 88 | function postIsBeingUnVoted(auth, data, currentData) { 89 | return postCanBeVoted(data, currentData) 90 | && currentData.voted - data.voted == 1 91 | && !(auth.uid in data.voters) 92 | && (auth.uid in currentData.voters) 93 | } 94 | function postRepliedIsBeingIncreased(data, currentData) { 95 | return postIsNotDeleted(currentData) 96 | && postDataIsNotChanged(data, currentData) 97 | && postContentsIsNotChanged(data, currentData) 98 | && postVoteIsNotChanged(data, currentData) 99 | && data.replied - currentData.replied == 1 100 | } 101 | 102 | match /check/imAdmin { 103 | allow read: if userIsAdmin(request.auth) 104 | } 105 | 106 | match /check/requireModeration { 107 | allow read: if !requireModeration() 108 | } 109 | 110 | match /check/requireAnonymousModeration { 111 | allow read: if !requireAnonymousModeration() 112 | } 113 | 114 | match /pending_posts/{document=**} { 115 | // allows only admin can see list 116 | allow list: if userIsAdmin(request.auth) 117 | 118 | // allows owner can read 119 | allow read: if userIsOwner(request.auth, resource.data) 120 | 121 | // allows admin can read 122 | allow read: if userIsAdmin(request.auth) 123 | 124 | // allows user can create 125 | allow create: if userIsLoggedIn(request.auth) 126 | && postIsValid(request.resource.data) 127 | && userWillBeOwner(request.auth, request.resource.data) 128 | 129 | // allows admin can update 130 | allow update: if userIsAdmin(request.auth) 131 | 132 | // allows admin can delete 133 | allow delete: if userIsAdmin(request.auth) 134 | } 135 | 136 | match /posts/{document=**} { 137 | // allows anyone can see list 138 | allow list: if true 139 | 140 | // allows anyone can read 141 | allow read: if true 142 | 143 | // allows admin to create post 144 | allow create: if userIsAdmin(request.auth) 145 | 146 | // allows user to create post 147 | allow create: if userIsVerified(request.auth) 148 | && !requireModeration() 149 | && postIsValid(request.resource.data) 150 | && userWillBeOwner(request.auth, request.resource.data) 151 | 152 | // allows anonymous to create post 153 | allow create: if userIsLoggedIn(request.auth) 154 | && !requireAnonymousModeration() 155 | && postIsValid(request.resource.data) 156 | && userWillBeOwner(request.auth, request.resource.data) 157 | 158 | // allows admin to edit post 159 | allow update: if userIsAdmin(request.auth) 160 | && postIsBeingEdited(request.resource.data, resource.data) 161 | 162 | // allows owner to edit post 163 | allow update: if userIsOwner(request.auth, request.resource.data) 164 | && postIsBeingEdited(request.resource.data, resource.data) 165 | && ( 166 | (userIsVerified(request.auth) && !requireModeration()) 167 | || (userIsLoggedIn(request.auth) && !requireAnonymousModeration()) 168 | ) 169 | 170 | // allows admin / owner to delete post 171 | allow update: if (userIsAdmin(request.auth) || userIsOwner(request.auth, resource.data)) 172 | && postIsBeingDeleted(request.resource.data, resource.data) 173 | 174 | // allows user to vote post 175 | allow update: if userIsVerified(request.auth) 176 | && ( 177 | (postIsBeingVoted(request.auth, request.resource.data, resource.data)) 178 | || (postIsBeingUnVoted(request.auth, request.resource.data, resource.data)) 179 | ) 180 | 181 | // allows user to increase post reply 182 | allow update: if userIsVerified(request.auth) 183 | && !requireModeration() 184 | && postRepliedIsBeingIncreased(request.resource.data, resource.data) 185 | 186 | // allows anonymous to increase post reply 187 | allow update: if userIsLoggedIn(request.auth) 188 | && !requireAnonymousModeration() 189 | && postRepliedIsBeingIncreased(request.resource.data, resource.data) 190 | 191 | allow delete: if userIsAdmin(request.auth) 192 | } 193 | } 194 | } -------------------------------------------------------------------------------- /packages/core/src/components/PendingPostCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import Card from '@mui/material/Card' 3 | import CardHeader from '@mui/material/CardHeader' 4 | import CardContent from '@mui/material/CardContent' 5 | import Avatar from '@mui/material/Avatar' 6 | import Button from '@mui/material/Button' 7 | import Typography from '@mui/material/Typography' 8 | import IconButton from '@mui/material/IconButton' 9 | import { useCallback, useState } from 'react' 10 | import ModeEditTwoToneIcon from '@mui/icons-material/ModeEditTwoTone' 11 | import DeleteTwoToneIcon from '@mui/icons-material/DeleteTwoTone' 12 | import { Composer } from 'components/PostComposer' 13 | import { EMPTY_SYMBOL } from 'constants/composer' 14 | import { Content, Timestamp } from 'components/PostCard' 15 | import Slide from '@mui/material/Slide' 16 | import { useLocale } from 'components/LocaleProvider' 17 | import GradingTwoToneIcon from '@mui/icons-material/GradingTwoTone' 18 | 19 | import DoNotDisturbAltIcon from '@mui/icons-material/DoNotDisturbAlt' 20 | import { PendingPost } from 'types/PendingPost' 21 | import Link from '@mui/material/Link' 22 | import { useApprovePendingPostCommand, useEditPendingPostCommand, useRejectPendingPostCommand } from 'services/post' 23 | 24 | import Tooltip from '@mui/material/Tooltip' 25 | import { PendingPostUpdateResult } from 'enums/PostUpdateResult' 26 | import { useSnackbar } from 'notistack' 27 | 28 | type ReviewPostCardProps = { 29 | children?: JSX.Element | null, 30 | post: PendingPost 31 | } 32 | 33 | export const ReviewPostCard = ({ post, children }: ReviewPostCardProps) => { 34 | const { messages, functions } = useLocale() 35 | 36 | const { enqueueSnackbar } = useSnackbar() 37 | const [isDeleting, toggleDeleting] = useState(false) 38 | const [isEditing, toggleEditing] = useState(false) 39 | 40 | const [contents, setContents] = useState('') 41 | 42 | const editPendingPostCommand = useEditPendingPostCommand() 43 | const rejectPendingPostCommand = useRejectPendingPostCommand() 44 | const approvePendingPostCommand = useApprovePendingPostCommand() 45 | 46 | const editPendingPost = useCallback(async () => { 47 | const result = await editPendingPostCommand(post, contents) 48 | 49 | if (result === PendingPostUpdateResult.SUCCESS) { 50 | enqueueSnackbar(messages.pendingPostUpdated, { 51 | variant: 'success' 52 | }) 53 | } 54 | toggleEditing(false) 55 | }, [ 56 | post, 57 | contents, 58 | enqueueSnackbar, 59 | messages, 60 | toggleEditing 61 | ]) 62 | const rejectPendingPost = useCallback(async () => { 63 | const result = await rejectPendingPostCommand(post) 64 | 65 | if (result === PendingPostUpdateResult.SUCCESS) { 66 | enqueueSnackbar(messages.pendingPostDeleted, { 67 | variant: 'success' 68 | }) 69 | } 70 | toggleDeleting(false) 71 | }, [ 72 | toggleDeleting, 73 | post, 74 | enqueueSnackbar, 75 | messages 76 | ]) 77 | const approvePendingPost = useCallback(async () => { 78 | const result = await approvePendingPostCommand(post) 79 | 80 | if (result === PendingPostUpdateResult.SUCCESS) { 81 | enqueueSnackbar(messages.pendingPostApproved, { 82 | variant: 'success' 83 | }) 84 | } 85 | }, [ 86 | post, 87 | enqueueSnackbar, 88 | messages 89 | ]) 90 | 91 | return ( 92 | 96 | 99 | } 100 | title={ 101 | 102 | {(post.author.name || messages.anonymous)} 103 | ({post.createdAt ? functions.formatDateDistance(post.createdAt, new Date(), { addSuffix: true }) : '...'}) 104 | 105 | } 106 | sx={{ 107 | mb: -3 108 | }} 109 | 110 | action={!post.deletedAt && ( 111 |
116 | {isDeleting 117 | ? ( 118 | 119 | 124 | 125 | 129 | 130 | ) 131 | : isEditing 132 | ? ( 133 | 134 | 139 | 140 | 144 | 148 | 149 | 150 | ) 151 | : ( 152 | 153 | 154 | 155 | toggleEditing(true)}> 156 | 157 | 158 | 159 | 160 | {post.postId 161 | ? ( 162 | toggleDeleting(true)}> 163 | 164 | 165 | ) 166 | : ( 167 | toggleDeleting(true)}> 168 | 169 | 170 | )} 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | )} 179 |
180 | )} 181 | /> 182 | 183 | {isEditing 184 | ? ( 185 | { 189 | setContents(value) 190 | }} 191 | /> 192 | ) 193 | : ( 194 | 197 | {post.deletedAt 198 | ? ( 199 | 200 | {messages.deletedAt} {functions.formatDateDistance(post.deletedAt, new Date(), { addSuffix: true })} 201 | 202 | ) 203 | : ( 204 | 205 | {post.contents} 206 | 207 |
208 | {post.url} 209 | 210 | {post.updatedAt && ( 211 | 214 | {messages.updatedAt} {functions.formatDateDistance(post.updatedAt, new Date(), { addSuffix: true })} 215 | 216 | )} 217 | 218 |
219 | )} 220 | 221 |
222 | )} 223 |
224 | 225 | 229 | {children} 230 | 231 |
232 | ) 233 | } 234 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 |

4 | 5 | Discuzz 6 | 7 |

8 |

9 |

10 | 11 | Open source Comment System 12 | 13 |

14 |

15 | 16 |

17 |
18 |

19 | 20 | ![Discuzz](./docs/img.jpg) 21 | 22 |

23 |
24 | 25 |
26 | Table of contents 27 | 28 | --- 29 | 30 | - [Homepage](#homepage) 31 | - [Features](#features) 32 | - [Usage](#usage) 33 | - [Examples](#examples) 34 | - [Firebase](#firebase) 35 | - [Web Component](#web-component) 36 | - [React Component](#react-component) 37 | - [Advanced usages](#advanced-usages) 38 | - [Contributing](#contributing) 39 | - [Changelog](#changelog) 40 | - [License](#license) 41 | 42 | --- 43 | 44 |
45 | 46 | ## **Homepage** 47 | 48 | [discuzz.mph.am](https://discuzz.mph.am/) 49 | 50 | ## **Features** 51 | 52 | - Discuzz is an open source comment system, that you can embed in your website to increase reader engagement, grow audience and traffic. 53 | - Supporting Firestore as the data storage, with Realtime and Offline support. You can use Discuzz easily without any backend server. 54 | - With Firebase Auth support, you can provide many ways to authenticate for your users. 55 | - You can easily config the access control, to adjust permissions (Example: open to all people, or only authenticated users, or you can also turn on moderation mode for every comments) 56 | - Customizable theme, with built-in light/dark theme. 57 | - Also, you can [write your own Authentication provider and Data provider](#advanced-usages), and configure them with Discuzz. 58 | 59 | **To suggest anything, please join our [Discussion board](https://github.com/discuzz-app/discuzz/discussions).** 60 | 61 | 62 | ## **Usage** 63 | 64 | You can embed Discuzz in many ways: 65 | - As a Web Component 66 | - As a React Component 67 | - ... 68 | 69 | ### **Examples** 70 | There are several example integrations, which you can [check here](https://github.com/discuzz-app?q=example) 71 | 72 | ### **Firebase** 73 | 74 | If you want to use Firebase as the Authentication & Data provider, you'd need to create a Firebase project, and add a web platform. It will give you the config parameters. 75 | 76 | ![ABI](./docs/firebase-web-code.png) 77 | 78 | ### **Web Component** 79 | You can embed Discuzz in your website with the following code 80 | 81 | ```html 82 | 83 | 87 | ``` 88 | 89 | **Example** 90 | ```html 91 | 92 | 96 | ``` 97 | 98 | ### **React Component** 99 | 100 | **Install dependencies** 101 | 1) Discuzz component 102 | ```bash 103 | yarn add @discuzz/discuzz 104 | ``` 105 | 2) Locale 106 | ```bash 107 | yarn add @discuzz/locale-en date-fns 108 | ``` 109 | 3) Auth & Data provider 110 | ```bash 111 | yarn add @discuzz/auth-firebase @discuzz/data-firestore firebase 112 | ``` 113 | 114 | **Example component usage** 115 | ```jsx 116 | import { Discuzz } from '@discuzz/discuzz' 117 | 118 | import LocaleProviderEn from '@discuzz/locale-en' 119 | import AuthFirebase from '@discuzz/auth-firebase' 120 | import DataFirestore from '@discuzz/data-firestore' 121 | 122 | function App() { 123 | return ( 124 | 141 | ) 142 | } 143 | ``` 144 | 145 | 146 | ### **Advanced usages** 147 | 148 | 149 | **Code splitting & Lazy load** 150 | 151 | You can config Discuzz to load services and providers on-demand with `Suspense`. 152 | 153 | ```jsx 154 | import { lazy, Suspense } from 'react' 155 | import { Discuzz, loadService } from '@discuzz/discuzz' 156 | 157 | const LocaleProviderEn = lazy(() => import('@discuzz/locale-en')) 158 | 159 | const AuthFirebase = loadService(() => import('@discuzz/auth-firebase')) 160 | const DataFirestore = loadService(() => import('@discuzz/data-firestore')) 161 | 162 | function App() { 163 | return ( 164 | Loading...}> 165 | 182 | 183 | ); 184 | } 185 | ``` 186 | 187 | On NextJS, you can lazy load modules with `next/dynamic`. 188 | 189 | ```jsx 190 | import lazy from 'next/dynamic' 191 | import { Discuzz, loadService } from '@discuzz/discuzz' 192 | 193 | const LocaleProviderEn = lazy(() => import('@discuzz/locale-en'), { ssr: false }) 194 | 195 | const AuthFirebase = loadService(() => import('@discuzz/auth-firebase')) 196 | const DataFirestore = loadService(() => import('@discuzz/data-firestore')) 197 | 198 | function App() { 199 | return ( 200 | 217 | ); 218 | } 219 | ``` 220 | 221 | 222 | **Markdown support** 223 | 224 | ```bash 225 | yarn add @discuzz/viewer-markdown @discuzz/composer-markdown rich-markdown-editor styled-components 226 | ``` 227 | ```jsx 228 | import { Discuzz } from '@discuzz/discuzz' 229 | 230 | const LocaleProviderEn = lazy(() => import('@discuzz/locale-en')) 231 | const ComposerMarkdown = lazy(() => import('@discuzz/composer-markdown')) 232 | const ViewerMarkdown = lazy(() => import('@discuzz/viewer-markdown')) 233 | 234 | const AuthFirebase = loadService(() => import('@discuzz/auth-firebase')) 235 | const DataFirestore = loadService(() => import('@discuzz/data-firestore')) 236 | 237 | function App() { 238 | return ( 239 | Loading...}> 240 | 261 | 262 | ) 263 | } 264 | ``` 265 | 266 | 267 | **Theming** 268 | 269 | By default, Discuzz will check the current user's browser light/dark preference to setup theme palette. 270 | 271 | You can set it manually by passing `light` or `dark` to the `theme` parameter. 272 | 273 | Discuzz is built on top of MUI library. You can fully customize by passing a theme object into the `theme` parameter. 274 | 275 | 276 | **Custom locale provider** 277 | 278 | You could write your own locale provider, using `createProvider` function, then pass it to the `` component. 279 | 280 | **Custom data & authentication provider** 281 | 282 | You could also write your own data & authentication provider to using other services instead of Firebase, as long as it fullfills the `Auth` and `Data` type. 283 | 284 | **Tip:** You can take a look at [auth-firebase](./packages/auth-firebase) and [data-firestore](./packages/data-firestore). 285 | 286 | 287 | ## **Contributing** 288 | 289 | Please contribute using [GitHub Flow](https://guides.github.com/introduction/flow). Create a branch, add commits, and then [open a pull request](https://github.com/@discuzz-app/discuzz/compare). 290 | 291 | ## **License** 292 | 293 | This project is licensed under the [GNU General Public License v3.0](https://opensource.org/licenses/gpl-3.0.html) - see the [`LICENSE`](LICENSE) file for details. 294 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 |

4 | 5 | Discuzz 6 | 7 |

8 |

9 |

10 | 11 | Open source Comment System 12 | 13 |

14 |

15 | 16 |

17 |
18 |

19 | 20 | ![Discuzz](./docs/img.jpg) 21 | 22 |

23 |
24 | 25 |
26 | Table of contents 27 | 28 | --- 29 | 30 | - [Homepage](#homepage) 31 | - [Features](#features) 32 | - [Usage](#usage) 33 | - [Examples](#examples) 34 | - [Firebase](#firebase) 35 | - [Web Component](#web-component) 36 | - [React Component](#react-component) 37 | - [Advanced usages](#advanced-usages) 38 | - [Contributing](#contributing) 39 | - [Changelog](#changelog) 40 | - [License](#license) 41 | 42 | --- 43 | 44 |
45 | 46 | ## **Homepage** 47 | 48 | [discuzz.mph.am](https://discuzz.mph.am/) 49 | 50 | ## **Features** 51 | 52 | - Discuzz is an open source comment system, that you can embed in your website to increase reader engagement, grow audience and traffic. 53 | - Supporting Firestore as the data storage, with Realtime and Offline support. You can use Discuzz easily without any backend server. 54 | - With Firebase Auth support, you can provide many ways to authenticate for your users. 55 | - You can easily config the access control, to adjust permissions (Example: open to all people, or only authenticated users, or you can also turn on moderation mode for every comments) 56 | - Customizable theme, with built-in light/dark theme. 57 | - Also, you can [write your own Authentication provider and Data provider](#advanced-usages), and configure them with Discuzz. 58 | 59 | **To suggest anything, please join our [Discussion board](https://github.com/discuzz-app/discuzz/discussions).** 60 | 61 | 62 | ## **Usage** 63 | 64 | You can embed Discuzz in many ways: 65 | - As a Web Component 66 | - As a React Component 67 | - ... 68 | 69 | ### **Examples** 70 | There are several example integrations, which you can [check here](https://github.com/discuzz-app?q=example) 71 | 72 | ### **Firebase** 73 | 74 | If you want to use Firebase as the Authentication & Data provider, you'd need to create a Firebase project, and add a web platform. It will give you the config parameters. 75 | 76 | ![ABI](./docs/firebase-web-code.png) 77 | 78 | ### **Web Component** 79 | You can embed Discuzz in your website with the following code 80 | 81 | ```html 82 | 83 | 87 | ``` 88 | 89 | **Example** 90 | ```html 91 | 92 | 96 | ``` 97 | 98 | ### **React Component** 99 | 100 | **Install dependencies** 101 | 1) Discuzz component 102 | ```bash 103 | yarn add @discuzz/discuzz 104 | ``` 105 | 2) Locale 106 | ```bash 107 | yarn add @discuzz/locale-en date-fns 108 | ``` 109 | 3) Auth & Data provider 110 | ```bash 111 | yarn add @discuzz/auth-firebase @discuzz/data-firestore firebase 112 | ``` 113 | 114 | **Example component usage** 115 | ```jsx 116 | import { Discuzz } from '@discuzz/discuzz' 117 | 118 | import LocaleProviderEn from '@discuzz/locale-en' 119 | import AuthFirebase from '@discuzz/auth-firebase' 120 | import DataFirestore from '@discuzz/data-firestore' 121 | 122 | function App() { 123 | return ( 124 | 141 | ) 142 | } 143 | ``` 144 | 145 | 146 | ### **Advanced usages** 147 | 148 | 149 | **Code splitting & Lazy load** 150 | 151 | You can config Discuzz to load services and providers on-demand with `Suspense`. 152 | 153 | ```jsx 154 | import { lazy, Suspense } from 'react' 155 | import { Discuzz, loadService } from '@discuzz/discuzz' 156 | 157 | const LocaleProviderEn = lazy(() => import('@discuzz/locale-en')) 158 | 159 | const AuthFirebase = loadService(() => import('@discuzz/auth-firebase')) 160 | const DataFirestore = loadService(() => import('@discuzz/data-firestore')) 161 | 162 | function App() { 163 | return ( 164 | Loading...}> 165 | 182 | 183 | ); 184 | } 185 | ``` 186 | 187 | On NextJS, you can lazy load modules with `next/dynamic`. 188 | 189 | ```jsx 190 | import lazy from 'next/dynamic' 191 | import { Discuzz, loadService } from '@discuzz/discuzz' 192 | 193 | const LocaleProviderEn = lazy(() => import('@discuzz/locale-en'), { ssr: false }) 194 | 195 | const AuthFirebase = loadService(() => import('@discuzz/auth-firebase')) 196 | const DataFirestore = loadService(() => import('@discuzz/data-firestore')) 197 | 198 | function App() { 199 | return ( 200 | 217 | ); 218 | } 219 | ``` 220 | 221 | 222 | **Markdown support** 223 | 224 | ```bash 225 | yarn add @discuzz/viewer-markdown @discuzz/composer-markdown rich-markdown-editor styled-components 226 | ``` 227 | ```jsx 228 | import { Discuzz } from '@discuzz/discuzz' 229 | 230 | const LocaleProviderEn = lazy(() => import('@discuzz/locale-en')) 231 | const ComposerMarkdown = lazy(() => import('@discuzz/composer-markdown')) 232 | const ViewerMarkdown = lazy(() => import('@discuzz/viewer-markdown')) 233 | 234 | const AuthFirebase = loadService(() => import('@discuzz/auth-firebase')) 235 | const DataFirestore = loadService(() => import('@discuzz/data-firestore')) 236 | 237 | function App() { 238 | return ( 239 | Loading...}> 240 | 261 | 262 | ) 263 | } 264 | ``` 265 | 266 | 267 | **Theming** 268 | 269 | By default, Discuzz will check the current user's browser light/dark preference to setup theme palette. 270 | 271 | You can set it manually by passing `light` or `dark` to the `theme` parameter. 272 | 273 | Discuzz is built on top of MUI library. You can fully customize by passing a theme object into the `theme` parameter. 274 | 275 | 276 | **Custom locale provider** 277 | 278 | You could write your own locale provider, using `createProvider` function, then pass it to the `` component. 279 | 280 | **Custom data & authentication provider** 281 | 282 | You could also write your own data & authentication provider to using other services instead of Firebase, as long as it fullfills the `Auth` and `Data` type. 283 | 284 | **Tip:** You can take a look at [auth-firebase](./packages/auth-firebase) and [data-firestore](./packages/data-firestore). 285 | 286 | 287 | ## **Contributing** 288 | 289 | Please contribute using [GitHub Flow](https://guides.github.com/introduction/flow). Create a branch, add commits, and then [open a pull request](https://github.com/@discuzz-app/discuzz/compare). 290 | 291 | ## **License** 292 | 293 | This project is licensed under the [GNU General Public License v3.0](https://opensource.org/licenses/gpl-3.0.html) - see the [`LICENSE`](LICENSE) file for details. 294 | -------------------------------------------------------------------------------- /packages/discuzz/README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 |

4 | 5 | Discuzz 6 | 7 |

8 |

9 |

10 | 11 | Open source Comment System 12 | 13 |

14 |

15 | 16 |

17 |
18 |

19 | 20 | ![Discuzz](./docs/img.jpg) 21 | 22 |

23 |
24 | 25 |
26 | Table of contents 27 | 28 | --- 29 | 30 | - [Homepage](#homepage) 31 | - [Features](#features) 32 | - [Usage](#usage) 33 | - [Examples](#examples) 34 | - [Firebase](#firebase) 35 | - [Web Component](#web-component) 36 | - [React Component](#react-component) 37 | - [Advanced usages](#advanced-usages) 38 | - [Contributing](#contributing) 39 | - [Changelog](#changelog) 40 | - [License](#license) 41 | 42 | --- 43 | 44 |
45 | 46 | ## **Homepage** 47 | 48 | [discuzz.mph.am](https://discuzz.mph.am/) 49 | 50 | ## **Features** 51 | 52 | - Discuzz is an open source comment system, that you can embed in your website to increase reader engagement, grow audience and traffic. 53 | - Supporting Firestore as the data storage, with Realtime and Offline support. You can use Discuzz easily without any backend server. 54 | - With Firebase Auth support, you can provide many ways to authenticate for your users. 55 | - You can easily config the access control, to adjust permissions (Example: open to all people, or only authenticated users, or you can also turn on moderation mode for every comments) 56 | - Customizable theme, with built-in light/dark theme. 57 | - Also, you can [write your own Authentication provider and Data provider](#advanced-usages), and configure them with Discuzz. 58 | 59 | **To suggest anything, please join our [Discussion board](https://github.com/discuzz-app/discuzz/discussions).** 60 | 61 | 62 | ## **Usage** 63 | 64 | You can embed Discuzz in many ways: 65 | - As a Web Component 66 | - As a React Component 67 | - ... 68 | 69 | ### **Examples** 70 | There are several example integrations, which you can [check here](https://github.com/discuzz-app?q=example) 71 | 72 | ### **Firebase** 73 | 74 | If you want to use Firebase as the Authentication & Data provider, you'd need to create a Firebase project, and add a web platform. It will give you the config parameters. 75 | 76 | ![ABI](./docs/firebase-web-code.png) 77 | 78 | ### **Web Component** 79 | You can embed Discuzz in your website with the following code 80 | 81 | ```html 82 | 83 | 87 | ``` 88 | 89 | **Example** 90 | ```html 91 | 92 | 96 | ``` 97 | 98 | ### **React Component** 99 | 100 | **Install dependencies** 101 | 1) Discuzz component 102 | ```bash 103 | yarn add @discuzz/discuzz 104 | ``` 105 | 2) Locale 106 | ```bash 107 | yarn add @discuzz/locale-en date-fns 108 | ``` 109 | 3) Auth & Data provider 110 | ```bash 111 | yarn add @discuzz/auth-firebase @discuzz/data-firestore firebase 112 | ``` 113 | 114 | **Example component usage** 115 | ```jsx 116 | import { Discuzz } from '@discuzz/discuzz' 117 | 118 | import LocaleProviderEn from '@discuzz/locale-en' 119 | import AuthFirebase from '@discuzz/auth-firebase' 120 | import DataFirestore from '@discuzz/data-firestore' 121 | 122 | function App() { 123 | return ( 124 | 141 | ) 142 | } 143 | ``` 144 | 145 | 146 | ### **Advanced usages** 147 | 148 | 149 | **Code splitting & Lazy load** 150 | 151 | You can config Discuzz to load services and providers on-demand with `Suspense`. 152 | 153 | ```jsx 154 | import { lazy, Suspense } from 'react' 155 | import { Discuzz, loadService } from '@discuzz/discuzz' 156 | 157 | const LocaleProviderEn = lazy(() => import('@discuzz/locale-en')) 158 | 159 | const AuthFirebase = loadService(() => import('@discuzz/auth-firebase')) 160 | const DataFirestore = loadService(() => import('@discuzz/data-firestore')) 161 | 162 | function App() { 163 | return ( 164 | Loading...}> 165 | 182 | 183 | ); 184 | } 185 | ``` 186 | 187 | On NextJS, you can lazy load modules with `next/dynamic`. 188 | 189 | ```jsx 190 | import lazy from 'next/dynamic' 191 | import { Discuzz, loadService } from '@discuzz/discuzz' 192 | 193 | const LocaleProviderEn = lazy(() => import('@discuzz/locale-en'), { ssr: false }) 194 | 195 | const AuthFirebase = loadService(() => import('@discuzz/auth-firebase')) 196 | const DataFirestore = loadService(() => import('@discuzz/data-firestore')) 197 | 198 | function App() { 199 | return ( 200 | 217 | ); 218 | } 219 | ``` 220 | 221 | 222 | **Markdown support** 223 | 224 | ```bash 225 | yarn add @discuzz/viewer-markdown @discuzz/composer-markdown rich-markdown-editor styled-components 226 | ``` 227 | ```jsx 228 | import { Discuzz } from '@discuzz/discuzz' 229 | 230 | const LocaleProviderEn = lazy(() => import('@discuzz/locale-en')) 231 | const ComposerMarkdown = lazy(() => import('@discuzz/composer-markdown')) 232 | const ViewerMarkdown = lazy(() => import('@discuzz/viewer-markdown')) 233 | 234 | const AuthFirebase = loadService(() => import('@discuzz/auth-firebase')) 235 | const DataFirestore = loadService(() => import('@discuzz/data-firestore')) 236 | 237 | function App() { 238 | return ( 239 | Loading...}> 240 | 261 | 262 | ) 263 | } 264 | ``` 265 | 266 | 267 | **Theming** 268 | 269 | By default, Discuzz will check the current user's browser light/dark preference to setup theme palette. 270 | 271 | You can set it manually by passing `light` or `dark` to the `theme` parameter. 272 | 273 | Discuzz is built on top of MUI library. You can fully customize by passing a theme object into the `theme` parameter. 274 | 275 | 276 | **Custom locale provider** 277 | 278 | You could write your own locale provider, using `createProvider` function, then pass it to the `` component. 279 | 280 | **Custom data & authentication provider** 281 | 282 | You could also write your own data & authentication provider to using other services instead of Firebase, as long as it fullfills the `Auth` and `Data` type. 283 | 284 | **Tip:** You can take a look at [auth-firebase](./packages/auth-firebase) and [data-firestore](./packages/data-firestore). 285 | 286 | 287 | ## **Contributing** 288 | 289 | Please contribute using [GitHub Flow](https://guides.github.com/introduction/flow). Create a branch, add commits, and then [open a pull request](https://github.com/@discuzz-app/discuzz/compare). 290 | 291 | ## **License** 292 | 293 | This project is licensed under the [GNU General Public License v3.0](https://opensource.org/licenses/gpl-3.0.html) - see the [`LICENSE`](LICENSE) file for details. 294 | -------------------------------------------------------------------------------- /packages/core/src/components/PostCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEvent, useCallback, useState, Fragment } from 'react' 2 | import Card from '@mui/material/Card' 3 | import CardHeader from '@mui/material/CardHeader' 4 | import CardContent from '@mui/material/CardContent' 5 | import Avatar from '@mui/material/Avatar' 6 | import Button from '@mui/material/Button' 7 | import Typography from '@mui/material/Typography' 8 | import IconButton from '@mui/material/IconButton' 9 | import InputAdornment from '@mui/material/InputAdornment' 10 | import OutlinedInput from '@mui/material/OutlinedInput' 11 | 12 | import { Post } from 'types/Post' 13 | import ModeEditTwoToneIcon from '@mui/icons-material/ModeEditTwoTone' 14 | import DeleteTwoToneIcon from '@mui/icons-material/DeleteTwoTone' 15 | import { Composer } from 'components/PostComposer' 16 | import { EMPTY_SYMBOL } from 'constants/composer' 17 | import { useAuth } from 'components/AuthProvider' 18 | import { Content } from './components/Content' 19 | import Slide from '@mui/material/Slide' 20 | import { useLocale } from 'components/LocaleProvider' 21 | import ThumbUpIcon from '@mui/icons-material/ThumbUp' 22 | import ThumbUpOutlinedIcon from '@mui/icons-material/ThumbUpOutlined' 23 | import { Timestamp } from './components/Timestamp' 24 | import { useAddPostCommand, useEditPostCommand, useRemovePostCommand, useTogglePostVoteCommand } from 'services/post' 25 | import Tooltip from '@mui/material/Tooltip' 26 | import { useSizeCheck } from 'hooks/useSizeCheck' 27 | import { CancelButton } from 'components/Buttons/CancelButton' 28 | import { ReplyButton } from 'components/Buttons/ReplyButton' 29 | import { ShareButton } from 'components/Buttons/ShareButton' 30 | import { useTheme } from '@mui/material' 31 | import { QuickReplyButton } from 'components/Buttons/QuickReplyButton' 32 | import { ReadMoreButton } from 'components/Buttons/ReadMoreButton' 33 | import { ExpandButton } from 'components/Buttons/ExpandButton' 34 | import { LikeButton } from 'components/Buttons/LikeButton' 35 | import Collapse from '@mui/material/Collapse' 36 | import { PostList } from 'components/PostList' 37 | import { useSnackbar } from 'notistack' 38 | import { PostUpdateResult } from 'enums/PostUpdateResult' 39 | export { Content } from './components/Content' 40 | export type { ContentProps } from './components/Content' 41 | export { Timestamp } from './components/Timestamp' 42 | 43 | type PostCardProps = { 44 | post: Post, 45 | level: number, 46 | fullCollapsed?: boolean 47 | } 48 | 49 | export const PostCard = ({ post, level, fullCollapsed = false }: PostCardProps) => { 50 | const { messages, functions } = useLocale() 51 | const smSize = useSizeCheck('sm') 52 | const theme = useTheme() 53 | const { user } = useAuth() 54 | const { enqueueSnackbar } = useSnackbar() 55 | const [isDeleting, toggleDeleting] = useState(false) 56 | const [isEditing, toggleEditing] = useState(false) 57 | const [isReplying, toggleReplying] = useState(false) 58 | const [isExpanding, toggleExpanding] = useState(level < 1) 59 | const [isFullCollapsed, toggleFullCollapsed] = useState(fullCollapsed) 60 | 61 | const [contents, setContents] = useState('') 62 | const [replyingContents, setReplyingContents] = useState('') 63 | 64 | const [isPostingReply, togglePostingReply] = useState(false) 65 | 66 | const addPostCommand = useAddPostCommand() 67 | const editPostCommand = useEditPostCommand() 68 | const removePostCommand = useRemovePostCommand() 69 | const togglePostVoteCommand = useTogglePostVoteCommand() 70 | 71 | const addPost = useCallback(async (event) => { 72 | event.preventDefault() 73 | togglePostingReply(true) 74 | 75 | const newContents = replyingContents 76 | setReplyingContents('') 77 | 78 | const result = await addPostCommand( 79 | newContents, 80 | undefined, 81 | post 82 | ) 83 | 84 | if (result === PostUpdateResult.UPDATED) { 85 | enqueueSnackbar(messages.postAdded, { 86 | variant: 'success' 87 | }) 88 | } else if (result === PostUpdateResult.PENDING) { 89 | enqueueSnackbar(messages.postSubmitted, { 90 | variant: 'warning' 91 | }) 92 | } 93 | 94 | togglePostingReply(false) 95 | toggleFullCollapsed(false) 96 | toggleExpanding(true) 97 | setTimeout(() => toggleReplying(false), 300) 98 | }, [ 99 | post, 100 | addPostCommand, 101 | enqueueSnackbar, 102 | replyingContents, 103 | togglePostingReply, 104 | setReplyingContents, 105 | toggleFullCollapsed, 106 | toggleExpanding, 107 | toggleReplying, 108 | messages 109 | ]) 110 | 111 | const editPost = useCallback(async (event: MouseEvent) => { 112 | event.stopPropagation() 113 | const result = await editPostCommand(post, contents) 114 | if (result === PostUpdateResult.UPDATED) { 115 | enqueueSnackbar(messages.postUpdated, { 116 | variant: 'success' 117 | }) 118 | } else if (result === PostUpdateResult.PENDING) { 119 | enqueueSnackbar(messages.postChangeSubmitted, { 120 | variant: 'warning' 121 | }) 122 | } 123 | toggleEditing(false) 124 | }, [ 125 | post, 126 | editPostCommand, 127 | toggleEditing, 128 | contents, 129 | enqueueSnackbar, 130 | messages 131 | ]) 132 | 133 | const removePost = useCallback(async (event: MouseEvent) => { 134 | event.stopPropagation() 135 | const result = await removePostCommand(post) 136 | 137 | if (result === PostUpdateResult.UPDATED) { 138 | enqueueSnackbar(messages.postDeleted, { 139 | variant: 'warning' 140 | }) 141 | } 142 | 143 | toggleDeleting(false) 144 | }, [ 145 | post, 146 | removePostCommand, 147 | toggleDeleting, 148 | enqueueSnackbar, 149 | messages 150 | ]) 151 | 152 | const togglePostVote = useCallback(async () => { 153 | await togglePostVoteCommand(post) 154 | }, [togglePostVoteCommand, post]) 155 | 156 | return ( 157 | 0 163 | ? { 164 | bgcolor: 'primary.contrastText', 165 | '&:before': { 166 | content: '""', 167 | display: 'block', 168 | width: '6px', 169 | height: '2px', 170 | position: 'absolute', 171 | marginLeft: '-7px', 172 | background: theme.palette.mode === 'dark' ? '#303030' : '#e5e5e5', 173 | marginTop: '24px' 174 | } 175 | } 176 | : {}) 177 | }}> 178 | 181 | } 182 | onClick={() => { 183 | toggleFullCollapsed(!isFullCollapsed) 184 | }} 185 | title={ 186 | 187 | {(post.author.name || messages.anonymous)} 188 | {!smSize && ({post.createdAt ? functions.formatDateDistance(post.createdAt, new Date(), { addSuffix: true }) : '...'})} 189 | 190 | } 191 | sx={{ 192 | bgcolor: 'divider', 193 | cursor: isFullCollapsed ? 's-resize' : 'n-resize', 194 | pt: 1, 195 | pb: 1 196 | }} 197 | 198 | action={ 199 |
204 | {!post.deletedAt && user && (user.isAdmin || user.uid === post.author.id) && ( 205 | 206 | {isDeleting 207 | ? ( 208 | 209 | { 211 | event.stopPropagation() 212 | toggleDeleting(false) 213 | }} 214 | sx={{ 215 | mr: 0.5 216 | }} 217 | > 218 | {messages.cancel} 219 | 220 | 224 | 225 | ) 226 | : isEditing 227 | ? ( 228 | 229 | { 231 | event.stopPropagation() 232 | toggleEditing(false) 233 | }} 234 | sx={{ 235 | mr: 0.5 236 | }} 237 | > 238 | {messages.cancel} 239 | 240 | 241 | 245 | 249 | 250 | 251 | ) 252 | : ( 253 | 254 | 255 | { 256 | event.stopPropagation() 257 | toggleDeleting(true) 258 | }}> 259 | 260 | 261 | 262 | 263 | { 264 | event.stopPropagation() 265 | toggleEditing(true) 266 | }}> 267 | 268 | 269 | 270 | 271 | 272 | )} 273 | 274 | 275 | )} 276 | 277 | 281 | 282 |
283 | } 284 | /> 285 | 286 | 287 | 288 | {isEditing 289 | ? ( 290 | { 294 | setContents(value) 295 | }} 296 | /> 297 | ) 298 | : ( 299 | 302 | {post.deletedAt 303 | ? ( 304 | 305 | {messages.deletedAt} {functions.formatDateDistance(post.deletedAt, new Date(), { addSuffix: true })} 306 | 307 | ) 308 | : ( 309 | 310 | {post.contents} 311 | 312 | {post.updatedAt && ( 313 | 316 | {messages.updatedAt} {functions.formatDateDistance(post.updatedAt, new Date(), { addSuffix: true })} 317 | 318 | )} 319 | 320 | 321 | )} 322 | 323 | )} 324 | 325 |
333 | 334 | 339 |
343 | { 350 | setReplyingContents(event.target.value) 351 | }} 352 | placeholder={messages.replyHere} 353 | fullWidth 354 | startAdornment={ 355 | 359 | 362 | 363 | 364 | 365 | } 366 | endAdornment={ 367 | 368 | toggleReplying(false)} 373 | > 374 | {messages.cancel} 375 | 376 | 377 | 381 |
382 | 383 |
384 |
385 |
386 | } 387 | /> 388 | 389 |
390 | 394 |
402 |
403 | : } /> 409 | 410 | toggleReplying(true)} 412 | /> 413 | 414 | toggleExpanding(!isExpanding)} 418 | /> 419 | 420 |
421 | 424 | 425 |
426 |
427 |
428 | 429 |
433 | {isExpanding 434 | ? ( 435 | 440 | ) 441 | : null} 442 |
443 |
444 |
445 | ) 446 | } 447 | --------------------------------------------------------------------------------