Merge branch 'develop' into main

This commit is contained in:
Alexis
2021-03-21 11:21:40 +01:00
22 changed files with 513 additions and 78 deletions

37
package-lock.json generated
View File

@@ -1377,6 +1377,12 @@
} }
} }
}, },
"@types/uuid": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz",
"integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==",
"dev": true
},
"@types/webpack": { "@types/webpack": {
"version": "4.41.26", "version": "4.41.26",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.26.tgz", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.26.tgz",
@@ -9959,6 +9965,14 @@
"tough-cookie": "~2.5.0", "tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0", "tunnel-agent": "^0.6.0",
"uuid": "^3.3.2" "uuid": "^3.3.2"
},
"dependencies": {
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
}
} }
}, },
"require-directory": { "require-directory": {
@@ -10604,6 +10618,14 @@
"faye-websocket": "^0.11.3", "faye-websocket": "^0.11.3",
"uuid": "^3.4.0", "uuid": "^3.4.0",
"websocket-driver": "^0.7.4" "websocket-driver": "^0.7.4"
},
"dependencies": {
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
}
} }
}, },
"sockjs-client": { "sockjs-client": {
@@ -11838,10 +11860,9 @@
"dev": true "dev": true
}, },
"uuid": { "uuid": {
"version": "3.4.0", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
"dev": true
}, },
"v8-compile-cache": { "v8-compile-cache": {
"version": "2.2.0", "version": "2.2.0",
@@ -12666,6 +12687,14 @@
"requires": { "requires": {
"ansi-colors": "^3.0.0", "ansi-colors": "^3.0.0",
"uuid": "^3.3.2" "uuid": "^3.3.2"
},
"dependencies": {
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
}
} }
}, },
"webpack-merge": { "webpack-merge": {

View File

@@ -10,11 +10,13 @@
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"uuid": "^8.3.2",
"vue": "^3.0.0", "vue": "^3.0.0",
"vue-router": "^4.0.0-0", "vue-router": "^4.0.0-0",
"vuex": "^4.0.0-0" "vuex": "^4.0.0-0"
}, },
"devDependencies": { "devDependencies": {
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^2.33.0", "@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0", "@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-babel": "~4.5.0",

View File

@@ -4,16 +4,19 @@
<div class="fs-content"> <div class="fs-content">
<router-view /> <router-view />
</div> </div>
<toasts-list />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import Navbar from "@/components/Navbar.vue"; import Navbar from "@/components/Navbar.vue";
import ToastsList from "@/components/toast/ToastsList.vue";
export default defineComponent({ export default defineComponent({
components: { components: {
Navbar Navbar,
ToastsList
} }
}); });
</script> </script>

View File

@@ -0,0 +1,30 @@
import axios from "axios";
/**
* Fetches the API for all celestial bodies
* @returns Array of celestials in the API
*/
export const fetchCelestials = (): Promise<any> => {
return axios
.get("https://api.le-systeme-solaire.net/rest/bodies/")
.then(res => {
return res.data.bodies;
});
};
/**
* Fetches the API for all celestial bodies
* @returns Array of celestials in the API
*/
export const fetchCelestial = (slug: string): Promise<any> => {
return axios
.get(`https://api.le-systeme-solaire.net/rest/bodies/${slug}`)
.then(res => {
return res.data;
});
};
export default {
fetchCelestials,
fetchCelestial
};

View File

@@ -0,0 +1,36 @@
import axios from "axios";
/**
* Get the excerpt of the wikipedia (fr) article for the specific celestial object
* @param celestial A celestial object
*/
export const fetchWikipediaExcerpt = async (celestial: any): Promise<any> => {
let query = 'https://fr.wikipedia.org/w/api.php?action=opensearch';
switch (celestial.id) {
case "soleil":
case "lune":
query += `&search=${celestial.id}`;
break;
case "planète à lunes":
query += `&search=${celestial.id}%20planète`;
break;
default:
query += `&search=${celestial.id}%20${celestial.type}`;
break;
}
query += "&limit=1&namespace=0&format=json";
console.log(query);
const req = await axios.get(query);
return;
};
export default {
fetchWikipediaExcerpt
}

View File

@@ -1,3 +1,14 @@
@keyframes toggleFavHeart {
0%, 20% {
color: #d01d35;
transform: scale(1.2);
}
100% {
color: #d01d35;
transform: scale(1);
}
}
@keyframes fadeInBg { @keyframes fadeInBg {
from { from {
opacity: 0; opacity: 0;
@@ -16,6 +27,15 @@
} }
} }
@keyframes fadeOut {
from {
opacity: 100%;
}
to {
opacity: 0;
}
}
@keyframes fromBottom { @keyframes fromBottom {
from { from {
transform: translateY(20px); transform: translateY(20px);
@@ -25,6 +45,24 @@
} }
} }
@keyframes slideFromRight {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
@keyframes slideFromLeft {
from {
transform: translateX(0);
}
to {
transform: translateX(100%);
}
}
@keyframes paneBgAround { @keyframes paneBgAround {
0% { 0% {
background-position: -10vw 0%; background-position: -10vw 0%;

View File

@@ -1 +1,2 @@
@import url('https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Round');
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,700;1,400;1,700&family=Poppins:wght@400;600;700;800;900&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,700;1,400;1,700&family=Poppins:wght@400;600;700;800;900&display=swap');

View File

@@ -66,6 +66,18 @@ a {
} }
} }
button {
&.no-style {
padding: 0;
background: none;
border: none;
cursor: pointer;
&:focus {
outline: none;
}
}
}
hr { hr {
border-color: rgba($white, .2); border-color: rgba($white, .2);
} }
@@ -140,13 +152,14 @@ hr {
width: 100%; width: 100%;
background-size: cover; background-size: cover;
background-attachment: fixed; background-attachment: fixed;
background-repeat: no-repeat;
z-index: -1; z-index: -1;
opacity: 33%; opacity: 33%;
pointer-events: none; pointer-events: none;
} }
&.bg-fade-in { &.bg-fade-in {
&:after { &:after {
width: 120vw; background-size: 120vw;
opacity: 0; opacity: 0;
animation: fadeInBg 2s ease-out 0s 1 forwards, animation: fadeInBg 2s ease-out 0s 1 forwards,
paneBgAround 35s ease-in-out 0s infinite forwards; paneBgAround 35s ease-in-out 0s infinite forwards;

View File

@@ -8,6 +8,16 @@ $space-blue-600: #234e69;
$space-blue-500: #457b9d; $space-blue-500: #457b9d;
$space-blue-300: #a8dadc; $space-blue-300: #a8dadc;
$blue: #306BAC;
$green: #1c8267;
$orange: #ff9844;
$red: #9f2042;
$info: $blue;
$valid: $green;
$warning: $orange;
$danger: $red;
$primary: $space-blue-700; $primary: $space-blue-700;
$primary-light: $space-blue-600; $primary-light: $space-blue-600;

View File

@@ -23,8 +23,8 @@ export default defineComponent({
link: "/" link: "/"
}, },
{ {
text: "Célestes", text: "Astres",
link: "/celestes" link: "/astres"
}, },
{ {
text: "À propos", text: "À propos",

View File

@@ -42,6 +42,15 @@
</p> </p>
</div> </div>
<div class="card-info">
<router-link
:to="{ name: 'CelesteSingle', params: { slug: celestial.id } }"
class="no-style"
>
<span class="material-icons-round">info</span>
</router-link>
</div>
<div class="card-content"> <div class="card-content">
<div v-if="celestial.aroundPlanet"> <div v-if="celestial.aroundPlanet">
<p> <p>
@@ -75,12 +84,13 @@
</div> </div>
<div class="card-actions"> <div class="card-actions">
<router-link <button
:to="{ name: 'CelesteSingle', params: { slug: celestial.id } }" @click="toggleFav(celestial)"
class="btn btn-primary" class="favourite no-style"
:class="isFav ? 'active' : ''"
> >
Détails <span class="icon material-icons-round">favorite</span>
</router-link> </button>
</div> </div>
</div> </div>
</div> </div>
@@ -88,6 +98,9 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { v4 as uuidv4 } from "uuid";
import store from "@/store";
export default defineComponent({ export default defineComponent({
name: "celestial-card", name: "celestial-card",
@@ -96,8 +109,38 @@ export default defineComponent({
type: "unknown" type: "unknown"
}; };
}, },
computed: {
isFav() {
if (store.getters.isFav(this.celestial?.id)) {
return true;
}
return false;
}
},
props: { props: {
celestial: Object celestial: Object
},
methods: {
toggleFav: (celestial: any) => {
store.dispatch("toggleFav", celestial.id);
if (store.getters.isFav(celestial.id)) {
store.dispatch("toasts/pushToast", {
id: uuidv4(),
title: "Favori ajouté",
message: `${celestial.name} a été ajouté à la liste des favoris.`,
category: "valid",
timer: 3
});
} else {
store.dispatch("toasts/pushToast", {
id: uuidv4(),
title: "Favori retiré",
message: `${celestial.name} a été retiré de la liste des favoris.`,
category: "valid",
timer: 3
});
}
}
} }
}); });
</script> </script>
@@ -121,8 +164,18 @@ export default defineComponent({
.card-icon { .card-icon {
display: block; display: block;
position: absolute; position: absolute;
top: 0; top: 20px;
bottom: 20px;
left: 0;
right: 0; right: 0;
user-select: none;
pointer-events: none;
opacity: 4%;
img {
height: 100%;
width: 100%;
z-index: -1;
}
} }
> * { > * {
@@ -135,6 +188,30 @@ export default defineComponent({
flex: 1 1 auto; flex: 1 1 auto;
min-height: 30px; min-height: 30px;
} }
.card-info {
position: absolute;
top: 0;
right: 0;
}
.card-actions {
color: #afafaf;
.favourite {
.icon {
color: #afafaf;
}
&.active {
.icon {
position: relative;
display: inline-block;
will-change: font-size;
animation: toggleFavHeart 0.6s cubic-bezier(0.17, 0.89, 0.32, 1.49);
animation-fill-mode: forwards;
}
}
}
}
} }
} }
</style> </style>

View File

@@ -71,7 +71,7 @@ export default defineComponent({
data() { data() {
return { return {
filters: { filters: {
all: false, all: true,
planets: false, planets: false,
moons: false, moons: false,
stars: false, stars: false,

View File

@@ -0,0 +1,98 @@
<template>
<div class="toast-card" :class="[toast.category, closing ? 'closing' : '']">
<div class="toast-title">
<slot name="title" />
</div>
<div class="toast-message">
<slot name="message" />
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import store from "@/store";
export default defineComponent({
name: "ToastCard",
data() {
return {
closing: false
};
},
props: {
toast: Object
},
methods: {
// Counts down until 0 is reached
countdown(timeToLive) {
if (timeToLive > 0) {
console.log(timeToLive);
setTimeout(() => {
timeToLive = timeToLive - 1;
this.countdown(timeToLive);
}, 900);
} else {
// When timer ends
this.closing = true;
setTimeout(() => {
this.close();
}, 1200);
}
},
close() {
store.dispatch("toasts/purgeToast", this.toast.id);
}
},
mounted() {
// Starts countdown to initial timer value
this.countdown(this.toast.timer);
}
});
</script>
<style lang="scss" scoped>
.toast-card {
position: relative;
width: 300px;
padding: 20px 30px 26px;
border-radius: 5px;
animation: fadeIn 1s cubic-bezier(0.175, 1, 0.32, 1),
slideFromRight 1s cubic-bezier(0.175, 1, 0.32, 1);
overflow: hidden;
.toast-title {
margin-bottom: 5px;
font-size: 16px;
font-weight: $fw-bold;
}
&:after {
position: absolute;
display: block;
content: "";
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 6px;
background: rgba($fs-black, 33%);
}
&.info {
background-color: $info;
}
&.valid {
background-color: $valid;
}
&.warning {
color: $fs-black;
background-color: $warning;
}
&.danger {
background-color: $danger;
}
&.closing {
animation: fadeOut 1.2s cubic-bezier(0.175, 1, 0.32, 1),
slideFromLeft 1.2s cubic-bezier(0.175, 1, 0.32, 1);
}
}
</style>

View File

@@ -0,0 +1,45 @@
<template>
<div class="toast-wrapper">
<div class="toast-item" v-for="toast in queue" :key="toast.id">
<toast-card :variant="toast.category" :toast="toast">
<template #title>{{ toast.title }}</template>
<template #message>
{{ toast.message }}
</template>
</toast-card>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import ToastCard from "./ToastCard.vue";
export default defineComponent({
name: "ToastsList",
components: {
ToastCard
},
computed: {
queue() {
return this.$store.state.toasts.queue;
}
}
});
</script>
<style lang="scss" scoped>
.toast-wrapper {
position: fixed;
top: 90px;
height: 100%;
right: 0;
pointer-events: none;
.toast-item {
pointer-events: all;
margin-right: 20px;
margin-bottom: 20px;
}
}
</style>

View File

@@ -21,6 +21,11 @@ export const addCelestialType = (celestial: any) => {
return celestial; return celestial;
}; };
export default { export const addCelestialsType = (celestials: any) => {
addCelestialType return celestials.map((e: any) => addCelestialType(e));
};
export default {
addCelestialType,
addCelestialsType
}; };

View File

@@ -19,12 +19,12 @@ const routes: Array<RouteRecordRaw> = [
component: About component: About
}, },
{ {
path: "/celestes", path: "/astres",
name: "CelesteAll", name: "CelesteAll",
component: Celestials component: Celestials
}, },
{ {
path: "/celestes/:slug", path: "/astres/:slug",
name: "CelesteSingle", name: "CelesteSingle",
component: Celestial, component: Celestial,
props: true props: true

2
src/shims-vue.d.ts vendored
View File

@@ -4,3 +4,5 @@ declare module '*.vue' {
const component: DefineComponent<{}, {}, any> const component: DefineComponent<{}, {}, any>
export default component export default component
} }
declare module 'uuid';

View File

@@ -1,8 +1,52 @@
import { createStore } from "vuex"; import { createStore } from "vuex";
import toastStore from "./toasts";
export default createStore({ export default createStore({
state: {}, state: () => ({
mutations: {}, user: {
actions: {}, avatarUrl: "",
modules: {} favourites: Array<{ id: string }>()
}
}),
getters: {
// Returns the number of favs for the user
favCount: (state): number => {
return state.user.favourites.length;
},
// Returns true if celestial is fav
isFav: state => (celestialId: string) => {
return state.user.favourites.find(fav => fav.id === celestialId);
}
},
mutations: {
addFav: (state, celestialId: string) => {
state.user.favourites.push({ id: celestialId });
},
removeFav: (state, celestialId: string) => {
state.user.favourites = state.user.favourites.filter(
fav => fav.id != celestialId
);
}
},
actions: {
toggleFav: ({ commit, getters }, celestialId: string) => {
// If the celestial is not faved
if (!getters.isFav(celestialId)) {
// ... favs it
commit("addFav", celestialId);
} else {
// ...else unfavs
commit("removeFav", celestialId);
}
}
},
modules: {
toasts: {
namespaced: true,
state: toastStore.state,
mutations: toastStore.mutations,
actions: toastStore.actions
}
}
}); });

27
src/store/toasts/index.ts Normal file
View File

@@ -0,0 +1,27 @@
export default {
state: {
queue: Array<{
id: string;
title: string;
message: string;
category: string;
timer: number;
}>()
},
mutations: {
pushToast: (state: any, toast: any) => {
state.queue.push(toast);
},
purgeToast: (state: any, toastId: string) => {
state.queue = state.queue.filter((toast: any) => toast.id != toastId);
}
},
actions: {
pushToast: ({ commit }: any, toast: any) => {
commit("pushToast", toast);
},
purgeToast: ({ commit }: any, toastId: string) => {
commit("purgeToast", toastId);
}
}
};

View File

@@ -3,11 +3,11 @@
<div class="splash fade-in-children"> <div class="splash fade-in-children">
<h2 class="heading-1">Full Skies</h2> <h2 class="heading-1">Full Skies</h2>
<p class="heading-2"> <p class="heading-2">
L'application Full Skies explore les cieux à votre place ! Explorez les cieux et le système solaire avec Full Skies !
</p> </p>
<p class="heading-2"> <p class="heading-2">
Embarquez pour un voyage stellaire ; destination : le système solaire et Embarquez pour un voyage stellaire ; destination : le système solaire et
ses astres. ses astres si proches.
</p> </p>
</div> </div>
</section> </section>

View File

@@ -1,6 +1,6 @@
<template> <template>
<section class="celestial bg-image"> <section class="celestial bg-image">
<div v-if="loaded"> <div v-if="celestialLoaded">
<header> <header>
<h1 class="heading-1">{{ celestial.name }}</h1> <h1 class="heading-1">{{ celestial.name }}</h1>
</header> </header>
@@ -19,7 +19,9 @@
import { defineComponent } from "vue"; import { defineComponent } from "vue";
// API // API
import axios from "axios"; import { fetchCelestial } from "@/api/le-systeme-solaire";
// import { fetchWikipediaExcerpt } from "@/api/wikipedia";
// Global methods // Global methods
import { addCelestialType } from "@/plugins/methods"; import { addCelestialType } from "@/plugins/methods";
import NestLoader from "@/components/NestLoader.vue"; import NestLoader from "@/components/NestLoader.vue";
@@ -32,8 +34,9 @@ export default defineComponent({
data() { data() {
return { return {
celestial: false, celestial: {},
loaded: false, celestialLoaded: false,
excerpt: "",
error: "" error: ""
}; };
}, },
@@ -47,30 +50,18 @@ export default defineComponent({
mounted() { mounted() {
// Fetches from API... // Fetches from API...
this.fetchCelestial() fetchCelestial(this.slug)
.then(res => { .then(res => {
// Adds type after fake loading (it's just to showcase the spinner tbh) this.celestial = addCelestialType(res);
setTimeout(() => { this.celestialLoaded = true;
this.celestial = addCelestialType(res); return this.celestial;
this.loaded = true;
}, 1000);
}) })
.catch(() => { .catch(() => {
this.error = "Impossible de récupérer l'astre demandé."; this.error = "Impossible de récupérer l'astre demandé.";
})
.then(() => {
// fetchWikipediaExcerpt(celestial);
}); });
},
methods: {
/**
* Fetches celestial body from API
*/
fetchCelestial(): Promise<any> {
return axios
.get(`https://api.le-systeme-solaire.net/rest/bodies/${this.slug}`)
.then(res => {
return res.data;
});
}
} }
}); });
</script> </script>

View File

@@ -16,12 +16,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent, onMounted, ref } from "vue";
// API // API
import axios from "axios"; import { fetchCelestials } from "@/api/le-systeme-solaire";
// Global methods // Global methods
import { addCelestialType } from "@/plugins/methods"; import { addCelestialsType } from "@/plugins/methods";
import CelestialsList from "@/components/celestials/CelestialsList.vue"; import CelestialsList from "@/components/celestials/CelestialsList.vue";
import NestLoader from "@/components/NestLoader.vue"; import NestLoader from "@/components/NestLoader.vue";
@@ -34,38 +35,21 @@ export default defineComponent({
}, },
data() { data() {
return { return {
celestials: Array<any>(),
error: "" error: ""
}; };
}, },
mounted() { setup() {
this.fetchCelestials() const celestials = ref(Array<any>());
.then(res => {
this.celestials = this.addType(res);
})
.catch(() => {
this.error = "Impossible de récupérer les astres.";
});
},
methods: { const getCelestials = async () => {
/** celestials.value = await fetchCelestials();
* Fetches celestial bodies from API celestials.value = addCelestialsType(celestials.value);
*/ };
fetchCelestials(): Promise<any> {
return axios onMounted(getCelestials);
.get("https://api.le-systeme-solaire.net/rest/bodies/")
.then(res => { return { celestials };
return res.data.bodies;
});
},
/**
* Assign a type from the celestial object provided
*/
addType(bodies: Array<any>): Array<any> {
return bodies.map((e: any) => addCelestialType(e));
}
} }
}); });
</script> </script>