From 84dfeffe8512b4b7cbcda8b9c56b2ba51f10aa27 Mon Sep 17 00:00:00 2001 From: Damien Vitrac Date: Thu, 5 Dec 2024 11:17:34 +0100 Subject: [PATCH] Add ability to bookmark projects See gh-1664 --- start-client/dev/api.mock.json | 5 +- start-client/src/components/Application.js | 18 +- .../src/components/common/builder/Fields.js | 56 ++++- .../src/components/common/builder/Loading.js | 5 +- .../src/components/common/favorite/Add.js | 185 ++++++++++++++++ .../components/common/favorite/Favorite.js | 26 +++ .../src/components/common/favorite/Modal.js | 191 +++++++++++++++++ .../src/components/common/favorite/Utils.js | 16 ++ .../src/components/common/favorite/index.js | 1 + .../src/components/common/form/Button.js | 7 +- .../src/components/common/history/Modal.js | 24 ++- .../src/components/common/icons/Icons.js | 66 ++++++ .../src/components/common/icons/index.js | 3 + .../src/components/common/layout/SideLeft.js | 53 +++-- start-client/src/components/reducer/App.js | 73 ++++++- start-client/src/styles/_dark.scss | 26 ++- start-client/src/styles/_main.scss | 23 +- start-client/src/styles/_responsive.scss | 2 +- start-client/src/styles/favorite.scss | 197 ++++++++++++++++++ start-client/src/styles/history.scss | 31 +++ start-client/webpack.common.js | 11 +- start-client/webpack.prod.js | 2 +- 22 files changed, 985 insertions(+), 36 deletions(-) create mode 100644 start-client/src/components/common/favorite/Add.js create mode 100644 start-client/src/components/common/favorite/Favorite.js create mode 100644 start-client/src/components/common/favorite/Modal.js create mode 100644 start-client/src/components/common/favorite/Utils.js create mode 100644 start-client/src/components/common/favorite/index.js create mode 100644 start-client/src/styles/favorite.scss diff --git a/start-client/dev/api.mock.json b/start-client/dev/api.mock.json index de9d03c8933..da16d3ef3e4 100644 --- a/start-client/dev/api.mock.json +++ b/start-client/dev/api.mock.json @@ -1388,7 +1388,10 @@ "packaging": { "type": "single-select", "default": "jar", - "values": [{ "id": "jar", "name": "Jar" }, { "id": "war", "name": "War" }] + "values": [ + { "id": "jar", "name": "Jar" }, + { "id": "war", "name": "War" } + ] }, "javaVersion": { "type": "single-select", diff --git a/start-client/src/components/Application.js b/start-client/src/components/Application.js index 0add014ec42..594035c0587 100644 --- a/start-client/src/components/Application.js +++ b/start-client/src/components/Application.js @@ -25,6 +25,7 @@ const Explore = lazy(() => import('./common/explore/Explore')) const Share = lazy(() => import('./common/share/Share')) const History = lazy(() => import('./common/history/History')) const HotKeys = lazy(() => import('./common/builder/HotKeys')) +const Favorite = lazy(() => import('./common/favorite/Favorite')) export default function Application() { const { @@ -34,6 +35,9 @@ export default function Application() { share: shareOpen, explore: exploreOpen, history: historyOpen, + favorite: favoriteOpen, + favoriteAdd: favoriteAddOpen, + favoriteOptions, list, dependencies, } = useContext(AppContext) @@ -73,7 +77,9 @@ export default function Application() { share: false, explore: false, nav: false, - history: false, + history: favoriteOptions.back === 'history', + favorite: favoriteOptions.back === 'favorite', + favoriteAdd: false, }, }) } @@ -120,6 +126,10 @@ export default function Application() { dispatch({ type: 'UPDATE', payload: { share: true } }) } + const onFavoriteAdd = () => { + dispatch({ type: 'UPDATE', payload: { favoriteAdd: true } }) + } + return ( <> @@ -157,6 +167,7 @@ export default function Application() { onSubmit={onSubmit} onShare={onShare} onExplore={onExplore} + onFavoriteAdd={onFavoriteAdd} refExplore={buttonExplore} refSubmit={buttonSubmit} refDependency={buttonDependency} @@ -177,6 +188,11 @@ export default function Application() { onClose={onEscape} /> + ) diff --git a/start-client/src/components/common/builder/Fields.js b/start-client/src/components/common/builder/Fields.js index 9b94940eca4..76cc16f147d 100644 --- a/start-client/src/components/common/builder/Fields.js +++ b/start-client/src/components/common/builder/Fields.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types' import get from 'lodash/get' -import React, { useContext } from 'react' +import React, { useContext, useRef, useState, useEffect } from 'react' import Actions from './Actions' import Control from './Control' @@ -18,12 +18,15 @@ function Fields({ onSubmit, onExplore, onShare, + onFavoriteAdd, refExplore, refSubmit, refDependency, generating, }) { + const wrapper = useRef(null) const windowsUtils = useWindowsUtils() + const [dropdown, setDropdown] = useState(false) const { config, dispatch, dependencies } = useContext(AppContext) const { values, @@ -34,6 +37,19 @@ function Fields({ dispatchInitializr({ type: 'UPDATE', payload: args }) } + useEffect(() => { + const clickOutside = event => { + const children = get(wrapper, 'current') + if (children && !children.contains(event.target)) { + setDropdown(false) + } + } + document.addEventListener('mousedown', clickOutside) + return () => { + document.removeEventListener('mousedown', clickOutside) + } + }, []) + return ( <>
@@ -183,9 +199,40 @@ function Fields({ > Explore - + + + + {dropdown && ( +
+ + +
+ )} +
) @@ -196,6 +243,7 @@ Fields.propTypes = { onSubmit: PropTypes.func.isRequired, onExplore: PropTypes.func.isRequired, onShare: PropTypes.func.isRequired, + onFavoriteAdd: PropTypes.func.isRequired, refExplore: PropTypes.oneOfType([ PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) }), diff --git a/start-client/src/components/common/builder/Loading.js b/start-client/src/components/common/builder/Loading.js index e61fee269ec..8b8222a96ba 100644 --- a/start-client/src/components/common/builder/Loading.js +++ b/start-client/src/components/common/builder/Loading.js @@ -12,8 +12,9 @@ export default function Loading() {
- - + + +
diff --git a/start-client/src/components/common/favorite/Add.js b/start-client/src/components/common/favorite/Add.js new file mode 100644 index 00000000000..b36f080c220 --- /dev/null +++ b/start-client/src/components/common/favorite/Add.js @@ -0,0 +1,185 @@ +import PropTypes from 'prop-types' +import get from 'lodash/get' +import React, { useEffect, useRef, useContext, useState } from 'react' +import { CSSTransition, TransitionGroup } from 'react-transition-group' +import { clearAllBodyScrollLocks, disableBodyScroll } from 'body-scroll-lock' +import queryString from 'query-string' +import { toast } from 'react-toastify' +import FieldInput from '../builder/FieldInput' +import { Button } from '../form' +import { AppContext } from '../../reducer/App' +import { InitializrContext } from '../../reducer/Initializr' +import { + getLabelFromList, + getLabelFromDepsList, + getBookmarkDefaultName, +} from './Utils' + +function FavoriteItem({ value }) { + const { config } = useContext(AppContext) + const params = queryString.parse(value) + const deps = get(params, 'dependencies', '') + .split(',') + .filter(dep => !!dep) + return ( +
+ + + Project{' '} + + {getLabelFromList( + get(config, 'lists.project'), + get(params, 'type') + )} + + {`, `} + Language{' '} + + {getLabelFromList( + get(config, 'lists.language'), + get(params, 'language') + )} + + {`, `} + Spring Boot{' '} + + {getLabelFromList( + get(config, 'lists.boot'), + get(params, 'platformVersion') + )} + + + + {deps.length === 0 && 'No dependency'} + {deps.length > 0 && ( + <> + Dependencies:{' '} + + {deps + .map(dep => + getLabelFromDepsList(get(config, 'lists.dependencies'), dep) + ) + .join(', ')} + + + )} + + +
+ ) +} + +FavoriteItem.propTypes = { + value: PropTypes.string.isRequired, +} + +function Add({ open, onClose }) { + const wrapper = useRef(null) + const { share } = useContext(InitializrContext) + const { dispatch, favoriteOptions } = useContext(AppContext) + const [name, setName] = useState() + const input = useRef(null) + + const title = get(favoriteOptions, 'title', '') || 'Bookmark' + const button = get(favoriteOptions, 'button', '') || 'Save' + const value = get(favoriteOptions, 'favorite.value', '') || share + const nameFav = get(favoriteOptions, 'favorite.name', '') + + useEffect(() => { + setName(nameFav || `${getBookmarkDefaultName()}`) + }, [setName, open, nameFav]) + + useEffect(() => { + const clickOutside = event => { + const children = get(wrapper, 'current') + if (children && !children.contains(event.target)) { + onClose() + } + } + document.addEventListener('mousedown', clickOutside) + return () => { + document.removeEventListener('mousedown', clickOutside) + } + }, [onClose]) + + useEffect(() => { + if (get(wrapper, 'current') && open) { + disableBodyScroll(get(wrapper, 'current')) + } + if (get(input, 'current')) { + get(input, 'current').focus() + } + return () => { + clearAllBodyScrollLocks() + } + }, [wrapper, open]) + + const onSubmit = e => { + e.preventDefault() + if (!get(favoriteOptions, 'button', '')) { + dispatch({ type: 'ADD_FAVORITE', payload: { values: value, name } }) + toast.success('Project bookmarked') + } else { + dispatch({ + type: 'UPDATE_FAVORITE', + payload: { name, favorite: favoriteOptions.favorite }, + }) + } + onClose() + } + + return ( + + {open && ( + +
+
+
+
+

{title}

+
+
+ + { + setName(`${event.target.value}`) + }} + /> +
+
+ + +
+
+
+
+
+ )} +
+ ) +} + +Add.propTypes = { + open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, +} + +export default Add diff --git a/start-client/src/components/common/favorite/Favorite.js b/start-client/src/components/common/favorite/Favorite.js new file mode 100644 index 00000000000..2116f98dfb0 --- /dev/null +++ b/start-client/src/components/common/favorite/Favorite.js @@ -0,0 +1,26 @@ +import '../../../styles/favorite.scss' + +import PropTypes from 'prop-types' +import React from 'react' + +import Modal from './Modal' +import Add from './Add' +import { Overlay } from '../form' + +function Favorite({ open, add, onClose }) { + return ( + <> + + + + + ) +} + +Favorite.propTypes = { + open: PropTypes.bool.isRequired, + add: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, +} + +export default Favorite diff --git a/start-client/src/components/common/favorite/Modal.js b/start-client/src/components/common/favorite/Modal.js new file mode 100644 index 00000000000..a587c6c8dd1 --- /dev/null +++ b/start-client/src/components/common/favorite/Modal.js @@ -0,0 +1,191 @@ +import PropTypes from 'prop-types' +import get from 'lodash/get' +import React, { useEffect, useRef, useContext } from 'react' +import { CSSTransition, TransitionGroup } from 'react-transition-group' +import { clearAllBodyScrollLocks, disableBodyScroll } from 'body-scroll-lock' +import queryString from 'query-string' +import { AppContext } from '../../reducer/App' +import { getLabelFromList, getLabelFromDepsList } from './Utils' +import { IconEdit, IconDelete } from '../icons' + +function FavoriteItem({ name, value, onClose, onRemove, onUpdate }) { + const { config } = useContext(AppContext) + const params = queryString.parse(value) + const deps = get(params, 'dependencies', '') + .split(',') + .filter(dep => !!dep) + return ( +
  • + { + onClose() + }} + > + {name} + + + Project{' '} + + {getLabelFromList( + get(config, 'lists.project'), + get(params, 'type') + )} + + {`, `} + Language{' '} + + {getLabelFromList( + get(config, 'lists.language'), + get(params, 'language') + )} + + {`, `} + Spring Boot{' '} + + {getLabelFromList( + get(config, 'lists.boot'), + get(params, 'platformVersion') + )} + + + + {deps.length === 0 && 'No dependency'} + {deps.length > 0 && ( + <> + Dependencies:{' '} + + {deps + .map(dep => + getLabelFromDepsList( + get(config, 'lists.dependencies'), + dep + ) + ) + .join(', ')} + + + )} + + + + + +
  • + ) +} + +FavoriteItem.propTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + onRemove: PropTypes.func.isRequired, + onUpdate: PropTypes.func.isRequired, +} + +function Modal({ open, onClose }) { + const wrapper = useRef(null) + const { favorites, dispatch } = useContext(AppContext) + + useEffect(() => { + const clickOutside = event => { + const children = get(wrapper, 'current') + if (children && !children.contains(event.target)) { + onClose() + } + } + document.addEventListener('mousedown', clickOutside) + return () => { + document.removeEventListener('mousedown', clickOutside) + } + }, [onClose]) + + useEffect(() => { + if (get(wrapper, 'current') && open) { + disableBodyScroll(get(wrapper, 'current')) + } + return () => { + clearAllBodyScrollLocks() + } + }, [wrapper, open]) + + const onRemove = favorite => { + dispatch({ type: 'REMOVE_FAVORITE', payload: favorite }) + } + + const onUpdate = favorite => { + dispatch({ + type: 'UPDATE', + payload: { + favorite: false, + favoriteAdd: true, + favoriteOptions: { + title: 'Edit bookmark', + button: 'Update', + back: 'favorite', + favorite, + }, + }, + }) + } + + return ( + + {open && ( + +
    +
    +
    +

    Bookmarks

    +
    +
    +
    + {favorites.map(favorite => ( + { + onRemove(favorite) + }} + onUpdate={() => { + onUpdate(favorite) + }} + /> + ))} +
    +
    + +
    +
    +
    + )} +
    + ) +} + +Modal.propTypes = { + open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, +} + +export default Modal diff --git a/start-client/src/components/common/favorite/Utils.js b/start-client/src/components/common/favorite/Utils.js new file mode 100644 index 00000000000..624bc782e0a --- /dev/null +++ b/start-client/src/components/common/favorite/Utils.js @@ -0,0 +1,16 @@ +import { DateTime } from 'luxon' + +export function getLabelFromList(list, key) { + return list.find(item => item.key === key)?.text || key +} + +export function getLabelFromDepsList(list, key) { + return list.find(item => item.id === key)?.name || key +} + +export function getBookmarkDefaultName() { + const date = DateTime.now() + return `Bookmark ${date.toLocaleString( + DateTime.DATE_SHORT + )} ${date.toLocaleString(DateTime.TIME_24_SIMPLE)}` +} diff --git a/start-client/src/components/common/favorite/index.js b/start-client/src/components/common/favorite/index.js new file mode 100644 index 00000000000..ab8695c9c6e --- /dev/null +++ b/start-client/src/components/common/favorite/index.js @@ -0,0 +1 @@ +export { default as Favorite } from './Favorite' diff --git a/start-client/src/components/common/form/Button.js b/start-client/src/components/common/form/Button.js index 20b08218b6f..f70e2a5dbd9 100644 --- a/start-client/src/components/common/form/Button.js +++ b/start-client/src/components/common/form/Button.js @@ -6,13 +6,16 @@ function Button({ onClick, children, variant, + className, hotkey, refButton, disabled, }) { return ( ) } diff --git a/start-client/src/components/common/icons/Icons.js b/start-client/src/components/common/icons/Icons.js index 9318ca56f1b..60db8713f52 100644 --- a/start-client/src/components/common/icons/Icons.js +++ b/start-client/src/components/common/icons/Icons.js @@ -399,3 +399,69 @@ export function IconHistory() { ) } + +export function IconFavorite() { + return ( + + ) +} + +export function IconDelete() { + return ( + + ) +} + +export function IconEdit() { + return ( + + ) +} diff --git a/start-client/src/components/common/icons/index.js b/start-client/src/components/common/icons/index.js index 7fa600b4088..8ae64b986c7 100644 --- a/start-client/src/components/common/icons/index.js +++ b/start-client/src/components/common/icons/index.js @@ -17,5 +17,8 @@ export { IconMoon } from './Icons' export { IconRemove } from './Icons' export { IconEnter } from './Icons' export { IconHistory } from './Icons' +export { IconFavorite } from './Icons' +export { IconEdit } from './Icons' +export { IconDelete } from './Icons' export { IconSpring } from './IconSpring' diff --git a/start-client/src/components/common/layout/SideLeft.js b/start-client/src/components/common/layout/SideLeft.js index 12596f1edaa..b20e9a454be 100644 --- a/start-client/src/components/common/layout/SideLeft.js +++ b/start-client/src/components/common/layout/SideLeft.js @@ -5,14 +5,14 @@ import { clearAllBodyScrollLocks, disableBodyScroll } from 'body-scroll-lock' import Header from './Header' import { AppContext } from '../../reducer/App' -import { IconGithub, IconHistory } from '../icons' +import { IconGithub, IconHistory, IconFavorite } from '../icons' function SideLeft() { const [isOpen, setIsOpen] = useState(false) const [lock, setLock] = useState(false) const wrapper = useRef(null) - const { nav, histories, dispatch } = useContext(AppContext) + const { nav, histories, dispatch, favorites } = useContext(AppContext) useEffect(() => { if (get(wrapper, 'current') && nav) { @@ -61,20 +61,43 @@ function SideLeft() { - {!isOpen && !lock && histories.length > 0 && ( + {!isOpen && !lock && ( <> -
    - + {favorites.length > 0 && ( + <> +
    + + + )} + {histories.length > 0 && ( + <> +
    + + + )} )}
    diff --git a/start-client/src/components/reducer/App.js b/start-client/src/components/reducer/App.js index 6f5763ff8e9..fb36a5f153e 100644 --- a/start-client/src/components/reducer/App.js +++ b/start-client/src/components/reducer/App.js @@ -13,6 +13,8 @@ export const defaultAppContext = { explore: false, share: false, history: false, + favorite: false, + favoriteAdd: false, nav: false, list: false, theme: 'light', @@ -22,7 +24,14 @@ export const defaultAppContext = { list: [], groups: [], }, + favoriteOptions: { + title: '', + button: '', + favorite: null, + back: '', + }, histories: [], + favorites: [], } const localStorage = @@ -80,6 +89,14 @@ export function reducer(state, action) { if (key === 'theme') { localStorage.setItem('springtheme', value) } + if (key === 'favoriteAdd' && !value) { + newState.favoriteOptions = { + title: '', + button: '', + favorite: null, + back: '', + } + } return key }) return newState @@ -110,7 +127,18 @@ export function reducer(state, action) { const histories = localStorage.getItem('histories') ? JSON.parse(localStorage.getItem('histories')) : [] - return { ...state, complete: true, config: json, dependencies, histories } + + const favorites = localStorage.getItem('favorites') + ? JSON.parse(localStorage.getItem('favorites')) + : [] + return { + ...state, + complete: true, + config: json, + dependencies, + histories, + favorites, + } } case 'ADD_HISTORY': { const newHistory = get(action, 'payload') @@ -128,6 +156,49 @@ export function reducer(state, action) { localStorage.setItem('histories', JSON.stringify([])) return { ...state, histories: [] } } + case 'ADD_FAVORITE': { + const favorites = [ + { + date: new Date().toISOString(), + name: get(action, 'payload.name'), + value: get(action, 'payload.values'), + }, + ...state.favorites, + ] + localStorage.setItem('favorites', JSON.stringify(favorites)) + return { ...state, favorites } + } + case 'UPDATE_FAVORITE': { + const favoriteToUpdate = get(action, 'payload.favorite') + const favorites = state.favorites.map(item => { + if ( + item.name === favoriteToUpdate.name && + item.date === favoriteToUpdate.date && + item.value === favoriteToUpdate.value + ) { + return { + ...item, + name: get(action, 'payload.name'), + } + } + return item + }) + localStorage.setItem('favorites', JSON.stringify(favorites)) + return { ...state, favorites } + } + case 'REMOVE_FAVORITE': { + const favoriteToRemove = get(action, 'payload') + const favorites = state.favorites.filter( + item => + !( + item.name === favoriteToRemove.name && + item.date === favoriteToRemove.date && + item.value === favoriteToRemove.value + ) + ) + localStorage.setItem('favorites', JSON.stringify(favorites)) + return { ...state, favorites } + } default: return state } diff --git a/start-client/src/styles/_dark.scss b/start-client/src/styles/_dark.scss index 6b00b3cfb0c..91d96ae3878 100644 --- a/start-client/src/styles/_dark.scss +++ b/start-client/src/styles/_dark.scss @@ -401,16 +401,34 @@ body.dark { background: $dark-background; } - .modal-share .modal-header { - background: $dark-background; - border-bottom: 1px solid $dark-border; + .modal-share, + .modal-add-favorite, + .modal-favorite { + .modal-header { + background: $dark-background; + border-bottom: 1px solid $dark-border; + } } - .modal-history-container { + .modal-history-container, + .modal-favorite-container, + .modal-add-favorite-container { border: 1px solid $dark-border; } .modal-content { background: $dark-background; } + .modal-add-favorite .favorite-desc { + background: $dark-background-secondary; + color: $dark-color; + } + .modal-favorite button.edit, + .modal-favorite button.remove, + .modal-share .modal-content button.favorite { + color: white; + } + .actions .button.clicked { + color: #000; + } .modal-content .list a.item { background: $dark-background-secondary; color: $dark-color; diff --git a/start-client/src/styles/_main.scss b/start-client/src/styles/_main.scss index 3151915409f..7b210879173 100644 --- a/start-client/src/styles/_main.scss +++ b/start-client/src/styles/_main.scss @@ -582,13 +582,32 @@ button.button { span { padding: 0.9rem 1.5rem 0.8rem; } + &.clicked { + color: white; + &:before { + opacity: 1; + } + } &:focus { box-shadow: 0 0 0 4px darken($light-border, 6); } - &:last-child { + &.last-child { margin-right: 0; } } + .dropdown { + position: relative; + .dropdown-items { + position: absolute; + bottom: 35px; + left: -50px; + text-align: center; + .button { + margin: 4px 0; + width: 160px; + } + } + } } .colset-main { @@ -1320,7 +1339,7 @@ ul.dependencies-list { width: 249.42px; } &-share { - width: 119px; + width: 62.3px; } &-dep { width: 241.8px; diff --git a/start-client/src/styles/_responsive.scss b/start-client/src/styles/_responsive.scss index 3ebe0d3953b..827ac116dc0 100644 --- a/start-client/src/styles/_responsive.scss +++ b/start-client/src/styles/_responsive.scss @@ -244,7 +244,7 @@ width: 88.55px; } .placeholder-button-share { - width: 82.77px; + width: 33px; } .placeholder-button-download { width: 108.73px; diff --git a/start-client/src/styles/favorite.scss b/start-client/src/styles/favorite.scss new file mode 100644 index 00000000000..ba82f2002dc --- /dev/null +++ b/start-client/src/styles/favorite.scss @@ -0,0 +1,197 @@ +@import 'variables'; +@import 'mixins'; + +$w_arrow: 12px; +$w: 1000px; +$w2: 500px; + +.modal-add-favorite, +.modal-favorite { + z-index: 10000; + position: fixed; + top: 50px; + left: 0; + right: 0; + + .modal-favorite-container, + .modal-add-favorite-container { + max-width: $w; + margin: 0 auto; + background: white; + } + + .modal-add-favorite-container { + max-width: $w2; + } + + @include transition(all $spring-transition-duration); + &:before { + $h: 60px; + content: ' '; + height: $h; + width: $w; + position: absolute; + bottom: -$h; + left: 0; + } + .modal-content { + padding: $spring-8points * 3; + padding-top: $spring-8points; + padding-bottom: $spring-8points * 2; + max-height: 70vh; + overflow: auto; + .list { + .name { + font-weight: bold; + display: block; + } + ul { + padding: 0; + margin: 0 0 10px; + } + li { + position: relative; + list-style: none; + padding: 1px 0; + margin: 0; + } + a.item { + display: block; + position: relative; + background: $light-background-seconday; + border-radius: 3px; + text-decoration: none; + padding: 5px 10px; + color: $light-color; + padding-right: 80px; + &:hover { + background: lighten($light-background-seconday, 2); + a { + opacity: 1; + } + } + } + .time { + width: 80px; + } + .time, + .desc, + .main, + .deps { + display: block; + } + } + } + .modal-header { + position: relative; + padding: 6px $spring-8points * 2 2px; + border-bottom: 1px solid #ebebeb; + h1 { + font-size: $spring-8points * 2.5; + line-height: $spring-8points * 2.5; + font-weight: 600; + } + .button { + position: absolute; + top: 11px; + right: 11px; + font-size: $spring-font-size - 3; + line-height: 0.7rem; + margin-right: 0; + } + } + .modal-action { + text-align: center; + border-top: 1px solid $light-border; + padding: 16px 0 8px; + } + button.remove, + button.edit { + $size: 38px; + display: block; + position: absolute; + width: $size; + right: 10px; + top: 50%; + margin-top: -(calc($size / 2)); + border: 0 none; + cursor: pointer; + background: transparent; + opacity: 0.4; + @include outline; + @include transition(all 150ms); + .a-content { + display: block; + outline: none; + box-shadow: none; + padding: 8px; + } + svg { + display: block; + } + &:hover { + opacity: 0.8; + } + &:focus { + opacity: 1; + } + } + button.edit { + $size: 32px; + right: 45px; + width: $size; + margin-top: -(calc($size / 2)); + svg { + .st0 { + fill: #000; + } + } + } +} + +.modal-enter { + opacity: 0; +} + +.modal-enter-active { + opacity: 1; + transition: all 300ms; +} + +.modal-exit { + opacity: 1; +} + +.modal-exit-active { + opacity: 0; + transition: all 300ms; +} + +.modal-add-favorite { + .control-inline { + flex: none; + display: block; + label { + text-align: left; + flex: none; + display: block; + } + .input { + margin: 0; + } + } + .modal-action { + padding: 16px 0 16px; + } + .favorite-desc { + background: $light-background-seconday; + padding: 8px; + margin: 8px 0; + border-radius: 3px; + .deps { + display: block; + } + } +} + +@import 'responsive'; diff --git a/start-client/src/styles/history.scss b/start-client/src/styles/history.scss index 9d1ace6786d..cfde1e91566 100644 --- a/start-client/src/styles/history.scss +++ b/start-client/src/styles/history.scss @@ -46,6 +46,7 @@ $w: 1000px; list-style: none; padding: 1px 0; margin: 0; + position: relative; } a.item { position: relative; @@ -73,6 +74,36 @@ $w: 1000px; display: block; } } + button.favorite { + $size: 42px; + display: block; + position: absolute; + width: $size; + right: 10px; + top: 50%; + margin-top: -(calc($size / 2)-4); + border: 0 none; + cursor: pointer; + background: transparent; + opacity: 0.4; + @include outline; + @include transition(all 150ms); + .a-content { + display: block; + outline: none; + box-shadow: none; + padding: 8px; + } + svg { + display: block; + } + &:hover { + opacity: 0.8; + } + &:focus { + opacity: 1; + } + } } .modal-header { position: relative; diff --git a/start-client/webpack.common.js b/start-client/webpack.common.js index aed7ad0ac7b..582bf9f0bc3 100644 --- a/start-client/webpack.common.js +++ b/start-client/webpack.common.js @@ -50,7 +50,16 @@ const config = { }, { test: /\.s[ac]ss$/i, - use: ['style-loader', 'css-loader', 'sass-loader'], + use: [ + 'style-loader', + 'css-loader', + { + loader: 'sass-loader', + options: { + warnRuleAsWarning: false, + }, + }, + ], }, { test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, diff --git a/start-client/webpack.prod.js b/start-client/webpack.prod.js index e255d8e928d..15c96637ac8 100644 --- a/start-client/webpack.prod.js +++ b/start-client/webpack.prod.js @@ -25,7 +25,7 @@ const config = { plugins: [ new BundleAnalyzerPlugin({ analyzerMode: 'static', - openAnalyzer: true, + openAnalyzer: false, generateStatsFile: true, statsFilename: '../analysis/stats.json', reportFilename: '../analysis/bundle-analyzer.html',