diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 00000000..282a923e --- /dev/null +++ b/.drone.yml @@ -0,0 +1,84 @@ +kind: pipeline +name: build +type: docker + +platform: + os: linux + arch: amd64 + +steps: + - name: compress + image: alpine + commands: + - rm -rf ./app.tar.bz2 + - tar -cjf ./app.tar.bz2 -C ./ . + - name: upload + image: drillster/drone-rsync + when: + branch: + - master + - develop + environment: + RSYNC_KEY: + from_secret: rsync_key + RSYNC_USER: + from_secret: rsync_user + PLUGIN_ARGS: -zz -O --no-perms + settings: + port: 22522 + hosts: + - vault48.org + source: ./ + user: ${rsync_user} + key: ${rsync_key} + target: /tmp/vault-frontend-${DRONE_BRANCH} + include: + - "app.tar.bz2" + exclude: + - "*" + - name: build + image: appleboy/drone-ssh + when: + branch: + - master + - develop + environment: + BUILD_PATH: + from_secret: build_path + ENV_PATH: + from_secret: env_path + settings: + host: vault48.org + username: + from_secret: rsync_user + key: + from_secret: rsync_key + envs: [build_path, env_path] + port: 22522 + script_stop: true + script: + - mkdir -p $${BUILD_PATH}/${DRONE_BRANCH} + - rm -rf $${BUILD_PATH}/${DRONE_BRANCH}/* + - cd $${BUILD_PATH}/${DRONE_BRANCH} + - tar -xjf /tmp/vault-frontend-${DRONE_BRANCH}/app.tar.bz2 -C ./ + - cp -a $${ENV_PATH}/${DRONE_BRANCH}/. $${BUILD_PATH}/${DRONE_BRANCH} + - docker-compose build + - docker-compose up -d + - name: telgram_notify + image: appleboy/drone-telegram + when: + status: + - success + - failure + settings: + token: + from_secret: telegram_token + to: + from_secret: telegram_chat_id + format: markdown + message: > + {{#success build.status}}🤓{{else}}😨{{/success}} + {{ datetime build.finished "01.02.2006 15:04:05" "UTC" }} [{{repo.name}} / {{commit.branch}}]({{ build.link }}) + ``` + {{ commit.message }} + ``` diff --git a/README.md b/README.md index a4cafcaa..6f0c941b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ This is [vault48.org](https://vault48.org) frontend. +[![Build Status](https://jenkins.vault48.org/api/badges/muerwre/vault-frontend/status.svg)](https://vault48.org/) + ### Installation 1. Clone this repo `git clone git@github.com:muerwre/vault-frontend.git` 2. Run `yarn install` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..dbddd04f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3' +services: + www: + restart: always + build: + context: . + dockerfile: docker/www/Dockerfile + ports: + - ${EXPOSE}:80 + volumes: + - /etc/localtime:/etc/localtime:ro diff --git a/docker/www/Dockerfile b/docker/www/Dockerfile new file mode 100644 index 00000000..04d4a3d1 --- /dev/null +++ b/docker/www/Dockerfile @@ -0,0 +1,13 @@ +# stage1 as builder +FROM node:10.13 as builder +COPY package.json yarn.lock ./ +RUN yarn +COPY . . +RUN yarn build + +FROM nginx:alpine +COPY docker/www/nginx.conf /etc/nginx/nginx.conf +RUN rm -rf /usr/share/nginx/html/* +COPY --from=builder /dist /usr/share/nginx/html +EXPOSE ${EXPOSE} 80 +ENTRYPOINT ["nginx", "-g", "daemon off;"] diff --git a/docker/www/nginx.conf b/docker/www/nginx.conf new file mode 100644 index 00000000..7fbacbd4 --- /dev/null +++ b/docker/www/nginx.conf @@ -0,0 +1,46 @@ +worker_processes 4; + +events { worker_connections 1024; } + +http { + server { + listen 80; + root /usr/share/nginx/html; + include /etc/nginx/mime.types; + + gzip on; + gzip_min_length 1000; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain application/xml application/javascript; + + ## All static files will be served directly. + location ~* ^.+\.(?:css|cur|js|jpe?g|gif|htc|ico|png|xml|otf|ttf|eot|woff|woff2|svg)$ { + access_log off; + expires 30d; + add_header Cache-Control public; + gzip_static on; + + ## No need to bleed constant updates. Send the all shebang in one + ## fell swoop. + tcp_nodelay off; + + ## Set the OS file cache. + open_file_cache max=3000 inactive=120s; + open_file_cache_valid 45s; + open_file_cache_min_uses 2; + open_file_cache_errors off; + } + + location / { + gzip_static on; + try_files $uri @index; + } + + location @index { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + expires -1; + try_files /index.html =404; + } + } +} + diff --git a/package.json b/package.json index ce7d74c9..da586cd3 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "scrypt": "^6.0.3", "sticky-sidebar": "^3.3.1", "throttle-debounce": "^2.1.0", + "tiny-slider-react": "^0.5.3", "tinycolor": "^0.0.1", "tslint": "^5.20.0", "tslint-config-airbnb": "^5.11.2", diff --git a/src/components/boris/BorisStatsGit/index.tsx b/src/components/boris/BorisStatsGit/index.tsx index 78ac86e4..d81ac721 100644 --- a/src/components/boris/BorisStatsGit/index.tsx +++ b/src/components/boris/BorisStatsGit/index.tsx @@ -30,7 +30,10 @@ const BorisStatsGit: FC = ({ stats }) => { return (
-
КОММИТС
+
+ КОММИТС + +
{stats.git .filter(data => data.commit && data.timestamp && data.subject) diff --git a/src/components/boris/BorisStatsGit/styles.module.scss b/src/components/boris/BorisStatsGit/styles.module.scss index f518ecc3..a0e7f64c 100644 --- a/src/components/boris/BorisStatsGit/styles.module.scss +++ b/src/components/boris/BorisStatsGit/styles.module.scss @@ -4,7 +4,14 @@ &__title { font: $font_12_semibold; text-transform: uppercase; - opacity: 0.3; margin: $gap * 2 0 $gap; + + span { + opacity: 0.3; + } + + img { + float: right; + } } } diff --git a/src/components/node/Comment/index.tsx b/src/components/comment/Comment/index.tsx similarity index 88% rename from src/components/node/Comment/index.tsx rename to src/components/comment/Comment/index.tsx index 839748b1..5f194158 100644 --- a/src/components/node/Comment/index.tsx +++ b/src/components/comment/Comment/index.tsx @@ -1,12 +1,12 @@ import React, { FC, HTMLAttributes, memo } from 'react'; import { CommentWrapper } from '~/components/containers/CommentWrapper'; -import { ICommentGroup, IComment } from '~/redux/types'; -import { CommentContent } from '~/components/node/CommentContent'; +import { ICommentGroup } from '~/redux/types'; +import { CommentContent } from '~/components/comment/CommentContent'; import styles from './styles.module.scss'; -import { nodeLockComment, nodeEditComment } from '~/redux/node/actions'; +import { nodeEditComment, nodeLockComment } from '~/redux/node/actions'; import { INodeState } from '~/redux/node/reducer'; import { CommentForm } from '../CommentForm'; -import { CommendDeleted } from '../CommendDeleted'; +import { CommendDeleted } from '../../node/CommendDeleted'; import * as MODAL_ACTIONS from '~/redux/modal/actions'; type IProps = HTMLAttributes & { diff --git a/src/components/node/Comment/styles.module.scss b/src/components/comment/Comment/styles.module.scss similarity index 100% rename from src/components/node/Comment/styles.module.scss rename to src/components/comment/Comment/styles.module.scss diff --git a/src/components/node/CommentContent/index.tsx b/src/components/comment/CommentContent/index.tsx similarity index 100% rename from src/components/node/CommentContent/index.tsx rename to src/components/comment/CommentContent/index.tsx diff --git a/src/components/node/CommentContent/styles.module.scss b/src/components/comment/CommentContent/styles.module.scss similarity index 100% rename from src/components/node/CommentContent/styles.module.scss rename to src/components/comment/CommentContent/styles.module.scss diff --git a/src/components/comment/CommentForm/index.tsx b/src/components/comment/CommentForm/index.tsx new file mode 100644 index 00000000..6481aa7e --- /dev/null +++ b/src/components/comment/CommentForm/index.tsx @@ -0,0 +1,234 @@ +import React, { FC, KeyboardEventHandler, memo, useCallback, useEffect, useMemo } from 'react'; +import { Textarea } from '~/components/input/Textarea'; +import styles from './styles.module.scss'; +import { Filler } from '~/components/containers/Filler'; +import { Button } from '~/components/input/Button'; +import assocPath from 'ramda/es/assocPath'; +import { IComment, IFileWithUUID, InputHandler } from '~/redux/types'; +import { connect } from 'react-redux'; +import * as NODE_ACTIONS from '~/redux/node/actions'; +import { selectNode } from '~/redux/node/selectors'; +import { LoaderCircle } from '~/components/input/LoaderCircle'; +import { Group } from '~/components/containers/Group'; +import { UPLOAD_SUBJECTS, UPLOAD_TARGETS, UPLOAD_TYPES } from '~/redux/uploads/constants'; +import uuid from 'uuid4'; +import * as UPLOAD_ACTIONS from '~/redux/uploads/actions'; +import { selectUploads } from '~/redux/uploads/selectors'; +import { IState } from '~/redux/store'; +import { getFileType } from '~/utils/uploader'; +import { getRandomPhrase } from '~/constants/phrases'; +import { ERROR_LITERAL } from '~/constants/errors'; +import { CommentFormAttaches } from '~/components/comment/CommentFormAttaches'; +import { CommentFormAttachButtons } from '~/components/comment/CommentFormButtons'; +import { CommentFormDropzone } from '~/components/comment/CommentFormDropzone'; + +const mapStateToProps = (state: IState) => ({ + node: selectNode(state), + uploads: selectUploads(state), +}); + +const mapDispatchToProps = { + nodePostComment: NODE_ACTIONS.nodePostComment, + nodeCancelCommentEdit: NODE_ACTIONS.nodeCancelCommentEdit, + nodeSetCommentData: NODE_ACTIONS.nodeSetCommentData, + uploadUploadFiles: UPLOAD_ACTIONS.uploadUploadFiles, +}; + +type IProps = ReturnType & + typeof mapDispatchToProps & { + id: number; + is_before?: boolean; + }; + +const CommentFormUnconnected: FC = memo( + ({ + node: { comment_data, is_sending_comment }, + uploads: { statuses, files }, + id, + is_before = false, + nodePostComment, + nodeSetCommentData, + uploadUploadFiles, + nodeCancelCommentEdit, + }) => { + const comment = useMemo(() => comment_data[id], [comment_data, id]); + + const onUpload = useCallback( + (files: File[]) => { + console.log(files); + + const items: IFileWithUUID[] = files.map( + (file: File): IFileWithUUID => ({ + file, + temp_id: uuid(), + subject: UPLOAD_SUBJECTS.COMMENT, + target: UPLOAD_TARGETS.COMMENTS, + type: getFileType(file), + }) + ); + + const temps = items.map(file => file.temp_id); + + nodeSetCommentData(id, assocPath(['temp_ids'], [...comment.temp_ids, ...temps], comment)); + uploadUploadFiles(items); + }, + [uploadUploadFiles, comment, id, nodeSetCommentData] + ); + + const onInput = useCallback( + text => { + nodeSetCommentData(id, assocPath(['text'], text, comment)); + }, + [nodeSetCommentData, comment, id] + ); + + useEffect(() => { + const temp_ids = (comment && comment.temp_ids) || []; + const added_files = temp_ids + .map(temp_uuid => statuses[temp_uuid] && statuses[temp_uuid].uuid) + .map(el => !!el && files[el]) + .filter(el => !!el && !comment.files.some(file => file && file.id === el.id)); + + const filtered_temps = temp_ids.filter( + temp_id => + statuses[temp_id] && + (!statuses[temp_id].uuid || !added_files.some(file => file.id === statuses[temp_id].uuid)) + ); + + if (added_files.length) { + nodeSetCommentData(id, { + ...comment, + temp_ids: filtered_temps, + files: [...comment.files, ...added_files], + }); + } + }, [statuses, files]); + + const isUploadingNow = useMemo(() => comment.temp_ids.length > 0, [comment.temp_ids]); + + const onSubmit = useCallback( + event => { + if (event) event.preventDefault(); + if (isUploadingNow || is_sending_comment) return; + + nodePostComment(id, is_before); + }, + [nodePostComment, id, is_before, isUploadingNow, is_sending_comment] + ); + + const onKeyDown = useCallback>( + ({ ctrlKey, key }) => { + if (!!ctrlKey && key === 'Enter') onSubmit(null); + }, + [onSubmit] + ); + + const images = useMemo( + () => comment.files.filter(file => file && file.type === UPLOAD_TYPES.IMAGE), + [comment.files] + ); + + const locked_images = useMemo( + () => + comment.temp_ids + .filter(temp => statuses[temp] && statuses[temp].type === UPLOAD_TYPES.IMAGE) + .map(temp_id => statuses[temp_id]), + [statuses, comment.temp_ids] + ); + + const audios = useMemo( + () => comment.files.filter(file => file && file.type === UPLOAD_TYPES.AUDIO), + [comment.files] + ); + + const locked_audios = useMemo( + () => + comment.temp_ids + .filter(temp => statuses[temp] && statuses[temp].type === UPLOAD_TYPES.AUDIO) + .map(temp_id => statuses[temp_id]), + [statuses, comment.temp_ids] + ); + + const onCancelEdit = useCallback(() => { + nodeCancelCommentEdit(id); + }, [nodeCancelCommentEdit, comment.id]); + + const placeholder = getRandomPhrase('SIMPLE'); + + const clearError = useCallback(() => nodeSetCommentData(id, { error: '' }), [ + id, + nodeSetCommentData, + ]); + + useEffect(() => { + if (comment.error) clearError(); + }, [comment.files, comment.text]); + + const setData = useCallback( + (data: Partial) => { + nodeSetCommentData(id, data); + }, + [nodeSetCommentData, id] + ); + + return ( + +
+
+