-
-
-
-
-
-
- {{ toProperCase(name) }}
-
-
-
-
-
- {{ item }}
-
-
- {{ item }}
-
-
-
-
-
+
+
+
![Photo of the roadster]()
+
+
+ {{ data.name }}
+
+
+ {{ data.id }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/VJson/index.js b/src/components/VJson/index.js
new file mode 100644
index 0000000..a2e633e
--- /dev/null
+++ b/src/components/VJson/index.js
@@ -0,0 +1,122 @@
+import { h, resolveComponent } from "vue"
+import {
+ toProperCase,
+ isLink,
+} from "../../helpers"
+import "./style.css"
+
+
+/*
+JSON types:
+- string
+- number
+- boolean
+- null
+- object
+- array
+*/
+
+
+export default {
+ // undefined allows any type
+ props: { json: undefined },
+
+ setup(props) {
+ const exceptions = [
+ "rockets",
+ "ships",
+ "launches",
+ "launchpads",
+ "landpads",
+ "capsules",
+ "crew",
+ ]
+
+ const exceptions2 = [
+ "rocket",
+ "ship",
+ "launchpad",
+ "landpad",
+ "payload",
+ "capsule",
+ "core",
+ ]
+
+
+ const convertVal = (key, val) => {
+ if (key === "youtube_id") return convert(`https://www.youtube.com/watch?v=${val}`)
+
+ else if ([...exceptions, ...exceptions2].includes(key)) {
+ const parsedKey = exceptions2.includes(key)
+ ? key === "launch"
+ ? "launches"
+ : key + "s"
+ : key
+
+ return Array.isArray(val)
+ ? h("ul", val.map(i =>
+ h("li", h(resolveComponent("router-link"), {
+ to: `/${parsedKey}/${i}`,
+ target: "_blank",
+ }, _ => i)))
+ )
+ : h(resolveComponent("router-link"), {
+ to: `/${parsedKey}/${val}`,
+ target: "_blank",
+ }, _ => val)
+ }
+
+ else return convert(val)
+ }
+
+ const convert = (data) => {
+ if (typeof data === "string") {
+ if (isLink(data))
+ return h("a", { href: data, target: "_blank" }, data)
+ else
+ return h("p", { class: "string" }, data)
+
+ } else if (typeof data === "number") {
+ return h("span", { class: "number" }, data)
+
+ } else if (typeof data === "boolean") {
+ return h("span", {
+ class: `bool ${data ? "bool-true" : "bool-false"}`
+ }, data ? "yes" : "no")
+
+ } else if (data === null || data === []) {
+ return h("span", { class: "null" }, "-")
+
+ } else if (typeof data === "object") {
+ if (Array.isArray(data)) {
+ return h("ul", { class: "array" }, data.map(
+ (item) => h("li", { class: "array-item" }, convert(item))
+ ))
+ } else {
+ const result = []
+
+ for (const key in data) {
+ result.push(
+ h("div", {
+ class: "key-val-wrapper"
+ }, [
+ h("dt", {
+ class: "key"
+ }, h("span", { class: "string" }, toProperCase(key))),
+
+ h("dd", {
+ class: "val"
+ // avoid `Non-function value encountered for default slot`
+ // by wrapping the child in a function
+ }, convertVal(key, data[key]))
+ ])
+ )
+ }
+
+ return h("dl", { class: "dict" }, result)
+ }
+ }
+ }
+
+ return _ => h("div", { "data-vjson": "" }, convert(props.json)) }
+}
diff --git a/src/components/VJson/style.css b/src/components/VJson/style.css
new file mode 100644
index 0000000..fb9409e
--- /dev/null
+++ b/src/components/VJson/style.css
@@ -0,0 +1,23 @@
+[data-vjson] .string { @apply text-slate-800 }
+
+[data-vjson] .key * { @apply text-black }
+
+[data-vjson] .number { @apply text-teal-800 }
+
+[data-vjson] .bool-true { @apply text-emerald-600 }
+
+[data-vjson] .bool-false { @apply text-rose-700 }
+
+[data-vjson] .null { @apply text-slate-500 }
+
+[data-vjson] .array-item { @apply p-1 }
+
+[data-vjson] .key { @apply text-sm font-medium text-gray-500 }
+
+[data-vjson] .val { @apply mt-1 text-sm text-gray-900 }
+
+[data-vjson] .key-val-wrapper { @apply bg-white px-4 py-5 }
+
+[data-vjson] .key-val-wrapper:not(:first-child) { @apply border-t }
+
+[data-vjson] a { @apply text-blue-800 underline }
diff --git a/src/functions.js b/src/functions.js
deleted file mode 100644
index c6d4d94..0000000
--- a/src/functions.js
+++ /dev/null
@@ -1,35 +0,0 @@
-export default {
-
- get: (path, options = {}, query = {}) => {
- if (Object.keys(options).length === 0 && Object.keys(query).length === 0) {
- return fetch(`https://api.spacexdata.com/v4${path}`)
- .then(res => {
- if (res.ok) {
- return res.json()
- } else { return { error: res.status } }
- })
- .then(json => {
- console.log(json)
- return json
- })
- .catch( error => { return { error: error } } )
- } else return fetch(
- `https://api.spacexdata.com/v4${path}/query`,
- {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ query: query, options: options })
- })
- .then(res => {
- if (res.ok) {
- return res.json()
- } else { return new Error(res.status) }
- })
- .then(json => {
- console.log(json)
- return json
- })
- .catch( error => { return error } )
- }
-
-}
\ No newline at end of file
diff --git a/src/helpers/api.js b/src/helpers/api.js
new file mode 100644
index 0000000..fc22d61
--- /dev/null
+++ b/src/helpers/api.js
@@ -0,0 +1,29 @@
+export async function apiGet (path, options = {}, query = {}){
+ if (Object.keys(options).length === 0 && Object.keys(query).length === 0) {
+ return fetch(`${api}${path}`)
+ .then(res => {
+ if (res.ok) {
+ return res.json()
+ } else { return { error: res.status } }
+ })
+ .then(json => {
+ return json
+ })
+ .catch( error => { return { error: error } } )
+ } else return fetch(
+ `${api}${path}/query`,
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ query: query, options: options })
+ })
+ .then(res => {
+ if (res.ok) {
+ return res.json()
+ } else { return new Error(res.status) }
+ })
+ .then(json => {
+ return json
+ })
+ .catch( error => { return error } )
+}
diff --git a/src/helpers/index.js b/src/helpers/index.js
new file mode 100644
index 0000000..75514e9
--- /dev/null
+++ b/src/helpers/index.js
@@ -0,0 +1,12 @@
+import {
+ toProperCase,
+ isLink,
+} from "./string.js"
+import { apiGet } from "./api.js"
+
+
+export {
+ toProperCase,
+ isLink,
+ apiGet,
+}
diff --git a/src/helpers/string.js b/src/helpers/string.js
new file mode 100644
index 0000000..f8f8bd0
--- /dev/null
+++ b/src/helpers/string.js
@@ -0,0 +1,11 @@
+// Converts snake_case to Proper Case
+export const toProperCase = str => str
+ .replace(/([A-Z])/g, (match) => ` ${match}`)
+ .replace(/^./, (match) => match.toUpperCase()).replaceAll("_", " ")
+
+
+// Decide if a string is a valid link
+export const isLink = str => {
+ const regex = /(http|https):\/\/(\w+:{0,1}\w*)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%!\-\/]))?/
+ return regex.test(str)
+}
diff --git a/src/main.js b/src/main.js
index 92f6f37..eb27748 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,9 +1,51 @@
-import { createApp } from 'vue'
-import App from './App.vue'
-import router from "./router/index"
-import "./styles.css"
-
-createApp(App)
- .use(router)
- .provide("api", "https://api.spacexdata.com/v4")
- .mount('#app')
+import { createApp } from "vue"
+import { createRouter, createWebHashHistory } from "vue-router"
+import App from "./App.js"
+import "./style.css"
+
+
+const paths = [
+ "crew",
+ "rockets",
+ "ships",
+ "launches",
+ "history",
+ "launchpads",
+ "landpads",
+ "roadster",
+ "capsules",
+ "payloads",
+ "cores",
+]
+
+const router = createRouter({
+ history: createWebHashHistory(),
+ linkActiveClass: "active",
+ watchQuery: true,
+ routes: [
+ {
+ path: "/",
+ name: "home",
+ component: _ => import("./components/Home.vue")
+ },
+ ...paths.map(route => { return {
+ path: `/${route}`,
+ name: route,
+ component: _ => import("./components/Page.vue")
+ }}),
+ ...paths.map(route => { return {
+ path: `/${route}/:id`,
+ name: `get-one-${route}`,
+ component: _ => import("./components/GetOne.vue")
+ }}),
+ {
+ path: "/:path*",
+ name: "404",
+ component: _ => import("./components/404.js")
+ },
+ ]
+})
+
+const app = createApp(App)
+app.use(router)
+app.mount("#app")
diff --git a/src/router/index.js b/src/router/index.js
deleted file mode 100644
index 680b543..0000000
--- a/src/router/index.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { createRouter, createWebHashHistory } from "vue-router"
-
-let paths = [
- "capsules",
- "cores",
- "crew",
- "history",
- "landpads",
- "launches",
- "launchpads",
- "payloads",
- "roadster",
- "rockets",
- "ships",
-]
-
-paths.forEach((route, index) => { paths[index] = {
- path: `/${route}`,
- name: route.charAt(0).toUpperCase() + route.slice(1),
- component: _ => { return import("../components/Page.vue")}
-}})
-
-const routes = [
- {
- path: "/",
- name: "Home",
- component: _ => { return import("../components/Home.vue") }
- }
-].concat(paths)
-
-const router = createRouter({
- history: createWebHashHistory(),
- linkActiveClass: "active",
- watchQuery: true,
- routes,
-})
-
-export default router
\ No newline at end of file
diff --git a/src/styles.css b/src/style.css
similarity index 57%
rename from src/styles.css
rename to src/style.css
index 2103b64..45db19e 100644
--- a/src/styles.css
+++ b/src/style.css
@@ -1,10 +1,10 @@
-/* ./src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
height: 100%;
+ scrollbar-gutter: stable;
}
body {
@@ -13,18 +13,6 @@ body {
background: linear-gradient(117deg, rgba(16,0,47,1) 0%, rgba(43,11,102,1) 35%, rgba(0,139,143,1) 100%) no-repeat fixed;
}
-h1 {
- @apply text-4xl font-semibold py-2;
-}
-
-h2 {
- @apply text-2xl font-semibold py-1;
-}
+h1 { @apply text-4xl font-semibold py-2 }
-a {
- @apply underline text-blue-500;
-}
-
-.active {
- @apply brightness-75;
-}
+h2 { @apply text-2xl font-semibold py-1 }
diff --git a/tailwind.config.js b/tailwind.config.js
index ee1eb20..60de417 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,10 +1,16 @@
+const defaultTheme = require("tailwindcss/defaultTheme")
+
module.exports = {
- content: [
- "./index.html",
- "./src/**/*.vue",
- ],
- theme: {
- extend: {},
- },
- plugins: [],
+ content: [
+ "./index.html",
+ "./src/**/*.{vue,js}",
+ ],
+ theme: {
+ extend: {
+ fontFamily: {
+ sans: ["Inter var", ...defaultTheme.fontFamily.sans],
+ }
+ },
+ },
+ plugins: [],
}
diff --git a/vite.config.js b/vite.config.js
index 68067f3..06a4392 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,9 +1,13 @@
-import { defineConfig } from 'vite'
-import vue from '@vitejs/plugin-vue'
+import { defineConfig } from "vite"
+import vue from "@vitejs/plugin-vue"
+
-// https://vitejs.dev/config/
export default defineConfig({
- plugins: [vue()],
- base: "/spacexkit",
- build: { outDir: "docs" }
+ plugins: [vue()],
+ define: {
+ api: JSON.stringify("https://api.spacexdata.com/v4"),
+ appVersion: JSON.stringify(process.env.npm_package_version),
+ },
+ base: "/spacexkit/",
+ build: { outDir: "docs" }
})