Browse Source

Adds primitive table and Intl date/number formatting

main
koehr 4 months ago
parent
commit
2ccced6845
  1. 21
      LICENSE
  2. 21
      README.md
  3. 12
      package.json
  4. 130
      src/App.vue
  5. 65
      src/app.css
  6. BIN
      src/assets/logo.png
  7. BIN
      src/assets/star-jedi-outline.woff2
  8. BIN
      src/assets/star-jedi-special.woff2
  9. 52
      src/components/HelloWorld.vue
  10. 80
      src/components/SortableTable.vue
  11. 43
      src/components/TableOptions.vue
  12. 12
      src/composables/useDateFormatting.ts
  13. 13
      src/composables/useNumberFormatting.ts
  14. 1
      src/main.ts
  15. 49
      src/types.d.ts
  16. 32
      tsconfig.json
  17. 15
      vite.config.ts
  18. 10
      yarn.lock

21
LICENSE

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Norman Köhring
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

21
README.md

@ -1,11 +1,20 @@ @@ -1,11 +1,20 @@
# Vue 3 + Typescript + Vite
# Star Wars Character
This template should help get you started developing with Vue 3 and Typescript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
A small tool to search for StarWars characters, because: Why not?
## Recommended IDE Setup
## Overview
- [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar)
This web application is written in Typescript, using Vue 3 and Vite. It comes with a recommended IDE setup, that I personally don't use, but I add it for completeness and all the VSCode fans out there: [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar)
I make heavy use of `<script setup>` blocks and Vue's composition API. To learn more, check out the [script setup docs](https://vuejs.org/api/sfc-script-setup.html) and the [Composition API overview](https://vuejs.org/api/composition-api-setup.html#basic-usage).
## Type Support For `.vue` Imports in TS
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's `.vue` type support plugin by running `Volar: Switch TS Plugin on/off` from VSCode command palette.
## Acknowlegments
API data types are taken from https://github.com/amitmtrn/swapi-ts/blob/main/src/SWApi.ts but no other code.
The header font is Star Jedi: https://www.dafontfree.io/star-jedi-font-family/
## License
This web application is licensed under the permissive MIT license. See [LICENSE](LICENSE).

12
package.json

@ -8,12 +8,12 @@ @@ -8,12 +8,12 @@
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.2.25"
"vue": "3.2.31"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.2.0",
"typescript": "^4.5.4",
"vite": "^2.8.0",
"vue-tsc": "^0.29.8"
"@vitejs/plugin-vue": "2.2.2",
"typescript": "4.5.5",
"vite": "2.8.4",
"vue-tsc": "0.29.8"
}
}
}

130
src/App.vue

@ -1,21 +1,119 @@ @@ -1,21 +1,119 @@
<script setup lang="ts">
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import HelloWorld from './components/HelloWorld.vue'
import { ref, computed } from 'vue'
import TableOptions from './components/TableOptions.vue'
import SortableTable from './components/SortableTable.vue'
const apiResult: IPeople = {
'people/1': {
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "https://swapi.dev/api/planets/1/",
"films": [
"https://swapi.dev/api/films/1/",
"https://swapi.dev/api/films/2/",
"https://swapi.dev/api/films/3/",
"https://swapi.dev/api/films/6/"
],
"species": [],
"vehicles": [
"https://swapi.dev/api/vehicles/14/",
"https://swapi.dev/api/vehicles/30/"
],
"starships": [
"https://swapi.dev/api/starships/12/",
"https://swapi.dev/api/starships/22/"
],
"created": "2014-12-09T13:50:51.644000Z",
"edited": "2014-12-20T21:17:56.891000Z",
"url": "https://swapi.dev/api/people/1/"
},
'people/2': {
"name": "C-3PO",
"height": "167",
"mass": "75",
"hair_color": "n/a",
"skin_color": "gold",
"eye_color": "yellow",
"birth_year": "112BBY",
"gender": "n/a",
"homeworld": "https://swapi.dev/api/planets/1/",
"films": [
"https://swapi.dev/api/films/1/",
"https://swapi.dev/api/films/2/",
"https://swapi.dev/api/films/3/",
"https://swapi.dev/api/films/4/",
"https://swapi.dev/api/films/5/",
"https://swapi.dev/api/films/6/"
],
"species": [
"https://swapi.dev/api/species/2/"
],
"vehicles": [],
"starships": [],
"created": "2014-12-10T15:10:51.357000Z",
"edited": "2014-12-20T21:17:50.309000Z",
"url": "https://swapi.dev/api/people/2/"
},
'people/3': {
"name": "R2-D2",
"height": "96",
"mass": "32",
"hair_color": "n/a",
"skin_color": "white, blue",
"eye_color": "red",
"birth_year": "33BBY",
"gender": "n/a",
"homeworld": "https://swapi.dev/api/planets/8/",
"films": [
"https://swapi.dev/api/films/1/",
"https://swapi.dev/api/films/2/",
"https://swapi.dev/api/films/3/",
"https://swapi.dev/api/films/4/",
"https://swapi.dev/api/films/5/",
"https://swapi.dev/api/films/6/"
],
"species": [
"https://swapi.dev/api/species/2/"
],
"vehicles": [],
"starships": [],
"created": "2014-12-10T15:11:50.376000Z",
"edited": "2014-12-20T21:17:50.311000Z",
"url": "https://swapi.dev/api/people/3/"
}
}
const fields: IFieldDefinition[] = [
{ field: 'url', label: 'ID', format: 'id' },
{ field: 'name', label: 'Name', format: 'string' },
{ field: 'height', label: 'Height', format: 'height' },
{ field: 'mass', label: 'Weight', format: 'mass' },
{ field: 'created', label: 'Created', format: 'date' },
{ field: 'edited', label: 'Edited', format: 'date' },
{ field: 'homeworld', label: 'Homeworld', format: 'link' },
]
const limit = ref(10)
const sortBy = ref('id')
const filter = ref('')
const data = computed<IPerson[]>(() => {
const people = Object.values(apiResult)
return people.filter(person => {
const name = person.name.toLowerCase()
const filter_ = filter.value.toLowerCase()
return name.indexOf(filter_) >= 0
})
})
</script>
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld msg="Hello Vue 3 + TypeScript + Vite" />
<h1>Star Wars Characters</h1>
<TableOptions v-model:filter="filter" v-model:limit="limit" />
<SortableTable v-bind="{ data, limit, fields }" v-model:sortBy="sortBy" />
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

65
src/app.css

@ -0,0 +1,65 @@ @@ -0,0 +1,65 @@
@font-face {
font-family: 'StarJediOutline';
src: url('./assets/star-jedi-outline.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'StarJediSpecial';
src: url('./assets/star-jedi-special.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
:root {
--page-bg: #000;
--page-fg: #99D;
--hilight: #FE2;
--font-size: 20px;
}
body {
max-width: 1200px;
min-height: 100vh;
margin: 0 auto;
padding: 0;
background: var(--page-bg);
font: var(--font-size)/1.5 sans-serif;
color: var(--page-fg);
}
#app {
display: flex;
flex: 0 0 100%;
flex-flow: column nowrap;
min-height: 100vh;
margin: 0 0 0 var(--margin);
}
h1 {
font-family: 'StarJediOutline', serif;
font-size: 84px;
color: var(--hilight);
text-align: center;
}
table {
width: 100%;
margin: 1em auto;
}
input, select {
background: var(--page-bg);
border: 1px solid var(--page-fg);
padding: .5em 1em;
color: var(--hilight);
}
input:focus {
outline: 1px solid var(--hilight);
border: 1px solid var(--hilight);
}
select > option {
padding-inline: 0;
}

BIN
src/assets/logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

BIN
src/assets/star-jedi-outline.woff2

Binary file not shown.

BIN
src/assets/star-jedi-special.woff2

Binary file not shown.

52
src/components/HelloWorld.vue

@ -1,52 +0,0 @@ @@ -1,52 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<p>
Recommended IDE setup:
<a href="https://code.visualstudio.com/" target="_blank">VSCode</a>
+
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
</p>
<p>See <code>README.md</code> for more information.</p>
<p>
<a href="https://vitejs.dev/guide/features.html" target="_blank">
Vite Docs
</a>
|
<a href="https://v3.vuejs.org/" target="_blank">Vue 3 Docs</a>
</p>
<button type="button" @click="count++">count is: {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test hot module replacement.
</p>
</template>
<style scoped>
a {
color: #42b983;
}
label {
margin: 0 0.5em;
font-weight: bold;
}
code {
background-color: #eee;
padding: 2px 4px;
border-radius: 4px;
color: #304455;
}
</style>

80
src/components/SortableTable.vue

@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
<script setup lang="ts">
import createDateFormatter from '!composable/useDateFormatting'
import createNumberFormatter from '!composable/useNumberFormatting'
const props = defineProps<{
data: GenericData;
fields: IFieldDefinition[];
sortBy: string;
limit: number;
}>()
const emit = defineEmits(['update:sortBy'])
const formatDate = createDateFormatter()
const formatMass = createNumberFormatter('kilogram')
const formatHeight = createNumberFormatter('centimeter')
function setSortBy (field: string) {
emit('update:sortBy', field)
}
function formatValue (value: string|number, format: string) {
switch (format) {
case 'height': return formatHeight(value)
case 'mass': return formatMass(value)
case 'date': return formatDate(value)
case 'id':
// id is actually an URL like https://swapi.api/people/3/
// but we want to show only "3"
const parts = value.replace(/\/$/, '').split('/')
return parts[parts.length - 1]
default: return value
}
}
</script>
<template>
<table>
<tr>
<th v-for="{ label, field } in fields">
<button @click="setSortBy(field)">
{{ label }}
<template v-if="field === sortBy"></template>
</button>
</th>
</tr>
<tr v-for="datum,i in data">
<td v-for="{ field, format } in fields">
<template v-if="format === 'link'">
<a href="#">{{ datum[field] }}</a>
</template>
<template v-else>
{{ formatValue(datum[field], format) }}
</template>
</td>
</tr>
</table>
</template>
<style scoped>
th {
background-color: #FFF2;
}
button {
width: 100%;
padding: .5em;
font-size: 1.2em;
background-color: transparent;
color: var(--page-fg);
border: none;
}
td {
text-align: center;
background-color: #FFF2;
padding: .5em 0;
}
td:nth-of-type(odd) {
background-color: #FFF1;
}
</style>

43
src/components/TableOptions.vue

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
<script setup lang="ts">
const { filter = '', limit = 100 } = defineProps<{
filter: string;
limit: number,
}>()
const emit = defineEmits(['update:filter', 'update:limit'])
function updateFilter (event) {
emit('update:filter', event.target.value)
}
function updateLimit (event) {
emit('update:limit', parseInt(event.target.value))
}
</script>
<template>
<form id="table-options">
<label>
Search:
<input type="text" :value="filter" @input="updateFilter" placeholder="Luke Skywalker" />
</label>
<label>
Rows per page:
<select :value="`${limit}`" @change="updateLimit">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</label>
</form>
</template>
<style scoped>
#table-options {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-items: center;
}
</style>

12
src/composables/useDateFormatting.ts

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
export default function useDateFormatting (locales: string[] = []): (dateString: string) => string {
// Formatter for DateTimes with the browsers default locale
const formatter = new Intl.DateTimeFormat(locales, {
dateStyle: 'short',
timeStyle: 'short',
})
return dateString => {
const date = new Date(dateString)
return formatter.format(date)
}
}

13
src/composables/useNumberFormatting.ts

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
// https://tc39.es/proposal-unified-intl-numberformat/section6/locales-currencies-tz_proposed_out.html#sec-issanctionedsimpleunitidentifier
export default function useNumberFormatting (
unit: string,
style: string = 'unit',
locales: string[] = [],
): (value: number) => string {
const formatter = new Intl.NumberFormat(locales, { style, unit, unitDisplay: 'narrow' })
return value => {
return formatter.format(value)
}
}

1
src/main.ts

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
import './app.css'
createApp(App).mount('#app')

49
src/types.d.ts vendored

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
interface IPeople {
[key: string]: IPerson;
}
interface IPlanets {
[key: string]: IPlanet;
}
interface IPerson {
birth_year: string;
eye_color: string;
films: string[] | IFilm[];
gender: string;
hair_color: string;
height: string;
homeworld: string | IPlanet;
mass: string;
name: string;
skin_color: string;
created: Date;
edited: Date;
species: string[];
starships: string[];
url: string;
vehicles: string[];
}
interface IPlanet {
climate: string;
created: Date;
diameter: string;
edited: Date;
films: string[];
gravity: string;
name: string;
orbital_period: string;
population: string;
residents: string[];
rotation_period: string;
surface_water: string;
terrain: string;
url: string;
}
interface IFieldDefinition {
field: string,
label: string,
format: string,
}

32
tsconfig.json

@ -9,8 +9,34 @@ @@ -9,8 +9,34 @@
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"]
"lib": [
"esnext",
"dom"
],
"paths": {
"@/*": [
"./src/*"
],
"!component/*": [
"./src/components/*"
],
"!composable/*": [
"./src/composables/*"
],
"!api/*": [
"./src/api/*"
]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

15
vite.config.ts

@ -1,7 +1,20 @@ @@ -1,7 +1,20 @@
import { fileURLToPath, URL } from "url"
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()]
plugins: [
vue({
reactivityTransform: true,
})
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
"!component": fileURLToPath(new URL("./src/components", import.meta.url)),
"!composable": fileURLToPath(new URL("./src/composables", import.meta.url)),
"!api": fileURLToPath(new URL("./src/api", import.meta.url)),
}
}
})

10
yarn.lock

@ -39,7 +39,7 @@ @@ -39,7 +39,7 @@
resolved "https://registry.yarnpkg.com/@emmetio/scanner/-/scanner-1.0.0.tgz#065b2af6233fe7474d44823e3deb89724af42b5f"
integrity sha512-8HqW8EVqjnCmWXVpqAOZf+EGESdkR27odcMMMGefgKXtar00SoYNSryGv//TELI4T3QFsECo78p+0lmalk/CFA==
"@vitejs/plugin-vue@^2.2.0":
"@vitejs/plugin-vue@2.2.2":
version "2.2.2"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-2.2.2.tgz#df5d4464ad8cb97c9fb7407a1e5a3a34f716febb"
integrity sha512-3C0s45VOwIFEDU+2ownJOpb0zD5fnjXWaHVOLID2R1mYOlAx3doNBFnNbVjaZvpke/L7IdPJXjpyYpXZToDKig==
@ -739,7 +739,7 @@ token-stream@1.0.0: @@ -739,7 +739,7 @@ token-stream@1.0.0:
resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-1.0.0.tgz#cc200eab2613f4166d27ff9afc7ca56d49df6eb4"
integrity sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=
typescript@^4.5.4:
typescript@4.5.5:
version "4.5.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3"
integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==
@ -749,7 +749,7 @@ upath@^2.0.1: @@ -749,7 +749,7 @@ upath@^2.0.1:
resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b"
integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==
vite@^2.8.0:
vite@2.8.4:
version "2.8.4"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.8.4.tgz#4e52a534289b7b4e94e646df2fc5556ceaa7336b"
integrity sha512-GwtOkkaT2LDI82uWZKcrpRQxP5tymLnC7hVHHqNkhFNknYr0hJUlDLfhVRgngJvAy3RwypkDCWtTKn1BjO96Dw==
@ -895,7 +895,7 @@ vscode-vue-languageservice@0.29.8: @@ -895,7 +895,7 @@ vscode-vue-languageservice@0.29.8:
vscode-pug-languageservice "0.29.8"
vscode-typescript-languageservice "0.29.8"
vue-tsc@^0.29.8:
vue-tsc@0.29.8:
version "0.29.8"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-0.29.8.tgz#f4d8de5dd8756107c878489ccf9178d2d72fff47"
integrity sha512-pT0wLRjvRuSmB+J4WJT6uuV9mO0KtSSXEAtaVXZQzyk5+DJdbLIQTbRce/TXSkfqt1l1WogO78RjtOJFiMCgfQ==
@ -903,7 +903,7 @@ vue-tsc@^0.29.8: @@ -903,7 +903,7 @@ vue-tsc@^0.29.8:
"@volar/shared" "0.29.8"
vscode-vue-languageservice "0.29.8"
vue@^3.2.25:
vue@3.2.31:
version "3.2.31"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.31.tgz#e0c49924335e9f188352816788a4cca10f817ce6"
integrity sha512-odT3W2tcffTiQCy57nOT93INw1auq5lYLLYtWpPYQQYQOOdHiqFct9Xhna6GJ+pJQaF67yZABraH47oywkJgFw==

Loading…
Cancel
Save