mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
Add gallery wall view, and new lightbox (#1008)
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
#### 💥 **Note: After upgrading, all scene file sizes will be 0B until a new [scan](/settings?tab=tasks) is run.
|
||||
#### 💥 Note: After upgrading, all scene file sizes will be 0B until a new [scan](/settings?tab=tasks) is run.
|
||||
|
||||
### ✨ New Features
|
||||
* Add gallery wall view.
|
||||
* Add organized flag for scenes, galleries and images.
|
||||
* Allow configuration of visible navbar items.
|
||||
|
||||
### 🎨 Improvements
|
||||
* Pagination support and general improvements for image lightbox.
|
||||
* Add mouse click support for CDP scrapers.
|
||||
* Add gallery tabs to performer and studio pages.
|
||||
* Add gallery scrapers to scraper page.
|
||||
|
||||
@@ -186,7 +186,6 @@ export const Gallery: React.FC = () => {
|
||||
|
||||
<Tab.Content>
|
||||
<Tab.Pane eventKey="images">
|
||||
{/* <GalleryViewer gallery={gallery} /> */}
|
||||
<GalleryImagesPanel gallery={gallery} />
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="add">
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
import { queryFindGalleries } from "src/core/StashService";
|
||||
import { GalleryCard } from "./GalleryCard";
|
||||
import GalleryWallCard from "./GalleryWallCard";
|
||||
import { EditGalleriesDialog } from "./EditGalleriesDialog";
|
||||
import { DeleteGalleriesDialog } from "./DeleteGalleriesDialog";
|
||||
import { ExportDialog } from "../Shared/ExportDialog";
|
||||
@@ -212,7 +213,15 @@ export const GalleryList: React.FC<IGalleryList> = ({
|
||||
);
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.Wall) {
|
||||
return <h1>TODO</h1>;
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="GalleryWall">
|
||||
{result.data.findGalleries.galleries.map((gallery) => (
|
||||
<GalleryWallCard key={gallery.id} gallery={gallery} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,52 +1,36 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import FsLightbox from "fslightbox-react";
|
||||
import { useLightbox } from "src/hooks";
|
||||
import "flexbin/flexbin.css";
|
||||
|
||||
interface IProps {
|
||||
gallery: Partial<GQL.GalleryDataFragment>;
|
||||
gallery: GQL.GalleryDataFragment;
|
||||
}
|
||||
|
||||
export const GalleryViewer: React.FC<IProps> = ({ gallery }) => {
|
||||
const [lightboxToggle, setLightboxToggle] = useState(false);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const images = gallery?.images ?? [];
|
||||
const showLightbox = useLightbox({ images, showNavigation: false });
|
||||
|
||||
const openImage = (index: number) => {
|
||||
setCurrentIndex(index);
|
||||
setLightboxToggle(!lightboxToggle);
|
||||
};
|
||||
|
||||
const photos = !gallery.images
|
||||
? []
|
||||
: gallery.images.map((file) => file.paths.image ?? "");
|
||||
const thumbs = !gallery.images
|
||||
? []
|
||||
: gallery.images.map((file, index) => (
|
||||
<div
|
||||
role="link"
|
||||
tabIndex={index}
|
||||
key={file.checksum ?? index}
|
||||
onClick={() => openImage(index)}
|
||||
onKeyPress={() => openImage(index)}
|
||||
>
|
||||
<img
|
||||
src={file.paths.thumbnail ?? ""}
|
||||
loading="lazy"
|
||||
className="gallery-image"
|
||||
alt={file.title ?? index.toString()}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
const thumbs = images.map((file, index) => (
|
||||
<div
|
||||
role="link"
|
||||
tabIndex={index}
|
||||
key={file.checksum ?? index}
|
||||
onClick={() => showLightbox(index)}
|
||||
onKeyPress={() => showLightbox(index)}
|
||||
>
|
||||
<img
|
||||
src={file.paths.thumbnail ?? ""}
|
||||
loading="lazy"
|
||||
className="gallery-image"
|
||||
alt={file.title ?? index.toString()}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="gallery">
|
||||
<div className="flexbin">{thumbs}</div>
|
||||
<FsLightbox
|
||||
sourceIndex={currentIndex}
|
||||
toggler={lightboxToggle}
|
||||
sources={photos}
|
||||
key={gallery.id!}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
68
ui/v2.5/src/components/Galleries/GalleryWallCard.tsx
Normal file
68
ui/v2.5/src/components/Galleries/GalleryWallCard.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { RatingStars, TruncatedText } from "src/components/Shared";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { useGalleryLightbox } from "src/hooks";
|
||||
|
||||
const CLASSNAME = "GalleryWallCard";
|
||||
const CLASSNAME_FOOTER = `${CLASSNAME}-footer`;
|
||||
const CLASSNAME_IMG = `${CLASSNAME}-img`;
|
||||
const CLASSNAME_TITLE = `${CLASSNAME}-title`;
|
||||
|
||||
interface IProps {
|
||||
gallery: GQL.GallerySlimDataFragment;
|
||||
}
|
||||
|
||||
const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
|
||||
const intl = useIntl();
|
||||
const showLightbox = useGalleryLightbox(gallery.id);
|
||||
|
||||
const orientation =
|
||||
(gallery?.cover?.file.width ?? 0) > (gallery.cover?.file.height ?? 0)
|
||||
? "landscape"
|
||||
: "portrait";
|
||||
const cover = gallery?.cover?.paths.thumbnail ?? "";
|
||||
const title = gallery.title ?? gallery.path;
|
||||
const performerNames = gallery.performers.map((p) => p.name);
|
||||
const performers =
|
||||
performerNames.length >= 2
|
||||
? [...performerNames.slice(0, -2), performerNames.slice(-2).join(" & ")]
|
||||
: performerNames;
|
||||
|
||||
return (
|
||||
<>
|
||||
<section
|
||||
className={`${CLASSNAME} ${CLASSNAME}-${orientation}`}
|
||||
onClick={showLightbox}
|
||||
onKeyPress={showLightbox}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<RatingStars rating={gallery.rating} />
|
||||
<img src={cover} alt="" className={CLASSNAME_IMG} />
|
||||
<footer className={CLASSNAME_FOOTER}>
|
||||
<Link
|
||||
to={`/galleries/${gallery.id}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{title && (
|
||||
<TruncatedText
|
||||
text={title}
|
||||
lineCount={1}
|
||||
className={CLASSNAME_TITLE}
|
||||
/>
|
||||
)}
|
||||
<TruncatedText text={performers.join(", ")} />
|
||||
<div>
|
||||
{gallery.date && TextUtils.formatDate(intl, gallery.date)}
|
||||
</div>
|
||||
</Link>
|
||||
</footer>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GalleryWallCard;
|
||||
@@ -96,3 +96,116 @@ $galleryTabWidth: 450px;
|
||||
height: calc(1.5em + 0.75rem + 2px);
|
||||
}
|
||||
}
|
||||
|
||||
.GalleryWall {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 auto;
|
||||
width: 96vw;
|
||||
|
||||
/* Prevents last row from consuming all space and stretching images to oblivion */
|
||||
&::after {
|
||||
content: "";
|
||||
flex: auto;
|
||||
flex-grow: 9999;
|
||||
}
|
||||
}
|
||||
|
||||
.GalleryWallCard {
|
||||
height: auto;
|
||||
padding: 2px;
|
||||
position: relative;
|
||||
|
||||
$width: 96vw;
|
||||
|
||||
&-landscape {
|
||||
flex-grow: 2;
|
||||
width: 96vw;
|
||||
}
|
||||
|
||||
&-portrait {
|
||||
flex-grow: 1;
|
||||
width: 96vw;
|
||||
}
|
||||
|
||||
@mixin galleryWidth($width) {
|
||||
height: ($width / 3) * 2;
|
||||
|
||||
&-landscape {
|
||||
width: $width;
|
||||
}
|
||||
|
||||
&-portrait {
|
||||
width: $width / 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
@include galleryWidth(96vw);
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
@include galleryWidth(48vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
@include galleryWidth(32vw);
|
||||
}
|
||||
|
||||
&-img {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center 20%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.3));
|
||||
bottom: 0;
|
||||
padding: 1rem;
|
||||
position: absolute;
|
||||
text-shadow: 1px 1px 3px black;
|
||||
transition: 0s opacity;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.GalleryWallCard-title {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover &-footer {
|
||||
opacity: 1;
|
||||
transition: 1s opacity;
|
||||
transition-delay: 500ms;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.RatingStars {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
|
||||
&-unfilled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-filled {
|
||||
filter: drop-shadow(1px 1px 1px #222);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import _ from "lodash";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import FsLightbox from "fslightbox-react";
|
||||
import Mousetrap from "mousetrap";
|
||||
import {
|
||||
FindImagesQueryResult,
|
||||
@@ -9,7 +8,7 @@ import {
|
||||
} from "src/core/generated-graphql";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { queryFindImages } from "src/core/StashService";
|
||||
import { useImagesList } from "src/hooks";
|
||||
import { useImagesList, useLightbox } from "src/hooks";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
@@ -22,25 +21,45 @@ import { ExportDialog } from "../Shared/ExportDialog";
|
||||
|
||||
interface IImageWallProps {
|
||||
images: GQL.SlimImageDataFragment[];
|
||||
onChangePage: (page: number) => void;
|
||||
currentPage: number;
|
||||
pageCount: number;
|
||||
}
|
||||
|
||||
const ImageWall: React.FC<IImageWallProps> = ({ images }) => {
|
||||
const [lightboxToggle, setLightboxToggle] = useState(false);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const ImageWall: React.FC<IImageWallProps> = ({
|
||||
images,
|
||||
onChangePage,
|
||||
currentPage,
|
||||
pageCount,
|
||||
}) => {
|
||||
const handleLightBoxPage = useCallback(
|
||||
(direction: number) => {
|
||||
if (direction === -1) {
|
||||
if (currentPage === 1) return false;
|
||||
onChangePage(currentPage - 1);
|
||||
} else {
|
||||
if (currentPage === pageCount) return false;
|
||||
onChangePage(currentPage + 1);
|
||||
}
|
||||
return direction === -1 || direction === 1;
|
||||
},
|
||||
[onChangePage, currentPage, pageCount]
|
||||
);
|
||||
|
||||
const openImage = (index: number) => {
|
||||
setCurrentIndex(index);
|
||||
setLightboxToggle(!lightboxToggle);
|
||||
};
|
||||
const showLightbox = useLightbox({
|
||||
images,
|
||||
showNavigation: false,
|
||||
pageCallback: handleLightBoxPage,
|
||||
pageHeader: `Page ${currentPage} / ${pageCount}`,
|
||||
});
|
||||
|
||||
const photos = images.map((image) => image.paths.image ?? "");
|
||||
const thumbs = images.map((image, index) => (
|
||||
<div
|
||||
role="link"
|
||||
tabIndex={index}
|
||||
key={image.id}
|
||||
onClick={() => openImage(index)}
|
||||
onKeyPress={() => openImage(index)}
|
||||
onClick={() => showLightbox(index)}
|
||||
onKeyPress={() => showLightbox(index)}
|
||||
>
|
||||
<img
|
||||
src={image.paths.thumbnail ?? ""}
|
||||
@@ -51,32 +70,9 @@ const ImageWall: React.FC<IImageWallProps> = ({ images }) => {
|
||||
</div>
|
||||
));
|
||||
|
||||
// FsLightbox doesn't update unless the key updates
|
||||
const key = images.map((i) => i.id).join(",");
|
||||
|
||||
function onLightboxOpen() {
|
||||
// disable mousetrap
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(Mousetrap as any).pause();
|
||||
}
|
||||
|
||||
function onLightboxClose() {
|
||||
// re-enable mousetrap
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(Mousetrap as any).unpause();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="gallery">
|
||||
<div className="flexbin">{thumbs}</div>
|
||||
<FsLightbox
|
||||
sourceIndex={currentIndex}
|
||||
toggler={lightboxToggle}
|
||||
sources={photos}
|
||||
key={key}
|
||||
onOpen={onLightboxOpen}
|
||||
onClose={onLightboxClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -125,7 +121,7 @@ export const ImageList: React.FC<IImageList> = ({
|
||||
};
|
||||
};
|
||||
|
||||
const listData = useImagesList({
|
||||
const { template, onSelectChange } = useImagesList({
|
||||
zoomable: true,
|
||||
selectable: true,
|
||||
otherOperations,
|
||||
@@ -150,12 +146,7 @@ export const ImageList: React.FC<IImageList> = ({
|
||||
filterCopy.itemsPerPage = 1;
|
||||
filterCopy.currentPage = index + 1;
|
||||
const singleResult = await queryFindImages(filterCopy);
|
||||
if (
|
||||
singleResult &&
|
||||
singleResult.data &&
|
||||
singleResult.data.findImages &&
|
||||
singleResult.data.findImages.images.length === 1
|
||||
) {
|
||||
if (singleResult.data.findImages.images.length === 1) {
|
||||
const { id } = singleResult!.data!.findImages!.images[0];
|
||||
// navigate to the image player page
|
||||
history.push(`/images/${id}`);
|
||||
@@ -228,7 +219,7 @@ export const ImageList: React.FC<IImageList> = ({
|
||||
selecting={selectedIds.size > 0}
|
||||
selected={selectedIds.has(image.id)}
|
||||
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
|
||||
listData.onSelectChange(image.id, selected, shiftKey)
|
||||
onSelectChange(image.id, selected, shiftKey)
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -238,7 +229,9 @@ export const ImageList: React.FC<IImageList> = ({
|
||||
result: FindImagesQueryResult,
|
||||
filter: ListFilterModel,
|
||||
selectedIds: Set<string>,
|
||||
zoomIndex: number
|
||||
zoomIndex: number,
|
||||
onChangePage: (page: number) => void,
|
||||
pageCount: number
|
||||
) {
|
||||
if (!result.data || !result.data.findImages) {
|
||||
return;
|
||||
@@ -252,11 +245,15 @@ export const ImageList: React.FC<IImageList> = ({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// if (filter.displayMode === DisplayMode.List) {
|
||||
// return <ImageListTable images={result.data.findImages.images} />;
|
||||
// }
|
||||
if (filter.displayMode === DisplayMode.Wall) {
|
||||
return <ImageWall images={result.data.findImages.images} />;
|
||||
return (
|
||||
<ImageWall
|
||||
images={result.data.findImages.images}
|
||||
onChangePage={onChangePage}
|
||||
currentPage={filter.currentPage}
|
||||
pageCount={pageCount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,15 +261,24 @@ export const ImageList: React.FC<IImageList> = ({
|
||||
result: FindImagesQueryResult,
|
||||
filter: ListFilterModel,
|
||||
selectedIds: Set<string>,
|
||||
zoomIndex: number
|
||||
zoomIndex: number,
|
||||
onChangePage: (page: number) => void,
|
||||
pageCount: number
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
{maybeRenderImageExportDialog(selectedIds)}
|
||||
{renderImages(result, filter, selectedIds, zoomIndex)}
|
||||
{renderImages(
|
||||
result,
|
||||
filter,
|
||||
selectedIds,
|
||||
zoomIndex,
|
||||
onChangePage,
|
||||
pageCount
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return listData.template;
|
||||
return template;
|
||||
};
|
||||
|
||||
@@ -16,9 +16,8 @@ import {
|
||||
Icon,
|
||||
LoadingIndicator,
|
||||
} from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { useLightbox, useToast } from "src/hooks";
|
||||
import { TextUtils } from "src/utils";
|
||||
import FsLightbox from "fslightbox-react";
|
||||
import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
|
||||
import { PerformerOperationsPanel } from "./PerformerOperationsPanel";
|
||||
import { PerformerScenesPanel } from "./PerformerScenesPanel";
|
||||
@@ -39,7 +38,6 @@ export const Performer: React.FC = () => {
|
||||
// Performer state
|
||||
const [imagePreview, setImagePreview] = useState<string | null>();
|
||||
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
|
||||
const [lightboxToggle, setLightboxToggle] = useState(false);
|
||||
const { data, loading: performerLoading, error } = useFindPerformer(id);
|
||||
const performer = data?.findPerformer || ({} as Partial<GQL.Performer>);
|
||||
|
||||
@@ -51,6 +49,10 @@ export const Performer: React.FC = () => {
|
||||
? performer.image_path ?? ""
|
||||
: imagePreview ?? `${performer.image_path}?default=true`;
|
||||
|
||||
const showLightbox = useLightbox({
|
||||
images: [{ paths: { thumbnail: activeImage, image: activeImage } }],
|
||||
});
|
||||
|
||||
// Network state
|
||||
const [loading, setIsLoading] = useState(false);
|
||||
const isLoading = performerLoading || loading;
|
||||
@@ -318,10 +320,7 @@ export const Performer: React.FC = () => {
|
||||
{imageEncoding ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
) : (
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => setLightboxToggle(!lightboxToggle)}
|
||||
>
|
||||
<Button variant="link" onClick={() => showLightbox()}>
|
||||
<img className="performer" src={activeImage} alt="Performer" />
|
||||
</Button>
|
||||
)}
|
||||
@@ -342,7 +341,6 @@ export const Performer: React.FC = () => {
|
||||
<div className="performer-tabs">{renderTabs()}</div>
|
||||
</div>
|
||||
</div>
|
||||
<FsLightbox toggler={lightboxToggle} sources={[activeImage]} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ import { ErrorMessage, LoadingIndicator, Icon } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { ScenePlayer } from "src/components/ScenePlayer";
|
||||
import { TextUtils, JWUtils } from "src/utils";
|
||||
import * as Mousetrap from "mousetrap";
|
||||
import Mousetrap from "mousetrap";
|
||||
import { SceneMarkersPanel } from "./SceneMarkersPanel";
|
||||
import { SceneFileInfoPanel } from "./SceneFileInfoPanel";
|
||||
import { SceneEditPanel } from "./SceneEditPanel";
|
||||
|
||||
35
ui/v2.5/src/components/Shared/RatingStars.tsx
Normal file
35
ui/v2.5/src/components/Shared/RatingStars.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from "react";
|
||||
import Icon from "./Icon";
|
||||
|
||||
const CLASSNAME = "RatingStars";
|
||||
const CLASSNAME_FILLED = `${CLASSNAME}-filled`;
|
||||
const CLASSNAME_UNFILLED = `${CLASSNAME}-unfilled`;
|
||||
|
||||
interface IProps {
|
||||
rating?: number | null;
|
||||
}
|
||||
|
||||
export const RatingStars: React.FC<IProps> = ({ rating }) =>
|
||||
rating ? (
|
||||
<div className={CLASSNAME}>
|
||||
<Icon icon={["fas", "star"]} className={CLASSNAME_FILLED} />
|
||||
<Icon
|
||||
icon={[rating >= 2 ? "fas" : "far", "star"]}
|
||||
className={rating >= 2 ? CLASSNAME_FILLED : CLASSNAME_UNFILLED}
|
||||
/>
|
||||
<Icon
|
||||
icon={[rating >= 3 ? "fas" : "far", "star"]}
|
||||
className={rating >= 3 ? CLASSNAME_FILLED : CLASSNAME_UNFILLED}
|
||||
/>
|
||||
<Icon
|
||||
icon={[rating >= 4 ? "fas" : "far", "star"]}
|
||||
className={rating >= 4 ? CLASSNAME_FILLED : CLASSNAME_UNFILLED}
|
||||
/>
|
||||
<Icon
|
||||
icon={[rating === 5 ? "fas" : "far", "star"]}
|
||||
className={rating === 5 ? CLASSNAME_FILLED : CLASSNAME_UNFILLED}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
@@ -23,3 +23,4 @@ export { default as SuccessIcon } from "./SuccessIcon";
|
||||
export { default as ErrorMessage } from "./ErrorMessage";
|
||||
export { default as TruncatedText } from "./TruncatedText";
|
||||
export { BasicCard } from "./BasicCard";
|
||||
export { RatingStars } from "./RatingStars";
|
||||
|
||||
@@ -182,3 +182,17 @@ button.collapse-button.btn-primary:not(:disabled):not(.disabled):active {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.RatingStars {
|
||||
&-unfilled {
|
||||
path {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
|
||||
&-filled {
|
||||
path {
|
||||
fill: gold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user