move move frontend to progress-test
This commit is contained in:
15
app/javascript/components/Alert/Alert.tsx
Normal file
15
app/javascript/components/Alert/Alert.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React, { FC } from "react";
|
||||
|
||||
type Props = {
|
||||
children: any
|
||||
}
|
||||
|
||||
export const Alert: FC<Props> = ({ children }) => (
|
||||
<div
|
||||
className="w-full md:my-2 p-2 bg-red-600 items-center text-red-100 leading-none lg:rounded flex lg:inline-flex"
|
||||
role="alert"
|
||||
>
|
||||
<span className="flex px-2 py-1 font-bold">Oops!</span>
|
||||
<span className="font-semibold mr-2 text-left flex-auto">{children}</span>
|
||||
</div>
|
||||
);
|
||||
1
app/javascript/components/Alert/index.ts
Normal file
1
app/javascript/components/Alert/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Alert } from "./Alert";
|
||||
38
app/javascript/components/AlertV2/AlertV2.tsx
Normal file
38
app/javascript/components/AlertV2/AlertV2.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { FC } from 'react'
|
||||
import { MdDone, MdError, MdInfo, MdWarning } from 'react-icons/md'
|
||||
|
||||
type AlertSeverity = 'error' | 'warning' | 'info' | 'success'
|
||||
|
||||
const ICONS = {
|
||||
error: <MdError />,
|
||||
warning: <MdWarning />,
|
||||
info: <MdInfo />,
|
||||
success: <MdDone />
|
||||
}
|
||||
|
||||
const COLOR_CLASSES = {
|
||||
error: 'bg-red-300 border-red-600 text-red-800',
|
||||
warning: 'bg-orange-300 border-orange-600 text-orange-800',
|
||||
info: 'bg-blue-300 border-blue-600 text-blue-800',
|
||||
success: 'bg-green-300 border-green-600 text-green-800',
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
severity?: AlertSeverity
|
||||
text?: string
|
||||
}
|
||||
|
||||
export const AlertV2: FC<Props> = ({
|
||||
severity = 'info',
|
||||
text = '',
|
||||
}) => (
|
||||
<div className={`flex rounded shadow p-4 mx-auto my-2 ${COLOR_CLASSES[severity]}`}>
|
||||
<div className="text-xl my-auto pr-2">
|
||||
{ICONS[severity]}
|
||||
</div>
|
||||
<span>
|
||||
{text}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
2
app/javascript/components/AlertV2/index.ts
Normal file
2
app/javascript/components/AlertV2/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { AlertV2 } from "./AlertV2";
|
||||
export type { Props as AlertV2Props } from "./AlertV2";
|
||||
221
app/javascript/components/Appbar/Appbar.tsx
Normal file
221
app/javascript/components/Appbar/Appbar.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import React, { FC, Fragment, useState } from 'react'
|
||||
import { useHistory, useLocation } from 'react-router';
|
||||
import { Menu, Transition } from '@headlessui/react'
|
||||
import { ChartBarIcon, ClipboardListIcon } from '@heroicons/react/outline'
|
||||
|
||||
import { Dialog } from '../Dialog'
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useCurrentUser } from '../../contexts';
|
||||
import { RootState } from '../../services/store';
|
||||
import { classNames } from '../../utils';
|
||||
import { DashboardRoutePaths, QuestionRoutePaths, SessionRoutePaths } from '../../routes'
|
||||
import { turnOff } from '../../services/store/unsavedChanges';
|
||||
import { CurrentUserAvatar } from "../CurrentUserAvatar";
|
||||
|
||||
const UserMenu: FC = () => {
|
||||
const { user } = useCurrentUser();
|
||||
const history = useHistory();
|
||||
const [confirmLogout, setConfirmLogout] = useState(false)
|
||||
const unsavedChanges = useSelector((state: RootState) => state.unsavedChanges)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const doLogout = () => {
|
||||
setConfirmLogout(false)
|
||||
dispatch(turnOff())
|
||||
history.push('/')
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
if (unsavedChanges && !confirmLogout) {
|
||||
setConfirmLogout(true)
|
||||
} else {
|
||||
doLogout()
|
||||
}
|
||||
}
|
||||
|
||||
const [newPath, setNewPath] = useState<string>()
|
||||
|
||||
const handleForcedRedirect = () => {
|
||||
if (!newPath) return
|
||||
|
||||
dispatch(turnOff())
|
||||
setNewPath(undefined)
|
||||
history.push(newPath)
|
||||
}
|
||||
|
||||
const handleLinkClick = (pathname: string) => {
|
||||
if (unsavedChanges) {
|
||||
setNewPath(pathname)
|
||||
} else {
|
||||
history.push(pathname)
|
||||
}
|
||||
}
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
onClick: () => { handleLinkClick(SessionRoutePaths.show) },
|
||||
label: 'Perfil'
|
||||
},
|
||||
{
|
||||
onClick: handleLogout,
|
||||
label: 'Sair'
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
isOpen={!!newPath}
|
||||
setIsOpen={(value) => setNewPath(value ? newPath : undefined)}
|
||||
onConfirmation={handleForcedRedirect}
|
||||
title="Modificações não Salvas"
|
||||
text="Todas as alterações serão descartadas. Deseja continuar?"
|
||||
/>
|
||||
<Dialog
|
||||
isOpen={confirmLogout}
|
||||
setIsOpen={setConfirmLogout}
|
||||
onConfirmation={handleLogout}
|
||||
title="Modificações não Salvas"
|
||||
text="Todas as alterações serão descartadas. Deseja continuar?"
|
||||
/>
|
||||
<Menu as="div" className="relative h-full">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Menu.Button
|
||||
className="h-full flex flex-row px-2 items-center hover:bg-primary-dark text-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
|
||||
>
|
||||
<span className="hidden md:block pr-2">
|
||||
{user?.name}
|
||||
</span>
|
||||
<div className="w-12">
|
||||
<CurrentUserAvatar />
|
||||
</div>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
show={open}
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
static
|
||||
className="z-50 origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none cursor-pointer"
|
||||
>
|
||||
{menuItems.map((item) => (
|
||||
<Menu.Item key={`menu-item-${item.label}`} onClick={item.onClick}>
|
||||
{({ active }) => (
|
||||
<span
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100' : '',
|
||||
'block px-4 py-2 text-sm text-gray-900'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Links: FC = () => {
|
||||
const unsavedChanges = useSelector((state: RootState) => state.unsavedChanges)
|
||||
const dispatch = useDispatch()
|
||||
const location = useLocation()
|
||||
const history = useHistory()
|
||||
|
||||
const [newPath, setNewPath] = useState<string>()
|
||||
|
||||
const handleForcedRedirect = () => {
|
||||
if (!newPath) return
|
||||
|
||||
dispatch(turnOff())
|
||||
setNewPath(undefined)
|
||||
history.push(newPath)
|
||||
}
|
||||
|
||||
const handleLinkClick = (pathname: string) => {
|
||||
if (unsavedChanges) {
|
||||
setNewPath(pathname)
|
||||
} else {
|
||||
history.push(pathname)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const links = [{
|
||||
icon: <ChartBarIcon className="w-6" />,
|
||||
tabel: 'Painel',
|
||||
pathname: DashboardRoutePaths.index,
|
||||
isCurrent: location.pathname.includes('dashboard'),
|
||||
},
|
||||
{
|
||||
icon: <ClipboardListIcon className="w-6" />,
|
||||
tabel: 'Edição',
|
||||
pathname: QuestionRoutePaths.index,
|
||||
isCurrent: location.pathname.includes('question'),
|
||||
}]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
isOpen={!!newPath}
|
||||
setIsOpen={(value) => setNewPath(value ? newPath : undefined)}
|
||||
onConfirmation={handleForcedRedirect}
|
||||
title="Modificações não Salvas"
|
||||
text="Todas as alterações serão descartadas. Deseja continuar?"
|
||||
/>
|
||||
<div className="h-full flex items-center pl-4">
|
||||
{links.map((link) => (
|
||||
<button
|
||||
className={`h-full flex items-center px-2 mx-2 text-gray-300 hover:bg-primary-dark ${link.isCurrent ? 'underline bg-primary-dark' : ''}`}
|
||||
key={`navbar-link-${link.pathname}`}
|
||||
onClick={() => handleLinkClick(link.pathname)}
|
||||
>
|
||||
<span className="pr-2 ">
|
||||
{link.icon}
|
||||
</span>
|
||||
{link.tabel}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Logo: FC = () => (
|
||||
<div className="h-full grid place-items-center">
|
||||
<img
|
||||
alt="Símbolo do Unifeso"
|
||||
className="hidden md:block h-12 w-auto"
|
||||
src={'unifesoLogo'}
|
||||
/>
|
||||
<img
|
||||
alt="Logotipo do Unifeso"
|
||||
className="md:hidden h-12 w-auto"
|
||||
src={'unifesoLogoCompact'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const Appbar = () => {
|
||||
return (
|
||||
<div className="px-4 bg-primary-normal flex items-center justify-between h-16 shadow-md">
|
||||
<div className="flex h-full">
|
||||
<Logo />
|
||||
<Links />
|
||||
</div>
|
||||
<UserMenu />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
1
app/javascript/components/Appbar/index.ts
Normal file
1
app/javascript/components/Appbar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Appbar";
|
||||
63
app/javascript/components/AvatarEditor/AvatarEditor.tsx
Normal file
63
app/javascript/components/AvatarEditor/AvatarEditor.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React, { FC, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
import { Alert } from "../Alert";
|
||||
import { Button } from "../Button";
|
||||
import { PhotoCrop } from "./PhotoCrop";
|
||||
import { useCurrentUser } from "../../contexts";
|
||||
import { Modal } from "../Modal";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
setIsOpen: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const AvatarEditor: FC<Props> = ({ isOpen, setIsOpen }) => {
|
||||
const [croppedImage, setCroppedImage] = useState<any>()
|
||||
const [alert, setAlert] = useState<boolean>()
|
||||
const { refetch, authToken } = useCurrentUser()
|
||||
|
||||
const instance = axios.create({
|
||||
});
|
||||
|
||||
instance.defaults.headers.common.Authorization = `Bearer ${authToken}`;
|
||||
|
||||
const onSubmit = () => {
|
||||
instance
|
||||
.post("/update_avatar", {
|
||||
upload: croppedImage,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.status === 200) {
|
||||
setIsOpen(false)
|
||||
refetch()
|
||||
} else {
|
||||
setAlert(true);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setAlert(true);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Alterar Imagem de Perfil"
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
buttons={
|
||||
<>
|
||||
<Button onClick={() => setIsOpen(false)}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="primary" onClick={() => onSubmit()}>
|
||||
Salvar
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{alert && <Alert>Algo deu errado, tente novamente mais tarde.</Alert>}
|
||||
<PhotoCrop callback={setCroppedImage} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
49
app/javascript/components/AvatarEditor/PhotoCrop.tsx
Normal file
49
app/javascript/components/AvatarEditor/PhotoCrop.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { FC, useState } from "react";
|
||||
import PhotoCropper from "react-avatar-edit";
|
||||
|
||||
type Props = {
|
||||
callback: (value: any) => void
|
||||
}
|
||||
|
||||
const borderStyle: React.CSSProperties = {
|
||||
textAlign: 'center',
|
||||
margin: 'auto',
|
||||
borderStyle: 'dotted',
|
||||
borderWidth: '0.3rem',
|
||||
borderRadius: '0.3rem',
|
||||
}
|
||||
|
||||
export const PhotoCrop: FC<Props> = ({ callback }) => {
|
||||
const [result, setResult] = useState<any>();
|
||||
const onCrop = (cropped: any) => {
|
||||
setResult(cropped);
|
||||
callback(result);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
setResult(null);
|
||||
};
|
||||
|
||||
const onBeforeFileLoad = (elem: any) => {
|
||||
if (elem.target.files[0].size > 1000000) {
|
||||
elem.target.value = "";
|
||||
alert("A imagem selecionada é grande de mais!")
|
||||
}
|
||||
};
|
||||
|
||||
const dimention = 300;
|
||||
|
||||
return (
|
||||
<PhotoCropper
|
||||
borderStyle={borderStyle}
|
||||
label="Escolha uma imagem"
|
||||
width={dimention}
|
||||
height={dimention}
|
||||
imageWidth={dimention}
|
||||
imageHeight={dimention}
|
||||
onCrop={(e) => onCrop(e)}
|
||||
onClose={() => onClose()}
|
||||
onBeforeFileLoad={onBeforeFileLoad}
|
||||
/>
|
||||
);
|
||||
};
|
||||
1
app/javascript/components/AvatarEditor/index.ts
Normal file
1
app/javascript/components/AvatarEditor/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { AvatarEditor } from "./AvatarEditor";
|
||||
63
app/javascript/components/Button/Button.tsx
Normal file
63
app/javascript/components/Button/Button.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react'
|
||||
|
||||
const styleClasses = {
|
||||
primary: "bg-primary-normal hover:bg-primary-dark text-white",
|
||||
secondary: "bg-gray-200 hover:bg-gray-400 text-gray-800",
|
||||
disabled: "bg-gray-200 text-gray-600 cursor-not-allowed shadow-none hover:shadow-none",
|
||||
tertiary: "shadow-none hover:shadow-none drop-shadow-sm text-gray-900 hover:text-gray-600",
|
||||
}
|
||||
|
||||
export type ButtonProps = {
|
||||
type?: 'default' | 'primary' | 'tertiary';
|
||||
className?: string;
|
||||
children?: string | JSX.Element;
|
||||
htmlType?: 'submit' | 'button' | 'reset';
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const ButtonBase: React.ForwardRefRenderFunction<unknown, ButtonProps> = (props, ref) => {
|
||||
const {
|
||||
type = 'default',
|
||||
className = '',
|
||||
htmlType = 'button',
|
||||
onClick,
|
||||
disabled,
|
||||
children,
|
||||
...rest
|
||||
} = props
|
||||
const buttonRef = (ref as any) || React.createRef<HTMLElement>()
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>) => {
|
||||
if (htmlType !== 'submit') {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
if (disabled || !onClick) return
|
||||
|
||||
onClick(e)
|
||||
}
|
||||
|
||||
const extraClasses = () => {
|
||||
if (disabled) return styleClasses.disabled
|
||||
|
||||
if (type === 'primary') return styleClasses.primary
|
||||
|
||||
if (type === 'tertiary') return styleClasses.tertiary
|
||||
|
||||
return styleClasses.secondary
|
||||
}
|
||||
|
||||
return <button
|
||||
{...rest}
|
||||
type={htmlType}
|
||||
disabled={disabled}
|
||||
className={`transition duration-300 ease-in-out block text-center cursor-pointer p-2 px-8 rounded shadow-lg hover:shadow-lg ${extraClasses()} ${className}`}
|
||||
onClick={handleClick}
|
||||
ref={buttonRef}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
}
|
||||
|
||||
export const Button = React.forwardRef<unknown, ButtonProps>(ButtonBase)
|
||||
1
app/javascript/components/Button/index.ts
Normal file
1
app/javascript/components/Button/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Button } from "./Button";
|
||||
23
app/javascript/components/Card/Card.tsx
Normal file
23
app/javascript/components/Card/Card.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React, { FC } from "react";
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
action?: () => void
|
||||
children: any
|
||||
className?: string
|
||||
}
|
||||
export const Card: FC<Props> = ({
|
||||
title, action, children, className = '',
|
||||
}) => (
|
||||
<div className={`bg-white md:rounded shadow-sm border border-gray-300 w-full ${className}`}>
|
||||
<div className="border-b border-gray-300 bg-gray-100 md:rounded-t p-2 shadow-sm flex items-center">
|
||||
<span className="text-lg text-gray-800 flex-grow">{title}</span>
|
||||
{
|
||||
action ? action() : null
|
||||
}
|
||||
</div>
|
||||
<div className="p-4 h-full">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
1
app/javascript/components/Card/index.ts
Normal file
1
app/javascript/components/Card/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Card } from "./Card";
|
||||
19
app/javascript/components/CardGrid/CardGrid.tsx
Normal file
19
app/javascript/components/CardGrid/CardGrid.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { FC } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Grid = styled.div`
|
||||
display: grid;
|
||||
grid-gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr));
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
children: any
|
||||
}
|
||||
|
||||
export const CardGrid: FC<Props> = ({ children, className }) => (
|
||||
<Grid className={className}>
|
||||
{children}
|
||||
</Grid>
|
||||
);
|
||||
1
app/javascript/components/CardGrid/index.ts
Normal file
1
app/javascript/components/CardGrid/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { CardGrid } from "./CardGrid";
|
||||
@@ -0,0 +1,14 @@
|
||||
import React, {FC} from 'react'
|
||||
|
||||
import {useCurrentUser} from "../../contexts";
|
||||
import {UserAvatar} from "../UserAvatar";
|
||||
|
||||
export const CurrentUserAvatar: FC = () => {
|
||||
const {user} = useCurrentUser()
|
||||
|
||||
if (!user) return null
|
||||
|
||||
return (
|
||||
<UserAvatar user={user}/>
|
||||
)
|
||||
}
|
||||
1
app/javascript/components/CurrentUserAvatar/index.ts
Normal file
1
app/javascript/components/CurrentUserAvatar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './CurrentUserAvatar'
|
||||
46
app/javascript/components/Dialog/Dialog.tsx
Normal file
46
app/javascript/components/Dialog/Dialog.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React, { FC } from "react"
|
||||
import { Button } from "../Button"
|
||||
import { Modal } from '../Modal'
|
||||
|
||||
type DialogType = 'confirmation' | 'notice'
|
||||
|
||||
type Props = {
|
||||
type?: DialogType
|
||||
title: string
|
||||
isOpen?: boolean
|
||||
text?: any
|
||||
setIsOpen: (state: boolean) => void
|
||||
onConfirmation: () => void
|
||||
};
|
||||
|
||||
export const Dialog: FC<Props> = ({
|
||||
type = 'confirmation',
|
||||
title,
|
||||
isOpen: open = false,
|
||||
setIsOpen,
|
||||
onConfirmation,
|
||||
text,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
isOpen={open}
|
||||
setIsOpen={setIsOpen}
|
||||
buttons={
|
||||
<>
|
||||
{type === 'confirmation' &&
|
||||
<Button onClick={() => setIsOpen(false)}>
|
||||
Cancelar
|
||||
</Button>
|
||||
}
|
||||
<Button type="primary" onClick={onConfirmation}>
|
||||
Confirmar
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</Modal>
|
||||
)
|
||||
};
|
||||
|
||||
1
app/javascript/components/Dialog/index.ts
Normal file
1
app/javascript/components/Dialog/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Dialog";
|
||||
62
app/javascript/components/Disclosures/Disclosures.tsx
Normal file
62
app/javascript/components/Disclosures/Disclosures.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React, { FC, Fragment } from 'react'
|
||||
import { Disclosure, Transition } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/outline'
|
||||
|
||||
type Item = {
|
||||
title?: string | JSX.Element
|
||||
body?: string | JSX.Element
|
||||
icon?: JSX.Element
|
||||
}
|
||||
|
||||
type Props = {
|
||||
items: Item[]
|
||||
}
|
||||
|
||||
export const Disclosures: FC<Props> = ({
|
||||
items
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-full grid grid-cols-1 gap-3">
|
||||
<div className="w-full max-w-md p-2 mx-auto bg-white rounded-2xl">
|
||||
{items.map((item) => (
|
||||
<Disclosure as="div" className="my-2 rounded border">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button as={Fragment}>
|
||||
<button className="flex p-2 bg-gray-200 w-full justify-between">
|
||||
<div className="grid place-items-center">
|
||||
{item.icon}
|
||||
</div>
|
||||
<span className="pl-2">
|
||||
{item.title}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
className={`${open ? 'transform rotate-180' : ''} w-5 h-5 text-gray-800`}
|
||||
/>
|
||||
</button>
|
||||
</Disclosure.Button>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
|
||||
<Disclosure.Panel
|
||||
className="p-2 bg-gray-100"
|
||||
>
|
||||
{item.body ?? 'Nenhum comentário.'}
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
1
app/javascript/components/Disclosures/index.ts
Normal file
1
app/javascript/components/Disclosures/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Disclosures";
|
||||
39
app/javascript/components/Input/Input.tsx
Normal file
39
app/javascript/components/Input/Input.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, {ChangeEvent} from 'react'
|
||||
import {v4 as uuid} from 'uuid'
|
||||
|
||||
export type Props = React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> & {
|
||||
label?: string
|
||||
}
|
||||
|
||||
const ButtonBase: React.ForwardRefRenderFunction<unknown, Props> = (props, ref) => {
|
||||
const {
|
||||
className = '',
|
||||
onChange,
|
||||
label,
|
||||
...rest
|
||||
} = props
|
||||
const inputRef = (ref as any) || React.createRef<HTMLElement>()
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!onChange) return
|
||||
|
||||
onChange(e)
|
||||
}
|
||||
|
||||
const id = uuid()
|
||||
|
||||
return (
|
||||
<span>
|
||||
{label ?? <label htmlFor={id}>{label}</label>}
|
||||
<input
|
||||
{...rest}
|
||||
id={id}
|
||||
className={`block rounded p-1 w-full border-gray-400 border shadow-sm ${className}`}
|
||||
onChange={handleChange}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export const Input = React.forwardRef<unknown, Props>(ButtonBase)
|
||||
1
app/javascript/components/Input/index.ts
Normal file
1
app/javascript/components/Input/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './Input'
|
||||
19
app/javascript/components/InputGroup/InputGroup.tsx
Normal file
19
app/javascript/components/InputGroup/InputGroup.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { FC } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledInputGroup = styled.div`
|
||||
&:first-of-type {
|
||||
margin-top: 0
|
||||
}
|
||||
&:last-of-type {
|
||||
margin-bottom: 0
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
children?: any
|
||||
}
|
||||
|
||||
export const InputGroup: FC<Props> = ({ children }) => (
|
||||
<StyledInputGroup className="mt-4 mb-2">{children}</StyledInputGroup>
|
||||
);
|
||||
1
app/javascript/components/InputGroup/index.ts
Normal file
1
app/javascript/components/InputGroup/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { InputGroup } from "./InputGroup";
|
||||
57
app/javascript/components/List/List.tsx
Normal file
57
app/javascript/components/List/List.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { FC } from 'react'
|
||||
|
||||
type ListItemIconProps = {
|
||||
icon: JSX.Element
|
||||
}
|
||||
|
||||
const ListItemIcon: FC<ListItemIconProps> = ({ icon }) => {
|
||||
return (
|
||||
<div className="grid place-items-center pr-3">
|
||||
{icon}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ListItemTextProps = {
|
||||
text?: string
|
||||
}
|
||||
|
||||
const ListItemText: FC<ListItemTextProps> = ({ text }) => {
|
||||
return (
|
||||
<div className="pl-2">
|
||||
<p>
|
||||
{text ?? ''}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ListItemProps = {
|
||||
icon?: JSX.Element
|
||||
text?: string
|
||||
}
|
||||
|
||||
export const ListItem: FC<ListItemProps> = ({ icon, text, children }) => {
|
||||
return (
|
||||
<li className="flex py-2 border-t border-b border-gray-200">
|
||||
{icon && <ListItemIcon icon={icon} />}
|
||||
{text && <ListItemText text={text} />}
|
||||
{children}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
type ListProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const List: FC<ListProps> = ({
|
||||
className = '',
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<ul className={`list-none p-0 m-0 ${className}`}>
|
||||
{children}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
1
app/javascript/components/List/index.ts
Normal file
1
app/javascript/components/List/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./List";
|
||||
82
app/javascript/components/Modal/Modal.tsx
Normal file
82
app/javascript/components/Modal/Modal.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { FC, Fragment } from 'react'
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
setIsOpen: (state: boolean) => void
|
||||
buttons?: any,
|
||||
title: string,
|
||||
className?: string,
|
||||
}
|
||||
|
||||
export const Modal: FC<Props> = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
children,
|
||||
buttons,
|
||||
title,
|
||||
className = '',
|
||||
}) => {
|
||||
const closeModal = () => {
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
as="div"
|
||||
className={`fixed inset-0 z-10 overflow-y-auto ${className}`}
|
||||
onClose={closeModal}
|
||||
>
|
||||
<div className="min-h-screen px-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-gray-900 bg-opacity-50" />
|
||||
</Transition.Child>
|
||||
|
||||
<span
|
||||
className="inline-block h-screen align-middle"
|
||||
aria-hidden="true"
|
||||
>
|
||||
​
|
||||
</span>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<div className="inline-block w-full max-w-md p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white shadow-xl rounded">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900 mt-2 mb-4"
|
||||
>
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{buttons &&
|
||||
<div className="mt-4 grid grid-flow-col gap-3">
|
||||
{buttons}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
1
app/javascript/components/Modal/index.ts
Normal file
1
app/javascript/components/Modal/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Modal";
|
||||
106
app/javascript/components/Navegator/Navegator.tsx
Normal file
106
app/javascript/components/Navegator/Navegator.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React, { FC, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import { FaHome, FaPlus } from "react-icons/fa";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { turnOff } from "../../services/store/unsavedChanges";
|
||||
import { RootState } from "../../services/store";
|
||||
import { Dialog } from "../Dialog";
|
||||
import {QuestionRoutePaths} from "../../routes";
|
||||
|
||||
const HorizontalMenu = styled.ul`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
& > li {
|
||||
display: inline;
|
||||
cursor: pointer;
|
||||
}
|
||||
& > li {
|
||||
display: inline;
|
||||
}
|
||||
& > li > div {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
`;
|
||||
|
||||
type ItemProps = {
|
||||
className?: string
|
||||
children?: any
|
||||
}
|
||||
|
||||
const Item: FC<ItemProps> = ({ children, className }) => (
|
||||
<div className={`hover:text-white ${className || ""}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
type Props = {
|
||||
home?: boolean
|
||||
newQuestion?: boolean
|
||||
children?: any
|
||||
}
|
||||
|
||||
export const Navigator: FC<Props> = ({
|
||||
home = false, newQuestion = false, children,
|
||||
}) => {
|
||||
const [confirmLeaveDialog, setConfirmLeaveDialog] = useState(false);
|
||||
const unsavedChanges = useSelector((state: RootState) => state.unsavedChanges)
|
||||
const dispatch = useDispatch()
|
||||
const history = useHistory();
|
||||
|
||||
const confirmLeave = () => {
|
||||
dispatch(turnOff());
|
||||
history.push(QuestionRoutePaths.index);
|
||||
};
|
||||
|
||||
const goHome = () => {
|
||||
if (unsavedChanges) {
|
||||
setConfirmLeaveDialog(true);
|
||||
} else {
|
||||
confirmLeave();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
isOpen={confirmLeaveDialog}
|
||||
setIsOpen={(value) => setConfirmLeaveDialog(value)}
|
||||
onConfirmation={confirmLeave}
|
||||
title="Modificações não Salvas"
|
||||
text="Todas as alterações serão descartadas. Deseja continuar?"
|
||||
/>
|
||||
<div className="flex p-1 text-md px-2 sm:px-8 text-gray-400 bg-primary-dark shadow-md" style={{ maxHeight: "34.4px" }}>
|
||||
<HorizontalMenu className="list-none">
|
||||
{home
|
||||
&& (
|
||||
<Item>
|
||||
<button onClick={() => goHome()} className="flex">
|
||||
<FaHome className="my-auto" />
|
||||
<span className="pl-3">Início</span>
|
||||
</button>
|
||||
</Item>
|
||||
)}
|
||||
{
|
||||
(newQuestion) ? (
|
||||
<Item>
|
||||
<Link to="/questions/new" className="flex">
|
||||
<FaPlus className="my-auto" />
|
||||
<span className="pl-3">Nova Questão</span>
|
||||
</Link>
|
||||
</Item>
|
||||
) : null
|
||||
}
|
||||
{children}
|
||||
</HorizontalMenu>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
1
app/javascript/components/Navegator/index.ts
Normal file
1
app/javascript/components/Navegator/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Navigator } from "./Navegator";
|
||||
27
app/javascript/components/UserAvatar/UserAvatar.tsx
Normal file
27
app/javascript/components/UserAvatar/UserAvatar.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React, {FC} from "react";
|
||||
import {User} from "../../__generated__/graphql-schema";
|
||||
import BoringAvatar from "boring-avatars";
|
||||
|
||||
type Props = {
|
||||
user: User
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const UserAvatar: FC<Props> = ({user, className}) => {
|
||||
return (
|
||||
<div className={`rounded-full border-2 border-primary-light shadow ${className || ''}`}>
|
||||
{user.avatarUrl ?
|
||||
<img
|
||||
src={`${process.env.REACT_APP_BACKEND_URL}/${user.avatarUrl}`}
|
||||
alt={`Avatar do usuário ${user.name}`}
|
||||
/>
|
||||
: <BoringAvatar
|
||||
size={"100%"}
|
||||
name={user.name}
|
||||
variant="pixel"
|
||||
colors={["#595F72", "#575D90", "#84A07C", "#C3D350", "#E6F14A"]}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
1
app/javascript/components/UserAvatar/index.ts
Normal file
1
app/javascript/components/UserAvatar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { UserAvatar } from "./UserAvatar";
|
||||
16
app/javascript/components/index.ts
Normal file
16
app/javascript/components/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export * from "./Alert";
|
||||
export * from "./AlertV2";
|
||||
export * from "./Button";
|
||||
export * from "./Card";
|
||||
export * from "./CardGrid";
|
||||
export * from "./InputGroup";
|
||||
export * from "./AvatarEditor";
|
||||
export * from "./Navegator";
|
||||
export * from "./UserAvatar";
|
||||
export * from "./Dialog";
|
||||
export * from "./Appbar";
|
||||
export * from "./Modal";
|
||||
export * from "./List";
|
||||
export * from "./Disclosures";
|
||||
export * from './CurrentUserAvatar'
|
||||
export * from './Input'
|
||||
Reference in New Issue
Block a user