Merge branch 'feat/devices'

This commit is contained in:
Sam Carlton 2021-04-18 22:13:52 -05:00
commit 3da46a63da
16 changed files with 453 additions and 35 deletions

View file

@ -5,6 +5,7 @@ import buildAppList from './helpers/build-app-list.js'
import buildGamesList from './helpers/build-game-list.js'
import buildHomebrewList from './helpers/build-homebrew-list.js'
import buildVideoList from './helpers/build-video-list.js'
import buildDeviceList from './helpers/build-device-list.js'
import { videosRelatedToApp } from './helpers/related.js'
import { buildVideoPayload, buildAppBenchmarkPayload } from './helpers/build-payload.js'
@ -14,6 +15,7 @@ import {
getAppType,
getAppEndpoint,
getVideoEndpoint,
isVideo
} from './helpers/app-derived.js'
import { makeSearchableList } from './helpers/searchable-list.js'
@ -61,6 +63,11 @@ class BuildLists {
path: '/static/homebrew-list.json',
buildMethod: buildHomebrewList,
},
{
name: 'device',
path: '/static/device-list.json',
buildMethod: buildDeviceList,
},
// Secondary Derivative built lists
// Always goes after initial lists
@ -225,10 +232,10 @@ class BuildLists {
this.lists[listKey].forEach( app => {
const isVideo = (app.category === undefined)
// const isVideo = (app.category === undefined)
const appType = getAppType( app )
if ( isVideo ) {
if ( isVideo( app ) ) {
// this.endpointMaps.eleventy.add({
// route: getVideoEndpoint(app),
// payload: buildVideoPayload( app, this.allVideoAppsList, this.lists.video )
@ -273,9 +280,14 @@ class BuildLists {
relatedVideos
} )
} else if ( appType === 'device' ) {
// Add device endpoint
// console.log('Added to nuxt endpoints', app.endpoint )
this.endpointMaps.nuxt.set( app.endpoint , { listing: app } )
} else {
// Add game or other endpoint
// console.log('Added to nuxt endpoints', getAppEndpoint(app))
// console.log('Added to nuxt endpoints', app.endpoint )
this.endpointMaps.nuxt.set( getAppEndpoint(app), { app } )
}

View file

@ -147,6 +147,10 @@ export default {
label: 'Categories',
url: '/categories',
},
{
label: 'Devices',
url: '/devices',
},
{
label: 'Benchmarks',
url: '/benchmarks',

View file

@ -16,12 +16,12 @@
<input
id="search"
ref="search"
:autofocus="autofocus"
v-model="query"
aria-label="Type here to Search"
class="appearance-none w-full text-white font-hairline sm:text-5xl outline-none bg-transparent p-3"
type="search"
placeholder="Type to Search"
autofocus
autocomplete="off"
@keyup="queryResults(query); scrollInputToTop()"
>
@ -222,6 +222,10 @@ export default {
type: Number,
default: null
},
autofocus: {
type: Boolean,
default: true
},
quickButtons: {
type: Array,
default: () => [

View file

@ -1,5 +1,10 @@
// App Data that is derived from other app data
export function isDevice ( listing ) {
if ( !listing.hasOwnProperty('endpoint') ) return false
return listing.endpoint.startsWith('/device/')
}
export function isVideo ( app ) {
return app.hasOwnProperty('thumbnail') && app.hasOwnProperty('timestamps')
@ -13,6 +18,10 @@ export function getAppType ( app ) {
return 'video'
}
if ( isDevice( app ) ) {
return 'device'
}
if(app.category !== Object(app.category)) {
console.warn('app has no categories', app)

View file

@ -1,7 +1,6 @@
import { promises as fs } from 'fs'
import MarkdownIt from 'markdown-it'
import slugify from 'slugify'
import axios from 'axios'
import statuses, { getStatusName } from './statuses'
@ -9,17 +8,11 @@ import appStoreGenres from './app-store/genres.js'
import parseDate from './parse-date'
import { eitherMatches } from './matching.js'
import { getAppEndpoint } from './app-derived'
import { makeSlug } from './slug.js'
const md = new MarkdownIt()
const makeSlug = name => slugify(name, {
lower: true,
strict: true
})
const getTokenLinks = function ( childTokens ) {
const tokenList = []
@ -264,10 +257,7 @@ export default async function () {
if (isHeading && token.type === 'inline') {
categoryTitle = token.content
categorySlug = slugify(token.content, {
lower: true,
strict: true
})
categorySlug = makeSlug( token.content )
// appList[categorySlug] = []
}

View file

@ -0,0 +1,33 @@
import axios from 'axios'
import { makeSlug } from './slug.js'
export function getDeviceEndpoint ( slug ) {
return `/device/${ slug }`
}
export default async function () {
const devicesJsonUrl = `${process.env.VFUNCTIONS_URL}/api/devices`
const rawDeviceList = await axios.get(devicesJsonUrl)
.then( response => {
return response.data
})
.catch(function (error) {
// handle error
console.warn('Error fetching device list', error)
})
return rawDeviceList.filter( device => ( device.type !== 'ios' ) ).map( device => {
const slug = makeSlug( device.name )
return {
...device,
slug,
endpoint: getDeviceEndpoint( slug ),
}
})
}

View file

@ -1,10 +1,8 @@
import { promises as fs } from 'fs'
import slugify from 'slugify'
import axios from 'axios'
// import { statuses } from './build-app-list'
import { getAppEndpoint } from './app-derived'
import { makeSlug } from './slug.js'
// console.log('process.env.GAMES_SOURCE', process.env.GAMES_SOURCE)
@ -101,10 +99,7 @@ export default async function () {
if (isPlayable(game) && statusesTranslations.hasOwnProperty(environmentName(game)) === false) continue
// Generate slug
const slug = slugify(game.Games, {
lower: true,
strict: true
})
const slug = makeSlug( game.Games )
// Find index of game is list so far
const gameIndex = gameList.findIndex(game => {

View file

@ -1,11 +1,11 @@
import slugify from 'slugify'
import axios from 'axios'
import { fuzzyMatchesWholeWord } from './matching.js'
import { byTimeThenNull } from './sort-list.js'
import { getVideoEndpoint } from './app-derived.js'
import parseDate from './parse-date'
import { makeSlug } from './slug.js'
const inTimestamps = ( name, video ) => {
@ -150,10 +150,7 @@ export default async function ( applist ) {
if (fetchedVideos[videoId].title === 'Deleted video') continue
// Build video slug
const slug = slugify(`${fetchedVideos[videoId].title}-i-${videoId}`, {
lower: true,
strict: true
})
const slug = makeSlug( `${fetchedVideos[videoId].title}-i-${videoId}` )
const apps = []
// Generate new tag set based on api data

View file

@ -1,12 +1,8 @@
// Universal JS imports only
import slugify from 'slugify'
import { makeSlug } from './slug.js'
export function makeCategorySlug ( categoryName ) {
return slugify(categoryName, {
lower: true,
strict: true
})
return makeSlug( categoryName )
}

37
helpers/devices.js Normal file
View file

@ -0,0 +1,37 @@
import { getStatusName } from './statuses.js'
export const macAppleSiliconStatuses = new Set([
'native',
'rosetta'
])
export function deviceSupportsApp ( device, app ) {
// const statuses = {
// '✅': 'native',
// '✳️': 'rosetta',
// '⏹': 'no-in-progress',
// '🚫': 'no',
// '🔶': 'unreported',
// }
const appStatus = getStatusName( app.text )
if ( device.type === 'intel') {
return true
}
if ( device.type === 'mac-apple-silicon') {
return macAppleSiliconStatuses.has( appStatus )
}
// if ( device.type === 'ios') {
// return
// }
return false
}

9
helpers/slug.js Normal file
View file

@ -0,0 +1,9 @@
// Universal JS imports only
import slugify from 'slugify'
export function makeSlug ( name ) {
return slugify(name, {
lower: true,
strict: true
})
}

11
package-lock.json generated
View file

@ -13,6 +13,7 @@
"@open-wc/webpack-import-meta-loader": "^0.4.7",
"@zip.js/zip.js": "^2.2.6",
"axios": "^0.21.0",
"chance": "^1.1.7",
"cross-env": "^5.2.0",
"jsdom": "^16.4.0",
"lazysizes": "^5.3.0-beta1",
@ -4842,6 +4843,11 @@
"node": ">=8"
}
},
"node_modules/chance": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/chance/-/chance-1.1.7.tgz",
"integrity": "sha512-bua/2cZEfzS6qPm0vi3JEvGNbriDLcMj9lKxCQOjUcCJRcyjA7umP0zZm6bKWWlBN04vA0L99QGH/CZQawr0eg=="
},
"node_modules/character-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz",
@ -29210,6 +29216,11 @@
}
}
},
"chance": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/chance/-/chance-1.1.7.tgz",
"integrity": "sha512-bua/2cZEfzS6qPm0vi3JEvGNbriDLcMj9lKxCQOjUcCJRcyjA7umP0zZm6bKWWlBN04vA0L99QGH/CZQawr0eg=="
},
"character-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz",

View file

@ -26,6 +26,7 @@
"@open-wc/webpack-import-meta-loader": "^0.4.7",
"@zip.js/zip.js": "^2.2.6",
"axios": "^0.21.0",
"chance": "^1.1.7",
"cross-env": "^5.2.0",
"jsdom": "^16.4.0",
"lazysizes": "^5.3.0-beta1",

View file

@ -3,6 +3,7 @@ import dotenv from 'dotenv'
import config from '../nuxt.config.js'
import { getAppType } from '../helpers/app-derived.js'
import { deviceSupportsApp } from '../helpers/devices.js'
import { makeLastUpdatedFriendly } from '../helpers/parse-date'
@ -84,10 +85,24 @@ export class AppTemplate {
render( data ) {
const { app: { payload: { app, relatedVideos = [] } } } = data
const {
app: { payload: { app, relatedVideos = [] } },
'device-list': deviceList
} = data
// console.log('deviceList', deviceList)
// console.log('video.payload', Object.keys(video.payload))
const appDeviceSupport = deviceList.map( device => {
const supportsApp = deviceSupportsApp( device, app )
return {
...device,
emoji: supportsApp ? '✅' : '🚫',
ariaLabel: `${app.name} has ${ supportsApp ? '' : 'not' } been reported to work on ${device.name}`
}
})
const lastUpdatedFriendly = makeLastUpdatedFriendly( app.lastUpdated )
const relatedLinksHtml = renderPageLinksHtml( app.relatedLinks )
@ -109,6 +124,24 @@ export class AppTemplate {
<div class="links space-y-6 sm:space-x-6 mb-8">
${ relatedLinksHtml }
</div>
<div class="device-support w-full">
<h2 class="subtitle text-xl md:text-2xl font-bold mb-3">
Device Support
</h2>
<div class="device-support-apps overflow-x-auto overflow-y-visible md:whitespace-no-wrap py-2 space-x-2 space-y-3">
${ appDeviceSupport.map( device => /* html */`
<a
href="${ device.endpoint }"
role="button"
class="relative inline-flex items-center leading-5 font-bold text-white border border-transparent focus:outline-none focus:border-indigo-600 neumorphic-shadow-inner bg-darker hover:bg-indigo-400 active:bg-indigo-600 transition duration-150 ease-in-out text-xs rounded-lg px-4 py-2"
aria-label="${ device.ariaLabel }"
>${ device.emoji } ${ device.name }</a>
`).join('') }
</div>
</div>
</div>
${ relatedVideos.length > 0 ? /* html */`

211
pages/device/_slug.vue Normal file
View file

@ -0,0 +1,211 @@
<template>
<section class="container py-24">
<div class="flex flex-col items-center">
<h1 class="title text-3xl md:text-5xl font-hairline leading-tight text-center pb-4">
{{ device.name }}
</h1>
<div class="summary space-y-4 max-w-2xl">
<div class="flex justify-center py-3">
<LinkButton
v-if="device.amazonUrl"
:href="device.amazonUrl"
target="_blank"
>
Check Pricing
</LinkButton>
</div>
<h2
class="subtitle md:text-lg text-center"
>
App support for {{ device.name }}
</h2>
<h2
v-if="supportedAppList.length !== 0"
class="subtitle md:text-lg text-center"
>
Supported apps include {{ supportedAppList.join(', ') }}.
</h2>
</div>
<Search
:app-list="deviceAppList"
:quick-buttons="quickButtons"
:autofocus="false"
:initial-limit="50"
@update:query="query = $event"
>
<template v-slot:before-search>
<div class="empty-div" />
</template>
</Search>
<div class="flex flex-col md:flex-row space-x-0 space-y-4 md:space-y-0 md:space-x-4">
<LinkButton
:href="`https://github.com/ThatGuySam/doesitarm/issues?q=is%3Aissue+${query}`"
class="text-xs"
>
Request an App with Github
</LinkButton>
<LinkButton
:href="`https://twitter.com/DoesItARM/status/1330027384041508865`"
class="text-xs"
>
Request an App with Twitter
</LinkButton>
<LinkButton
:href="`/apple-silicon-app-test/`"
class="text-xs"
>
Scan Your Own App
</LinkButton>
</div>
</div>
</section>
</template>
<script>
import Search from '~/components/search.vue'
import LinkButton from '~/components/link-button.vue'
// import { categories } from '~/helpers/categories.js'
import { deviceSupportsApp } from '~/helpers/devices.js'
export default {
async asyncData ({ params: { slug } }) {
const { default: Chance } = await import('chance')
const { allList } = await import('~/helpers/get-list.js')
const { default: deviceList } = await import('~/static/device-list.json')
const charCode = slug.charCodeAt( slug.length-2 )
const shuffler = new Chance( charCode )
const device = deviceList.find( device => {
return device.slug === slug
})
// console.log( 'device', device )
const deviceAppList = allList.map( app => {
const appIsSupported = deviceSupportsApp( device, app )
return {
name: app.name,
status: app.status,
slug: app.slug,
// endpoint: app.endpoint,
text: appIsSupported ? `✅ Supported on ${device.name}` : `🚫 Not yet reported working on ${device.name}`,
lastUpdated: app.lastUpdated,
category: app.category,
// searchLinks: makeAppSearchLinks( app, (new Set(videoList)) )
}
})
const supportedApps = deviceAppList.filter( app => {
const supported = app.text.startsWith('✅')
const hasNotAllowedCategory = ([
'no-category',
'homebrew',
'games',
]).some( categorySlug => (app.category.slug === categorySlug) )
// console.log('hasNonStandardCategory', app.category.slug, hasNonStandardCategory)
return supported && !hasNotAllowedCategory
})
const featuredApps = shuffler.shuffle( supportedApps ).slice(0, 12)
// console.log('featuredApps', featuredApps[0])
return {
slug,
device,
featuredApps,
deviceAppList
}
},
components: {
Search,
LinkButton
},
data: function () {
return {
query: '',
quickButtons: []
}
},
computed: {
supportedAppList () {
return this.featuredApps.map(app => app.name)
},
title () {
return `App support list for ${this.device.name}`
},
description () {
return `Check the the latest reported support status of apps and software on ${this.device.name}.`
},
structuredData () {
return {
"@context": "https://schema.org",
// https://developers.google.com/search/docs/data-types/faqpage
// https://schema.org/FAQPage
"@type": "FAQPage",
"mainEntity": this.deviceAppList.map( app => {
return {
// https://schema.org/Question
"@type": "Question",
"name": `Does ${app.name} work on ${ this.device.name }?`,
"acceptedAnswer": {
// https://schema.org/Answer
"@type": "Answer",
"text": app.text
}
}
})
}
}
},
head() {
return {
title: this.title,
meta: [
// hid is used as unique identifier. Do not use `vmid` for it as it will not work
{
'hid': 'description',
'name': 'description',
'content': this.description
},
// Twitter Card
{
'hid': 'twitter:title',
'property': 'twitter:title',
'content': this.title
},
{
'hid': 'twitter:description',
'property': 'twitter:description',
'content': this.description
},
{
'property': 'twitter:url',
'content': `${process.env.URL}${this.$nuxt.$route.path}`
},
],
__dangerouslyDisableSanitizers: ['script'],
script: [{ innerHTML: JSON.stringify(this.structuredData), type: 'application/ld+json' }]
}
}
}
</script>

76
pages/devices.vue Normal file
View file

@ -0,0 +1,76 @@
<template>
<section class="container py-24">
<div class="flex flex-col">
<h1 class="title text-2xl leading-tight mb-6">
Devices
</h1>
<div class="line-separator border-white border-t-2 mb-12" />
<!-- deviceList: {{ deviceList }} -->
<ul class="device-list space-y-3">
<li
v-for="(device, i) in deviceList"
:key="`${device.slug}-${i}`"
:ref="`${device.slug}-row`"
class="relative"
>
<!-- device.endpoint: {{ device.endpoint }} -->
<a
:href="device.endpoint"
class="flex justify-start items-center inset-x-0 text-3xl md:text-4xl hover:bg-darkest border-2 border-white border-opacity-0 hover:border-opacity-50 focus:outline-none focus:bg-gray-50 duration-300 ease-in-out rounded-lg space-x-3 -mx-5 px-5 md:pr-64 py-3"
style="transition-property: border;"
>
<div class="font-hairline">
<div>{{ device.name }}</div>
<!-- <div class="text-xs opacity-75 mb-3">{{ device.appNames }}</div> -->
</div>
<div></div>
</a>
</li>
</ul>
</div>
</section>
</template>
<script>
import LinkButton from '~/components/link-button.vue'
export default {
async asyncData () {
const { default: deviceList } = await import('~/static/device-list.json')
return {
deviceList
}
},
components: {
LinkButton
},
data: function () {
return {}
},
// computed: {
// deviceList () {
// return deviceList
// }
// },
head() {
return {
title: `Categories of App Support for Apple Silicon - Does It ARM`,
// meta: [
// // hid is used as unique identifier. Do not use `vmid` for it as it will not work
// {
// hid: 'description',
// name: 'description',
// content: 'My custom description'
// }
// ]
}
}
}
</script>