mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-08 02:52:12 +00:00
Merge pull request #303 from storytold/bflat-fy-model-search
model search page and new components
This commit is contained in:
44 files changed
+809
-107
No files matched your search
@@ -3,13 +3,14 @@
|
||||
.button {
|
||||
border-radius: 0.5rem;
|
||||
outline: none;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
font-size: 15px !important;
|
||||
font-weight: 500 !important;
|
||||
padding: 10px 20px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
.button-primary,
|
||||
@@ -19,19 +20,19 @@
|
||||
border: 2.5px solid;
|
||||
border-color: $primary;
|
||||
transition: 0.2s;
|
||||
color: $white;
|
||||
color: $white !important;
|
||||
}
|
||||
.button-primary:hover {
|
||||
background-color: lighten($primary, 5%);
|
||||
border-color: lighten($primary, 5%);
|
||||
color: $white;
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
.button-secondary,
|
||||
.button-secondary:focus,
|
||||
.button-secondary:active {
|
||||
background-color: $secondary;
|
||||
color: $white;
|
||||
color: $white !important;
|
||||
border: 2.5px solid;
|
||||
border-color: $secondary;
|
||||
transition: 0.2s;
|
||||
@@ -39,7 +40,7 @@
|
||||
|
||||
.button-secondary:hover {
|
||||
background-color: lighten($secondary, 5%);
|
||||
color: $white;
|
||||
color: $white !important;
|
||||
border: 2.5px solid;
|
||||
border-color: lighten($secondary, 5%);
|
||||
}
|
||||
@@ -48,7 +49,7 @@
|
||||
.button-destructive:focus,
|
||||
.button-destructive:active {
|
||||
background-color: $danger;
|
||||
color: $white;
|
||||
color: $white !important;
|
||||
border: 2.5px solid;
|
||||
border-color: $danger;
|
||||
transition: 0.2s;
|
||||
@@ -56,7 +57,7 @@
|
||||
|
||||
.button-destructive:hover {
|
||||
background-color: lighten($danger, 5%);
|
||||
color: $white;
|
||||
color: $white !important;
|
||||
border: 2.5px solid;
|
||||
border-color: lighten($danger, 5%);
|
||||
}
|
||||
@@ -65,7 +66,7 @@
|
||||
.button-small:focus,
|
||||
.button-small:active {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
font-size: 14px !important;
|
||||
padding: 8.25px 16px;
|
||||
transition: 0.2s;
|
||||
border: none;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import "./Button.scss";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
@@ -7,29 +8,68 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
label?: string;
|
||||
icon?: IconDefinition;
|
||||
small?: boolean;
|
||||
secondary?: boolean;
|
||||
danger?: boolean;
|
||||
variant?: "primary" | "secondary" | "danger";
|
||||
to?: string; // for internal links
|
||||
href?: string; // for external links
|
||||
target?: "_blank" | "_self";
|
||||
}
|
||||
|
||||
export default function Button({
|
||||
label,
|
||||
icon,
|
||||
small,
|
||||
secondary,
|
||||
danger,
|
||||
variant = "primary",
|
||||
to,
|
||||
href,
|
||||
target = "_self",
|
||||
...rest
|
||||
}: ButtonProps) {
|
||||
const buttonClasses = `button ${small ? "button-small" : ""}
|
||||
${
|
||||
secondary
|
||||
? "button-secondary"
|
||||
: danger
|
||||
? "button-destructive"
|
||||
: "button-primary"
|
||||
} `;
|
||||
let buttonClass = `button`;
|
||||
|
||||
if (small) {
|
||||
buttonClass += ` button-small`;
|
||||
}
|
||||
|
||||
switch (variant) {
|
||||
case "primary":
|
||||
buttonClass += " button-primary";
|
||||
break;
|
||||
case "secondary":
|
||||
buttonClass += " button-secondary";
|
||||
break;
|
||||
case "danger":
|
||||
buttonClass += " button-destructive";
|
||||
break;
|
||||
default:
|
||||
buttonClass += " button-primary";
|
||||
break;
|
||||
}
|
||||
|
||||
if (to) {
|
||||
return (
|
||||
<Link to={to} className={buttonClass}>
|
||||
{icon && <FontAwesomeIcon icon={icon} className="me-2" />}
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
if (href) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target={target}
|
||||
rel="noopener noreferrer"
|
||||
className={buttonClass}
|
||||
>
|
||||
{icon && <FontAwesomeIcon icon={icon} className="me-2" />}
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
// If neither 'to' nor 'href' are provided (regular button)
|
||||
return (
|
||||
<button className={buttonClasses} type="button" {...rest}>
|
||||
<button className={buttonClass} type="button" {...rest}>
|
||||
{icon && <FontAwesomeIcon icon={icon} className="me-2" />}
|
||||
{label}
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Button";
|
||||
@@ -0,0 +1,39 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.checkable-tag {
|
||||
font-weight: 500;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 13px;
|
||||
padding: 6px 12px;
|
||||
background-color: $secondary;
|
||||
color: $white;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
border: 2px solid $secondary;
|
||||
|
||||
&.checked {
|
||||
background-color: lighten($secondary, 10%) !important;
|
||||
color: $white;
|
||||
border: 2px solid $primary !important;
|
||||
}
|
||||
|
||||
.remove-icon {
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($secondary, 5%);
|
||||
border: 2px solid lighten($secondary, 5%);
|
||||
}
|
||||
|
||||
&:hover > .remove-icon {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import "./CheckableTag.scss";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faXmark } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface CheckableTagProps {
|
||||
tag: string;
|
||||
isSelected: boolean;
|
||||
onToggle: (tag: string) => void;
|
||||
}
|
||||
|
||||
export default function CheckableTag({
|
||||
tag,
|
||||
isSelected,
|
||||
onToggle,
|
||||
}: CheckableTagProps) {
|
||||
return (
|
||||
<div
|
||||
className={`d-flex align-items-center checkable-tag ${
|
||||
isSelected ? "checked" : ""
|
||||
}`}
|
||||
onClick={() => onToggle(tag)}
|
||||
>
|
||||
{tag}
|
||||
{isSelected && (
|
||||
<FontAwesomeIcon icon={faXmark} className="ms-2 remove-icon" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./CheckableTag";
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
|
||||
interface ContainerProps {
|
||||
children: React.ReactNode;
|
||||
type?: "full" | "padded" | "panel";
|
||||
}
|
||||
|
||||
export default function PageContainer({
|
||||
children,
|
||||
type = "panel",
|
||||
}: ContainerProps) {
|
||||
let containerClass = "";
|
||||
|
||||
switch (type) {
|
||||
case "full":
|
||||
containerClass = "container-fluid";
|
||||
break;
|
||||
case "padded":
|
||||
containerClass = "container";
|
||||
break;
|
||||
case "panel":
|
||||
containerClass = "";
|
||||
break;
|
||||
default:
|
||||
containerClass = "container-fluid";
|
||||
break;
|
||||
}
|
||||
|
||||
return <div className={containerClass}>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Container";
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
icon?: IconDefinition;
|
||||
label?: string;
|
||||
textArea?: boolean;
|
||||
}
|
||||
|
||||
export default function Input({ label, icon, textArea, ...rest }: InputProps) {
|
||||
return (
|
||||
<div>
|
||||
{label && <label className="sub-title">{label}</label>}
|
||||
|
||||
<div className={`form-group ${icon ? "input-icon" : ""}`}>
|
||||
{icon && (
|
||||
<FontAwesomeIcon icon={icon} className="form-control-feedback" />
|
||||
)}
|
||||
<input className="form-control" {...rest} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Input";
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import Input from "../Input";
|
||||
import { faSearch } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface ModelSearchProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export default function ModelSearch({ value, onChange }: ModelSearchProps) {
|
||||
return (
|
||||
<Input
|
||||
icon={faSearch}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder="Search..."
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./ModelSearch";
|
||||
@@ -0,0 +1,40 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.model-search-results {
|
||||
background-color: lighten($panel, 6%);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid lighten($panel, 12%);
|
||||
|
||||
.creator-name {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: $white;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.type-tag {
|
||||
font-weight: 600;
|
||||
border-radius: 0.35rem;
|
||||
font-size: 13px;
|
||||
padding: 4px 8px;
|
||||
background-color: $secondary;
|
||||
color: $white;
|
||||
transition: 0.2s;
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
height: 100%;
|
||||
|
||||
&.tt2 {
|
||||
color: #494ea9;
|
||||
background-color: #c6c7e1;
|
||||
}
|
||||
}
|
||||
|
||||
.model-details {
|
||||
color: rgba($white, 0.8);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from "react";
|
||||
import "./ModelSearchResults.scss";
|
||||
import { Link } from "react-router-dom";
|
||||
import Button from "../Button";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faChartSimple,
|
||||
faComment,
|
||||
faHeart,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface ModelSearchResultsProps {
|
||||
data: any[];
|
||||
}
|
||||
|
||||
function shortenNumber(num: number): string {
|
||||
if (Math.abs(num) < 1e3) return num.toString();
|
||||
if (Math.abs(num) < 1e6) return (num / 1e3).toFixed(1) + "k";
|
||||
if (Math.abs(num) < 1e9) return (num / 1e6).toFixed(1) + "M";
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
export default function ModelSearchResults({ data }: ModelSearchResultsProps) {
|
||||
return (
|
||||
<div className="row g-3">
|
||||
{data.map((item) => {
|
||||
return (
|
||||
<div className="col-12 col-lg-6" key={item.id}>
|
||||
<div className="model-search-results p-3">
|
||||
<h5 className="fw-semibold mb-0">{item.name}</h5>
|
||||
<p className="creator-name">
|
||||
by{" "}
|
||||
<Link className="fw-medium" to={`/profile/${item.creator}`}>
|
||||
{item.creator}
|
||||
</Link>
|
||||
</p>
|
||||
<div className="d-flex align-items-end">
|
||||
<div className="flex-grow-1">
|
||||
<span
|
||||
className={`type-tag ${
|
||||
item.type === "Tacotron2" ? "tt2" : "tt2"
|
||||
}`}
|
||||
>
|
||||
{item.type}
|
||||
</span>
|
||||
<div className="d-flex gap-3 model-details fw-medium mt-3">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faHeart} className="me-2" />
|
||||
{item.likes && shortenNumber(item.likes)}
|
||||
</div>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faChartSimple} className="me-2" />
|
||||
{item.uses && shortenNumber(item.uses)}
|
||||
</div>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faComment} className="me-2" />
|
||||
{item.comments && shortenNumber(item.comments)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button label="Use Voice" small={true} to="/" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./ModelSearchResults";
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
import CheckableTag from "../CheckableTag";
|
||||
|
||||
interface ModelTagsProps {
|
||||
tags: string[];
|
||||
selectedTags: string[];
|
||||
onSelectTag: (tag: string) => void;
|
||||
}
|
||||
|
||||
export default function ModelTags({
|
||||
tags,
|
||||
selectedTags,
|
||||
onSelectTag,
|
||||
}: ModelTagsProps) {
|
||||
return (
|
||||
<div className="d-flex gap-2 flex-wrap">
|
||||
{tags.map((tag) => (
|
||||
<CheckableTag
|
||||
key={tag}
|
||||
tag={tag}
|
||||
isSelected={selectedTags.includes(tag)}
|
||||
onToggle={onSelectTag}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./ModelTags";
|
||||
@@ -1,24 +0,0 @@
|
||||
import { container } from "data/animation";
|
||||
import { motion } from "framer-motion";
|
||||
import React from "react";
|
||||
|
||||
interface PageContainerProps {
|
||||
children: React.ReactNode;
|
||||
padding?: boolean;
|
||||
}
|
||||
|
||||
export default function PageContainer({
|
||||
children,
|
||||
padding,
|
||||
}: PageContainerProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={container}
|
||||
className={`${padding ? "container" : ""}`}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +1,22 @@
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { panel } from "data/animation";
|
||||
|
||||
interface PanelProps {
|
||||
children: React.ReactNode;
|
||||
padding?: boolean;
|
||||
mb?: boolean;
|
||||
mt?: boolean;
|
||||
full?: boolean;
|
||||
}
|
||||
|
||||
export default function Panel({ children, padding, mb, mt }: PanelProps) {
|
||||
export default function Panel({ children, padding, mb, mt, full }: PanelProps) {
|
||||
return (
|
||||
<motion.div className="container-panel" variants={panel}>
|
||||
<div className={full ? "container-panel mw-100" : "container-panel"}>
|
||||
<div
|
||||
className={`panel ${padding ? "p-3 py-4 p-md-4" : ""}
|
||||
${mb ? "mb-4" : ""} ${mt ? "mt-4" : ""}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Panel";
|
||||
@@ -0,0 +1,72 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.select__control {
|
||||
&.select {
|
||||
height: 48px;
|
||||
border-width: 2px;
|
||||
padding-left: 0.15rem;
|
||||
padding-right: 0.15rem;
|
||||
background-color: $input-dark;
|
||||
border-radius: 0.5rem;
|
||||
border-color: $input-dark !important;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
|
||||
&.no-padding {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($input-dark, 5%);
|
||||
border-color: lighten($input-dark, 5%) !important;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
border-color: $primary !important;
|
||||
}
|
||||
|
||||
&.with-icon {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
&.rounded-full {
|
||||
border-radius: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.select-option {
|
||||
background-color: $input-dark;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($input-dark, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
.select-input,
|
||||
.select-value {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
background: $input-dark;
|
||||
margin-top: 6px;
|
||||
padding: 0;
|
||||
border-radius: 0.5rem;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.select-separator {
|
||||
width: 1px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.select-placeholder {
|
||||
color: rgba($white, 0.5) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.select-small {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import "./Select.scss";
|
||||
import {
|
||||
default as ReactSelect,
|
||||
Props as ReactSelectProps,
|
||||
} from "react-select";
|
||||
|
||||
interface SelectProps extends ReactSelectProps {
|
||||
icon?: IconDefinition;
|
||||
label?: string;
|
||||
rounded?: boolean;
|
||||
small?: boolean;
|
||||
}
|
||||
|
||||
export default function Select({
|
||||
label,
|
||||
icon,
|
||||
rounded,
|
||||
small,
|
||||
...rest
|
||||
}: SelectProps) {
|
||||
const SelectFieldClass = {
|
||||
control: (state: any) =>
|
||||
state.isFocused
|
||||
? `select focused ${icon && "with-icon"} ${rounded && "rounded-full"}`
|
||||
: `select ${icon && "with-icon"} ${rounded && "rounded-full"}`,
|
||||
option: (state: any) =>
|
||||
state.isFocused ? "select-option" : "select-option",
|
||||
input: (state: any) => (state.isFocused ? "select-input" : "select-input"),
|
||||
placeholder: (state: any) =>
|
||||
state.isFocused ? "select-placeholder" : "select-placeholder",
|
||||
singleValue: (state: any) =>
|
||||
state.isFocused ? "select-value" : "select-value",
|
||||
menu: (state: any) =>
|
||||
state.isFocused ? "select-container" : "select-container",
|
||||
indicatorSeparator: (state: any) =>
|
||||
state.isFocused ? "select-separator" : "select-separator",
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{label && <label className="sub-title">{label}</label>}
|
||||
|
||||
<div
|
||||
className={`form-group ${icon ? "input-icon" : ""} ${
|
||||
small && "select-small"
|
||||
}`}
|
||||
>
|
||||
{icon && (
|
||||
<FontAwesomeIcon icon={icon} className="form-control-feedback" />
|
||||
)}
|
||||
<div className="w-100">
|
||||
<ReactSelect
|
||||
classNamePrefix="select"
|
||||
classNames={SelectFieldClass}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Select";
|
||||
@@ -35,8 +35,8 @@ export default function ShareButton(props: ShareButtonProps) {
|
||||
<Button
|
||||
label="Share Link"
|
||||
icon={faLink}
|
||||
secondary
|
||||
small
|
||||
variant="secondary"
|
||||
small={true}
|
||||
onClick={copyToClipboard}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
interface TextAreaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
label?: string;
|
||||
textArea?: boolean;
|
||||
}
|
||||
|
||||
export default function TextArea({ label, textArea, ...rest }: TextAreaProps) {
|
||||
return (
|
||||
<div>
|
||||
{label && <label className="sub-title">{label}</label>}
|
||||
|
||||
<div className="form-group">
|
||||
<textarea className="form-control" {...rest} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./TextArea";
|
||||
@@ -43,7 +43,7 @@ export default function MobileMenu(props: MobileMenuProps) {
|
||||
icon={faUser}
|
||||
label="Profile"
|
||||
small
|
||||
secondary
|
||||
variant="secondary"
|
||||
onClick={() => history.push(url)}
|
||||
/>
|
||||
</>
|
||||
@@ -58,7 +58,7 @@ export default function MobileMenu(props: MobileMenuProps) {
|
||||
<Button
|
||||
label="Menu"
|
||||
icon={faBars}
|
||||
secondary
|
||||
variant="secondary"
|
||||
small
|
||||
onClick={handleMenuButtonClick}
|
||||
/>
|
||||
|
||||
@@ -1,33 +1,61 @@
|
||||
import React from "react";
|
||||
import Panel from "../../common/Panel";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import Button from "components/common/Button";
|
||||
|
||||
interface Props {
|
||||
childProps?: any;
|
||||
titleIcon?: JSX.Element;
|
||||
titleComponent?: React.ComponentType;
|
||||
title?: string;
|
||||
subText: string;
|
||||
showButtons: boolean;
|
||||
actionButtons?: JSX.Element;
|
||||
interface PageHeaderProps {
|
||||
titleIcon?: IconDefinition;
|
||||
title: string;
|
||||
subText: string | React.ReactNode;
|
||||
full?: boolean;
|
||||
showButton?: boolean;
|
||||
extension?: React.ReactNode;
|
||||
buttonLabel?: string;
|
||||
buttonVariant?: "primary" | "secondary" | "danger";
|
||||
buttonTo?: string;
|
||||
}
|
||||
|
||||
export default function PageHeader({
|
||||
titleIcon,
|
||||
title,
|
||||
subText,
|
||||
full,
|
||||
showButton,
|
||||
extension,
|
||||
buttonLabel,
|
||||
buttonVariant = "primary",
|
||||
buttonTo,
|
||||
}: PageHeaderProps) {
|
||||
const icon = (
|
||||
<>{titleIcon && <FontAwesomeIcon icon={titleIcon} className="me-3" />}</>
|
||||
);
|
||||
|
||||
export default function PageHeader({ actionButtons, childProps, showButtons, subText, titleComponent: Title, title, titleIcon }: Props) {
|
||||
return <div className="container-panel hero-section py-4">
|
||||
<div className="panel">
|
||||
<div className="p-3 py-4 p-md-4">
|
||||
{ Title ? <Title {...childProps }/> : <h1 className="fw-bold text-center text-md-start">
|
||||
{ titleIcon }
|
||||
{ title }
|
||||
</h1> }
|
||||
<p className="text-center text-md-start pt-1">{ subText }</p>
|
||||
{ showButtons && (
|
||||
<div className="d-flex flex-column flex-md-row gap-3 justify-content-center justify-content-md-start mt-4">
|
||||
{ actionButtons }
|
||||
return (
|
||||
<div className="pt-3 pb-4 pt-lg-4">
|
||||
<Panel padding full={full}>
|
||||
<div className="d-flex flex-column gap-4">
|
||||
<div>
|
||||
<div className="d-flex">
|
||||
<h2 className="fw-bold flex-grow-1">
|
||||
{icon}
|
||||
{title}
|
||||
</h2>
|
||||
<div className="d-none d-md-block">
|
||||
{showButton && (
|
||||
<Button
|
||||
variant={buttonVariant}
|
||||
label={buttonLabel}
|
||||
to={buttonTo}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p>{subText}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{extension && <div>{extension}</div>}
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
// export default PageHeader;
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './PageHeader';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./PageHeader";
|
||||
+4
-2
@@ -13,6 +13,7 @@ interface PageHeaderModelViewProps {
|
||||
extras?: React.ReactNode;
|
||||
view?: "regular" | "edit" | "delete";
|
||||
modelType?: "TTS" | "V2V";
|
||||
full?: boolean;
|
||||
}
|
||||
|
||||
export default function PageHeaderModelView({
|
||||
@@ -25,15 +26,16 @@ export default function PageHeaderModelView({
|
||||
extras,
|
||||
view = "regular",
|
||||
modelType = "TTS",
|
||||
full,
|
||||
}: PageHeaderModelViewProps) {
|
||||
const icon = (
|
||||
<>{titleIcon && <FontAwesomeIcon icon={titleIcon} className="me-3" />}</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="pt-2 pt-lg-4 pb-4">
|
||||
<div className="pt-3 pb-4 pt-lg-4">
|
||||
{view === "regular" && (
|
||||
<Panel padding>
|
||||
<Panel padding full={full}>
|
||||
<div className="row gy-3">
|
||||
<div className="col-12 col-lg-8">
|
||||
<h2 className="fw-bold">
|
||||
|
||||
@@ -156,7 +156,7 @@ export default function SideNav(props: SideNavProps) {
|
||||
<Button
|
||||
label={t("loginButton")}
|
||||
small
|
||||
secondary
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
history.push("/login");
|
||||
handleNavLinkClick();
|
||||
@@ -194,7 +194,7 @@ export default function SideNav(props: SideNavProps) {
|
||||
icon={faUser}
|
||||
label={t("profileButton")}
|
||||
small
|
||||
secondary
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
history.push(url);
|
||||
handleNavLinkClick();
|
||||
@@ -209,7 +209,7 @@ export default function SideNav(props: SideNavProps) {
|
||||
icon={faSignOutAlt}
|
||||
label={t("logOutButton")}
|
||||
small
|
||||
danger
|
||||
variant="danger"
|
||||
onClick={async () => {
|
||||
await logoutHandler();
|
||||
handleNavLinkClick();
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import PageHeader from './PageHeader';
|
||||
|
||||
export {
|
||||
PageHeader,
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./useSearch";
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useState, useMemo } from "react";
|
||||
|
||||
export default function useSearch(initialData: any[]) {
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
let tempData = [...initialData];
|
||||
|
||||
if (searchTerm) {
|
||||
tempData = tempData.filter((item) =>
|
||||
item.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedTags.length) {
|
||||
tempData = tempData.filter((item) =>
|
||||
selectedTags.every((tag) => item.tags.includes(tag))
|
||||
);
|
||||
}
|
||||
|
||||
return tempData;
|
||||
}, [searchTerm, selectedTags, initialData]);
|
||||
|
||||
return {
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
selectedTags,
|
||||
setSelectedTags,
|
||||
filteredData,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./useShortenNumber";
|
||||
@@ -0,0 +1,6 @@
|
||||
export default function useShortenNumber(num: number): string {
|
||||
if (Math.abs(num) < 1e3) return num.toString();
|
||||
if (Math.abs(num) < 1e6) return (num / 1e3).toFixed(1) + "k";
|
||||
if (Math.abs(num) < 1e9) return (num / 1e6).toFixed(1) + "M";
|
||||
return num.toString();
|
||||
}
|
||||
@@ -782,7 +782,7 @@ styles with important tags:
|
||||
}
|
||||
|
||||
.select-placeholder {
|
||||
color: $white !important;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
|
||||
@@ -89,6 +89,7 @@ import SideNav from "components/layout/SideNav/SideNav";
|
||||
import MobileMenu from "components/layout/MobileMenu/MobileMenu";
|
||||
import { TopNav } from "./nav/TopNav";
|
||||
import { TestingPage } from "./pages/testing/TestingPage";
|
||||
import TtsModelSearchPage from "./pages/tts/tts_model_search/TtsModelSearchPage";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper;
|
||||
@@ -317,6 +318,10 @@ class PageContainer extends React.Component<
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="/dev-tts">
|
||||
<TtsModelSearchPage sessionWrapper={this.props.sessionWrapper} />
|
||||
</Route>
|
||||
|
||||
<Route path="/tts/result/:token/edit">
|
||||
<TtsResultEditPage sessionWrapper={this.props.sessionWrapper} />
|
||||
</Route>
|
||||
@@ -459,7 +464,7 @@ class PageContainer extends React.Component<
|
||||
</Route>
|
||||
|
||||
<Route path="/media/:token">
|
||||
<Media/>
|
||||
<Media />
|
||||
</Route>
|
||||
|
||||
<Route path="/moderation/user/list">
|
||||
@@ -580,7 +585,7 @@ class PageContainer extends React.Component<
|
||||
</Route>
|
||||
|
||||
<Route path="/testing">
|
||||
<TestingPage
|
||||
<TestingPage
|
||||
sessionSubscriptionsWrapper={
|
||||
this.props.sessionSubscriptionsWrapper
|
||||
}
|
||||
@@ -591,12 +596,15 @@ class PageContainer extends React.Component<
|
||||
</Route>
|
||||
|
||||
<Route path="/face-animation">
|
||||
<LipsyncEditor {...{
|
||||
enqueueInferenceJob: this.props.enqueueInferenceJob,
|
||||
sessionSubscriptionsWrapper: this.props.sessionSubscriptionsWrapper,
|
||||
inferenceJobs: this.props.inferenceJobs,
|
||||
inferenceJobsByCategory: this.props.inferenceJobsByCategory
|
||||
}} />
|
||||
<LipsyncEditor
|
||||
{...{
|
||||
enqueueInferenceJob: this.props.enqueueInferenceJob,
|
||||
sessionSubscriptionsWrapper:
|
||||
this.props.sessionSubscriptionsWrapper,
|
||||
inferenceJobs: this.props.inferenceJobs,
|
||||
inferenceJobsByCategory: this.props.inferenceJobsByCategory,
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="/commissions">
|
||||
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
|
||||
function PageHeaderWithImage(props: Props) {
|
||||
return (
|
||||
<div className="container-panel hero-section pt-3 pb-4 pt-lg-4">
|
||||
<div className="container-panel pt-3 pb-4 pt-lg-4">
|
||||
<div className="panel">
|
||||
<div className="row gx-3 flex-md-row-reverse">
|
||||
<div className="col-12 col-md-5 hero-img-container d-none d-md-block">
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import React from "react";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import Container from "components/common/Container";
|
||||
import PageHeader from "components/layout/PageHeader";
|
||||
import { faClock, faMessageDots } from "@fortawesome/pro-solid-svg-icons";
|
||||
import ModelSearch from "components/common/ModelSearch";
|
||||
import Panel from "components/common/Panel";
|
||||
import useSearch from "hooks/useSearch/useSearch";
|
||||
import ModelSearchResults from "components/common/ModelSearchResults";
|
||||
import ModelTags from "components/common/ModelTags";
|
||||
import Select from "components/common/Select";
|
||||
|
||||
interface TtsModelSearchPageProps {
|
||||
sessionWrapper: SessionWrapper;
|
||||
}
|
||||
|
||||
const dummyData = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Spongebob",
|
||||
tags: ["English", "High-pitched", "Character"],
|
||||
type: "Tacotron2",
|
||||
creator: "echelon",
|
||||
likes: 14000,
|
||||
uses: 1000000,
|
||||
comments: 250,
|
||||
time: new Date("2023-09-20T12:00:00Z").toISOString(),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Mariano Closs",
|
||||
tags: ["Spanish", "Low-pitched"],
|
||||
type: "Tacotron2",
|
||||
creator: "echelon",
|
||||
likes: 308290,
|
||||
uses: 30000000,
|
||||
comments: 1200,
|
||||
time: new Date("2023-10-12T12:00:00Z").toISOString(),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Cristiano Ronaldo",
|
||||
tags: ["Portuguese", "Low-pitched"],
|
||||
type: "Tacotron2",
|
||||
creator: "echelon",
|
||||
likes: 17500,
|
||||
uses: 40000000,
|
||||
comments: 250,
|
||||
time: new Date("2023-09-20T12:00:00Z").toISOString(),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Messi",
|
||||
tags: ["Portuguese", "Low-pitched"],
|
||||
type: "Tacotron2",
|
||||
creator: "echelon",
|
||||
likes: 17500,
|
||||
uses: 40000000,
|
||||
comments: 250,
|
||||
time: new Date("2023-09-20T12:00:00Z").toISOString(),
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Morty",
|
||||
tags: ["English", "High-pitched", "Character"],
|
||||
type: "Tacotron2",
|
||||
creator: "echelon",
|
||||
likes: 14000,
|
||||
uses: 1000000,
|
||||
comments: 250,
|
||||
time: new Date("2023-09-20T12:00:00Z").toISOString(),
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Bad Bunny",
|
||||
tags: ["Spanish", "Low-pitched"],
|
||||
type: "Tacotron2",
|
||||
creator: "echelon",
|
||||
likes: 308290,
|
||||
uses: 30000000,
|
||||
comments: 1200,
|
||||
time: new Date("2023-10-12T12:00:00Z").toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
const allTags = [
|
||||
"English",
|
||||
"Spanish",
|
||||
"Portuguese",
|
||||
"High-pitched",
|
||||
"Low-pitched",
|
||||
"Character",
|
||||
];
|
||||
|
||||
export default function TtsModelSearchPage(props: TtsModelSearchPageProps) {
|
||||
const {
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
selectedTags,
|
||||
setSelectedTags,
|
||||
filteredData,
|
||||
} = useSearch(dummyData);
|
||||
|
||||
const handleSelectTag = (tag: string) => {
|
||||
setSelectedTags((prev) => {
|
||||
if (prev.includes(tag)) {
|
||||
return prev.filter((t) => t !== tag);
|
||||
}
|
||||
return [...prev, tag];
|
||||
});
|
||||
};
|
||||
|
||||
const searchTts = (
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<ModelSearch value={searchTerm} onChange={setSearchTerm} />
|
||||
<ModelTags
|
||||
tags={allTags}
|
||||
selectedTags={selectedTags}
|
||||
onSelectTag={handleSelectTag}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const sortOptions = [
|
||||
{ value: "most liked", label: "Most Liked" },
|
||||
{ value: "most used", label: "Most Used" },
|
||||
{ value: "moset recent", label: "Most Recent" },
|
||||
];
|
||||
const sortTimeOptions = [
|
||||
{ value: "all time", label: "All Time" },
|
||||
{ value: "today", label: "Today" },
|
||||
{ value: "this week", label: "This Week" },
|
||||
{ value: "this month", label: "This Month" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Container type="panel">
|
||||
<PageHeader
|
||||
title="Text to Speech"
|
||||
titleIcon={faMessageDots}
|
||||
subText="Turn text into your favorite character's voice."
|
||||
showButton={true}
|
||||
buttonLabel="Create a voice"
|
||||
buttonVariant="secondary"
|
||||
buttonTo="/voice-designer"
|
||||
extension={searchTts}
|
||||
/>
|
||||
<Panel padding={true}>
|
||||
<div className="d-flex gap-2 mb-4">
|
||||
<Select
|
||||
small={true}
|
||||
options={sortOptions}
|
||||
defaultValue={sortOptions[0]}
|
||||
/>
|
||||
<Select
|
||||
small={true}
|
||||
icon={faClock}
|
||||
options={sortTimeOptions}
|
||||
defaultValue={sortTimeOptions[0]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ModelSearchResults data={filteredData} />
|
||||
</Panel>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import Panel from "components/common/Panel/Panel";
|
||||
import { faSave, faTrash } from "@fortawesome/pro-solid-svg-icons";
|
||||
import PageHeaderModelView from "components/layout/PageHeaderModelView/PageHeaderModelView";
|
||||
import Button from "components/common/Button/Button";
|
||||
import PageContainer from "components/common/PageContainer/PageContainer";
|
||||
import PageContainer from "components/common/Container";
|
||||
|
||||
interface VcModelDeletePageProps {
|
||||
sessionWrapper: SessionWrapper;
|
||||
|
||||
@@ -6,7 +6,7 @@ import Panel from "components/common/Panel/Panel";
|
||||
import { faEye, faFilePen, faSave } from "@fortawesome/pro-solid-svg-icons";
|
||||
import PageHeaderModelView from "components/layout/PageHeaderModelView/PageHeaderModelView";
|
||||
import Button from "components/common/Button/Button";
|
||||
import PageContainer from "components/common/PageContainer/PageContainer";
|
||||
import PageContainer from "components/common/Container";
|
||||
|
||||
interface VcModelEditPageProps {
|
||||
sessionWrapper: SessionWrapper;
|
||||
|
||||
@@ -17,7 +17,7 @@ import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapp
|
||||
import { RatingButtons } from "v2/view/_common/ratings/RatingButtons";
|
||||
import { RatingStats } from "v2/view/_common/ratings/RatingStats";
|
||||
import ShareButton from "components/common/ShareButton/ShareButton";
|
||||
import PageContainer from "components/common/PageContainer/PageContainer";
|
||||
import Container from "components/common/Container/Container";
|
||||
import VcGenerateAudioPanel from "../VcGenerateAudioPanel";
|
||||
|
||||
interface VcModelViewPageProps {
|
||||
@@ -97,7 +97,7 @@ export default function VcModelViewPage(props: VcModelViewPageProps) {
|
||||
const shareButton = <ShareButton url={shareUrl} />;
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Container type="full">
|
||||
<PageHeaderModelView
|
||||
title={title}
|
||||
subText={subText}
|
||||
@@ -180,6 +180,6 @@ export default function VcModelViewPage(props: VcModelViewPageProps) {
|
||||
sessionWrapper={props.sessionWrapper}
|
||||
/>
|
||||
</Panel>
|
||||
</PageContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user