mirror of
https://github.com/davegallant/rfd-fyi.git
synced 2025-10-03 15:16:00 +00:00
Migrate to vuetify and vite (#251)
This commit is contained in:
187
src/App.vue
187
src/App.vue
@@ -5,6 +5,7 @@ import Loading from "vue-loading-overlay";
|
||||
import { install } from "@github/hotkey";
|
||||
|
||||
import "vue-loading-overlay/dist/css/index.css";
|
||||
import { ref } from "vue";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -43,43 +44,11 @@ export default {
|
||||
.then((response) => {
|
||||
this.topics = response.data;
|
||||
this.isLoading = false;
|
||||
this.sortTable(this.sortColumn, false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err.response);
|
||||
});
|
||||
},
|
||||
sortTable: function sortTable(col, flipAscending) {
|
||||
if (this.sortColumn === col && flipAscending) {
|
||||
this.ascending = !this.ascending;
|
||||
}
|
||||
|
||||
var ascending = this.ascending;
|
||||
|
||||
localStorage.setItem("ascending", this.ascending);
|
||||
localStorage.setItem("sortColumn", col);
|
||||
this.sortColumn = col;
|
||||
|
||||
this.topics.sort(function (a, b) {
|
||||
if (a[col] > b[col]) {
|
||||
return ascending ? -1 : 1;
|
||||
} else if (a[col] < b[col]) {
|
||||
return ascending ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
isMobile() {
|
||||
if (
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
formatDate() {
|
||||
@@ -87,14 +56,6 @@ export default {
|
||||
return moment(String(v)).format("hh:mm A z (MM/DD)");
|
||||
};
|
||||
},
|
||||
columns() {
|
||||
return {
|
||||
Deal: "deal",
|
||||
Score: "score",
|
||||
Views: "total_views",
|
||||
"Last Reply": "last_post_time",
|
||||
};
|
||||
},
|
||||
filteredTopics() {
|
||||
return this.topics.filter((row) => {
|
||||
const titles = (
|
||||
@@ -117,11 +78,6 @@ export default {
|
||||
return v.replace(re, (matchedText) => `<mark>${matchedText}</mark>`);
|
||||
};
|
||||
},
|
||||
showBeforeTargetDate() {
|
||||
const now = new Date();
|
||||
const target = new Date('2025-08-20T00:00:00');
|
||||
return now < target;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Loading,
|
||||
@@ -129,92 +85,80 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<body>
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
id="filter"
|
||||
placeholder="Filter"
|
||||
<script setup>
|
||||
const headers = [
|
||||
{ title: "Deal", value: "title", align: "center" },
|
||||
{ title: "Score", value: "score", align: "center", sortable: true },
|
||||
{ title: "Views", value: "total_views", align: "center", sortable: true },
|
||||
{
|
||||
title: "Last Post",
|
||||
value: "last_post_time",
|
||||
align: "center",
|
||||
sortable: true,
|
||||
},
|
||||
];
|
||||
const sortBy = ref([{ key: "score", order: "desc" }]); // Vuetify 3 format
|
||||
</script>
|
||||
|
||||
data-hotkey="/"
|
||||
v-model="filter"
|
||||
v-on:keyup.enter="createFilterRoute(this.filter.toString())"
|
||||
v-on:keyup.escape="this.$refs.filter.blur()"
|
||||
ref="filter"
|
||||
/>
|
||||
<table class="table table-hover">
|
||||
<thead class="thead text-muted">
|
||||
<tr>
|
||||
<th
|
||||
v-for="(col, key) in columns"
|
||||
v-on:click="sortTable(col, true)"
|
||||
:key="col"
|
||||
>
|
||||
{{ key }}
|
||||
<div
|
||||
class="arrow"
|
||||
v-if="col == sortColumn"
|
||||
v-bind:class="ascending ? 'arrow_up' : 'arrow_down'"
|
||||
></div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<loading
|
||||
v-model:active="isLoading"
|
||||
color="#ccc"
|
||||
opacity="0"
|
||||
loader="bars"
|
||||
:is-full-page="false"
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main>
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<body>
|
||||
<v-text-field
|
||||
v-model="filter"
|
||||
label="Filter"
|
||||
density="comfortable"
|
||||
ref="filter"
|
||||
@keyup.enter="createFilterRoute(filter.toString())"
|
||||
@keyup.esc="$refs.filter.blur()"
|
||||
data-hotkey="/"
|
||||
hide-details
|
||||
/>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="filteredTopics"
|
||||
:sort-by="sortColumn"
|
||||
:sort-desc="!ascending"
|
||||
v-model:sortBy="sortBy"
|
||||
>
|
||||
</loading>
|
||||
<tr
|
||||
scope="row"
|
||||
v-for="(topic, index) in filteredTopics"
|
||||
:key="`topic.topic_id-${index}`"
|
||||
>
|
||||
<td scope="col">
|
||||
<template #item.title="{ item }">
|
||||
<a
|
||||
:href="`https://forums.redflagdeals.com${topic.web_path}`"
|
||||
:href="`https://forums.redflagdeals.com${item.web_path}`"
|
||||
target="_blank"
|
||||
v-html="
|
||||
highlightMatches(
|
||||
topic.title + ' [' + topic.Offer.dealer_name + '] '
|
||||
item.title + ' [' + item.Offer.dealer_name + '] '
|
||||
)
|
||||
"
|
||||
></a>
|
||||
<a
|
||||
:href="`${topic.Offer.url}`"
|
||||
target="_blank"
|
||||
v-if="topic.Offer.url"
|
||||
><span class="material-symbols-outlined"> link </span></a
|
||||
<a :href="item.Offer.url" target="_blank" v-if="item.Offer.url">
|
||||
<span class="material-symbols-outlined"> link </span>
|
||||
</a>
|
||||
<span v-else class="material-symbols-outlined"> link_off </span>
|
||||
</template>
|
||||
|
||||
<template #item.score="{ item }">
|
||||
<span v-if="item.score > 0" class="green-score"
|
||||
>+{{ item.score }}</span
|
||||
>
|
||||
<span v-if="!topic.Offer.url" class="material-symbols-outlined">
|
||||
link_off
|
||||
</span>
|
||||
</td>
|
||||
<td v-if="topic.score > 0" scope="col" class="green-score">
|
||||
+{{ topic.score }}
|
||||
</td>
|
||||
<td v-if="topic.score < 0" scope="col" class="red-score">
|
||||
{{ topic.score }}
|
||||
</td>
|
||||
<td v-if="topic.score == 0" scope="col">
|
||||
{{ topic.score }}
|
||||
</td>
|
||||
<td scope="col">{{ topic.total_views }}</td>
|
||||
<td scope="col">{{ formatDate(topic.last_post_time) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="showBeforeTargetDate">
|
||||
<footer class="fixed-bottom">
|
||||
PSA: <a href="https://rfd.fyi">rfd.fyi</a> will not be renewed after 2025-08-20. Please use <a href="https://rfd.davegallant.ca">rfd.davegallant.ca</a>.
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
<span v-else-if="item.score < 0" class="red-score">{{
|
||||
item.score
|
||||
}}</span>
|
||||
<span v-else>{{ item.score }}</span>
|
||||
</template>
|
||||
|
||||
<template #item.last_post_time="{ item }">
|
||||
{{ formatDate(item.last_post_time) }}
|
||||
</template>
|
||||
|
||||
<template #loading>
|
||||
<v-progress-linear indeterminate color="grey" />
|
||||
</template>
|
||||
</v-data-table>
|
||||
</body>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@@ -230,5 +174,4 @@ export default {
|
||||
background: #ffc;
|
||||
color: black;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -2,11 +2,10 @@ import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "bootstrap/dist/js/bootstrap.min.js";
|
||||
|
||||
import "./theme.css";
|
||||
|
||||
import { registerPlugins } from "@/plugins";
|
||||
|
||||
const routes = [];
|
||||
|
||||
const router = createRouter({
|
||||
@@ -16,5 +15,7 @@ const router = createRouter({
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
registerPlugins(app);
|
||||
|
||||
app.use(router);
|
||||
app.mount("#app");
|
||||
|
3
src/plugins/README.md
Normal file
3
src/plugins/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Plugins
|
||||
|
||||
Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally.
|
12
src/plugins/index.js
Normal file
12
src/plugins/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* plugins/index.js
|
||||
*
|
||||
* Automatically included in `./src/main.js`
|
||||
*/
|
||||
|
||||
// Plugins
|
||||
import vuetify from './vuetify'
|
||||
|
||||
export function registerPlugins (app) {
|
||||
app.use(vuetify)
|
||||
}
|
19
src/plugins/vuetify.js
Normal file
19
src/plugins/vuetify.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* plugins/vuetify.js
|
||||
*
|
||||
* Framework documentation: https://vuetifyjs.com`
|
||||
*/
|
||||
|
||||
// Styles
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import 'vuetify/styles'
|
||||
|
||||
// Composables
|
||||
import { createVuetify } from 'vuetify'
|
||||
|
||||
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||
export default createVuetify({
|
||||
theme: {
|
||||
defaultTheme: 'system',
|
||||
},
|
||||
})
|
@@ -2,11 +2,6 @@ body {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
min-width: 100%;
|
||||
@@ -47,22 +42,3 @@ footer {
|
||||
.red-score {
|
||||
color: rgb(175, 21, 21) !important;
|
||||
}
|
||||
|
||||
.arrow_down {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB8AAAAaCAYAAABPY4eKAAAAAXNSR0IArs4c6QAAAvlJREFUSA29Vk1PGlEUHQaiiewslpUJiyYs2yb9AyRuJGm7c0VJoFXSX9A0sSZN04ULF12YEBQDhMCuSZOm1FhTiLY2Rky0QPlQBLRUsICoIN/0PCsGyox26NC3eTNn3r3n3TvnvvsE1PkwGo3yUqkkEQqFgw2Mz7lWqwng7ztN06mxsTEv8U0Aam5u7r5EInkplUol/f391wAJCc7nEAgE9Uwmkzo4OPiJMa1Wq6cFs7Ozt0H6RqlUDmJXfPIx+qrX69Ti4mIyHA5r6Wq1egND+j+IyW6QAUoul18XiUTDNHaSyGazKcZtdgk8wqhUKh9o/OMvsVgsfHJy0iWqVrcQNRUMBnd6enqc9MjISAmRP3e73T9al3XnbWNjIw2+KY1Gc3imsNHR0YV4PP5+d3e32h3K316TySQFoX2WyWR2glzIO5fLTSD6IElLNwbqnFpbWyO/96lCoai0cZjN5kfYQAYi5H34fL6cxWIZbya9iJyAhULBHAqFVlMpfsV/fHxMeb3er+Vy+VUzeduzwWC45XA4dlD/vEXvdDrj8DvURsYEWK3WF4FA4JQP9mg0WrHZbEYmnpa0NxYgPVObm5teiLABdTQT8a6vrwdRWhOcHMzMzCiXlpb2/yV6qDttMpkeshEzRk4Wo/bfoe4X9vb2amzGl+HoXNT29vZqsVi0sK1jJScG+Xx+HGkL4Tew2TPi5zUdQQt9otPpuBk3e0TaHmMDh1zS7/f780S0zX6Yni+NnBj09fUZUfvudDrNZN+GkQbl8Xi8RLRtHzsB9Hr9nfn5+SjSeWUCXC7XPq5kw53wsNogjZNohYXL2EljstvtrAL70/mVaW8Y4OidRO1/gwgbUMvcqGmcDc9aPvD1gnTeQ+0nmaInokRj0nHh+uvIiVOtVvt2a2vLv7Ky0tL3cRTXIcpPAwMDpq6R4/JXE4vFQ5FI5CN+QTaRSFCYc8vLy1l0rge4ARe5kJ/d27kYkLXoy2Jo4C7K8CZOsEBvb+9rlUp1xNXPL7v3IDwxvPD6AAAAAElFTkSuQmCC");
|
||||
}
|
||||
.arrow_up {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAaCAYAAACgoey0AAAAAXNSR0IArs4c6QAAAwpJREFUSA21Vt1PUmEYP4dvkQ8JFMwtBRocWAkDbiqXrUWXzU1rrTt0bdVqXbb1tbW16C9IBUSmm27cODdneoXjputa6069qwuW6IIBIdLvdaF4OAcOiGeDc87zPs/vd57P96WpFq7p6enbGo1mjKZpeTabjU1MTCRagGnOZHFxcXxtbe1XKpUq7+zslJeXl//Mz8+Hy+Uy3RxSE9qTk5M3otFooVQqgef4Wl9f343FYoEmoISrxuNxFX5f9vb2jhn/PxUKhfLS0tIPfFifUESRUMV8Pv/M6XReRm5rTGQyGeXxeGxYe1ezeBpBOBx2rKysbO7v79d4Wy3Y2Nj4GQqFbgnhaugxwiuGJx99Pp9FLBbXxYTXvTqd7v3MzIy6riIWGxJnMpl7AwMD14xGYyMsSq1WUyQdUqn0eSPlusQIsbGrq+vl4OCgvhFQZd1utyv1en0gEolcqsi47nWJlUrlG5fLZVcoFFy2nDKSDpIWlUoVTCQSEk4lCHmJMZ2GTCbTiMVikfIZ88l7enoos9l8dXt7+z6fDicxSJUokqDX6xXcl2wCROoc0vQCWL3sNfLOSdzR0fHY4XC4tVotl40gmVwup9xuN4OQv+UyqCFGH9rg7SOGYVRcBs3IEG4J0nVnamrqOtvuBDGGgQg9+wHFcVEi4a0LNkbdd6TrPKo8ODc311mteIIYjT/a398/jK+s1jnVM0kXoufCFvq0GuiIGEVgQIhfoygM1QrteEa9dAL7ITiYCt4RMabOK5AyKKzKWtvupLcRciu8D5J0EuDDPyT/Snd39yh6VtY2NhYQSR9G79Ds7OxdskRjEyAufvb7/cPoO5Z6e1+xtVKrq6vfcFzyi/A3ZrPZ3GdNSlwgo5ekE4X2RIQGf2C1WlufFE0GBeGWYQ8YERWLxQtnUVB830MKLZfL9RHir8lkssCn2G751tZWEWe03zTKm15YWPiEiXXTYDB0Ig/t7yd8PRws4EicwWHxO4jHD8/C5HiTTqd1BwcHFozKU89origB+y/kmzgYpgOBQP4fGmUiZmJ+WNgAAAAASUVORK5CYII=");
|
||||
}
|
||||
.arrow {
|
||||
float: right;
|
||||
width: 10px;
|
||||
height: 12px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
background-position-y: bottom;
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: medium;
|
||||
}
|
||||
|
Reference in New Issue
Block a user