Can add and remove images from items

main
Ashelyn Dawn 4 years ago
parent 5be94c3ddd
commit 3a7a6959e9

@ -8,6 +8,11 @@ router.get('/:uuid/:size', async (req, res) => {
res.end(image.file)
})
router.post('/:uuid/featured', async (req, res) => {
const item = await db.item.setFeatured(req.params.uuid)
res.json(item)
})
router.delete('/:uuid', async (req, res) => {
const item = await db.item.removeImage(req.params.uuid)
res.json(item)

@ -1,5 +1,7 @@
const router = module.exports = require('express-promise-router')()
const parseJSON = require('body-parser').json()
const bodyParser = require('body-parser')
const parseJSON = bodyParser.json()
const b64 = require('base64-async')
const db = require('../db')
const validate = require('./middleware/validators')
@ -63,7 +65,7 @@ router.post('/:uuid', parseJSON, itemValidators, async (req, res) => {
res.json(item)
})
router.post('/:uuid/images', upload.single('image'), async (req, res) => {
router.post('/:uuid/images', upload.single('image'), bodyParser.json({limit: '5MB'}), async (req, res) => {
// Handle either image upload body or JSON body
try {
if(req.file)

@ -2,12 +2,9 @@ import styles from './actionBar.module.css'
export default function AdminActionBar({title, children}) {
return (
<>
<div className={styles.actionBar}>
<h2>{title}</h2>
{children}
</div>
<span className={styles.spacer}/>
</>
<div className={styles.actionBar}>
<h2>{title}</h2>
{children}
</div>
)
}

@ -26,8 +26,10 @@ module.exports = [{
],
associations: [
{name: 'cart', mapId: 'cartMap', columnPrefix: 'cart_'},
{name: 'coupon', mapId: 'couponMap', columnPrefix: 'coupon_'},
{name: 'payment', mapId: 'paymentMap', columnPrefix: 'payment_'}
{name: 'coupon', mapId: 'couponMap', columnPrefix: 'coupon_'}
],
collections: [
{name: 'payments', mapId: 'paymentMap', columnPrefix: 'payment_'}
]
},{
mapId: 'addressMap',

@ -2,6 +2,7 @@ const pg = require('../pg')
const joinjs = require('join-js').default;
const debug = require('debug')('sos:db:item')
const mappings = require('../mappings')
const dbUtil = require('../util')
const sharp = require('sharp')
@ -89,7 +90,6 @@ item.update = async (uuid, name, urlslug, description, price_cents, published) =
}
item.addImage = async (item_uuid, image_buffer, uploader_uuid) => {
// Default param chain: output as png
const source = sharp(image_buffer).png()
@ -115,6 +115,14 @@ item.addImage = async (item_uuid, image_buffer, uploader_uuid) => {
return joinjs.map(rows, mappings, 'itemMap', 'item_')[0]
}
item.setFeatured = (image_uuid) =>
dbUtil.executeFunction({
name: 'set_featured_image',
params: [image_uuid],
returnType: 'item',
single: true
})
const imageSizeQueries = {
'large': 'select * from sos.get_image_large($1)',
'thumb': 'select * from sos.get_image_thumb($1)'
@ -135,7 +143,7 @@ item.getImage = async (image_uuid, size) => {
item.removeImage = async (image_uuid) => {
const query = {
text: 'select * from sos.delete_image($1)',
text: 'select * from sos.remove_image($1)',
values: [
image_uuid
]

@ -1,20 +1,22 @@
create or replace view sos.v_item as
create or replace view sos.v_image as
select
"item".item_uuid,
"item".item_name,
"item".item_description,
"item".item_urlslug,
"item".item_price_cents,
"item".item_published,
"image".image_uuid,
"image".image_item_uuid,
"image".image_featured,
"image".image_mime_type,
"image".image_date_uploaded,
"user".user_email as uploader_email,
"user".user_email as uploader_email
from sos."image"
left join sos."user" on image.image_uploader_uuid = "user".user_uuid
order by image_date_uploaded asc;
create or replace view sos.v_item as
select
"item".*,
v_image.*,
coalesce(num_added - num_removed, 0) as item_number_in_stock
from sos."item"
left join sos."image" on item.item_uuid = image.image_item_uuid
left join sos."user" on image.image_uploader_uuid = "user".user_uuid
left join sos.v_image on item.item_uuid = v_image.image_item_uuid
left join
(
select

@ -151,11 +151,34 @@ begin
return query select * from sos.v_item where item_uuid = _item_uuid;
end; $function$;
create or replace function sos.set_featured_image(_item_uuid uuid, _image_uuid uuid)
create or replace function sos.remove_image(_image_uuid uuid)
returns setof sos.v_item
language plpgsql
as $function$
declare
_item_uuid uuid;
begin
select image_item_uuid into _item_uuid
from sos."image"
where image_uuid = _image_uuid;
delete from sos."image"
where image_uuid = _image_uuid;
return query select * from sos.v_item where item_uuid = _item_uuid;
end; $function$;
create or replace function sos.set_featured_image(_image_uuid uuid)
returns setof sos.v_item
language plpgsql
as $function$
declare
_item_uuid uuid;
begin
select image_item_uuid into _item_uuid
from sos."image"
where image_uuid = _image_uuid;
-- Un-feature all other images
update sos."image" set
image_featured = false

@ -0,0 +1,111 @@
import React, {useState, useEffect, useMemo, useRef} from 'react'
import {DateTime} from 'luxon'
import axios from 'axios'
import Link from 'next/link'
import {Button} from '@rmwc/button'
import ActionBar from '~/components/admin/actionBar'
import {Icon} from '@rmwc/icon'
import styles from '~/styles/imageGallery.module.css'
EditImages.getInitialProps = async ({ctx: {axios, query: {slug}}}) => {
const {data: item} = await axios.get(`/api/items/by-slug/${slug}`)
return {item}
}
export default function EditImages({item: _item}) {
const [item, setItem] = useState(_item)
const fileRef = useRef()
// Only re-sort images whenever item changes
const images = useMemo(() => item.images.slice(0).sort(
(a,b) => DateTime.fromISO(a.date_uploaded).diff(DateTime.fromISO(b.date_uploaded))
), [item])
async function selectImage(ev){
if(ev) ev.preventDefault()
fileRef.current.click()
}
useEffect(()=>{
fileRef.current.addEventListener('change', uploadImage)
return ()=>fileRef.current.removeEventListener('change', uploadImage)
}, [])
async function uploadImage(ev){
if(ev) ev.preventDefault()
const image = await readFileAsync(ev.target)
const {data: updatedItem} = await axios.post(`/api/items/${item.uuid}/images`, {image})
setItem(updatedItem)
}
function handleSetFeatured(image){
return async (ev) => {
if(ev) ev.preventDefault();
const {data: updatedItem} = await axios.post(`/api/images/${image.uuid}/featured`)
setItem(updatedItem)
}
}
function handleRemoveImage(image){
return async (ev) => {
if(ev) ev.preventDefault()
const {data: updatedItem} = await axios.delete(`/api/images/${image.uuid}`)
setItem(updatedItem)
}
}
return (
<>
<ActionBar title={`Editing images for "${item.name}"`}>
<Button outlined onClick={selectImage}>Add Image</Button>
</ActionBar>
<Link href={`/admin/items/${item.urlslug}`}><a><Button icon="arrow_left">Back to {item.name}</Button></a></Link>
<div className={styles.gallery}>
{images.map(image => (
<div key={image.uuid} className={styles.image}>
<img key={image.uuid} src={`/api/images/${image.uuid}/thumb`}/>
<div className={styles.details}>
<div>
<span>Uploaded {DateTime.fromISO(image.date_uploaded).toFormat('DD')}</span>
<span>by {image.uploader.email}</span>
</div>
<button title="Set Featured" onClick={handleSetFeatured(image)} className="buttonLink"><Icon icon={image.featured?"star":"star_outline"}/></button>
<button title="Remove Image" onClick={handleRemoveImage(image)} className="buttonLink"><Icon icon="delete"/></button>
</div>
</div>
))}
{!images.length ? (
<>
<p style={{marginTop: '100px'}}>No images added for this item.</p>
<Button outlined onClick={selectImage}>Add One!</Button>
</>
) : (
<p><Button outlined onClick={selectImage}>Add Image</Button></p>
)}
</div>
<input ref={fileRef} className={styles.upload} type="file" accept="image/*" />
</>
)
}
function readFileAsync(fileInput){
const reader = new FileReader()
return new Promise((resolve, reject) => {
reader.addEventListener('load', () => {
resolve(reader.result)
})
reader.addEventListener('error', reject)
reader.addEventListener('abort', reject)
reader.readAsDataURL(fileInput.files[0])
})
}

@ -2,6 +2,8 @@ import React from 'react'
import router from 'next/router'
import ActionBar from '~/components/admin/actionBar'
import {Button as RMWCButton} from '@rmwc/button'
import {FormController, Input, TextArea, DecimalInput, Button, Checkbox} from '~/components/form'
EditItem.getInitialProps = async ({ctx: {axios, query: {slug}}}) => {
@ -24,7 +26,9 @@ export default function EditItem({item}) {
return (
<>
<ActionBar title={`Editing ${item.name}`}/>
<ActionBar title={`Editing "${item.name}"`}>
<RMWCButton outlined onClick={()=>router.push(`/admin/items/${item.urlslug}/images`)}>Edit Images ({item.images.length})</RMWCButton>
</ActionBar>
<FormController url={`/api/items/${item.uuid}`} afterSubmit={afterUpdate}>
<Input name="name" initialValue={item.name} validate={stringLengthAtLeastOne} />
<TextArea name="description" initialValue={item.description} validate={stringLengthAtLeastOne} />

@ -1,5 +1,5 @@
import React, {useState} from 'react'
import Router from 'next/router'
import router from 'next/router'
import Link from 'next/link'
import axios from 'axios'
@ -34,7 +34,7 @@ export default function Items({items: _items}){
return (
<>
<ActionBar title="Items">
<Button outlined onClick={()=>Router.push('/admin/items/new')}>Create New Item</Button>
<Button outlined onClick={()=>router.push('/admin/items/new')}>Create New Item</Button>
</ActionBar>
<Table
columns={[

@ -17,8 +17,9 @@ export default function CheckoutComplete({order}){
let email = null
if(latestTransaction.payment.stripe)
email = latestTransaction.payment.stripe.reciept_email
const stripePayment = latestTransaction.payments.find(p => p.stripe !== null)
if(stripePayment)
email = stripePayment.stripe.reciept_email
return (
<>

@ -0,0 +1,61 @@
.gallery {
text-align: center;
padding: 0 50px;
--padding: 8px;
}
.image {
display: inline-block;
height: 300px;
width: auto;
margin: 10px;
background: white;
padding: var(--padding);
box-shadow: 0 2px 4px -1px rgba(0,0,0,.2),0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12);
position: relative;
}
.image img {
min-width: 300px;
background: rgb(77, 51, 80);
object-fit: contain;
height: 100%;
}
.image .details {
position: absolute;
height: 100px;
left: var(--padding);
right: var(--padding);
bottom: var(--padding);
background: linear-gradient(transparent, rgba(0,0,0,.3), black);
color: white;
display: flex;
flex-direction: row;
align-items: flex-end;
padding-bottom: var(--padding);
}
.details span {
display: block;
font-size: 12px;
}
.details div {
flex: 1;
}
.details button {
color: white !important;
}
.details button:last-child {
color: rgb(255, 122, 122) !important;
padding-right: 8px;
}
.upload {
display: none;
opacity: 0;
}
Loading…
Cancel
Save