Okay, I have a sample of this concept. I wrote this app, frontend and backend within 2 weeks last year. It's a bit rough around the edges, and it is not based on NR where the changes come in through a websocket. Instead there is a python webserver hanging behind it, with an API that handles the incoming requests. These API calls are defined in services files, which are not included, but that's what the imports are referring to.
Also please note that the code is ES6 based, which means it is not immediate 1-on-1 compatible with browsers, unless the browser does in fact support all those items. I haven't checked recently, and this is used with a webpack based build step.
The first file is the index.vue
file. This is an outer item, yet still a component, that shows more or less the entire home page. There is a CMS hanging behind it, which can be accessed through the menu bar if you are logged in. That menu bar is another component and not included in the scope of this post. This index page is a bootstrap grid based on rows and columns. I won't describe what everything does, but on this grid I have a set up with rows, where each row contains at most 3 cards. These cards are a specific component that is included below as well.
<template>
<div class="row flex-xl-nowrap">
<div class="col-12 col-md-3 col-xl-2 bd-sidebar">
<h4>Tags: </h4>
<b-list-group>
<template v-for="tag in tags">
<b-list-group-item :key="tag.id" @click="applyTagFilter(tag.id)" v-if="tag.id === filter_.activeTag" active>{{ tag.tag}}</b-list-group-item>
<b-list-group-item :key="tag.id" @click="applyTagFilter(tag.id)" v-else>{{ tag.tag }}</b-list-group-item>
</template>
</b-list-group>
</div>
<div>
<h1>Fic Recs</h1>
<p v-if="statusMsg !== ''">{{ statusMsg }}</p>
<div class="fics">
<div v-for="row in chunks" class="row"> <!-- Don't be me, define a :key here -->
<fic-work :fic="fic" v-for="fic in row" :key="fic.id" />
</div>
</div>
</div>
</div>
</template>
This <fic-work>
component defines a single card, and I show below how it looks like. The template is populated with the following javascript code:
<script>
import WorkService from '@/services/WorkService';
import TagService from '@/services/TagService';
import FicWork from '@/components/FicWork';
export default {
name: 'index',
data() {
return {
fics: [],
tags: [],
statusMsg: '',
filter_: {
activeTag: '',
},
};
},
mounted() {
this.statusMsg = 'Loading... Please wait';
this.initFics();
this.initTags();
},
computed: {
chunks() {
const chunkSize = 3;
return this.fics.reduce((accumulator, currentValue, currentIndex) => {
// Based on https://stackoverflow.com/a/37826698
const outerIndex = Math.floor(currentIndex / chunkSize);
if (!accumulator[outerIndex]) {
// eslint-disable-next-line no-param-reassign
accumulator[outerIndex] = [];
}
accumulator[outerIndex].push(currentValue);
return accumulator;
}, []);
},
},
methods: {
async initFics() {
const resp = await WorkService.fetchWorks();
this.fics = resp.data.works;
this.statusMsg = '';
},
async initTags() {
const resp = await TagService.fetchTags();
this.tags = resp.data.tags;
},
applyTagFilter(id) {
this.filter_.activeTag = id;
},
},
components: {
FicWork,
},
};
</script>
The <fic-work>
component looks as follows:
<template>
<b-card class="col-sm-12 col-md-3 px-0 mx-4">
<template slot="header" v-if="fic.internals.sticky">Featured</template>
<span slot="footer">{{ fic.fandoms.map((fandom) => fandom.name).join(', ') }}</span>
<h4 class="card-title"><a :href="fic.url">{{ fic.title }}</a></h4>
<h6>By: <span v-html="authors.join(', ')"></span></h6>
<span v-if="fic.ships.length > 0">Ships: <span v-html="ships.join(', ')"></span></span><br />
<hr />
<div v-html="fic.summary"></div>
<p>
<span>Rating: {{ fic.rating }}</span> |
<span>Chapters: {{ fic.n_chapters }}</span> |
<span>Word count: {{ fic.words_readable }}</span> |
<template v-if="fic.setting !== ''">
<span>Setting: {{ fic.setting }}</span> |
</template>
<template v-if="fic.complete">
<span>Complete</span>
</template>
<template v-else>
<span>Incomplete<template v-if="fic.status !== ''"> ({{ fic.status }})</template></span>
</template>
</p>
<p>Tags: <span v-html="tags.join(', ')"></span></p>
</b-card>
</template>
As you can see from the code above, the <fic-work>
component has only a single prop, fic
, which is an object. This object has a property authors
which is an array of objects, where in turn each object has at least 2 properties: url
and name
. fic
also has other properties set up like this, such as ships
, tags
and a bunch of numeric or string based properties. These are more or less directly coming in from the API, I'll see if I can grab an example from production to include at the bottom, but sanitised... this code is already more than enough to not sanitise
Finally, the javascript for this component is defined as follows:
<script>
export default {
name: 'fic-work',
props: {
fic: {
type: Object,
},
},
computed: {
authors() {
return this.fic.authors.map(author => `<a href=${author.url}>${author.name}</a>`);
},
ships() {
return this.fic.ships.map((ship) => {
if (ship.work_display_name.other_display_names.length > 0) {
return `<b>${ship.work_display_name.primary_display_name}</b> (${ship.work_display_name.other_display_names.join(', ')})`;
}
return `<b>${ship.work_display_name.primary_display_name}</b>`;
});
},
tags() {
return this.fic.tags.map(tag => `<span>${tag.tag}</span>`);
},
},
};
</script>
I'm using computed properties here to display the values with some HTML. If I had to redo this today I would probably write this slightly different so that the HTML ends up in the template with a v-for
setup, rather than in the property. Although the ships()
property would still more or less exist like that this way. As for the design, I worked this structure out on paper first. I have 3 discarded setups for this still lying around.
That's more or less what I would give you from this: work it out on paper first, when you think you're happy with it, start typing it out. If you're still happy with it at that point, work it out towards an interface. If in the interface it feels off, go back one step and see if you can improve it there. If not, go back to the drawing board to your structure and see how you can improve it.
As for what a single fic
object looks like, here's a redacted version of one that I just pulled from the API.
{
"authors": [{
"id": "5a8317ffcd19c712212327f6",
"name": "<author's name>",
"url": "<url to a page relevant to the author>"
}],
"characters": [],
"complete": true,
"fandoms": [{
"id": "5a744d5dcd19c73541d7e9e1",
"name": "The 100"
}],
"id": "5a831859cd19c712212327f7",
"internals": {
"pos": 1,
"sticky": false
},
"n_chapters": 1,
"n_words": 9706,
"rating": "Not rated",
"setting": "Modern AU (College)",
"ships": [{
"id": "5a7f395ecd19c7fb122edaf5",
"work_display_name": {
"other_display_names": [],
"primary_display_name": "Clarke Griffin/Lexa"
}
}],
"spoilers": "",
"status": "",
"summary": "<p>A summary with HTML tags, line <br /> breaks and more.</p>",
"tags": [{
"id": "5a81ac37cd19c713d940677f",
"tag": "Fluff"
}],
"title": "<title>",
"url": "<url>",
"words_readable": "9,706"
}
And as mentioned before, that one data value fics
at the very start in index.vue
is an array of these objects.