Skip to content

Commit

Permalink
Add url query params and search functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
sebellows committed May 1, 2020
1 parent cd6c116 commit ab6aaa6
Show file tree
Hide file tree
Showing 10 changed files with 473 additions and 309 deletions.
13 changes: 8 additions & 5 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ module.exports = {
node: true,
},

extends: ['plugin:vue/essential', 'eslint:recommended', '@vue/prettier'],

parserOptions: {
parser: 'babel-eslint',
},

rules: {
'no-console': 'off',
'no-debugger': 'off',
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'vue/no-unused-vars': 'warn',
},

Expand All @@ -26,5 +24,10 @@ module.exports = {
},
],

extends: ['plugin:vue/essential', 'eslint:recommended', '@vue/prettier'],
extends: [
'plugin:vue/essential',
'eslint:recommended',
'@vue/prettier',
'plugin:prettier/recommended',
],
};
23 changes: 11 additions & 12 deletions server/middleware/photos-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,23 @@ function chunk(arr, size) {

module.exports = function filterPhotos(
{ photos: data },
{ pageNumber = 1, limit = 8, ownername, title },
{ pageNumber = 1, limit = 8, ...queries },
) {
let records = data.photo;
let results = data.photo;

const intLimit = parseInt(limit, 10);
const intOffset = parseInt(pageNumber - 1, 10);
const queryKeys = ['description', 'ownername', 'title'];

if (ownername) {
const ownerRE = new RegExp(ownername.toLowerCase(), 'i');
records = records.filter((photo) => ownerRE.test(photo.ownername));
for (const query in queries) {
if (queryKeys.includes(query)) {
const queryRE = new RegExp(queries[query].toLowerCase(), 'i');
results = results.filter((photo) => queryRE.test(photo[query]));
}
}

if (title) {
const titleRE = new RegExp(title.toLowerCase(), 'i');
records = records.filter((photo) => titleRE.test(photo.title));
}

const photos = chunk(records, intLimit)[intOffset];
const pages = chunk(results, intLimit) || [];
const photos = chunk(results, intLimit)[intOffset];

return { totalPhotos: records.length, photos };
return { totalPhotos: results.length, pages: pages.length, photos };
};
238 changes: 3 additions & 235 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,243 +1,11 @@
<template>
<div id="app" class="container">
<icon-sprite />
<header class="text-center py-3">
<h1>Image Gallery</h1>
<form novalidate>
<div class="mx-auto mb-3" style="max-width: 400px;">
<label for="image-filter">Search</label>
<Autocomplete :items="records" itemKey="name" :value="query">
<div class="autocomplete mb-3" slot-scope="{ results, listeners: { keydown, blur } }">
<div class="input-group">
<mico-input
v-model="query"
class="autocomplete-input"
type="search"
placeholder="Search..."
name="image-filter"
:value="query"
autocomplete="off"
novalidate
/>
<div class="input-group-append">
<span class="input-group-text">
<mico-icon name="search" />
</span>
</div>
</div>
<ul v-show="results && results.length" class="autocomplete-menu">
<li v-for="result in results" :key="result.name">
<a href="#">{{ result.name }}</a>
</li>
</ul>
</div>
</Autocomplete>
<small>Displaying {{ imageCount }} of {{ totalImages }} images</small>
</div>
</form>
</header>

<gallery v-if="images.length">
<gallery-thumbnail
v-for="image in images"
:key="image.id"
:id="image.id"
href="#"
:src="image.src"
:height="image.height"
:width="image.width"
:caption="image.title"
@click="editImage(image)"
></gallery-thumbnail>
</gallery>

<mico-pagination :totalItems="totalImages" :perPage="imageCount" @updated="updateGallery" />

<mico-modal ref="modal" :open="openDialog" @close="cancelUpdate">
<template slot="modal-title" v-if="selectedRecord">{{ selectedRecord.title }}</template>
<image-form
v-if="selectedImage"
ref="image-form-dialog"
:image="selectedImage"
@cancel="cancelUpdate"
@submit="updateImage"
/>
</mico-modal>
<div id="app">
<router-view></router-view>
</div>
</template>

<script>
/* eslint-disable no-unused-vars */
import IconSprite from '@/views/partials/IconSprite';
import Autocomplete from '@/controllers/Autocomplete.js';
import ImageForm from '@/containers/ImageFormContainer';
import Gallery from '@/components/Gallery';
import GalleryThumbnail from '@/components/GalleryThumbnail';
import MicoIcon from '@/components/Icon';
import MicoInput from '@/components/TextInput';
import MicoModal from '@/components/Modal';
import MicoPagination from '@/components/Pagination';
import { clone, isDefined, shallowCompare } from '@/shared/utils/common';
export function imageModelFactory(data = {}) {
const {
id = '',
title = '',
ispublic = false,
description: descriptionContent = '',
ownername = '',
url_q_cdn: src = '',
width_q: width = '',
height_q: height = '',
...params
} = data;
const description = descriptionContent._content ?? '';
const dimensionsRE = /^width/g;
const dimensions = Object.entries(params)
.filter(([k, v]) => dimensionsRE.test(k))
.sort(([k, v], [k2, v2]) => v - v2)
.map(([k, v]) => {
const suffix = k.split('_')[1];
const h = params[`height_${suffix}`];
return `${v} × ${h}`;
})
.join(', ');
return {
id,
title,
ispublic,
description,
dimensions,
ownername,
src,
width,
height,
};
}
export default {
name: 'Home',
components: {
Autocomplete,
Gallery,
GalleryThumbnail,
IconSprite,
ImageForm,
MicoIcon,
MicoInput,
MicoModal,
MicoPagination,
},
data() {
return {
// Gallery
isLoading: false,
// A store for previously paginated pages
store: new Map(),
// Autocomplete, Gallery, & Pagination
imageCount: 8, // Number of images to display in gallery
totalImages: 100, // Total number of images handled by API
// Autocomplete
query: '', // Sent to API to fetch matching records
records: [], // All matching records in the API
results: [], // filtered records returned by API
// Pagination
pages: 13,
currentPage: 1,
// Gallery
images: [], // The current page's displayed images from API
isFiltered: false, // Are the images from filtered results from API
selectedImage: null, // The image data being currently edited
openDialog: false, // Has an image been clicked to have it's data edited
selectedRecord: null, // Store a clone of image data currently being edited
};
},
created() {
this.fetchResults();
},
mounted() {
this.$root.$on('close', this.cancelUpdate);
},
watch: {
currentPage: {
handler(currentPage, prevPage) {
if (currentPage !== prevPage) {
this.fetchResults();
}
},
// deep: true,
},
},
methods: {
async fetchResults() {
return fetch(`/api/photos?pageNumber=${this.currentPage}`)
.then((response) => {
if (!response.ok) {
throw new Error('Unable to load images.');
}
return response.json();
})
.then(({ totalPhotos, photos }) => {
this.pages = totalPhotos;
return photos.map((item) => imageModelFactory(item));
})
.then((images) => {
if (this.images.length) {
// Cache the previous images
this.store.set(this.currentPage, this.images);
}
this.images = [];
images.forEach(async (image, i) => {
const cachedImage = await this.$ls.get(`${image.id}`);
if (isDefined(cachedImage) && cachedImage instanceof Promise === false) {
this.images.push(cachedImage);
} else {
this.images.push(image);
}
});
})
.catch((err) => {
throw new Error(`Unable to complete request: ${err}`);
});
},
filterOffers(filtered) {
this.offers = filtered;
},
editImage(image) {
this.selectedImage = image;
this.selectedRecord = clone(image);
this.openDialog = true;
},
cancelUpdate() {
if (this.selectedRecord) {
this.selectedImage = clone(this.selectedRecord);
this.selectedRecord = null;
}
if (this.openDialog) this.openDialog = false;
},
updateImage(image) {
if (this.selectedRecord) {
if (!shallowCompare(this.selectedRecord, this.selectedImage)) {
this.$ls.set(this.selectedImage.id, this.selectedImage);
}
this.selectedRecord = null;
}
this.openDialog = false;
},
updateGallery($event, index) {
if (index <= this.pages && index >= 0) {
this.currentPage = index;
}
},
},
name: 'app',
};
</script>
4 changes: 3 additions & 1 deletion src/assets/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
--ease-in-duration: 0.3s;
--ease-out-duration: 0.4s;
--ease-in-out-duration: 0.5s;
--ease-in-out-timing-function: cubic-bezier(0.35, 0, 0.25, 1);
--fast-out-slow-in-timing: cubic-bezier(0.4, 0, 0.2, 1);
--linear-out-slow-in-timing: cubic-bezier(0, 0, 0.2, 0.1);
--fast-out-linear-in-timing: cubic-bezier(0.4, 0, 1, 1);
Expand Down Expand Up @@ -400,7 +401,8 @@ legend {
width: 100%;
padding: 0;
margin-bottom: var(--spacer-2);
font-size: 1.5rem;
font-size: 1rem;
font-weight: 700;
line-height: inherit;
color: inherit;
white-space: normal;
Expand Down
3 changes: 3 additions & 0 deletions src/assets/styles/utils.css
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,9 @@
* Sizing
*/

.w-auto {
width: auto !important;
}
.w-25 {
width: 25% !important;
}
Expand Down
Loading

0 comments on commit ab6aaa6

Please sign in to comment.