mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 01:38:44 +00:00
many new components
This commit is contained in:
1 parent
e4e7fa94b3
commit
ba31bf47f9
14 files changed
+345
-23
No files matched your search
@@ -0,0 +1,44 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.fy-accordion-item {
|
||||
border: 1px solid rgba($white, 0.12);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fy-accordion-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
background-color: $secondary;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.open {
|
||||
background-color: lighten($secondary, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
.fy-accordion-content {
|
||||
overflow: hidden;
|
||||
background-color: lighten($panel, 4%);
|
||||
|
||||
&.open {
|
||||
opacity: 1;
|
||||
max-height: 100%;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.fy-accordion-content table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.caret-icon {
|
||||
transition: transform 0.2s ease;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.caret-rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import "./Accordion.scss";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faChevronDown } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { animated, useSpring } from "@react-spring/web";
|
||||
|
||||
interface AccordionProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface AccordionItemProps {
|
||||
title: string;
|
||||
defaultOpen?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
title,
|
||||
defaultOpen = false,
|
||||
children,
|
||||
}: AccordionItemProps) {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const [contentHeight, setContentHeight] = useState<number | undefined>(
|
||||
defaultOpen ? undefined : 0
|
||||
);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && contentRef.current) {
|
||||
setContentHeight(contentRef.current.scrollHeight);
|
||||
} else {
|
||||
setContentHeight(0);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const heightProps = useSpring({
|
||||
height: `${contentHeight}px`,
|
||||
from: {
|
||||
height: defaultOpen ? "auto" : "0px",
|
||||
config: { tension: 300, friction: 25 },
|
||||
},
|
||||
});
|
||||
|
||||
const contentOpacityProps = useSpring({
|
||||
opacity: isOpen ? 1 : 0,
|
||||
config: { tension: 300, friction: 25 },
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="fy-accordion-item">
|
||||
<div
|
||||
className={`fy-accordion-header p-3 ${isOpen ? "open" : ""}`}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{title}
|
||||
<span className={`caret-icon ${isOpen ? "caret-rotated" : ""}`}>
|
||||
<FontAwesomeIcon icon={faChevronDown} />
|
||||
</span>
|
||||
</div>
|
||||
<animated.div style={heightProps}>
|
||||
<animated.div
|
||||
ref={contentRef}
|
||||
style={contentOpacityProps}
|
||||
className="fy-accordion-content"
|
||||
>
|
||||
{children}
|
||||
</animated.div>
|
||||
</animated.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Accordion({ children }: AccordionProps) {
|
||||
return <div className="d-flex flex-column gap-3">{children}</div>;
|
||||
}
|
||||
|
||||
Accordion.Item = AccordionItem;
|
||||
|
||||
export default Accordion;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Accordion";
|
||||
@@ -0,0 +1,31 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
table {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 8px 16px !important;
|
||||
}
|
||||
|
||||
.no-outer-border {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.no-outer-border > thead > tr > th:first-child,
|
||||
.no-outer-border > tbody > tr > td:first-child {
|
||||
border-left: none !important;
|
||||
}
|
||||
|
||||
.no-outer-border > thead > tr > th:last-child,
|
||||
.no-outer-border > tbody > tr > td:last-child {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
.no-outer-border > tbody > tr:last-child > td {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.data-table-property {
|
||||
background-color: lighten($panel, 7%) !important;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from "react";
|
||||
import "./DataTable.scss";
|
||||
|
||||
type Data = {
|
||||
property: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
interface DataTableProps {
|
||||
data: Data[];
|
||||
}
|
||||
|
||||
export default function DataTable({ data }: DataTableProps) {
|
||||
return (
|
||||
<table className="table no-outer-border">
|
||||
<tbody>
|
||||
{data.map((row, index) => (
|
||||
<tr key={index}>
|
||||
<td className="data-table-property">{row.property}</td>
|
||||
<td>{row.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./DataTable";
|
||||
@@ -0,0 +1,37 @@
|
||||
@import "scss/colours.scss";
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(
|
||||
-90deg,
|
||||
rgba($white, 10%) 25%,
|
||||
rgba(darken($white, 20%), 10%) 50%,
|
||||
rgba($white, 10%) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.5s infinite;
|
||||
border-radius: 0.25rem;
|
||||
width: 100%;
|
||||
height: 1em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton.rounded {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.skeleton.medium {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.skeleton.short {
|
||||
width: 20%;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import "./Skeleton.scss";
|
||||
|
||||
interface SkeletonProps {
|
||||
type?: "full" | "medium" | "short";
|
||||
rounded?: boolean;
|
||||
}
|
||||
|
||||
export default function Skeleton({ type = "full", rounded }: SkeletonProps) {
|
||||
const skeletonClass = `skeleton ${type} ${rounded && "rounded"}`;
|
||||
|
||||
return <div className={skeletonClass}></div>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./Skeleton";
|
||||
@@ -6,7 +6,7 @@ import Button from "components/common/Button";
|
||||
|
||||
interface PageHeaderProps {
|
||||
titleIcon?: IconDefinition;
|
||||
title: string;
|
||||
title: string | React.ReactNode;
|
||||
subText: string | React.ReactNode;
|
||||
full?: boolean;
|
||||
showButton?: boolean;
|
||||
|
||||
@@ -1517,7 +1517,7 @@ textarea {
|
||||
padding-left: var(--bs-gutter-x, 1rem);
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
max-width: 1080px;
|
||||
max-width: 1200px;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ interface AudioComponentProps {
|
||||
|
||||
const AudioComponent: React.FC<AudioComponentProps> = ({ mediaData }) => {
|
||||
return (
|
||||
<div>
|
||||
<audio controls>
|
||||
<div className="w-100">
|
||||
<audio className="w-100" controls>
|
||||
<source src={mediaData.public_bucket_path} />
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import AudioComponent from "./AudioComponent";
|
||||
import VideoComponent from "./VideoComponent";
|
||||
import ImageComponent from "./ImageComponent";
|
||||
import MediaData from "./MediaDataTypes";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import Container from "components/common/Container";
|
||||
import Panel from "components/common/Panel";
|
||||
import PageHeader from "components/layout/PageHeader";
|
||||
import Skeleton from "components/common/Skeleton";
|
||||
import Button from "components/common/Button";
|
||||
import { faCircleExclamation } from "@fortawesome/pro-solid-svg-icons";
|
||||
import Accordion from "components/common/Accordion";
|
||||
import DataTable from "components/common/DataTable";
|
||||
import { Gravatar } from "@storyteller/components/src/elements/Gravatar";
|
||||
|
||||
interface MediaPageProps {
|
||||
sessionWrapper: SessionWrapper;
|
||||
@@ -19,7 +28,7 @@ let dummyMediaData = {
|
||||
maybe_creator_user: {
|
||||
user_token: "u_00XGM6M2TE4J9",
|
||||
username: "hanashi",
|
||||
display_name: "hanashi",
|
||||
display_name: "Hanashi",
|
||||
gravatar_hash: "c45b453fcb1d27b348504ae7f5d6a6c",
|
||||
default_avatar: {
|
||||
image_index: 1,
|
||||
@@ -57,7 +66,12 @@ export default function MediaPage({ sessionWrapper }: MediaPageProps) {
|
||||
case "audio":
|
||||
return <AudioComponent mediaData={data} />;
|
||||
case "video":
|
||||
return <VideoComponent mediaData={data} />;
|
||||
return (
|
||||
<div className="ratio ratio-16x9">
|
||||
<VideoComponent mediaData={data} />
|
||||
</div>
|
||||
);
|
||||
|
||||
case "image":
|
||||
return <ImageComponent mediaData={data} />;
|
||||
default:
|
||||
@@ -65,13 +79,92 @@ export default function MediaPage({ sessionWrapper }: MediaPageProps) {
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (error || !mediaData) return <div>Media not found</div>;
|
||||
if (isLoading)
|
||||
return (
|
||||
<Container type="panel">
|
||||
<PageHeader
|
||||
title={<Skeleton type="medium" />}
|
||||
subText={<Skeleton type="short" />}
|
||||
/>
|
||||
<Panel padding={true}>
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-8">
|
||||
<h1 className="mb-0">
|
||||
<Skeleton />
|
||||
</h1>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<h1 className="mb-0">
|
||||
<Skeleton />
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</Container>
|
||||
);
|
||||
|
||||
if (error || !mediaData)
|
||||
return (
|
||||
<Container type="panel">
|
||||
<PageHeader
|
||||
titleIcon={faCircleExclamation}
|
||||
title="Media not found"
|
||||
subText="This media does not exist or is private."
|
||||
extension={
|
||||
<div className="d-flex">
|
||||
<Button label="Back to homepage" to="/" className="d-flex" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
||||
const mediaDetails = [
|
||||
{ property: "Type", value: mediaData.media_type },
|
||||
{ property: "Created at", value: mediaData.created_at },
|
||||
{ property: "Visibility", value: mediaData.creator_set_visibility },
|
||||
{ property: "Model Used", value: "Model Name" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{mediaData?.token}</h1>
|
||||
{renderMediaComponent(mediaData)}
|
||||
</div>
|
||||
<Container type="panel">
|
||||
<PageHeader
|
||||
title="Model name"
|
||||
subText={mediaData.maybe_creator_user.display_name}
|
||||
/>
|
||||
<Panel padding={true}>
|
||||
<div className="row g-4">
|
||||
<div className="col-12 col-lg-8">
|
||||
{renderMediaComponent(mediaData)}
|
||||
</div>
|
||||
<div className="col-12 col-lg-4">
|
||||
<div className="d-flex">
|
||||
<Gravatar
|
||||
size={50}
|
||||
username={mediaData.maybe_creator_user.display_name}
|
||||
avatarIndex={
|
||||
mediaData.maybe_creator_user.default_avatar.image_index
|
||||
}
|
||||
backgroundIndex={
|
||||
mediaData.maybe_creator_user.default_avatar.color_index
|
||||
}
|
||||
/>
|
||||
<div className="d-flex flex-column">
|
||||
<Link
|
||||
to={`/profile/${mediaData.maybe_creator_user.display_name}`}
|
||||
>
|
||||
{mediaData.maybe_creator_user.display_name}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Accordion>
|
||||
<Accordion.Item title="Details" defaultOpen={true}>
|
||||
<DataTable data={mediaDetails} />
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -4,15 +4,11 @@ interface VideoComponentProps {
|
||||
mediaData: MediaData;
|
||||
}
|
||||
|
||||
const VideoComponent: React.FC<VideoComponentProps> = ({ mediaData }) => {
|
||||
export default function VideoComponent({ mediaData }: VideoComponentProps) {
|
||||
return (
|
||||
<div>
|
||||
<video controls width="640" height="360">
|
||||
<source src={mediaData.public_bucket_path} />
|
||||
Your browser does not support the video element.
|
||||
</video>
|
||||
</div>
|
||||
<video className="rounded" controls width="100%" height="auto">
|
||||
<source src={mediaData.public_bucket_path} />
|
||||
Your browser does not support the video element.
|
||||
</video>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoComponent;
|
||||
}
|
||||
Reference in new issue
Block a user