Initial commit
This commit is contained in:
58
src/Components/CardWithButton/CardWithButton.css
Executable file
58
src/Components/CardWithButton/CardWithButton.css
Executable file
@@ -0,0 +1,58 @@
|
||||
.tile {
|
||||
--bgsize: 7px;
|
||||
--bgoffset: 7px;
|
||||
|
||||
width: 25vw;
|
||||
min-width: 275px;
|
||||
max-width: 300px;
|
||||
|
||||
margin: 0px 10px;
|
||||
padding: 20px 30px;
|
||||
|
||||
border: 1px solid;
|
||||
border-color: var(--mantine-color-contrast-filled);
|
||||
border-radius: var(--paper-radius);
|
||||
transform: translateY(0px);
|
||||
overflow: visible;
|
||||
|
||||
/*Move up on hover*/
|
||||
&:hover {
|
||||
transform: scale(105%);
|
||||
}
|
||||
|
||||
/*Background 45 degree lines */
|
||||
&::before,
|
||||
&::after {
|
||||
border-radius: var(--paper-radius);
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
rgba(255, 255, 255, 0) 33.33%,
|
||||
var(--mantine-color-contrast-filled) 33.33%,
|
||||
var(--mantine-color-contrast-filled) 50%,
|
||||
rgba(255, 255, 255, 0) 50%,
|
||||
rgba(255, 255, 255, 0) 83.33%,
|
||||
var(--mantine-color-contrast-filled) 83.33%,
|
||||
var(--mantine-color-contrast-filled) 100%
|
||||
);
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: -2;
|
||||
top: var(--bgoffset);
|
||||
left: var(--bgoffset);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: var(--bgsize) var(--bgsize);
|
||||
}
|
||||
&::before {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
background: var(--mantine-color-body);
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
/*make it so the button is on bottom of card*/
|
||||
.card_text {
|
||||
flex-grow: 1;
|
||||
}
|
||||
36
src/Components/CardWithButton/CardWithButton.tsx
Executable file
36
src/Components/CardWithButton/CardWithButton.tsx
Executable file
@@ -0,0 +1,36 @@
|
||||
import { Card, Text, Button, Title, Space } from "@mantine/core";
|
||||
import "./CardWithButton.css";
|
||||
import { Link } from "react-router";
|
||||
import CustomButton from "Components/CustomButton/CustomButton";
|
||||
|
||||
interface ButtonWithTextProps {
|
||||
title: string;
|
||||
text: string;
|
||||
buttonText: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
function ButtonWithText(props: ButtonWithTextProps) {
|
||||
return (
|
||||
<Card className="tile" shadow="sm" padding="lg" radius="md" withBorder>
|
||||
<Text fw={650} mb="sm" size="md">
|
||||
{props.title}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" className="card_text">
|
||||
{props.text}
|
||||
</Text>
|
||||
<Space h="md" />
|
||||
<Link to={props.link} className="invisible_link">
|
||||
<CustomButton
|
||||
color="contrast"
|
||||
text={props.buttonText}
|
||||
style="color"
|
||||
textAlign="center"
|
||||
textSize="lg"
|
||||
/>
|
||||
</Link>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ButtonWithText;
|
||||
85
src/Components/CodeTextArea/CodeTextArea.tsx
Executable file
85
src/Components/CodeTextArea/CodeTextArea.tsx
Executable file
@@ -0,0 +1,85 @@
|
||||
import { Textarea } from "@mantine/core";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface TextAreaProps {
|
||||
text: string;
|
||||
onTextChange: (text: string) => void;
|
||||
}
|
||||
|
||||
export function CodeTextArea(props: TextAreaProps) {
|
||||
const inputRef = useRef(null);
|
||||
const lineRef = useRef(null);
|
||||
|
||||
//Synchronize scrolling between the
|
||||
useEffect(() => {
|
||||
const inputEl = document.querySelector(".MoleculeEditInput");
|
||||
const lineEl = document.querySelector(".MoleculeEditLineNumber");
|
||||
if (!inputEl || !lineEl) return;
|
||||
|
||||
let isSyncing = false; // prevents circular scroll events
|
||||
let activeEl = null; // element currently being scrolled
|
||||
|
||||
const syncScroll = (source: Element, target: Element) => {
|
||||
if (isSyncing) return;
|
||||
isSyncing = true;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
target.scrollTop = source.scrollTop;
|
||||
isSyncing = false;
|
||||
});
|
||||
};
|
||||
|
||||
const onScroll = (e: Event) => {
|
||||
activeEl = e.target;
|
||||
if (activeEl === inputEl) {
|
||||
syncScroll(inputEl, lineEl);
|
||||
} else {
|
||||
syncScroll(lineEl, inputEl);
|
||||
}
|
||||
};
|
||||
|
||||
inputEl.addEventListener("scroll", onScroll, { passive: true });
|
||||
lineEl.addEventListener("scroll", onScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
inputEl.removeEventListener("scroll", onScroll);
|
||||
lineEl.removeEventListener("scroll", onScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
className="MoleculeEdit"
|
||||
value={props.text}
|
||||
ref={inputRef}
|
||||
onChange={(e) => {
|
||||
props.onTextChange(e.currentTarget.value);
|
||||
}}
|
||||
autoFocus
|
||||
wrap="no-wrap"
|
||||
leftSection={
|
||||
<div className="MoleculeEditLineNumber" ref={lineRef}>
|
||||
{Array.from({ length: props.text.split("\n").length }, (_, i) => {
|
||||
if (i > 1) {
|
||||
return i - 1;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}).join("\n")}
|
||||
</div>
|
||||
}
|
||||
classNames={{
|
||||
input: "MoleculeEditInput",
|
||||
wrapper: "MoleculeEditWrapper",
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// Stop arrow keys from reaching react-resizable-panels
|
||||
if (
|
||||
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)
|
||||
) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
></Textarea>
|
||||
);
|
||||
}
|
||||
105
src/Components/CustomButton/CustomButton.css
Normal file
105
src/Components/CustomButton/CustomButton.css
Normal file
@@ -0,0 +1,105 @@
|
||||
.colored {
|
||||
border: 2px solid transparent;
|
||||
border-radius: 5px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.colored:hover {
|
||||
background-color: color;
|
||||
}
|
||||
|
||||
.textAlign-left {
|
||||
text-align: left;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.textAlign-right {
|
||||
text-align: right;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
.textAlign-center {
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
* {
|
||||
align-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Variants */
|
||||
.outline {
|
||||
border-color: var(--color);
|
||||
background: transparent;
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
.outline:hover {
|
||||
background: color-mix(in srgb, var(--hovercolor) 20%, transparent);
|
||||
}
|
||||
|
||||
/* ###################### */
|
||||
|
||||
.color {
|
||||
background: var(--color);
|
||||
color: var(--textcolor);
|
||||
}
|
||||
|
||||
.color:hover {
|
||||
background-color: var(--hovercolor);
|
||||
}
|
||||
|
||||
.primary {
|
||||
--color: var(--mantine-color-primary-filled);
|
||||
--textcolor: var(--mantine-color-contrast-filled);
|
||||
--hovercolor: var(--mantine-color-secondary-filled);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
--color: var(--mantine-color-secondary-filled);
|
||||
--textcolor: var(--mantine-color-secondaryContrast-filled);
|
||||
--hovercolor: var(--mantine-color-primary-filled);
|
||||
}
|
||||
|
||||
.contrast {
|
||||
--color: var(--mantine-color-contrast-filled);
|
||||
--textcolor: var(--mantine-color-primary-filled);
|
||||
--hovercolor: var(--mantine-color-secondaryContrast-filled);
|
||||
}
|
||||
|
||||
.secondary-contrast {
|
||||
--color: var(--mantine-color-secondaryContrast-filled);
|
||||
--textcolor: var(--mantine-color-secondary-filled);
|
||||
--hovercolor: var(--mantine-color-contrast-filled);
|
||||
}
|
||||
|
||||
.accent {
|
||||
--color: var(--mantine-color-accent-filled);
|
||||
--textcolor: white;
|
||||
--hovercolor: var(--mantine-color-accent-filled-hover);
|
||||
}
|
||||
|
||||
.warning {
|
||||
--color: var(--mantine-color-yellow-filled);
|
||||
--textcolor: white;
|
||||
--hovercolor: var(--mantine-color-yellow-filled-hover);
|
||||
}
|
||||
|
||||
.error {
|
||||
--color: var(--mantine-color-red-filled);
|
||||
--textcolor: white;
|
||||
--hovercolor: var(--mantine-color-red-filled-hover);
|
||||
}
|
||||
|
||||
/* ###################### */
|
||||
|
||||
.subtle {
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
.subtle:hover {
|
||||
background-color: color-mix(in srgb, var(--color) 20%, transparent);
|
||||
}
|
||||
62
src/Components/CustomButton/CustomButton.tsx
Normal file
62
src/Components/CustomButton/CustomButton.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Space, Text, UnstyledButton, type MantineSize } from "@mantine/core";
|
||||
import type { ReactElement } from "react";
|
||||
import "./CustomButton.css";
|
||||
|
||||
interface CustomButtonProps {
|
||||
text?: string;
|
||||
icon?: ReactElement;
|
||||
style?: "outline" | "color" | "subtle";
|
||||
color?:
|
||||
| "primary"
|
||||
| "secondary"
|
||||
| "contrast"
|
||||
| "secondary-cotrast"
|
||||
| "accent"
|
||||
| "warning"
|
||||
| "error"
|
||||
| string;
|
||||
textAlign?: "left" | "center" | "right";
|
||||
textSize?: MantineSize;
|
||||
onClick?: React.MouseEventHandler;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function CustomButton({
|
||||
text,
|
||||
icon,
|
||||
style = "color",
|
||||
color = "contrast",
|
||||
textAlign = "center",
|
||||
textSize = "md",
|
||||
onClick = () => {},
|
||||
disabled = false,
|
||||
}: CustomButtonProps) {
|
||||
console.log(color);
|
||||
return (
|
||||
<UnstyledButton
|
||||
className={`colored ${style} ${color} textAlign-${textAlign}`}
|
||||
style={
|
||||
color !== "primary" &&
|
||||
color !== "secondary" &&
|
||||
color !== "contrast" &&
|
||||
color !== "secondary-contrast" &&
|
||||
color !== "accent" &&
|
||||
color !== "warning" &&
|
||||
color !== "error"
|
||||
? {
|
||||
"--color": color,
|
||||
"--textcolor": "white",
|
||||
"--hovercolor": `color-mix(in srgb,${color} 90%, black)`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{icon} {icon && <Space w="sm" />}
|
||||
<Text size={textSize}>{text}</Text>
|
||||
</UnstyledButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomButton;
|
||||
23
src/Components/Layout/Header/Header.css
Executable file
23
src/Components/Layout/Header/Header.css
Executable file
@@ -0,0 +1,23 @@
|
||||
.logo {
|
||||
height: 80%;
|
||||
}
|
||||
|
||||
.headerLogo {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-family: "Quicking";
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0px 5px;
|
||||
}
|
||||
33
src/Components/Layout/Header/Header.tsx
Executable file
33
src/Components/Layout/Header/Header.tsx
Executable file
@@ -0,0 +1,33 @@
|
||||
import { Burger, Image, Text } from "@mantine/core";
|
||||
import "./Header.css";
|
||||
import { useLayoutStore } from "Stores/LayoutStore";
|
||||
import { Link } from "react-router";
|
||||
import { baseUrl } from "GlobalVars";
|
||||
import { routes } from "Routes/Routes";
|
||||
|
||||
function Header() {
|
||||
const { is_navbar_open, set_navbar_open } = useLayoutStore();
|
||||
return (
|
||||
<div className="header">
|
||||
<Link className="headerLogo invisible_link" to={routes.MainPage.path}>
|
||||
<Image
|
||||
className="logo"
|
||||
w="auto"
|
||||
fit="contain"
|
||||
src={baseUrl + "/bitmap.png"}
|
||||
alt="image"
|
||||
/>
|
||||
<Text size="xl">QMolSim</Text>
|
||||
</Link>
|
||||
<Burger
|
||||
aria-label="sidebar toggle"
|
||||
opened={is_navbar_open}
|
||||
onClick={() => {
|
||||
set_navbar_open(!is_navbar_open);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
64
src/Components/Layout/Sidebar/Sidebar.css
Executable file
64
src/Components/Layout/Sidebar/Sidebar.css
Executable file
@@ -0,0 +1,64 @@
|
||||
.sidebar {
|
||||
height: 100%;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 25px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.sidebar_top {
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.sidebar_bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar_bottom_top {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
min-width: 0px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sidebar_bottom_bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
justify-content: space-evenly;
|
||||
min-width: 0px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.userInfo {
|
||||
padding-left: 15px;
|
||||
gap: 5px;
|
||||
min-width: 0px;
|
||||
}
|
||||
|
||||
.sidebar_bottom_button {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sidebar_bottom_button:hover {
|
||||
background-color: var(--mantine-color-secondary-filled);
|
||||
}
|
||||
165
src/Components/Layout/Sidebar/Sidebar.tsx
Executable file
165
src/Components/Layout/Sidebar/Sidebar.tsx
Executable file
@@ -0,0 +1,165 @@
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Divider,
|
||||
Modal,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
UnstyledButton,
|
||||
useMantineTheme,
|
||||
} from "@mantine/core";
|
||||
import "./Sidebar.css";
|
||||
import {
|
||||
IconDevicesPc,
|
||||
IconFileDescription,
|
||||
IconFlaskFilled,
|
||||
IconLogout,
|
||||
IconSettings2,
|
||||
IconUsers,
|
||||
type Icon,
|
||||
type IconProps,
|
||||
} from "@tabler/icons-react";
|
||||
import { Link, useLocation } from "react-router";
|
||||
import { useState, type ForwardRefExoticComponent } from "react";
|
||||
import { routes } from "Routes/Routes";
|
||||
import keycloak from "Api/Keycloak/Keycloak";
|
||||
import { AuthenticationStore } from "Stores/AuthenticationStore";
|
||||
import { baseUri, baseUrl } from "GlobalVars";
|
||||
import CustomButton from "Components/CustomButton/CustomButton";
|
||||
|
||||
interface SubtleLinkButtonProps {
|
||||
link: string;
|
||||
Icon: ForwardRefExoticComponent<IconProps & React.RefAttributes<Icon>>;
|
||||
text: string;
|
||||
color: string;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
function SubtleLinkButton(props: SubtleLinkButtonProps) {
|
||||
return (
|
||||
<Link to={props.link} className="invisible_link">
|
||||
<CustomButton
|
||||
icon={<props.Icon color={props.color} size={20} />}
|
||||
style="subtle"
|
||||
color={props.selected ? props.color : "contrast"}
|
||||
text={props.text}
|
||||
textAlign="left"
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function Sidebar() {
|
||||
const { profile } = AuthenticationStore();
|
||||
const [open, set_open] = useState(false);
|
||||
const theme = useMantineTheme();
|
||||
const location = useLocation(); // get current URL
|
||||
|
||||
return (
|
||||
<div className="sidebar">
|
||||
<Modal
|
||||
opened={open}
|
||||
onClose={() => set_open(false)}
|
||||
title=<Title size="lg">Выйти?</Title>
|
||||
centered
|
||||
withCloseButton={false}
|
||||
size="auto"
|
||||
>
|
||||
<div className="sidebar_bottom_bottom">
|
||||
<CustomButton
|
||||
text="Подтвердить"
|
||||
onClick={() => keycloak.logout({ redirectUri: baseUri + baseUrl })}
|
||||
textSize="lg"
|
||||
color="contrast"
|
||||
></CustomButton>
|
||||
<CustomButton
|
||||
onClick={() => set_open(false)}
|
||||
text="Отменить"
|
||||
textSize="lg"
|
||||
style="outline"
|
||||
color="contrast"
|
||||
></CustomButton>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<div className="sidebar_top">
|
||||
<SubtleLinkButton
|
||||
link={routes.ExperimentsPage.path}
|
||||
Icon={IconFlaskFilled}
|
||||
text="Эксперименты"
|
||||
color={theme.colors.teal[7]}
|
||||
selected={location.pathname.startsWith(routes.ExperimentsPage.path)}
|
||||
/>
|
||||
<SubtleLinkButton
|
||||
link={routes.MachinesPage.path}
|
||||
Icon={IconDevicesPc}
|
||||
text="Вычислительные системы"
|
||||
color={theme.colors.violet[7]}
|
||||
selected={location.pathname.startsWith(routes.MachinesPage.path)}
|
||||
/>
|
||||
<SubtleLinkButton
|
||||
link={routes.TeamsPage.path}
|
||||
Icon={IconUsers}
|
||||
text="Команды"
|
||||
color={theme.colors.grape[7]}
|
||||
selected={location.pathname.startsWith(routes.TeamsPage.path)}
|
||||
/>
|
||||
<SubtleLinkButton
|
||||
link={routes.DocumentationPage.path}
|
||||
Icon={IconFileDescription}
|
||||
text="Документация"
|
||||
color={theme.colors.blue[7]}
|
||||
selected={location.pathname.startsWith(routes.DocumentationPage.path)}
|
||||
/>
|
||||
</div>
|
||||
<div className="sidebar_bottom">
|
||||
{keycloak.authenticated && (
|
||||
<>
|
||||
<div className="sidebar_bottom_top">
|
||||
<Avatar radius="xl" />
|
||||
<Stack className="userInfo">
|
||||
<Text size="md">{profile?.username}</Text>
|
||||
<Text size="sm" c="dimmed" truncate="end">
|
||||
{profile?.email}
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
<Divider
|
||||
color="secondaryContrast"
|
||||
style={{ width: "100%", height: "5px", margin: "5px" }}
|
||||
/>
|
||||
<div className="sidebar_bottom_bottom">
|
||||
<Link to={routes.SettingsPage.path} className="invisible_link">
|
||||
<CustomButton
|
||||
style="subtle"
|
||||
icon={<IconSettings2 size={26} />}
|
||||
text="Настройки"
|
||||
color="contrast"
|
||||
textSize="lg"
|
||||
/>
|
||||
</Link>
|
||||
<CustomButton
|
||||
style="subtle"
|
||||
icon={<IconLogout size={26} />}
|
||||
text="Выйти"
|
||||
textSize="lg"
|
||||
color="contrast"
|
||||
onClick={() => {
|
||||
set_open(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!keycloak.authenticated && (
|
||||
<UnstyledButton onClick={() => keycloak.login()}>
|
||||
Войти / Зарегестрироваться
|
||||
</UnstyledButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sidebar;
|
||||
50
src/Components/ListCard/ExperimentsListCard.css
Normal file
50
src/Components/ListCard/ExperimentsListCard.css
Normal file
@@ -0,0 +1,50 @@
|
||||
.ExperimentsListCard {
|
||||
height: 120px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ExperimentSectionWithLine {
|
||||
border-right: 1px solid var(--mantine-color-contrast-filled);
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.ExperimentPill {
|
||||
background-color: var(--mantine-color-contrast-filled);
|
||||
color: var(--mantine-color-primary-filled);
|
||||
}
|
||||
|
||||
.ExperimentPill2 {
|
||||
border: 2px solid var(--mantine-color-accent-filled);
|
||||
background-color: transparent;
|
||||
color: var(--mantine-color-accent-filled);
|
||||
* {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.dateContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: right;
|
||||
gap: 5px;
|
||||
.p {
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
.TopRightContainer {
|
||||
justify-content: right;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
gap: 15px;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.RightExperimentSection {
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
120
src/Components/ListCard/ExperimentsListCard.tsx
Normal file
120
src/Components/ListCard/ExperimentsListCard.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Card, Pill, SimpleGrid, Text, UnstyledButton } from "@mantine/core";
|
||||
import "./ExperimentsListCard.css";
|
||||
import { useNavigate } from "react-router";
|
||||
import type { Experiment, TaskData } from "Types/Experiment/Experiment";
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
import type { MouseEvent } from "react";
|
||||
import { useExperimentStore } from "Stores/ExperimentStore";
|
||||
|
||||
function ExperimentsListCard(props: Experiment) {
|
||||
const navigate = useNavigate();
|
||||
const { selectExperiment, removeExperiment, tasks, teams } =
|
||||
useExperimentStore();
|
||||
|
||||
const team = teams.find((team) => {
|
||||
return team.id == props.team_id;
|
||||
});
|
||||
|
||||
const experiment_tasks = tasks.filter((a) => a.id in props.tasks_ids);
|
||||
|
||||
const handleDelete = () => {
|
||||
removeExperiment(props.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="ExperimentsListCard"
|
||||
onClick={() => {
|
||||
console.log(props.id);
|
||||
selectExperiment(props.id);
|
||||
navigate(props.id.toString());
|
||||
}}
|
||||
>
|
||||
<SimpleGrid cols={3}>
|
||||
<div className="ExperimentSectionWithLine">
|
||||
<Text mb="sm" size="md" style={{ textDecorationLine: "underline" }}>
|
||||
{props.name}
|
||||
</Text>
|
||||
<Text mb="sm" size="md">
|
||||
Team: {team ? team.name : "ERROR"}
|
||||
</Text>
|
||||
<Pill className="ExperimentPill">
|
||||
<Text mb="sm" size="md">
|
||||
Статус: {props.experiment_status}
|
||||
</Text>
|
||||
</Pill>
|
||||
</div>
|
||||
<div className="ExperimentSectionWithLine">
|
||||
<SimpleGrid cols={2} verticalSpacing="0px">
|
||||
<Text>Задачи:</Text>
|
||||
{experiment_tasks.map((task: TaskData<any>) => {
|
||||
return (
|
||||
<Text
|
||||
size="md"
|
||||
style={{
|
||||
textWrap: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{task.name}
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
{experiment_tasks.length == 0 ? (
|
||||
<Pill
|
||||
size="md"
|
||||
className="ExperimentPill2"
|
||||
style={{
|
||||
textWrap: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
Нет Задач
|
||||
</Pill>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
<div className="RightExperimentSection">
|
||||
<div className="TopRightContainer">
|
||||
<div className="dateContainer">
|
||||
<Text size="sm">{props.date_created.toLocaleDateString()} </Text>
|
||||
<Text size="sm">{props.date_created.toLocaleTimeString()}</Text>
|
||||
</div>
|
||||
<UnstyledButton
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
handleDelete();
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</UnstyledButton>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
display: "flex",
|
||||
justifyContent: "right",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text>
|
||||
{" "}
|
||||
Тип эксперимента:{" "}
|
||||
<Pill className="ExperimentPill2">{props.experiment_type}</Pill>
|
||||
</Text>
|
||||
</div>
|
||||
<Text color="secondary" size="sm">
|
||||
#{props.id}
|
||||
</Text>
|
||||
</div>
|
||||
</SimpleGrid>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExperimentsListCard;
|
||||
22
src/Components/MoleculeViewer/MoleculeViewer.css
Executable file
22
src/Components/MoleculeViewer/MoleculeViewer.css
Executable file
@@ -0,0 +1,22 @@
|
||||
.moleculeViewer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
overscroll-behavior: contain;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.atomEditor {
|
||||
position: absolute;
|
||||
border-radius: md;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.atomEditor * {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
269
src/Components/MoleculeViewer/MoleculeViewer.tsx
Executable file
269
src/Components/MoleculeViewer/MoleculeViewer.tsx
Executable file
@@ -0,0 +1,269 @@
|
||||
import Viewer from "miew-react";
|
||||
import Miew from "miew";
|
||||
import {
|
||||
Paper,
|
||||
UnstyledButton,
|
||||
useMantineColorScheme,
|
||||
useMantineTheme,
|
||||
} from "@mantine/core";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import "./MoleculeViewer.css";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import {
|
||||
IconAlertHexagon,
|
||||
IconArrowBarBoth,
|
||||
IconRefresh,
|
||||
} from "@tabler/icons-react";
|
||||
import MoleculeViewerMenu from "./MoleculeViewerMenu";
|
||||
import type { SelectedAtom } from "Types/Experiment/MoleculeEdit/MoleculeEdit";
|
||||
|
||||
const callbackOnResizeFinish = (
|
||||
dom_elem: HTMLElement,
|
||||
beginning_callback: () => void,
|
||||
end_callback: () => void,
|
||||
delay = 100, // ms after resize "finishes"
|
||||
) => {
|
||||
let timeoutId: number | undefined;
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
beginning_callback();
|
||||
if (timeoutId) {
|
||||
window.clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
timeoutId = window.setTimeout(() => {
|
||||
end_callback();
|
||||
}, delay);
|
||||
});
|
||||
|
||||
resizeObserver.observe(dom_elem);
|
||||
|
||||
return () => resizeObserver.disconnect(); // cleanup helper
|
||||
};
|
||||
|
||||
interface MoleculeViewerProps {
|
||||
moleculeData: string;
|
||||
selectedAtom: SelectedAtom | null;
|
||||
setSelectedAtom: (selectedAtom: SelectedAtom | null) => void;
|
||||
}
|
||||
|
||||
function MoleculeViewer(props: MoleculeViewerProps) {
|
||||
const theme = useMantineTheme();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
// объект загруженного редактора молекул для редактирования
|
||||
const [miew, setMiew] = useState<Miew | null>(null);
|
||||
const molViewerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isResizing, setIsResizing] = useState<boolean>(false);
|
||||
|
||||
const [viewingData, setViewingData] = useState<string>(props.moleculeData);
|
||||
|
||||
//при загрузке сохраняем объект miew
|
||||
const onInitMiew = (miew: Miew) => {
|
||||
setMiew(miew);
|
||||
if (miew && viewingData) {
|
||||
//прогружаем молекулу
|
||||
miew
|
||||
.load(viewingData, {
|
||||
sourceType: "immediate",
|
||||
fileType: "xyz",
|
||||
})
|
||||
.then(() => {
|
||||
if (props.selectedAtom) {
|
||||
props.setSelectedAtom(null);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.message == "Operation cancelled") {
|
||||
return;
|
||||
}
|
||||
notifications.show({
|
||||
color: "orange",
|
||||
radius: "md",
|
||||
title: "Ошибка при отображении молекулы",
|
||||
message:
|
||||
"Проверте правильность написания кода молекулы, в нем содержатся ошибки",
|
||||
icon: <IconAlertHexagon />,
|
||||
style: { paddingLeft: "5px" },
|
||||
});
|
||||
});
|
||||
}
|
||||
miew.setOptions({
|
||||
settings: {
|
||||
bg: {
|
||||
color: theme.colors.secondaryDark[7],
|
||||
},
|
||||
fogAlpha: 0.7,
|
||||
axes: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
//мемоизируем объект чтобы он не перегружаля при изменении [miew] состояния
|
||||
const MemoViewer = useMemo(
|
||||
() => <Viewer onInit={onInitMiew} />,
|
||||
[viewingData],
|
||||
);
|
||||
|
||||
//Замена станартного обработчика нажатия на молекулы
|
||||
useEffect(() => {
|
||||
if (molViewerRef.current && miew) {
|
||||
//INFO: при изменении размера окна обносить webgl
|
||||
const cleanup = callbackOnResizeFinish(
|
||||
molViewerRef.current,
|
||||
handleResizeStart,
|
||||
handleResizeEnd,
|
||||
);
|
||||
//@ts-expect-error Miew class not implementing typescript correctly
|
||||
miew.removeEventListener("newpick");
|
||||
//@ts-expect-error Miew class not implementing typescript correctly
|
||||
miew.addEventListener("newpick", handleClick);
|
||||
return () => {
|
||||
cleanup();
|
||||
//@ts-expect-error Miew class not implementing typescript correctly
|
||||
miew.removeEventListener("newpick");
|
||||
};
|
||||
}
|
||||
}, [molViewerRef, miew]);
|
||||
|
||||
//On selected Atom change, highlight it
|
||||
useEffect(() => {
|
||||
if (miew) {
|
||||
if (!props.selectedAtom) {
|
||||
//@ts-expect-error Miew class not implementing typescript correctly
|
||||
miew.select("");
|
||||
return;
|
||||
}
|
||||
//@ts-expect-error Miew class not implementing typescript correctly
|
||||
miew.select("serial " + props.selectedAtom.serial, false);
|
||||
//спрятать информацию встроенную
|
||||
//@ts-expect-error Miew class not implementing typescript correctly
|
||||
miew._msgAtomInfo.style.opacity = 0.0;
|
||||
//@ts-expect-error Miew class not implementing typescript correctly
|
||||
miew._msgAtomInfo.style.height = "0px";
|
||||
//@ts-expect-error Miew class not implementing typescript correctly
|
||||
miew._msgAtomInfo.style.overflow = "hidden";
|
||||
}
|
||||
}, [props.selectedAtom, miew]);
|
||||
|
||||
//при смене темы обновляем фон MoleculeViewer
|
||||
useEffect(() => {
|
||||
if (miew) {
|
||||
miew.setOptions({
|
||||
settings: {
|
||||
bg: {
|
||||
color: theme.colors.secondaryDark[7],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [colorScheme]);
|
||||
|
||||
//при нажятии на атом выбираем его
|
||||
const handleClick = (pick: { type: string; obj: { atom: SelectedAtom } }) => {
|
||||
if (miew) {
|
||||
if (pick.obj.atom) {
|
||||
props.setSelectedAtom(pick.obj.atom);
|
||||
} else {
|
||||
props.setSelectedAtom(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleResizeStart = () => {
|
||||
setIsResizing(true);
|
||||
};
|
||||
|
||||
const handleResizeEnd = () => {
|
||||
//@ts-expect-error Miew class not implementing typescript correctly
|
||||
miew._onResize();
|
||||
//@ts-expect-error Miew class not implementing typescript correctly
|
||||
miew._picker.handleResize();
|
||||
setTimeout(() => {
|
||||
setIsResizing(false);
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
document.body.style.overflow = "hidden"; // disable page scroll
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
document.body.style.overflow = "auto"; // re-enable scroll
|
||||
};
|
||||
|
||||
const refreshDisplay = () => {
|
||||
if (props.moleculeData != viewingData) {
|
||||
setViewingData(props.moleculeData);
|
||||
} else {
|
||||
if (miew) {
|
||||
//@ts-expect-error Miew class not implementing typescript correctly
|
||||
miew.resetView();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={molViewerRef} className="moleculeViewer">
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: theme.colors.secondaryDark[7],
|
||||
borderRadius: "5px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{MemoViewer}
|
||||
{isResizing && viewingData && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: theme.colors.secondaryDark[7], // optional dim
|
||||
opacity: 0.5,
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<IconArrowBarBoth size={80} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{props.selectedAtom && (
|
||||
<Paper className="atomEditor" radius="md" p="md">
|
||||
<MoleculeViewerMenu selectedAtom={props.selectedAtom} miew={miew} />
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "10px",
|
||||
top: "10px",
|
||||
display: "flex",
|
||||
zIndex: 5,
|
||||
width: "auto",
|
||||
}}
|
||||
>
|
||||
<UnstyledButton onClick={refreshDisplay}>
|
||||
<IconRefresh size={25} />
|
||||
</UnstyledButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MoleculeViewer;
|
||||
29
src/Components/MoleculeViewer/MoleculeViewerMenu.tsx
Executable file
29
src/Components/MoleculeViewer/MoleculeViewerMenu.tsx
Executable file
@@ -0,0 +1,29 @@
|
||||
import { Text, Divider } from "@mantine/core";
|
||||
import Miew from "miew";
|
||||
import "./MoleculeViewer.css";
|
||||
import type { SelectedAtom } from "Types/Experiment/MoleculeEdit/MoleculeEdit";
|
||||
|
||||
interface MoleculeViewerMenuProps {
|
||||
selectedAtom: SelectedAtom;
|
||||
miew: Miew | null;
|
||||
}
|
||||
|
||||
function MoleculeViewerMenu(props: MoleculeViewerMenuProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="moleculeViewerMenu">
|
||||
<>
|
||||
<Text ta="center">
|
||||
Атом: {props.selectedAtom.name} ({props.selectedAtom.serial})
|
||||
</Text>
|
||||
<Divider my="sm" />
|
||||
<Text ta="center">Положение X: {props.selectedAtom.position.x}</Text>
|
||||
<Text ta="center">Положение Y: {props.selectedAtom.position.y}</Text>
|
||||
<Text ta="center">Положение Z: {props.selectedAtom.position.z}</Text>
|
||||
</>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MoleculeViewerMenu;
|
||||
16
src/Components/PaginationContainer/PaginationContainer.css
Normal file
16
src/Components/PaginationContainer/PaginationContainer.css
Normal file
@@ -0,0 +1,16 @@
|
||||
.PaginationContainer {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.PaginationContents {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
12
src/Components/PaginationContainer/PaginationContainer.tsx
Normal file
12
src/Components/PaginationContainer/PaginationContainer.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Pagination } from "@mantine/core";
|
||||
import "./PaginationContainer.css";
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
export function PaginationContainer(a: PropsWithChildren) {
|
||||
return (
|
||||
<div className="PaginationContainer">
|
||||
<div className="PaginationContents">{a.children}</div>
|
||||
<Pagination total={10} color="accent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
src/Components/StripedBg/Background.css
Executable file
45
src/Components/StripedBg/Background.css
Executable file
@@ -0,0 +1,45 @@
|
||||
.background-light {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: white
|
||||
linear-gradient(
|
||||
135deg,
|
||||
rgba(110, 156, 223, 0.125) 0%,
|
||||
rgba(110, 156, 223, 0.125) 14.286%,
|
||||
rgba(110, 156, 223, 0.25) 14.286%,
|
||||
rgba(110, 156, 223, 0.25) 28.571%,
|
||||
rgba(110, 156, 223, 0.375) 28.571%,
|
||||
rgba(110, 156, 223, 0.375) 42.857%,
|
||||
rgba(110, 156, 223, 0.5) 42.857%,
|
||||
rgba(110, 156, 223, 0.5) 57.143%,
|
||||
rgba(110, 156, 223, 0.725) 57.143%,
|
||||
rgba(110, 156, 223, 0.725) 71.429%,
|
||||
rgba(110, 156, 223, 0.85) 71.429%,
|
||||
rgba(110, 156, 223, 0.85) 85.714%,
|
||||
rgba(110, 156, 223, 1) 85.714% 100%
|
||||
);
|
||||
}
|
||||
|
||||
.background-dark {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: black
|
||||
linear-gradient(
|
||||
135deg,
|
||||
rgba(36, 54, 162, 0.353) 0%,
|
||||
rgba(36, 54, 162, 0.353) 14.286%,
|
||||
rgba(36, 54, 162, 0.5) 14.286%,
|
||||
rgba(36, 54, 162, 0.5) 28.571%,
|
||||
rgba(36, 54, 162, 0.612) 28.571%,
|
||||
rgba(36, 54, 162, 0.612) 42.857%,
|
||||
rgba(36, 54, 162, 0.707) 42.857%,
|
||||
rgba(36, 54, 162, 0.707) 57.143%,
|
||||
rgba(36, 54, 162, 0.851) 57.143%,
|
||||
rgba(36, 54, 162, 0.851) 71.429%,
|
||||
rgba(36, 54, 162, 0.921) 71.429%,
|
||||
rgba(36, 54, 162, 0.921) 85.714%,
|
||||
rgba(36, 54, 162, 1) 85.714% 100%
|
||||
);
|
||||
}
|
||||
13
src/Components/StripedBg/Background.tsx
Executable file
13
src/Components/StripedBg/Background.tsx
Executable file
@@ -0,0 +1,13 @@
|
||||
import { useMantineColorScheme } from "@mantine/core";
|
||||
import "./Background.css";
|
||||
|
||||
function Background() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
return (
|
||||
<div
|
||||
className={colorScheme == "dark" ? "background-dark" : "background-light"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Background;
|
||||
Reference in New Issue
Block a user