1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-30 19:52:20 +01:00

Product form style

This commit is contained in:
vincent 2022-08-03 18:30:29 +02:00
parent 8f38ff79d7
commit be90d0720b
16 changed files with 290 additions and 202 deletions

BIN
app/frontend/images/no_avatar.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 619 B

After

Width:  |  Height:  |  Size: 792 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

View File

@ -7,6 +7,8 @@ import { FieldValues } from 'react-hook-form/dist/types/fields';
import { FormInput } from '../form/form-input';
import { FormComponent } from '../../models/form-component';
import { AbstractFormItemProps } from './abstract-form-item';
import { FabButton } from '../base/fab-button';
import { FilePdf, Trash } from 'phosphor-react';
export interface FileType {
id?: number,
@ -81,49 +83,41 @@ export const FormFileUpload = <TFieldValues extends FieldValues>({ id, register,
`${className || ''}`
].join(' ');
/**
* Returns placeholder text
*/
const placeholder = (): string => hasFile() ? t('app.shared.form_file_upload.edit') : t('app.shared.form_file_upload.browse');
return (
<div className={`form-file-upload fileinput ${classNames}`}>
<div className="filename-container">
{hasFile() && (
<div>
<i className="fa fa-file fileinput-exists" />
<span className="fileinput-filename">
{file.attachment_name}
</span>
</div>
)}
<div className={`form-file-upload ${classNames}`}>
{hasFile() && (
<span>{file.attachment_name}</span>
)}
<div className="actions">
{file?.id && file?.attachment_url && (
<a href={file.attachment_url}
target="_blank"
className="file-download"
className="fab-button"
rel="noreferrer">
<i className="fa fa-download"/>
<FilePdf size={24} />
</a>
)}
</div>
<span className="fileinput-button">
{!hasFile() && (
<span className="fileinput-new">{t('app.shared.form_file_upload.browse')}</span>
)}
{hasFile() && (
<span className="fileinput-exists">{t('app.shared.form_file_upload.edit')}</span>
)}
<FormInput type="file"
accept={accept}
register={register}
formState={formState}
rules={rules}
disabled={disabled}
error={error}
warning={warning}
id={`${id}[attachment_files]`}
onChange={onFileSelected}/>
</span>
{hasFile() && (
<a className="fileinput-exists fileinput-delete" onClick={onRemoveFile}>
<i className="fa fa-trash-o"></i>
</a>
)}
className="image-file-input"
accept={accept}
register={register}
formState={formState}
rules={rules}
disabled={disabled}
error={error}
warning={warning}
id={`${id}[attachment_files]`}
onChange={onFileSelected}
placeholder={placeholder()}/>
{hasFile() &&
<FabButton onClick={onRemoveFile} icon={<Trash size={20} weight="fill" />} className="is-main" />
}
</div>
</div>
);
};

View File

@ -8,7 +8,8 @@ import { FormInput } from '../form/form-input';
import { FormComponent } from '../../models/form-component';
import { AbstractFormItemProps } from './abstract-form-item';
import { FabButton } from '../base/fab-button';
import noAvatar from '../../../../images/no_avatar.png';
import noImage from '../../../../images/no_image.png';
import { Trash } from 'phosphor-react';
export interface ImageType {
id?: number,
@ -86,6 +87,11 @@ export const FormImageUpload = <TFieldValues extends FieldValues>({ id, register
}
}
/**
* Returns placeholder text
*/
const placeholder = (): string => hasImage() ? t('app.shared.form_image_upload.edit') : t('app.shared.form_image_upload.browse');
// Compose classnames from props
const classNames = [
`${className || ''}`
@ -94,25 +100,22 @@ export const FormImageUpload = <TFieldValues extends FieldValues>({ id, register
return (
<div className={`form-image-upload form-image-upload--${size} ${classNames}`}>
<div className={`image image--${size}`}>
<img src={image || noAvatar} />
<img src={image || noImage} />
</div>
<div className="buttons">
<FabButton className="select-button">
{!hasImage() && <span>{t('app.shared.form_image_upload.browse')}</span>}
{hasImage() && <span>{t('app.shared.form_image_upload.edit')}</span>}
<FormInput className="image-file-input"
type="file"
accept={accept}
register={register}
formState={formState}
rules={rules}
disabled={disabled}
error={error}
warning={warning}
id={`${id}[attachment_files]`}
onChange={onFileSelected}/>
</FabButton>
{hasImage() && <FabButton onClick={onRemoveFile} icon={<i className="fa fa-trash-o"/>} className="delete-image" />}
<div className="actions">
<FormInput className="image-file-input"
type="file"
accept={accept}
register={register}
formState={formState}
rules={rules}
disabled={disabled}
error={error}
warning={warning}
id={`${id}[attachment_files]`}
onChange={onFileSelected}
placeholder={placeholder()}/>
{hasImage() && <FabButton onClick={onRemoveFile} icon={<Trash size={20} weight="fill" />} className="is-main" />}
</div>
</div>
);

View File

@ -67,6 +67,7 @@ export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, re
disabled={typeof disabled === 'function' ? disabled(id) : disabled}
placeholder={placeholder}
accept={accept} />
{(type === 'file' && placeholder) && <span className='fab-button is-black file-placeholder'>{placeholder}</span>}
{addOn && <span onClick={addOnAction} className={`addon ${addOnClassName || ''} ${addOnAction ? 'is-btn' : ''}`}>{addOn}</span>}
</AbstractFormItem>
);

View File

@ -1,3 +1,4 @@
import { PencilSimple, Trash } from 'phosphor-react';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ProductCategory } from '../../../models/product-category';
@ -53,12 +54,12 @@ export const ManageProductCategory: React.FC<ManageProductCategoryProps> = ({ pr
);
case 'update':
return (<FabButton type='button'
icon={<i className="fas fa-pen" />}
icon={<PencilSimple size={20} weight="fill" />}
className="edit-btn"
onClick={toggleModal} />);
case 'delete':
return (<FabButton type='button'
icon={<i className="fa fa-trash" />}
icon={<Trash size={20} weight="fill" />}
className="delete-btn"
onClick={toggleModal} />);
}

View File

@ -65,7 +65,7 @@ export const ProductCategoriesItem: React.FC<ProductCategoriesItemProps> = ({ pr
</div>
<div className='drag-handle'>
<button {...attributes} {...listeners}>
<DotsSixVertical size={16} />
<DotsSixVertical size={20} />
</button>
</div>
</div>

View File

@ -17,6 +17,7 @@ import { FabAlert } from '../base/fab-alert';
import ProductCategoryAPI from '../../api/product-category';
import MachineAPI from '../../api/machine';
import ProductAPI from '../../api/product';
import { Plus } from 'phosphor-react';
interface ProductFormProps {
product: Product,
@ -194,6 +195,7 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
id="is_active"
formState={formState}
label={t('app.admin.store.product_form.is_show_in_store')}
tooltip={t('app.admin.store.product_form.active_price_info')}
className='span-3' />
</div>
@ -205,7 +207,6 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
<FormSwitch control={control}
id="is_active_price"
label={t('app.admin.store.product_form.is_active_price')}
tooltip={t('app.admin.store.product_form.is_active_price')}
defaultValue={isActivePrice}
onChange={toggleIsActivePrice}
className='span-3' />
@ -227,31 +228,43 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
label={t('app.admin.store.product_form.quantity_min')} />
</div>
</div>}
</div>
<hr />
<hr />
<div>
<h4>{t('app.admin.store.product_form.product_images')}</h4>
<FabAlert level="warning">
<HtmlTranslate trKey="app.admin.store.product_form.product_images_info" />
</FabAlert>
<div className="product-images">
<div>
<h4>{t('app.admin.store.product_form.product_images')}</h4>
<FabAlert level="warning">
<HtmlTranslate trKey="app.admin.store.product_form.product_images_info" />
</FabAlert>
<div className="product-images">
<div className="list">
{output.product_images_attributes.map((image, i) => (
<FormImageUpload key={i}
defaultImage={image}
id={`product_images_attributes[${i}]`}
accept="image/*"
size="large"
register={register}
setValue={setValue}
formState={formState}
className={image._destroy ? 'hidden' : ''}
onFileRemove={handleRemoveProductImage(i)}
defaultImage={image}
id={`product_images_attributes[${i}]`}
accept="image/*"
size="small"
register={register}
setValue={setValue}
formState={formState}
className={image._destroy ? 'hidden' : ''}
onFileRemove={handleRemoveProductImage(i)}
/>
))}
</div>
<FabButton onClick={addProductImage}>{t('app.admin.store.product_form.add_product_image')}</FabButton>
<FabButton
onClick={addProductImage}
className='is-info'
icon={<Plus size={24} />}>
{t('app.admin.store.product_form.add_product_image')}
</FabButton>
</div>
</div>
<hr />
<div>
<h4>{t('app.admin.store.product_form.assigning_category')}</h4>
<FabAlert level="warning">
<HtmlTranslate trKey="app.admin.store.product_form.assigning_category_info" />
@ -261,20 +274,24 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
id="product_category_id"
formState={formState}
label={t('app.admin.store.product_form.linking_product_to_category')} />
</div>
<hr />
<hr />
<div>
<h4>{t('app.admin.store.product_form.assigning_machines')}</h4>
<FabAlert level="warning">
<HtmlTranslate trKey="app.admin.store.product_form.assigning_machines_info" />
</FabAlert>
<FormChecklist options={machines}
control={control}
id="machine_ids"
formState={formState} />
control={control}
id="machine_ids"
formState={formState} />
</div>
<hr />
<hr />
<div>
<h4>{t('app.admin.store.product_form.product_description')}</h4>
<FabAlert level="warning">
<HtmlTranslate trKey="app.admin.store.product_form.product_description_info" />
@ -283,25 +300,38 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
paragraphTools={true}
limit={1000}
id="description" />
<div>
<h4>{t('app.admin.store.product_form.product_files')}</h4>
<FabAlert level="warning">
<HtmlTranslate trKey="app.admin.store.product_form.product_files_info" />
</FabAlert>
{output.product_files_attributes.map((file, i) => (
<FormFileUpload key={i}
defaultFile={file}
id={`product_files_attributes[${i}]`}
accept="application/pdf"
register={register}
setValue={setValue}
formState={formState}
className={file._destroy ? 'hidden' : ''}
onFileRemove={handleRemoveProductFile(i)}/>
))}
<FabButton onClick={addProductFile}>{t('app.admin.store.product_form.add_product_file')}</FabButton>
</div>
<hr />
<div>
<h4>{t('app.admin.store.product_form.product_files')}</h4>
<FabAlert level="warning">
<HtmlTranslate trKey="app.admin.store.product_form.product_files_info" />
</FabAlert>
<div className="product-documents">
<div className="list">
{output.product_files_attributes.map((file, i) => (
<FormFileUpload key={i}
defaultFile={file}
id={`product_files_attributes[${i}]`}
accept="application/pdf"
register={register}
setValue={setValue}
formState={formState}
className={file._destroy ? 'hidden' : ''}
onFileRemove={handleRemoveProductFile(i)}/>
))}
</div>
<FabButton
onClick={addProductFile}
className='is-info'
icon={<Plus size={24} />}>
{t('app.admin.store.product_form.add_product_file')}
</FabButton>
</div>
</div>
<div className="main-actions">
<FabButton type="submit" className="main-action-btn">{t('app.admin.store.product_form.save')}</FabButton>
</div>

View File

@ -1,6 +1,7 @@
import React from 'react';
import { FabButton } from '../base/fab-button';
import { Product } from '../../models/product';
import { PencilSimple, Trash } from 'phosphor-react';
interface ProductsListProps {
products: Array<Product>,
@ -42,10 +43,10 @@ export const ProductsList: React.FC<ProductsListProps> = ({ products, onEdit, on
<div className='actions'>
<div className='manage'>
<FabButton className='edit-btn' onClick={editProduct(product)}>
<i className='fas fa-pen' />
<PencilSimple size={20} weight="fill" />
</FabButton>
<FabButton className='delete-btn' onClick={deleteProduct(product.id)}>
<i className='fa fa-trash' />
<Trash size={20} weight="fill" />
</FabButton>
</div>
</div>

View File

@ -1,23 +1,23 @@
.fab-button {
color: black;
background-color: #fbfbfb;
display: inline-block;
height: 38px;
margin-bottom: 0;
font-weight: normal;
text-align: center;
white-space: nowrap;
vertical-align: middle;
touch-action: manipulation;
cursor: pointer;
background-image: none;
border: 1px solid #c9c9c9;
padding: 6px 12px;
display: inline-flex;
align-items: center;
border: 1px solid #c9c9c9;
border-radius: 4px;
background-color: #fbfbfb;
background-image: none;
font-size: 16px;
line-height: 1.5;
border-radius: 4px;
user-select: none;
text-align: center;
font-weight: normal;
text-decoration: none;
height: 38px;
color: black;
white-space: nowrap;
touch-action: manipulation;
cursor: pointer;
user-select: none;
&:hover {
background-color: #f2f2f2;
@ -45,32 +45,31 @@
&--icon {
margin-right: 0.5em;
display: flex;
}
&--icon-only {
display: flex;
}
// color variants
&.is-info {
border-color: var(--information);
background-color: var(--information);
color: var(--gray-soft-lightest);
@mixin colorVariant($color, $textColor) {
border-color: $color;
background-color: $color;
color: $textColor;
&:hover {
border-color: var(--information);
background-color: var(--information);
color: var(--gray-soft-lightest);
border-color: $color;
background-color: $color;
color: $textColor;
opacity: 0.75;
}
}
&.is-info {
@include colorVariant(var(--information), var(--gray-soft-lightest));
}
&.is-black {
border-color: var(--gray-hard-darkest);
background-color: var(--gray-hard-darkest);
color: var(--gray-soft-lightest);
&:hover {
border-color: var(--gray-hard-darkest);
background-color: var(--gray-hard-darkest);
color: var(--gray-soft-lightest);
opacity: 0.75;
}
@include colorVariant(var(--gray-hard-darkest), var(--gray-soft-lightest));
}
&.is-main {
@include colorVariant(var(--main), var(--gray-soft-lightest));
}
}

View File

@ -180,4 +180,15 @@
margin-top: 0.4rem;
color: var(--warning);
}
input[type='file'] {
opacity: 0;
width: 0;
height: 0;
margin: 0;
padding: 0;
}
.file-placeholder {
border: none;
}
}

View File

@ -1,4 +1,30 @@
.fileinput {
.form-file-upload {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.6rem;
border: 1px solid var(--gray-soft-dark);
border-radius: var(--border-radius);
background-color: var(--gray-soft-lightest);
.actions {
margin-left: auto;
display: flex;
align-items: center;
& > *:not(:first-child) {
margin-left: 1rem;
}
a {
display: flex;
}
.image-file-input {
margin-bottom: 0;
}
}
}
.nope-fileinput {
display: table;
border-collapse: separate;
position: relative;

View File

@ -1,48 +1,41 @@
.form-image-upload {
&--small {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.6rem;
border: 1px solid var(--gray-soft-dark);
border-radius: var(--border-radius);
background-color: var(--gray-soft-lightest);
}
&--large {}
.image {
background-color: #fff;
border: 1px solid var(--gray-soft);
padding: 4px;
display: inline-block;
&--small img {
width: 50px;
height: 50px;
}
&--large img {
width: 180px;
height: 180px;
}
}
.buttons {
flex-shrink: 0;
display: flex;
justify-content: center;
margin-top: 20px;
.select-button {
position: relative;
.image-file-input {
position: absolute;
z-index: 2;
opacity: 0;
top: 0;
left: 0;
}
object-fit: cover;
border-radius: var(--border-radius-sm);
overflow: hidden;
&--small {
width: 8rem;
height: 8rem;
}
.delete-image {
background-color: var(--error);
color: white;
&--large {}
img {
width: 100%;
}
}
&--large {
margin: 80px 40px;
}
.actions {
display: flex;
align-items: center;
& > *:not(:first-child) {
margin-left: 1rem;
}
&--small {
text-align: center;
.image-file-input {
margin: 0;
}
}
}

View File

@ -1,4 +1,62 @@
.product-images {
display: flex;
flex-wrap: wrap;
}
.product-form {
h4 {
margin: 0 0 2.4rem;
@include title-base;
}
hr {
margin: 4.8rem 0;
}
.flex {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 0 3.2rem;
& > * {
flex: 1 1 320px;
}
}
.layout {
@media (max-width: 1023px) {
.span-3,
.span-7 {
flex-basis: 50%;
}
}
@media (max-width: 767px) {
flex-wrap: wrap;
}
}
.price-data {
.layout {
align-items: center;
}
}
.product-images {
display: flex;
flex-direction: column;
.list {
margin-bottom: 2.4rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 2.4rem;
}
button { margin-left: auto; }
}
.product-documents {
display: flex;
flex-direction: column;
.list {
margin-bottom: 2.4rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(410px, 1fr));
gap: 2.4rem;
}
button { margin-left: auto; }
}
}

View File

@ -181,34 +181,4 @@
max-width: 1300px;
padding-right: 1.6rem;
padding-left: 1.6rem;
.product-form {
.flex {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 0 3.2rem;
& > * {
flex: 1 1 320px;
}
}
.layout {
@media (max-width: 1023px) {
.span-3,
.span-7 {
flex-basis: 50%;
}
}
@media (max-width: 767px) {
flex-wrap: wrap;
}
}
.price-data {
.layout {
align-items: center;
}
}
}
}

View File

@ -100,7 +100,7 @@ en:
delete_this_and_next: "This slot and the following"
delete_all: "All slots"
event_in_the_past: "Create a slot in the past"
confirm_create_event_in_the_past: "You are about to create a slot in the past. Are you sure you want to do this? Members will not be able to book this slot."
confirm_create_event_in_the_past: "You are about to create a slot in the past. Are you sure you want to do this? Members on the store? will not be able to book this slot."
edit_event: "Edit the event"
view_reservations: "View reservations"
legend: "Legend"
@ -1944,6 +1944,7 @@ en:
slug: "Name of URL"
is_show_in_store: "Available in the store"
is_active_price: "Activate the price"
active_price_info: "Is this product visible by the members on the store?"
price_and_rule_of_selling_product: "Price and rule for selling the product"
price: "Price of product"
quantity_min: "Minimum number of items for the shopping cart"