Skip to content

Commit

Permalink
Simplified and improved uris for categories and model details
Browse files Browse the repository at this point in the history
Improved vue component names
Extracted model filter to separate component and integrate with router /uri changes & deep linking/
  • Loading branch information
lwitkowski committed Aug 2, 2024
1 parent 4f35a4c commit f864831
Show file tree
Hide file tree
Showing 10 changed files with 240 additions and 218 deletions.
25 changes: 9 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,16 @@ This project aims at reviving [aero-offers.com](aero-offers.com) - invaluable so
Currently, the project is being onboarded to Azure Cloud (still WIP).

### TODO
- [x] deploy working ui, api and db to Azure
- [x] fix segelflug spider/crawler
- [x] good enough DB setup (cheap VM)
- [x] use Azure secrets for db credentials
- [x] setup cron triggers for crawlers, reclassifier and FX rates updater
- [x] human readable domain (aero-offers.pl)
- [x] fix aircraft type dropdown
- [ ] database daily backups
- [ ] infra as code (biceps or terraform)
- [ ] UI: consent banner for GA
- [ ] Infra: db daily backups
- [ ] Infra: infra as code (biceps or terraform)
- [ ] document infra and env topology
- [ ] fix other spiders/crawlers
- [ ] redirect from aero-offers.com
- [ ] fix & polish CSS in UI
- [ ] update/simplify legal subpage
- [ ] cookies info
- [ ] use https://github.com/weglide/GliderList
- [ ] admin panel for manual (re) classification (or community-based)
- [ ] Backend: fix and enable other spiders/crawlers
- [ ] UI: aero-offers.com
- [ ] Improve aircraft types structure and introduce 2 levels: glider (e.g Discus 2c 18m) and model (Discus 2cFES 18m) as prices between models sometimes differ significantly
- [ ] UI: fix & polish CSS in UI
- [ ] utilize https://github.com/weglide/GliderList as source of truth for glider types/models
- [ ] UI: admin panel for manual (re) classification (or community-based)
- [ ] crawler for Facebook Marketplace - do they have nice api?
- [ ] crawler for https://www.aircraft24.de
- [ ] crawler for http://www.airplanemart.com
Expand Down
5 changes: 1 addition & 4 deletions ui/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ export default [
eslintPluginPrettierRecommended,
{
files: ['**/*.vue', '**/*..js', '**/*..jsx', '**/*..cjs', '**/*..mjs'],
ignores: ['.gitignore'],
rules: {
'vue/multi-word-component-names': 'warn'
}
ignores: ['.gitignore']
}
]
19 changes: 16 additions & 3 deletions ui/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<router-link to="/">All Offers</router-link>
</div>
<div class="nav-element">
<router-link to="/gliders">Gliders</router-link>
<router-link to="/glider">Gliders</router-link>
</div>
<div class="nav-element">
<router-link to="/tmg">TMG</router-link>
Expand All @@ -21,6 +21,10 @@
<router-link to="/airplane">Airplanes</router-link>
</div>
</div>

<div id="aircraft-type-filter">
<AircraftModelFilter />
</div>
</div>
<div id="body">
<router-view :key="$route.path" />
Expand All @@ -35,8 +39,12 @@
/*global __COMMIT_HASH__*/
/*global __BUILD_TIMESTAMP__*/
import AircraftModelFilter from './components/AircraftModelFilter.vue'
export default {
components: {},
components: {
AircraftModelFilter
},
data() {
return {
buildInfo: 'Build: ' + __COMMIT_HASH__ + ', ' + __BUILD_TIMESTAMP__
Expand Down Expand Up @@ -150,7 +158,7 @@ export default {
color: #ffffff;
}
#nav a.router-link-exact-active {
#nav a.router-link-active {
color: #f71735;
text-decoration: none;
}
Expand Down Expand Up @@ -229,4 +237,9 @@ export default {
.tooltip:hover .tooltiptext {
visibility: visible;
}
#aircraft-type-filter {
padding: 5px;
width: 500px;
}
</style>
123 changes: 123 additions & 0 deletions ui/src/components/AircraftModelFilter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<template>
<multiselect
v-model="selected"
:options="available_aircraft_types"
group-values="models"
group-label="manufacturer"
:group-select="false"
label="model"
:placeholder="'Search ' + (aircraft_type || 'aircraft')"
:max-height="500"
>
<template #noResult>
<span v-if="aircraft_type">
{{ aircraft_type.charAt(0).toUpperCase() + aircraft_type.slice(1) }} not found. Try different phrase or
category.
</span>
<span v-else>Aircraft model not found. Try different phrase.</span>
</template>
</multiselect>
</template>

<script>
import axios from 'axios'
import Multiselect from 'vue-multiselect'
export default {
name: 'AircraftModelFilter',
components: {
Multiselect
},
data() {
return {
aircraft_type: null,
all_aircraft_types: [],
available_aircraft_types: [],
selected: null
}
},
watch: {
$route() {
const pathSegments = this.$route.path.split('/')
switch (pathSegments[1]) {
case 'glider':
case 'tmg':
case 'ultralight':
case 'airplane':
this.aircraft_type = pathSegments[1]
this.selected = null
this.updateAircraftTypes()
if (pathSegments.length == 4) {
this.selected = {
manufacturer: decodeURI(pathSegments[2]),
model: decodeURI(pathSegments[3])
}
} else {
this.selected = null
}
break
default:
this.aircraft_type = null
this.selected = null
}
},
selected(val) {
if (val == null) {
return
}
this.$router.push({
name: 'offer_details',
params: { aircraftType: val.aircraft_type, manufacturer: val.manufacturer, model: val.model }
})
}
},
created() {
this.loadAircraftTypes()
},
methods: {
loadAircraftTypes() {
axios.get(`/models`).then((response) => {
this.all_aircraft_types = response.data
this.updateAircraftTypes()
})
},
updateAircraftTypes() {
const new_available_aircraft_types = []
for (const manufacturer in this.all_aircraft_types) {
const modelsToDisplay = []
const modelsByAircraftType = this.all_aircraft_types[manufacturer].models
for (const type in modelsByAircraftType) {
if (this.aircraft_type == type || this.aircraft_type == null) {
for (const model in modelsByAircraftType[type]) {
modelsToDisplay.push({
aircraft_type: type,
manufacturer: manufacturer,
model: modelsByAircraftType[type][model]
})
}
}
}
new_available_aircraft_types.push({
manufacturer: manufacturer,
models: modelsToDisplay
})
}
this.available_aircraft_types = new_available_aircraft_types
}
}
}
</script>

<style lang="scss">
@import 'vue-multiselect/dist/vue-multiselect.css';
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
<p v-if="offer.manufacturer">
<router-link
:to="{
name: 'ModelInformation',
params: { manufacturer: offer.manufacturer, model: offer.model }
name: 'offer_details',
params: { aircraftType: offer.aircraft_type, manufacturer: offer.manufacturer, model: offer.model }
}"
>
<a>Model: {{ offer.manufacturer }} {{ offer.model }}</a>
Expand All @@ -34,7 +34,7 @@
<div class="icon">
<small>
<a :href="offer.url" target="_blank">
<img :src="'url_icon.png'" alt="Link to Offer" height="30" width="30" />
<img :src="'/url_icon.png'" alt="Link to Offer" height="30" width="30" />
</a>
</small>
</div>
Expand All @@ -43,7 +43,7 @@

<script>
export default {
name: 'OfferComponent',
name: 'OfferThumb',
props: {
offer: {
type: Object,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, it, expect } from 'vitest'

import { mount } from '@vue/test-utils'
import OfferComponent from '../OfferComponent.vue'
import OfferThumb from '../OfferThumb.vue'

describe('OfferComponent', () => {
describe('OfferThumb', () => {
it('renders properly', () => {
const offer = {
aircraft_typ: 'glider',
Expand All @@ -23,7 +23,7 @@ describe('OfferComponent', () => {
url: 'https://www.segelflug.de/osclass/index.php?page=item&id=42370'
}

const wrapper = mount(OfferComponent, { props: { offer: offer } })
const wrapper = mount(OfferThumb, { props: { offer: offer } })

expect(wrapper.text()).contains('Rolladen Schneider LS4').contains('2020-02-26')
})
Expand Down
83 changes: 22 additions & 61 deletions ui/src/router/index.js
Original file line number Diff line number Diff line change
@@ -1,85 +1,46 @@
import { createRouter, createWebHistory } from 'vue-router'
import Offers from '../views/Offers.vue'
import ModelInformation from '../views/ModelInformation.vue'
import OffersList from '../views/OffersList.vue'
import OfferDetails from '../views/OfferDetails.vue'
import { nextTick } from 'vue'

const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'All Offers',
component: Offers,
name: 'all_listing',
component: OffersList,
meta: {
title: 'Aircraft Offers Overview',
description:
'Aircraft Offers from various marketplaces. Ranging from Gliders like ASK21 to Cessna and much more (Gliders, TMG, Ultralight, Airplanes)'
},
props: {}
},
{
path: '/gliders',
name: 'Glider Offers',
component: Offers,
meta: {
title: 'Aircraft Offers - Glider Offers',
description: 'Glider Offers from various marketplaces with prices in EUR.'
},
props: {
aircraftType: 'glider'
}
},
{
path: '/tmg',
name: 'Touring Motorglider Offers',
component: Offers,
meta: {
title: 'Aircraft Offers - Touring Motor Glider Offers',
description:
'Touring Motor Glider (like Super Dimona, Stemme) offers from various marketplaces with prices in EUR.'
},
props: {
aircraftType: 'tmg'
}
},
{
path: '/ultralight',
name: 'Ultralight Offers',
component: Offers,
meta: {
title: 'Aircraft Offers - Ultralights',
description: 'Ultralight (like C42) offers from various marketplaces with prices in EUR.'
},
props: {
aircraftType: 'ultralight'
}
},
{
path: '/airplane',
name: 'Airplane Offers',
component: Offers,
meta: {
title: 'Aircraft Offers - Airplanes',
description: 'Small Airplane (like Cessna) offers from various marketplaces with prices in EUR.'
},
props: {
aircraftType: 'airplane'
}
props: true
},
{
path: '/model/:manufacturer/:model',
name: 'ModelInformation',
component: ModelInformation,
props: true
path: '/:aircraftType',
name: 'category_listing',
children: [
{
path: '/:aircraftType',
component: OffersList,
props: true
},
{
path: '/:aircraftType/:manufacturer/:model',
name: 'offer_details',
component: OfferDetails,
props: true
}
]
}
]
})

router.afterEach((to) => {
nextTick(() => {
// calculate title for ModelInformation dynamically
if (to.name === 'ModelInformation') {
document.title = `Model ${to.params.manufacturer} ${to.params.model}`
if (to.name === 'offer_details') {
document.title = `${to.params.aircraftType} ${to.params.manufacturer} ${to.params.model}`
} else {
document.title = to.meta.title
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import moment from 'moment'
import ChartistTooltip from 'chartist-plugin-tooltips-updated'
export default {
name: 'ModelInformation',
name: 'OfferDetails',
components: {},
props: {
Expand Down
Loading

0 comments on commit f864831

Please sign in to comment.