mirror of
https://github.com/ThatGuySam/doesitarm.git
synced 2026-05-18 06:44:46 -07:00
Merge branch 'feat/astro'
This commit is contained in:
commit
60f7cfa451
109 changed files with 17945 additions and 3583 deletions
22
.gitignore
vendored
22
.gitignore
vendored
|
|
@ -68,9 +68,6 @@ typings/
|
|||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# Nuxt generate
|
||||
dist
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
|
|
@ -80,12 +77,23 @@ dist
|
|||
# IDE
|
||||
.idea
|
||||
|
||||
# Stork Executable
|
||||
/stork-executable
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
|
||||
# Build Output
|
||||
dist
|
||||
/static/sitemap*
|
||||
/static/app-list.json
|
||||
/static/**/*.json
|
||||
/static/**/*.toml
|
||||
/static/**/*.st
|
||||
/commits-data.json
|
||||
/static/tailwind.css
|
||||
|
||||
# Other
|
||||
/static/app-list.json
|
||||
/README-temp.md
|
||||
/static/**/*.json
|
||||
/commits-data.json
|
||||
.DS_Store
|
||||
/static/tailwind.css
|
||||
/.vscode/snipsnap.code-snippets
|
||||
|
|
|
|||
2
.nvmrc
2
.nvmrc
|
|
@ -1 +1 @@
|
|||
v16.13
|
||||
v18
|
||||
|
|
|
|||
4
.vscode/extensions.json
vendored
Normal file
4
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -90,9 +90,9 @@ html {
|
|||
* @import "utilities/skew-transforms";
|
||||
*/
|
||||
|
||||
.container {
|
||||
/* .container {
|
||||
max-width: 1040px;
|
||||
}
|
||||
} */
|
||||
|
||||
.ease {
|
||||
transition-property: all;
|
||||
|
|
@ -153,19 +153,79 @@ html {
|
|||
.shimmer {
|
||||
animation: placeHolderShimmer 1s infinite;
|
||||
animation-timing-function: linear;
|
||||
background: #f6f7f8;
|
||||
background: linear-gradient(to right, rgba(0, 0, 0, 0.07) 8%, rgba(0, 0, 0, 0.45) 18%, rgba(0, 0, 0, 0.07) 33%);
|
||||
background-color: rgba( 0, 0, 0, 0.1 );
|
||||
background-image: linear-gradient( 90deg, rgba(0, 0, 0, 0.07) 8%, rgba(0, 0, 0, 0.45) 18%, rgba(0, 0, 0, 0.07) 33%);
|
||||
background-size: 200% 100px;
|
||||
background-position: 0 0, 100% 0;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
/* Original - https://codepen.io/ThatGuySam/pen/zYPPYwg */
|
||||
/* Animation delay so our shimmer looks staggered */
|
||||
|
||||
.shimmer *:nth-child(3n-2),
|
||||
*:nth-child(3n-2) > .shimmer {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.shimmer *:nth-child(3n-1),
|
||||
*:nth-child(3n-1) > .shimmer {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.shimmer,
|
||||
.shimmer * {
|
||||
/* Hide all text */
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.shimmer p,
|
||||
.shimmer img,
|
||||
.shimmer span,
|
||||
.shimmer time,
|
||||
.shimmer h1, .shimmer h2, .shimmer h3, .shimmer h4, .shimmer h5, .shimmer h6 {
|
||||
animation: placeHolderShimmer 1s infinite;
|
||||
animation-timing-function: linear;
|
||||
|
||||
/* Shimmer gets inserted as an animated background so it can shape to most elements */
|
||||
|
||||
/* Base shimmer color */
|
||||
background-color: rgba( 0, 0, 0, 0.1 );
|
||||
/* First and last color should be the same so that animation restart is seamless */
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 0, 0, 0.07) 8%,
|
||||
rgba(0, 0, 0, 0.13) 18%,
|
||||
rgba(0, 0, 0, 0.07) 33%
|
||||
);
|
||||
background-size: 200% 100px;
|
||||
background-attachment: fixed;
|
||||
|
||||
border: none;
|
||||
|
||||
/* Hide all text */
|
||||
color: transparent;
|
||||
|
||||
/* Hide img src */
|
||||
object-position: -99999px 99999px;
|
||||
}
|
||||
|
||||
/* Inline Shimmers so we get separated shimmer lines on text */
|
||||
.shimmer p,
|
||||
.shimmer span,
|
||||
.shimmer time {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@keyframes placeHolderShimmer {
|
||||
0% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -100% 0;
|
||||
}
|
||||
0% {
|
||||
background-position: 100% 0
|
||||
}
|
||||
100% {
|
||||
background-position: -100% 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
61
astro.config.mjs
Normal file
61
astro.config.mjs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { defineConfig } from 'astro/config'
|
||||
import vue from '@astrojs/vue'
|
||||
import tailwind from '@astrojs/tailwind'
|
||||
// Astro Netlify Reference
|
||||
// https://github.com/withastro/astro/tree/main/packages/integrations/netlify
|
||||
import netlify from '@astrojs/netlify/functions'
|
||||
// import sitemap from '@astrojs/sitemap'
|
||||
import partytown from '@astrojs/partytown'
|
||||
|
||||
|
||||
// import { viteCommonjs } from '@originjs/vite-plugin-commonjs'
|
||||
|
||||
import { makeViteDefinitions } from './helpers/public-runtime-config.mjs'
|
||||
|
||||
console.log( 'Running Astro Config File' )
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
publicDir: './static',
|
||||
site: 'https://doesitarm.com',
|
||||
integrations: [
|
||||
netlify({
|
||||
dist: new URL('./dist/', import.meta.url)
|
||||
}),
|
||||
// Astro Vue Reference
|
||||
// https://github.com/withastro/astro/tree/main/packages/integrations/vue
|
||||
vue(),
|
||||
tailwind(),
|
||||
// Sitemap Reference
|
||||
// https://github.com/withastro/astro/blob/main/packages/integrations/sitemap/src/index.ts
|
||||
// https://github.com/withastro/astro/tree/main/packages/integrations/sitemap#configuration
|
||||
// sitemap({
|
||||
// customPages: [
|
||||
// '/relative-url',
|
||||
// 'https://doesitarm.com/absolute-url',
|
||||
// ]
|
||||
// })
|
||||
partytown({
|
||||
// Add dataLayer.push as a forwarding-event.
|
||||
// https://github.com/withastro/astro/tree/main/packages/integrations/partytown#configforward
|
||||
config: { forward: [ 'dataLayer.push' ] },
|
||||
}),
|
||||
],
|
||||
// Vite options
|
||||
// https://docs.astro.build/en/reference/configuration-reference/#vite
|
||||
vite: {
|
||||
// Vite: https://vitejs.dev/config/#define
|
||||
// esbuild: https://esbuild.github.io/api/#define
|
||||
define: {
|
||||
...makeViteDefinitions()
|
||||
},
|
||||
// plugins: [
|
||||
// viteCommonjs()
|
||||
// ],
|
||||
build: {
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
19
ava.config.js
Normal file
19
ava.config.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
export default () => {
|
||||
return {
|
||||
require: [
|
||||
'dotenv/config',
|
||||
'esm',
|
||||
'tsconfig-paths/register'
|
||||
],
|
||||
// https://github.com/avajs/ava/blob/main/docs/recipes/watch-mode.md
|
||||
ignoredByWatcher: [
|
||||
'!**/*.{js,vue}',
|
||||
'./build',
|
||||
'./dist',
|
||||
'./.output',
|
||||
],
|
||||
// tap: true,
|
||||
// verbose: true,
|
||||
color: true
|
||||
}
|
||||
}
|
||||
537
build-lists.js
537
build-lists.js
|
|
@ -1,38 +1,90 @@
|
|||
import { dirname } from 'path'
|
||||
import { dirname, basename } from 'path'
|
||||
|
||||
import fs from 'fs-extra'
|
||||
import dotenv from 'dotenv'
|
||||
import semver from 'semver'
|
||||
import { PromisePool } from '@supercharge/promise-pool'
|
||||
import memoize from 'fast-memoize'
|
||||
import has from 'just-has'
|
||||
|
||||
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 { saveYouTubeVideos } from '~/helpers/api/youtube/build.js'
|
||||
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 {
|
||||
saveSitemap,
|
||||
getUrlsForAstroDefinedPages
|
||||
} from '~/helpers/api/sitemap/build.js'
|
||||
import { deviceSupportsApp } from '~/helpers/devices.js'
|
||||
import getListSummaryNumbers from '~/helpers/get-list-summary-numbers.js'
|
||||
import { logArraysDifference } from '~/helpers/array.js'
|
||||
|
||||
import { videosRelatedToApp } from './helpers/related.js'
|
||||
import { buildVideoPayload, buildAppBenchmarkPayload } from './helpers/build-payload.js'
|
||||
import {
|
||||
getRelatedVideos
|
||||
} from '~/helpers/related.js'
|
||||
import { buildVideoPayload, buildAppBenchmarkPayload } from '~/helpers/build-payload.js'
|
||||
|
||||
import { categories, getAppCategory } from './helpers/categories.js'
|
||||
import {
|
||||
categories,
|
||||
getCategoryKindName
|
||||
} from '~/helpers/categories.js'
|
||||
import {
|
||||
getAppType,
|
||||
getAppEndpoint,
|
||||
getVideoEndpoint,
|
||||
isVideo
|
||||
} from './helpers/app-derived.js'
|
||||
import { makeSearchableList } from './helpers/searchable-list.js'
|
||||
} from '~/helpers/app-derived.js'
|
||||
import { makeSearchableList } from '~/helpers/searchable-list.js'
|
||||
|
||||
import {
|
||||
writeStorkToml
|
||||
} from '~/helpers/stork/toml.js'
|
||||
import {
|
||||
KindListMemoized as KindList
|
||||
} from '~/helpers/api/kind.js'
|
||||
import {
|
||||
apiDirectory
|
||||
} from '~/helpers/api/config.js'
|
||||
|
||||
import {
|
||||
cliOptions
|
||||
} from '~/helpers/cli-options.js'
|
||||
|
||||
// Setup dotenv
|
||||
dotenv.config()
|
||||
|
||||
const commandArguments = process.argv
|
||||
const cliOptions = {
|
||||
withApi: commandArguments.includes('--with-api'),
|
||||
noLists: commandArguments.includes('--no-lists'),
|
||||
|
||||
let timeRunGetListArray = 0
|
||||
let timeRunGetListByCategories = 0
|
||||
|
||||
|
||||
|
||||
function normalizeVersion ( rawVersion ) {
|
||||
const containsNumbers = /\d+/.test( rawVersion )
|
||||
|
||||
if ( !containsNumbers ) {
|
||||
return '0.0.0'
|
||||
}
|
||||
|
||||
let version = rawVersion
|
||||
|
||||
// Parse each part
|
||||
version = version
|
||||
.split('.')
|
||||
.map( part => {
|
||||
// Trim leading zeros
|
||||
return part.replace(/^0+/, '')
|
||||
} )
|
||||
.join('.')
|
||||
|
||||
return semver.coerce(version)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
class BuildLists {
|
||||
|
||||
constructor () {
|
||||
|
|
@ -59,57 +111,39 @@ class BuildLists {
|
|||
// Main build methods
|
||||
{
|
||||
name: 'app',
|
||||
endpointPrefix: 'app',
|
||||
path: '/static/app-list.json',
|
||||
buildMethod: buildAppList,
|
||||
},
|
||||
{
|
||||
name: 'game',
|
||||
endpointPrefix: 'game',
|
||||
path: '/static/game-list.json',
|
||||
buildMethod: buildGamesList,
|
||||
},
|
||||
{
|
||||
name: 'homebrew',
|
||||
endpointPrefix: 'formula',
|
||||
path: '/static/homebrew-list.json',
|
||||
buildMethod: buildHomebrewList,
|
||||
},
|
||||
{
|
||||
name: 'device',
|
||||
endpointPrefix: 'device',
|
||||
path: '/static/device-list.json',
|
||||
buildMethod: buildDeviceList,
|
||||
},
|
||||
|
||||
// Secondary Derivative built lists
|
||||
// Always goes after initial lists
|
||||
// since it depend on them
|
||||
// since it depends on them
|
||||
{
|
||||
name: 'video',
|
||||
endpointPrefix: 'tv',
|
||||
path: '/static/video-list.json',
|
||||
buildMethod: async () => {
|
||||
|
||||
// console.log('this.getAllVideoAppsList()', this.getAllVideoAppsList())
|
||||
|
||||
return await buildVideoList( this.getAllVideoAppsList() )
|
||||
|
||||
|
||||
// const videoList = await buildVideoList( this.getAllVideoAppsList() )
|
||||
|
||||
// const extraVideos = []
|
||||
|
||||
// const multiplier = 12
|
||||
|
||||
// for (let i = 0; i < multiplier; i++) {
|
||||
// videoList.forEach( video => {
|
||||
// extraVideos.push({
|
||||
// ...video,
|
||||
// slug: video.slug + '-' + i,
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
|
||||
// return new Set([
|
||||
// ...videoList,
|
||||
// ...extraVideos
|
||||
// ].slice(0, 10 * 1000))
|
||||
},
|
||||
beforeSave: videoListSet => {
|
||||
this.allVideoAppsList = this.getAllVideoAppsList()
|
||||
|
|
@ -124,6 +158,28 @@ class BuildLists {
|
|||
}
|
||||
]
|
||||
|
||||
getListOptions ( listName ) {
|
||||
return this.listsOptions.find( listOption => listOption.name === listName )
|
||||
}
|
||||
|
||||
shouldHaveRelatedVideos ( app ) {
|
||||
const appType = getAppType( app )
|
||||
|
||||
const typeWithRelatedVideos = new Set([
|
||||
'app',
|
||||
'formula',
|
||||
'game'
|
||||
])
|
||||
|
||||
return typeWithRelatedVideos.has( appType )
|
||||
}
|
||||
|
||||
shouldHaveDeviceSupport ( app ) {
|
||||
const appType = getAppType( app )
|
||||
|
||||
return appType === 'app' || appType === 'formula' || appType === 'game'
|
||||
}
|
||||
|
||||
getAllVideoAppsList = () => {
|
||||
return new Set([
|
||||
...this.lists.app,
|
||||
|
|
@ -131,6 +187,69 @@ class BuildLists {
|
|||
])
|
||||
}
|
||||
|
||||
bundles = []
|
||||
async getSavedAppBundles ( options = {} ) {
|
||||
const {
|
||||
keepBundlesInMemory = true
|
||||
} = options
|
||||
|
||||
if ( !keepBundlesInMemory ) {
|
||||
// console.log('Getting bundles from file')
|
||||
return await fs.readJson('./static/app-bundles.json')
|
||||
}
|
||||
|
||||
// From here we get and store bundles
|
||||
|
||||
|
||||
// Check if any bundles are already in memory
|
||||
if ( this.bundles.length === 0 ) {
|
||||
// console.log('Storing bundles to memory')
|
||||
this.bundles = await fs.readJson('./static/app-bundles.json')
|
||||
}
|
||||
|
||||
// console.log('Getting bundles from memory')
|
||||
return this.bundles
|
||||
}
|
||||
|
||||
// Load the bundles from files
|
||||
// so that we don't have to keep them in memory
|
||||
async findAppBundle ( needleBundleIdentifier ) {
|
||||
const bundles = await this.getSavedAppBundles()
|
||||
|
||||
return bundles.find( ([
|
||||
storedAppBundleIdentifier,
|
||||
// versions
|
||||
]) => storedAppBundleIdentifier === needleBundleIdentifier )
|
||||
}
|
||||
|
||||
async getAppBundles ( app ) {
|
||||
return await Promise.all( app.bundleIds.map( async bundleIdentifier => {
|
||||
return await this.findAppBundle( bundleIdentifier )
|
||||
} ) )
|
||||
}
|
||||
|
||||
sortBundleVersions ( bundles ) {
|
||||
return bundles.map( bundle => {
|
||||
const [
|
||||
bundleIdentifier,
|
||||
versionsObject
|
||||
] = bundle
|
||||
|
||||
// Sort versions by semver
|
||||
const versions = Object.entries( versionsObject ).sort( ( [ aVersionRaw ], [ bVersionRaw ] ) => {
|
||||
const aVersion = normalizeVersion( aVersionRaw )
|
||||
const bVersion = normalizeVersion( bVersionRaw )
|
||||
|
||||
return semver.compare( bVersion, aVersion )
|
||||
} )
|
||||
|
||||
return [
|
||||
bundleIdentifier,
|
||||
versions
|
||||
]
|
||||
} )
|
||||
}
|
||||
|
||||
saveToJson = async function ( content, path ) {
|
||||
|
||||
// Write the list to JSON
|
||||
|
|
@ -149,24 +268,21 @@ class BuildLists {
|
|||
const hasSaveMethod = listOptions.hasOwnProperty('beforeSave')
|
||||
const saveMethod = hasSaveMethod ? listOptions.beforeSave : listSet => Array.from( listSet )
|
||||
|
||||
|
||||
|
||||
// console.log('listFullPath', listFullPath)
|
||||
|
||||
const saveableList = saveMethod( this.lists[listOptions.name] )
|
||||
|
||||
console.log('saveableList', typeof saveableList)
|
||||
// console.log('saveableList', typeof saveableList)
|
||||
|
||||
// Stringify one at a time to allow for large lists
|
||||
const saveableListJSON = '[' + saveableList.map(el => JSON.stringify(el)).join(',') + ']'
|
||||
|
||||
// Write the list to JSON
|
||||
await fs.writeFile(listFullPath, JSON.stringify( saveableList ))
|
||||
await fs.writeFile(listFullPath, saveableListJSON)
|
||||
|
||||
// Read back the JSON we just wrote to ensure it exists
|
||||
const savedListJSON = await fs.readFile(listFullPath, 'utf-8')
|
||||
|
||||
// console.log('savedListJSON', savedListJSON)
|
||||
|
||||
const savedList = JSON.parse(savedListJSON)
|
||||
|
||||
// Import the created JSON File
|
||||
return savedList
|
||||
return
|
||||
}
|
||||
|
||||
// Run all listsOprions methods
|
||||
|
|
@ -194,11 +310,152 @@ class BuildLists {
|
|||
return
|
||||
}
|
||||
|
||||
saveApiEndpoints = async function ( listOptions ) {
|
||||
getListArray ( listName ) {
|
||||
console.log(`getListArray run ${ timeRunGetListArray += 1 } times`)
|
||||
|
||||
await PromisePool
|
||||
.withConcurrency(1000)
|
||||
.for( Array.from( this.lists[listOptions.name] ) )
|
||||
return Array.from( this.lists[ listName ] )
|
||||
}
|
||||
|
||||
getListArrayMemoized = memoize( this.getListArray.bind( this ) )
|
||||
|
||||
makeAppsByCategory () {
|
||||
// Intialize empty category lists
|
||||
// so empty categories still get defined
|
||||
const emptyCategories = Object.fromEntries(
|
||||
Object.keys( categories ).map( categorySlug => [ categorySlug, [] ])
|
||||
)
|
||||
|
||||
const appsByCategory = this.getListArrayMemoized( 'app' ).reduce( ( categories, app ) => {
|
||||
|
||||
const categorySlug = app.category.slug
|
||||
|
||||
if ( !categories[categorySlug] ) {
|
||||
throw Error(`Category ${categorySlug} not defined`)
|
||||
}
|
||||
|
||||
categories[categorySlug].push( app )
|
||||
|
||||
return categories
|
||||
}, emptyCategories )
|
||||
|
||||
return appsByCategory
|
||||
}
|
||||
|
||||
getAppsByCategory = this.makeAppsByCategory//memoize( this.makeAppsByCategory.bind( this ) )
|
||||
|
||||
getAppCategoryList ( category ) {
|
||||
const categoryLists = this.getAppsByCategory()
|
||||
|
||||
return categoryLists[category]
|
||||
}
|
||||
|
||||
makeKindLists () {
|
||||
const getters = Object.fromEntries( Object.entries( this.lists ).map( ([ listName ]) => {
|
||||
|
||||
const listEndpointPrefix = this.getListOptions( listName ).endpointPrefix
|
||||
|
||||
return [
|
||||
// Key
|
||||
listEndpointPrefix,
|
||||
() => this.getListArrayMemoized( listName )
|
||||
]
|
||||
} ) )
|
||||
|
||||
// Add getters for categories
|
||||
// Homebrew category overrides Homebrew app type from above
|
||||
for ( const categorySlug in categories ) {
|
||||
// Throw if category already defined
|
||||
if ( getters[categorySlug] ) throw Error(`Category ${categorySlug} already defined`)
|
||||
|
||||
getters[categorySlug] = () => this.getAppCategoryList( categorySlug )
|
||||
}
|
||||
|
||||
const kindLists = {}
|
||||
|
||||
for ( const kindSlug in getters ) {
|
||||
// Throw if kindSlug already defined
|
||||
if ( kindLists[kindSlug] ) throw Error(`Kind ${kindSlug} already defined`)
|
||||
|
||||
kindLists[ kindSlug ] = new KindList({
|
||||
// Get list method
|
||||
list: getters[ kindSlug ],
|
||||
kindSlug
|
||||
})
|
||||
}
|
||||
|
||||
return kindLists
|
||||
}
|
||||
|
||||
get kindRoutes () {
|
||||
return Object.entries( this.makeKindLists() ).map( ([ kindSlug, kindList ]) => {
|
||||
return kindList.routes
|
||||
}).flat()
|
||||
}
|
||||
|
||||
async saveKinds () {
|
||||
|
||||
const kindLists = this.makeKindLists()
|
||||
|
||||
// Save the lists
|
||||
for ( const kindSlug in kindLists ) {
|
||||
|
||||
console.log('\n', `-- Starting kind lists for ${ kindSlug }`)
|
||||
|
||||
const endpointMethodName = `Finished kind lists for ${ kindSlug }`
|
||||
console.time(endpointMethodName)
|
||||
|
||||
|
||||
const kindList = kindLists[ kindSlug ]
|
||||
|
||||
// Ensure the base directory exists
|
||||
await fs.ensureDir( kindList.basePath )
|
||||
|
||||
for ( const file of kindList.apiFiles ) {
|
||||
// console.log('kindPage.items', kindPage)
|
||||
|
||||
await this.saveToJson( file.content, file.path )
|
||||
}
|
||||
|
||||
console.timeEnd(endpointMethodName)
|
||||
console.log( '\n\n' )
|
||||
}
|
||||
|
||||
const kindIndex = categories
|
||||
|
||||
// Delete no-category
|
||||
delete kindIndex['no-category']
|
||||
|
||||
// Store sample names into kindIndex as description
|
||||
for ( const categorySlug in kindIndex ) {
|
||||
const kindName = getCategoryKindName( categorySlug )
|
||||
|
||||
// Skip empty categories
|
||||
if ( kindLists[ kindName ].list.length === 0 ) continue
|
||||
|
||||
kindIndex[ categorySlug ].description = kindLists[ kindName ].summary.sampleNames
|
||||
// Save kindName
|
||||
kindIndex[ categorySlug ].kindName = kindName
|
||||
}
|
||||
|
||||
// Save the index
|
||||
await this.saveToJson( kindIndex, `${ apiDirectory }/kind/index.json` )
|
||||
|
||||
}
|
||||
|
||||
saveApiEndpoints = async ( listOptions ) => {
|
||||
|
||||
const apiListDirectory = `${ apiDirectory }/${ listOptions.endpointPrefix }`
|
||||
|
||||
const poolSize = 1000
|
||||
|
||||
// Store app bundles to memory
|
||||
await this.getSavedAppBundles({
|
||||
keepBundlesInMemory: true
|
||||
})
|
||||
|
||||
const { errors } = await PromisePool
|
||||
.withConcurrency( poolSize )
|
||||
.for( this.getListArrayMemoized( listOptions.name ) )
|
||||
.process(async ( listEntry, index, pool ) => {
|
||||
// console.log('listEntry', listEntry)
|
||||
|
||||
|
|
@ -211,31 +468,71 @@ class BuildLists {
|
|||
|
||||
} = listEntry
|
||||
|
||||
const endpointPath = `./static/api${endpoint}.json`
|
||||
const endpointPath = `${ apiDirectory }${ endpoint }.json`
|
||||
const endpointDirectory = dirname(endpointPath)
|
||||
|
||||
// Stop if the endpoint is already exists
|
||||
if (fs.existsSync(endpointPath)) {
|
||||
console.log(`Path "${endpointPath}" already exists`)
|
||||
|
||||
return
|
||||
// Add related videos
|
||||
if ( this.shouldHaveRelatedVideos( listEntry ) ) {
|
||||
listEntry.relatedVideos = getRelatedVideos({
|
||||
listing: listEntry,
|
||||
videoListSet: this.lists.video,
|
||||
appListSet: this.allVideoAppsList
|
||||
})
|
||||
}
|
||||
|
||||
// console.log(`Saving endpoint "${endpoint}" to "${endpointPath}"`)
|
||||
// Add App Bundles
|
||||
if ( Array.isArray( listEntry.bundleIds ) ) {
|
||||
listEntry.bundles = await this.getAppBundles( listEntry )
|
||||
|
||||
listEntry.bundles = this.sortBundleVersions( listEntry.bundles )
|
||||
}
|
||||
|
||||
|
||||
// Add device support
|
||||
if ( this.shouldHaveDeviceSupport( listEntry ) ) {
|
||||
const deviceList = this.getListArrayMemoized( 'device' )
|
||||
|
||||
listEntry.deviceSupport = deviceList.map( device => {
|
||||
const supportsApp = deviceSupportsApp( device, listEntry )
|
||||
return {
|
||||
...device,
|
||||
emoji: supportsApp ? '✅' : '🚫',
|
||||
ariaLabel: `${ listEntry.name } has ${ supportsApp ? '' : 'not' } been reported to work on ${ device.name }`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const hasSaveMethod = has( listOptions, 'beforeSave' )
|
||||
const saveMethod = hasSaveMethod ? listOptions.beforeSave : listSet => Array.from( listSet )
|
||||
|
||||
const [ saveableEntry ] = saveMethod( new Set( [ listEntry ] ) )
|
||||
|
||||
// Ensure the directory exists
|
||||
await fs.ensureDir( endpointDirectory )
|
||||
|
||||
// Write the endpoint to JSON
|
||||
await this.saveToJson( listEntry, endpointPath )
|
||||
await this.saveToJson( saveableEntry, endpointPath )
|
||||
})
|
||||
|
||||
if ( errors.length !== 0 ) {
|
||||
throw new Error( errors )
|
||||
}
|
||||
|
||||
// Save each enpoint's data
|
||||
// for ( const listEntry of this.lists[listOptions.name] ) {
|
||||
// Count saved files
|
||||
const fileCount = fs.readdirSync( apiListDirectory ).length
|
||||
|
||||
console.log( fileCount, 'Files saved in', apiListDirectory )
|
||||
console.log( this.lists[listOptions.name].size, 'Entries' )
|
||||
|
||||
if ( fileCount !== this.lists[listOptions.name].size ) {
|
||||
const listSlugs = Array.from( this.lists[listOptions.name] ).map( listEntry => listEntry.slug )
|
||||
const fileNames = fs.readdirSync( apiListDirectory ).map( fileName => basename(fileName).split('.')[0] )
|
||||
|
||||
logArraysDifference( listSlugs, fileNames )
|
||||
|
||||
throw new Error( `Files (${ fileCount }) don\'t match list count in ${ apiListDirectory }(${ this.lists[listOptions.name].size }).` )
|
||||
}
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
// Save app lists to JSON
|
||||
|
|
@ -266,33 +563,53 @@ class BuildLists {
|
|||
console.timeEnd(methodName)
|
||||
|
||||
if ( cliOptions.withApi ) {
|
||||
console.log('Saving individual endpoints...')
|
||||
console.log('\n', `-- Starting individual /${ listOptions.name } endpoints`)
|
||||
|
||||
const endpointMethodName = `Saved /${ listOptions.name } endpoints`
|
||||
const endpointMethodName = `Finished individual /${ listOptions.name } endpoints`
|
||||
console.time(endpointMethodName)
|
||||
|
||||
await this.saveApiEndpoints( listOptions )
|
||||
|
||||
console.timeEnd(endpointMethodName)
|
||||
console.log( '\n\n' )
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
console.log('Save lists finished')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
async saveAllAppsSummary () {
|
||||
const summaryNumbers = getListSummaryNumbers( [
|
||||
...this.getListArray('app'),
|
||||
...this.getListArray('game'),
|
||||
...this.getListArray('homebrew'),
|
||||
] )
|
||||
|
||||
await this.saveToJson( summaryNumbers, `${apiDirectory}/all-apps-summary.json` )
|
||||
|
||||
return summaryNumbers
|
||||
}
|
||||
|
||||
async build () {
|
||||
|
||||
await saveYouTubeVideos()
|
||||
|
||||
// Pull in and layer data from all sources
|
||||
await this.buildLists()
|
||||
|
||||
// Save the data to respective files as lists
|
||||
await this.saveAppLists()
|
||||
|
||||
await this.saveAllAppsSummary()
|
||||
|
||||
// Save kind lists
|
||||
await this.saveKinds()
|
||||
|
||||
// console.log('appList', appList)
|
||||
|
||||
// console.log('this.allVideoAppsList', this.allVideoAppsList.length, this.allVideoAppsList[0])
|
||||
|
||||
// Add list based routes
|
||||
for ( const listKey in this.lists ) {
|
||||
|
|
@ -331,14 +648,12 @@ class BuildLists {
|
|||
}
|
||||
|
||||
// Add standard app endpoint
|
||||
if ( appType === 'app' || appType === 'formula' ) {
|
||||
if ( this.shouldHaveRelatedVideos( app ) ) {
|
||||
|
||||
const relatedVideos = videosRelatedToApp( app, this.lists.video ).map(video => {
|
||||
// console.log('video', video)
|
||||
return {
|
||||
...video,
|
||||
endpoint: `${getAppEndpoint(app)}/benchmarks#${video.id}`
|
||||
}
|
||||
const relatedVideos = getRelatedVideos({
|
||||
listing: app,
|
||||
videoListSet: this.lists.video,
|
||||
appListSet: this.allVideoAppsList
|
||||
})
|
||||
|
||||
// Add app or formula endpoint
|
||||
|
|
@ -373,33 +688,45 @@ class BuildLists {
|
|||
this.endpointMaps.nuxt.set( '/kind/' + slug, {} )
|
||||
})
|
||||
|
||||
|
||||
// Save Nuxt Endpoints
|
||||
// await this.saveToJson(Array.from(this.endpointMaps.nuxt), './static/nuxt-endpoints.json')
|
||||
|
||||
// // Save Eleventy Endpoints
|
||||
// await this.saveToJson(Array.from(this.endpointMaps.eleventy), './static/eleventy-endpoints.json')
|
||||
|
||||
// console.log('this.endpointMaps.eleventy /app/chrome', this.endpointMaps.eleventy.get( '/app/chrome' ))
|
||||
|
||||
|
||||
// Filter eleventy endpoints
|
||||
// this.endpointMaps.eleventy = new Set([
|
||||
// ['/app/chrome', this.endpointMaps.eleventy.get( '/app/chrome' )]
|
||||
// ])
|
||||
|
||||
if ( !cliOptions.withApi ) {
|
||||
for ( const [ endpointSetName, endpointSet ] of Object.entries(this.endpointMaps) ) {
|
||||
// Save Endpoints
|
||||
await this.saveToJson(Array.from( endpointSet , ([route, payload]) => ({ route, payload })), `./static/${endpointSetName}-endpoints.json`)
|
||||
}
|
||||
|
||||
// Save sitemap endpoints
|
||||
await this.saveToJson(Object.values(this.endpointMaps).map( endpointSet => {
|
||||
return Array.from( endpointSet , ([route, payload]) => ({ route, payload }) )
|
||||
} ).flat(1), './static/sitemap-endpoints.json')
|
||||
for ( const [ endpointSetName, endpointSet ] of Object.entries(this.endpointMaps) ) {
|
||||
// Save Endpoints
|
||||
await this.saveToJson(Array.from( endpointSet , ([route, payload]) => ({ route, payload })), `./static/${endpointSetName}-endpoints.json`)
|
||||
}
|
||||
|
||||
const sitemapEndpoints = Object.values(this.endpointMaps).map( endpointSet => {
|
||||
return Array.from( endpointSet , ([route, payload]) => ({ route, payload }) )
|
||||
} ).flat(1)
|
||||
|
||||
// Add kind routes to sitemap
|
||||
this.kindRoutes.forEach( route => {
|
||||
sitemapEndpoints.push({
|
||||
route,
|
||||
payload: {}
|
||||
})
|
||||
} )
|
||||
|
||||
// Add routes for Astro pages
|
||||
const astroPageUrls = await getUrlsForAstroDefinedPages()
|
||||
astroPageUrls.forEach( url => {
|
||||
sitemapEndpoints.push({
|
||||
route: url,
|
||||
payload: {}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
// Save sitemap endpoints
|
||||
console.log('Building Sitemap JSON')
|
||||
await this.saveToJson( sitemapEndpoints, './static/sitemap-endpoints.json')
|
||||
|
||||
// Save XML Sitemap
|
||||
console.log('Building XML Sitemap')
|
||||
await saveSitemap( sitemapEndpoints.map( ({ route }) => route ) )
|
||||
|
||||
// Save stork toml index
|
||||
console.log('Building Stork toml index')
|
||||
await writeStorkToml( sitemapEndpoints )
|
||||
|
||||
console.log('Total Nuxt Endpoints', this.endpointMaps.nuxt.size )
|
||||
console.log('Total Eleventy Endpoints', this.endpointMaps.eleventy.size )
|
||||
console.log('Total Endpoints', this.endpointMaps.nuxt.size + this.endpointMaps.eleventy.size )
|
||||
|
|
@ -412,9 +739,3 @@ class BuildLists {
|
|||
const listBuilder = new BuildLists()
|
||||
|
||||
listBuilder.build()
|
||||
|
||||
// export default async function () {
|
||||
// const listBuilder = new BuildLists()
|
||||
|
||||
// return await listBuilder.build()
|
||||
// }
|
||||
|
|
|
|||
23
build-stork.js
Normal file
23
build-stork.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import fs from 'fs-extra'
|
||||
|
||||
import {
|
||||
downloadStorkExecutable,
|
||||
// writeStorkToml
|
||||
} from './helpers/stork/toml.js'
|
||||
|
||||
;(async () => {
|
||||
|
||||
console.log( 'Downloading Stork executable...' )
|
||||
await downloadStorkExecutable()
|
||||
|
||||
// console.log( 'Building Stork index TOML...' )
|
||||
|
||||
// Get Sitemap Endpoints JSON
|
||||
// const sitemap = await fs.readJson( './static/sitemap-endpoints.json' )
|
||||
|
||||
// await writeStorkToml( sitemap )
|
||||
|
||||
// From here we hand off to the Stork executable
|
||||
|
||||
process.exit()
|
||||
})()
|
||||
|
|
@ -60,6 +60,9 @@
|
|||
<script>
|
||||
|
||||
import axios from 'axios'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import { isNuxt } from '~/helpers/environment.js'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
|
@ -78,6 +81,10 @@ export default {
|
|||
inputClassGroups: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
uuid: {
|
||||
type: String,
|
||||
default: uuid()
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
|
|
@ -90,7 +97,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
inputId () {
|
||||
return `all-updates-subscribe-${this._uid}`
|
||||
return `all-updates-subscribe-${ this.uuid }`
|
||||
},
|
||||
inputClasslist () {
|
||||
const defaultClassGroups = {
|
||||
|
|
@ -115,6 +122,7 @@ export default {
|
|||
return Object.values(mergedClassGroups)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async trySubmit () {
|
||||
console.log('Trying submit')
|
||||
|
|
@ -124,10 +132,15 @@ export default {
|
|||
|
||||
// const pagePath = $nuxt.$route.path
|
||||
|
||||
// console.log('this.$config.allUpdateSubscribe', this.$config.allUpdateSubscribe)
|
||||
const allUpdateSubscribe = isNuxt( this ) ? this.$config.allUpdateSubscribe : global.$config.allUpdateSubscribe
|
||||
|
||||
console.log('allUpdateSubscribe', allUpdateSubscribe)
|
||||
|
||||
|
||||
// https://stackoverflow.com/questions/51995070/post-data-to-a-google-form-with-ajax/55496118#55496118
|
||||
const actionUrl = this.$config.allUpdateSubscribe
|
||||
const actionUrl = allUpdateSubscribe
|
||||
|
||||
console.log('actionUrl', actionUrl)
|
||||
|
||||
axios({
|
||||
method: 'post',
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
<template>
|
||||
<div class="doesitarm-carbon-wrapper">
|
||||
<component
|
||||
:is="'script'"
|
||||
v-once
|
||||
id="_carbonads_js"
|
||||
|
||||
:is="'script'"
|
||||
src="https://cdn.carbonads.com/carbon.js?serve=CK7DVK3M&placement=doesitarmcom"
|
||||
type="text/javascript"
|
||||
async
|
||||
class="include-on-static"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default {
|
|||
links () {
|
||||
return [
|
||||
{
|
||||
label: 'Scan Your Own App',
|
||||
label: 'Scan Apps',
|
||||
href: '/apple-silicon-app-test/'
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@
|
|||
:href="item.url"
|
||||
:class="[
|
||||
'mt-1 block px-3 py-2 rounded-md text-base font-medium text-gray-300 hover:text-white hover:bg-gray-700 focus:outline-none focus:text-white focus:bg-gray-700 transition duration-150 ease-in-out',
|
||||
($nuxt.$route.path === item.url) ? 'text-white bg-gray-900 hover:text-white' : 'text-gray-300 hover:bg-gray-700'
|
||||
(currentPath === item.url) ? 'text-white bg-gray-900 hover:text-white' : 'text-gray-300 hover:bg-gray-700'
|
||||
]"
|
||||
>
|
||||
{{ item.label }}
|
||||
|
|
@ -109,7 +109,7 @@
|
|||
:href="item.url"
|
||||
:class="[
|
||||
'px-3 py-2 rounded-md text-sm font-medium leading-5 focus:outline-none focus:text-white focus:bg-gray-700 transition duration-150 ease-in-out',
|
||||
($nuxt.$route.path === item.url) ? 'text-white bg-darker hover:text-white neumorphic-shadow' : 'text-gray-300 hover:bg-darker hover:neumorphic-shadow'
|
||||
(currentPath === item.url) ? 'text-white bg-darker hover:text-white neumorphic-shadow' : 'text-gray-300 hover:bg-darker hover:neumorphic-shadow'
|
||||
]"
|
||||
>
|
||||
{{ item.label }}
|
||||
|
|
@ -141,7 +141,7 @@
|
|||
<a
|
||||
:class="[
|
||||
'underline px-3 py-2 rounded-md text-xs font-medium leading-5 focus:outline-none focus:text-white focus:bg-gray-700 transition duration-150 ease-in-out',
|
||||
//($nuxt.$route.path === item.url) ? 'text-white bg-darker hover:text-white neumorphic-shadow' : 'text-gray-300 hover:bg-darker hover:neumorphic-shadow'
|
||||
//(currentPath === item.url) ? 'text-white bg-darker hover:text-white neumorphic-shadow' : 'text-gray-300 hover:bg-darker hover:neumorphic-shadow'
|
||||
]"
|
||||
href="https://prf.hn/l/7JG0bEj"
|
||||
>
|
||||
|
|
@ -162,6 +162,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
// https://anguscroll.com/just/just-has
|
||||
import has from 'just-has'
|
||||
|
||||
import LinkButton from '~/components/link-button.vue'
|
||||
|
||||
export default {
|
||||
|
|
@ -203,6 +206,21 @@ export default {
|
|||
])
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentPath () {
|
||||
// If we have a nuxt context, use that.
|
||||
if ( has( this, [ '$nuxt' ]) ) {
|
||||
return this.$nuxt.$route.path
|
||||
}
|
||||
|
||||
// If we have a location object, use that.
|
||||
if ( typeof window !== 'undefined' && typeof window.location === 'object' ) {
|
||||
return window.location.pathname
|
||||
}
|
||||
|
||||
return '/'
|
||||
}
|
||||
}
|
||||
// data: function () {
|
||||
// return {
|
||||
// // isOpen: false
|
||||
|
|
|
|||
565
components/search-stork.vue
Normal file
565
components/search-stork.vue
Normal file
|
|
@ -0,0 +1,565 @@
|
|||
<template>
|
||||
<div
|
||||
ref="search-container"
|
||||
class="search-container w-full space-y-4"
|
||||
>
|
||||
<slot name="before-search">
|
||||
<div class="list-summary-wrapper flex justify-center text-center text-sm">
|
||||
<ListSummary
|
||||
v-if="summary !== null"
|
||||
:custom-numbers="summary"
|
||||
class="max-w-4xl"
|
||||
/>
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<div class="search-input relative space-y-4">
|
||||
<div>
|
||||
<input
|
||||
id="search"
|
||||
ref="search"
|
||||
v-model="userTextQuery"
|
||||
:autofocus="autofocus"
|
||||
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"
|
||||
autocomplete="off"
|
||||
@keyup="handleSearchInput"
|
||||
>
|
||||
<div class="search-input-separator border-white border-t-2" />
|
||||
</div>
|
||||
<div class="quick-buttons overflow-x-auto whitespace-no-wrap space-x-2">
|
||||
<button
|
||||
v-for="button in quickButtons"
|
||||
:key="button.query"
|
||||
:class="[
|
||||
'inline-block text-xs rounded-lg py-1 px-2',
|
||||
'border-2 border-white focus:outline-none',
|
||||
hasActiveFilter( button.query ) ? 'border-opacity-50 bg-darkest' : 'border-opacity-0 neumorphic-shadow-inner'
|
||||
]"
|
||||
:aria-label="`Filter list by ${button.label}`"
|
||||
@click="toggleFilter(button.query); queryResults(query)"
|
||||
>{{ button.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Carbon class="carbon-inline-wide" />
|
||||
|
||||
<div
|
||||
ref="search-container"
|
||||
class="search-container relative divide-y divide-gray-700 w-full rounded-lg border border-gray-700 bg-gradient-to-br from-darker to-dark my-8 px-5"
|
||||
>
|
||||
<svg style="display: none;">
|
||||
<defs>
|
||||
<path
|
||||
id="chevron-right"
|
||||
fill-rule="evenodd"
|
||||
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<!-- hasStartedAnyQuery: {{ hasStartedAnyQuery }} -->
|
||||
|
||||
<template v-if="chunkedListings.length === 0">
|
||||
<div
|
||||
class="text-center py-4"
|
||||
>
|
||||
<span>No apps found for</span>
|
||||
<span
|
||||
v-for="term in userTerms"
|
||||
:key="term"
|
||||
class="font-bold border rounded px-1 pb-1 mx-1"
|
||||
>{{ term }}</span>
|
||||
|
||||
<template v-if="isFilteredList">
|
||||
<span>within</span>
|
||||
|
||||
<span
|
||||
v-for="term in baseFilters"
|
||||
:key="term"
|
||||
class="font-bold border rounded px-1 pb-1 mx-1"
|
||||
>{{ term }}</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="baseFilters.length > 0"
|
||||
class="w-full flex justify-center p-6"
|
||||
>
|
||||
<LinkButton
|
||||
href="/"
|
||||
:class="[
|
||||
'px-3 py-2 rounded-md text-sm pointer-events-auto focus:outline-none focus:text-white focus:bg-gray-700 transition duration-150 ease-in-out',
|
||||
'text-gray-300 hover:bg-darker hover:neumorphic-shadow'
|
||||
]"
|
||||
:class-groups="{
|
||||
shadow: 'hover:neumorphic-shadow',
|
||||
bg: 'hover:bg-darker',
|
||||
}"
|
||||
>
|
||||
<span>Search everything</span>
|
||||
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<use href="#chevron-right" />
|
||||
</svg>
|
||||
</LinkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ul
|
||||
v-for="(listingsChunk, chunkIndex) in chunkedListings"
|
||||
:key="`listings-chunk-${ chunkIndex }`"
|
||||
class="listings-container divide-y divide-gray-700"
|
||||
>
|
||||
<!-- <pre>
|
||||
{{ listingsChunk }}
|
||||
</pre> -->
|
||||
<li
|
||||
v-for="(listing, listingIndex) in listingsChunk"
|
||||
:key="`${ listing.slug }-${ listingIndex }`"
|
||||
:ref="`${ listing.slug }-row`"
|
||||
:data-app-slug="listing.slug"
|
||||
class="relative"
|
||||
>
|
||||
<!-- app.endpoint: {{ app.endpoint }} -->
|
||||
<a
|
||||
:href="listing.endpoint"
|
||||
:class="[
|
||||
'flex flex-col justify-center inset-x-0 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-y-2 -mx-5 pl-5 md:pl-20 pr-6 md:pr-64 py-5',
|
||||
listing?.linkClass
|
||||
]"
|
||||
style="transition-property: border;"
|
||||
>
|
||||
|
||||
|
||||
<div class="absolute hidden left-0 h-12 w-12 rounded-full md:flex items-center justify-center bg-darker">
|
||||
{{ getIconForListing( listing ) }}
|
||||
</div>
|
||||
|
||||
<h3
|
||||
v-html="listing.name"
|
||||
/>
|
||||
|
||||
<div class="text-sm leading-5 font-bold">
|
||||
{{ listing.text }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="listing.storkResult"
|
||||
class="text-xs leading-5 font-light"
|
||||
>
|
||||
<div
|
||||
v-for="(excerpt, excerptIndex) in listing.storkResult.excerpts"
|
||||
:key="`excerpt-${ excerptIndex }`"
|
||||
class="result-excerpt space-y-3"
|
||||
>
|
||||
<div
|
||||
v-for="(range, rangeIndex) in makeHighlightedMarkup( excerpt )"
|
||||
:key="`range-${ rangeIndex }`"
|
||||
|
||||
v-html="range"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- listing.lastUpdated: {{ listing.lastUpdated }} -->
|
||||
<template v-if="listing.lastUpdated">
|
||||
<small
|
||||
class="text-xs opacity-50"
|
||||
>
|
||||
<RelativeTime
|
||||
:timestamp="listing.lastUpdated.timestamp"
|
||||
class="text-xs opacity-50"
|
||||
/>
|
||||
</small>
|
||||
</template>
|
||||
|
||||
<!-- listing.endpoint: {{ listing.endpoint }} -->
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
<div
|
||||
class="search-item-options relative md:absolute md:inset-0 w-full pointer-events-none"
|
||||
>
|
||||
<div class="search-item-options-container h-full flex justify-center md:justify-end items-center pb-4 md:py-4 md:px-4">
|
||||
<LinkButton
|
||||
v-for="link in getSearchLinks( listing )"
|
||||
:key="`${ listing.slug }-${ link.label.toLowerCase() }`"
|
||||
:href="link.href"
|
||||
:class="[
|
||||
'px-3 py-2 rounded-md text-sm pointer-events-auto focus:outline-none focus:text-white focus:bg-gray-700 transition duration-150 ease-in-out',
|
||||
'text-gray-300 hover:bg-darker hover:neumorphic-shadow'
|
||||
]"
|
||||
:class-groups="{
|
||||
// general: 'relative inline-flex items-center rounded-md px-4 py-2',
|
||||
// font: 'leading-5 font-bold',
|
||||
// text: 'text-white',
|
||||
// border: 'border border-transparent focus:outline-none focus:border-indigo-600',
|
||||
shadow: 'hover:neumorphic-shadow',
|
||||
bg: 'hover:bg-darker',
|
||||
// transition: 'transition duration-150 ease-in-out'
|
||||
}"
|
||||
>
|
||||
{{ link.label }}
|
||||
</LinkButton>
|
||||
|
||||
<LinkButton
|
||||
v-if="listing.endpoint.length"
|
||||
:href="listing.endpoint"
|
||||
:class="[
|
||||
'px-3 py-2 rounded-md text-sm pointer-events-auto focus:outline-none focus:text-white focus:bg-gray-700 transition duration-150 ease-in-out',
|
||||
'text-gray-300 hover:bg-darker hover:neumorphic-shadow'
|
||||
]"
|
||||
:class-groups="{
|
||||
// general: 'relative inline-flex items-center rounded-md px-4 py-2',
|
||||
// font: 'leading-5 font-bold',
|
||||
// text: 'text-white',
|
||||
// border: 'border border-transparent focus:outline-none focus:border-indigo-600',
|
||||
shadow: 'hover:neumorphic-shadow',
|
||||
bg: 'hover:bg-darker',
|
||||
// transition: 'transition duration-150 ease-in-out'
|
||||
}"
|
||||
>
|
||||
<span>Details</span>
|
||||
|
||||
<svg
|
||||
class="h-5 w-5 -mr-2"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<use href="#chevron-right" />
|
||||
</svg>
|
||||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="showingInitialList"
|
||||
class="list-navigation"
|
||||
>
|
||||
<nav
|
||||
class="pagination w-full flex gap-6 justify-center py-4"
|
||||
>
|
||||
<LinkButton
|
||||
v-if="previousPageUrl"
|
||||
:href="previousPageUrl"
|
||||
|
||||
:class="[
|
||||
'w-32 justify-end',
|
||||
'rounded-md text-sm pointer-events-auto focus:outline-none focus:text-white focus:bg-gray-700 transition duration-150 ease-in-out px-3 py-2',
|
||||
'text-gray-300 hover:bg-darker hover:neumorphic-shadow'
|
||||
]"
|
||||
:class-groups="{
|
||||
shadow: 'hover:neumorphic-shadow',
|
||||
bg: 'hover:bg-darker',
|
||||
}"
|
||||
>
|
||||
<svg
|
||||
class="-scale-x-100 h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<use href="#chevron-right" />
|
||||
</svg>
|
||||
|
||||
<span>Previous</span>
|
||||
</LinkButton>
|
||||
|
||||
<LinkButton
|
||||
v-if="nextPageUrl"
|
||||
:href="nextPageUrl"
|
||||
|
||||
:class="[
|
||||
'w-32',
|
||||
'px-3 py-2 rounded-md text-sm pointer-events-auto focus:outline-none focus:text-white focus:bg-gray-700 transition duration-150 ease-in-out',
|
||||
'text-gray-300 hover:bg-darker hover:neumorphic-shadow'
|
||||
]"
|
||||
:class-groups="{
|
||||
shadow: 'hover:neumorphic-shadow',
|
||||
bg: 'hover:bg-darker',
|
||||
}"
|
||||
>
|
||||
<span>Next</span>
|
||||
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<use href="#chevron-right" />
|
||||
</svg>
|
||||
</LinkButton>
|
||||
</nav>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="list-end-area flex justify-center py-6">
|
||||
<ListEndButtons
|
||||
:query="userTextQuery"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import scrollIntoView from 'scroll-into-view-if-needed'
|
||||
|
||||
import {
|
||||
defaultStatusFilters,
|
||||
} from '~/helpers/statuses.js'
|
||||
import {
|
||||
getIconForListing
|
||||
} from '~/helpers/app-derived.js'
|
||||
import {
|
||||
StorkClient,
|
||||
StorkFilters,
|
||||
makeHighlightedMarkup,
|
||||
makeHighlightedResultTitle
|
||||
} from '~/helpers/stork/browser.js'
|
||||
|
||||
import Carbon from '~/components/carbon-inline.vue'
|
||||
import LinkButton from '~/components/link-button.vue'
|
||||
import RelativeTime from '~/components/relative-time.vue'
|
||||
import ListSummary from '~/components/list-summary.vue'
|
||||
import ListEndButtons from '~/components/list-end-buttons.vue'
|
||||
|
||||
let storkClient = null
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Carbon,
|
||||
ListSummary,
|
||||
RelativeTime,
|
||||
LinkButton,
|
||||
ListEndButtons
|
||||
},
|
||||
props: {
|
||||
kindPage: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
initialLimit: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
quickButtons: {
|
||||
type: Array,
|
||||
default: () => defaultStatusFilters
|
||||
},
|
||||
baseFilters: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
listSummary: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
userTextQuery: '',
|
||||
filterQueryList: [],
|
||||
hasStartedAnyQuery: false,
|
||||
listingsResults: [],
|
||||
waitingForQuery: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
storkQuery () {
|
||||
return [
|
||||
this.userTextQuery.trim(),
|
||||
...this.filterQueryList
|
||||
].join(' ')
|
||||
},
|
||||
appList () {
|
||||
return this.kindPage.items
|
||||
},
|
||||
initialList () {
|
||||
return this.initialLimit !== null ? this.appList.slice(0, this.initialLimit) : this.appList
|
||||
},
|
||||
listings () {
|
||||
// Build filler listings to use while loading results
|
||||
if ( this.waitingForQuery ) return Array( 10 ).fill({ name: 'Loading', slug: 'loading', endpoint: '', linkClass: 'shimmer pointer-events-none' })
|
||||
|
||||
if ( this.showingInitialList ) return this.initialList
|
||||
|
||||
return this.listingsResults
|
||||
},
|
||||
// Chunk results to avoid having a parent node with more than 60 child nodes.
|
||||
chunkedListings () {
|
||||
|
||||
const listings = [
|
||||
...this.listings
|
||||
]
|
||||
|
||||
const size = 25
|
||||
const chunks = []
|
||||
|
||||
while (listings.length > 0)
|
||||
chunks.push(listings.splice(0, size))
|
||||
|
||||
return chunks
|
||||
},
|
||||
isFilteredList () {
|
||||
return this.baseFilters.length > 0
|
||||
},
|
||||
hasSearchInputText () {
|
||||
return this.userTextQuery.length > 0
|
||||
},
|
||||
hasAnyUserFilters () {
|
||||
return this.userFilters.length > 0
|
||||
},
|
||||
hasAnyUserTerms () {
|
||||
return this.userTerms.length > 0
|
||||
},
|
||||
showingInitialList () {
|
||||
return !this.hasAnyUserTerms
|
||||
},
|
||||
inputTerms () {
|
||||
return this.userTextQuery.trim().split(' ')
|
||||
},
|
||||
userFilters () {
|
||||
// console.log('filterQueryList', )
|
||||
return this.filterQueryList.filter( filterTerm => {
|
||||
return !this.baseFilters.includes( filterTerm )
|
||||
})
|
||||
},
|
||||
userTerms () {
|
||||
// If out input is empty, return just the user filters
|
||||
if ( !this.hasSearchInputText ) return this.userFilters
|
||||
|
||||
return [
|
||||
...this.inputTerms,
|
||||
...this.userFilters
|
||||
]
|
||||
},
|
||||
summary () {
|
||||
if ( this.listSummary !== null ) {
|
||||
return this.listSummary
|
||||
}
|
||||
|
||||
if ( !!this.kindPage.summary ) {
|
||||
return this.kindPage.summary
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
previousPageUrl () {
|
||||
if ( this.kindPage.previousPage.length === 0 ) return null
|
||||
|
||||
return this.kindPage.previousPage
|
||||
},
|
||||
nextPageUrl () {
|
||||
if ( this.kindPage.nextPage.length === 0 ) return null
|
||||
|
||||
return this.kindPage.nextPage
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// Setup stork client
|
||||
storkClient = new StorkClient()
|
||||
|
||||
// Store filter instance
|
||||
this.storkFilters = new StorkFilters()
|
||||
|
||||
// Add initial filters
|
||||
this.storkFilters.setFromStringArray( this.baseFilters )
|
||||
|
||||
},
|
||||
methods: {
|
||||
makeHighlightedMarkup,
|
||||
makeHighlightedResultTitle,
|
||||
|
||||
getIconForListing,
|
||||
|
||||
getSearchLinks (app) {
|
||||
return app?.searchLinks || []
|
||||
},
|
||||
// Search tools
|
||||
hasActiveFilter ( filter ) {
|
||||
return this.filterQueryList.includes( filter )
|
||||
},
|
||||
toggleFilter ( newFilterQuery ) {
|
||||
|
||||
this.storkFilters.toggleFilter( newFilterQuery )
|
||||
|
||||
this.filterQueryList = this.storkFilters.list
|
||||
},
|
||||
scrollInputToTop () {
|
||||
scrollIntoView(this.$refs['search-container'], {
|
||||
block: 'start',
|
||||
behavior: 'smooth'
|
||||
})
|
||||
},
|
||||
|
||||
// Called on input and when a filter is toggled
|
||||
async queryResults ( rawQuery ) {
|
||||
|
||||
console.log( 'query', this.storkQuery )
|
||||
|
||||
// If our query is empty
|
||||
// then bail
|
||||
if ( this.storkQuery.trim().length === 0 ) return
|
||||
|
||||
this.waitingForQuery = true
|
||||
|
||||
this.$emit('update:query', rawQuery)
|
||||
|
||||
// Declare that at least one query has been made
|
||||
this.hasStartedAnyQuery = true
|
||||
|
||||
// console.log('rawQuery', rawQuery)
|
||||
|
||||
const requiredTerms = this.storkQuery.split(' ')
|
||||
|
||||
const storkQuery = await storkClient.lazyQuery( this.storkQuery, requiredTerms )
|
||||
|
||||
// If the query response is empty
|
||||
// then return
|
||||
if ( storkQuery === null ) {
|
||||
return
|
||||
}
|
||||
|
||||
// console.log( 'storkQuery', storkQuery )
|
||||
|
||||
this.listingsResults = storkQuery.results.map( result => {
|
||||
return {
|
||||
name: makeHighlightedResultTitle( result ),
|
||||
endpoint: result.entry.url,
|
||||
slug: '',
|
||||
category: {
|
||||
slug: 'uncategorized'
|
||||
},
|
||||
storkResult: result
|
||||
}
|
||||
})
|
||||
|
||||
// Switch from loading state and reveal the results
|
||||
this.waitingForQuery = false
|
||||
|
||||
// console.log('this.listingsResults', this.listingsResults)
|
||||
},
|
||||
|
||||
handleSearchInput ( event ) {
|
||||
const inputValue = event.target.value
|
||||
|
||||
this.scrollInputToTop()
|
||||
this.queryResults( inputValue )
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
|
||||
<small class="data-credit text-sm opacity-75 text-center mb-4">
|
||||
<span>Data generously provided by </span>
|
||||
<div
|
||||
class="data-credit flex gap-1 justify-center text-xs opacity-75 text-center mb-4"
|
||||
>
|
||||
<span>Includes data generously provided by </span>
|
||||
<span>
|
||||
<a
|
||||
href="https://twitter.com/__tosh"
|
||||
|
|
@ -15,6 +16,5 @@
|
|||
class="font-bold"
|
||||
>Apple Silicon Games</a>
|
||||
</span>
|
||||
</small>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@
|
|||
|
||||
<div class="cover-bottom h-full">
|
||||
|
||||
<slot>
|
||||
<!-- Default slot -->
|
||||
</slot>
|
||||
|
||||
<slot name="cover-bottom">
|
||||
<!-- Bottom -->
|
||||
</slot>
|
||||
|
|
|
|||
86
helpers/api/client.js
Normal file
86
helpers/api/client.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// Based on GitHub Proxy demo
|
||||
// https://gist.github.com/v1vendi/75d5e5dad7a2d1ef3fcb48234e4528cb
|
||||
|
||||
// Example uses:
|
||||
|
||||
// DoesItAPI.get() // GET /api
|
||||
// DoesItAPI.apps.get() // GET /api/apps.json
|
||||
// DoesItAPI.apps(7).get() // GET /api/apps/7.json
|
||||
// DoesItAPI.apps(7).whatever.delete() // DELETE /api/apps/7/whatever.json
|
||||
// DoesItAPI.apps.put({ whatever: 1 })
|
||||
|
||||
// GET /api/tiles/public/static/3/4/2.json?turn=37038&games=wot
|
||||
// DoesItAPI.tiles.public.static(3)(4)(`${2}.json`).get({ turn: 37, games: 'wot' })
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
import { getApiUrl } from '~/helpers/url.js'
|
||||
|
||||
// const defaultFetchMethod = (...args) => console.log(...args) // mock
|
||||
|
||||
const defaultFetchMethod = async function (...args) {
|
||||
return axios(...args)
|
||||
.then( response => response.data )
|
||||
.catch( error => {
|
||||
console.error( error )
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
|
||||
const HTTP_METHODS = [
|
||||
'GET',
|
||||
// 'POST',
|
||||
// 'PUT',
|
||||
// 'DELETE',
|
||||
// 'PATCH'
|
||||
]
|
||||
|
||||
const apiBaseUrl = `${ getApiUrl().replace(/\/$/, '') }/api`
|
||||
|
||||
|
||||
export function generateAPI ( {
|
||||
apiUrl = apiBaseUrl,
|
||||
fetchMethod = defaultFetchMethod
|
||||
} = {} ) {
|
||||
|
||||
// console.log( 'apiUrl', apiUrl )
|
||||
|
||||
// a hack, so we can use field either as property or a method
|
||||
const callable = () => {}
|
||||
callable.url = apiUrl
|
||||
|
||||
return new Proxy(callable, {
|
||||
get({ url }, propKey) {
|
||||
// If we're just getting the url, return it
|
||||
if ( propKey === 'url' ) return `${ url }.json`
|
||||
|
||||
// If we're using an HTTP method
|
||||
// then do a request to the url
|
||||
if ( HTTP_METHODS.includes(propKey.toUpperCase()) ) {
|
||||
return (data) => fetchMethod({
|
||||
url: `${ url }.json`,
|
||||
method: propKey.toUpperCase(),
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// Otherwise drill down to the next property
|
||||
// Example:
|
||||
// From DoesItAPI.kind...
|
||||
// To DoesItAPI.kind.apps...
|
||||
return generateAPI({ apiUrl: `${url}/${propKey}` })
|
||||
|
||||
},
|
||||
// Handles when () goes after a property key
|
||||
// Example: DoesItAPI() or DoesItAPI.app()
|
||||
// Proxy.handler.apply - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/apply
|
||||
apply({ url }, thisArg, [arg] = []) {
|
||||
const apiUrl = arg ? `${url}/${arg}` : url
|
||||
return generateAPI({ apiUrl: apiUrl })
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
export const DoesItAPI = generateAPI()
|
||||
1
helpers/api/config.js
Normal file
1
helpers/api/config.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const apiDirectory = './static/api'
|
||||
128
helpers/api/kind.js
Normal file
128
helpers/api/kind.js
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
// import memoize from 'fast-memoize'
|
||||
import memoizeGetters from 'memoize-getters'
|
||||
|
||||
import getListSummaryNumbers from '~/helpers/get-list-summary-numbers.js'
|
||||
|
||||
import {
|
||||
apiDirectory
|
||||
} from '~/helpers/api/config.js'
|
||||
import {
|
||||
PaginatedList,
|
||||
defaultPerPage
|
||||
} from '~/helpers/api/pagination.js'
|
||||
import {
|
||||
makeSummaryOfListings
|
||||
} from '~/helpers/categories.js'
|
||||
|
||||
|
||||
const defaultExludedProperties = [
|
||||
'bundles',
|
||||
]
|
||||
|
||||
function excludeExtaKindData ( { rawKindPage, excludes = defaultExludedProperties } = {} ) {
|
||||
|
||||
return {
|
||||
...rawKindPage,
|
||||
items: rawKindPage.items.map( item => {
|
||||
for ( const exclude of excludes ) {
|
||||
delete item[ exclude ]
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function makeKindEndpoint ({ kindSlug, number = null }) {
|
||||
if ( number ) {
|
||||
return `/kind/${ kindSlug }/${ number }`
|
||||
}
|
||||
|
||||
return `/kind/${ kindSlug }`
|
||||
}
|
||||
|
||||
function makeKindDirPath ( kindSlug ) {
|
||||
return `${ apiDirectory }${ makeKindEndpoint({ kindSlug }) }`
|
||||
}
|
||||
|
||||
function makeKindFilePath ({ kindSlug, number }) {
|
||||
return `${ apiDirectory }${ makeKindEndpoint({ kindSlug, number }) }.json`
|
||||
}
|
||||
|
||||
|
||||
// let timeSummaryRun = 0
|
||||
|
||||
export class KindList extends PaginatedList {
|
||||
constructor({
|
||||
list,
|
||||
kindSlug,
|
||||
perPage = defaultPerPage
|
||||
}) {
|
||||
super({ list, perPage })
|
||||
|
||||
this.kindSlug = kindSlug
|
||||
}
|
||||
|
||||
baseRoute = makeKindEndpoint({ kindSlug: this.kindSlug })
|
||||
|
||||
makeSummary () {
|
||||
// console.log( `Summary run ${ timeSummaryRun += 1 } times` )
|
||||
return {
|
||||
...getListSummaryNumbers( this.list ),
|
||||
sampleNames: makeSummaryOfListings({ list: this.list }),
|
||||
sampleNamesShort: makeSummaryOfListings({
|
||||
list: this.list,
|
||||
length: 5,
|
||||
random: true,
|
||||
suffix: ''
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
summary = this.makeSummary()
|
||||
|
||||
get routes () {
|
||||
return this.pages.map( kindPage => {
|
||||
return makeKindEndpoint({
|
||||
kindSlug: this.kindSlug,
|
||||
number: kindPage.number
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
get basePath () {
|
||||
return makeKindDirPath( this.kindSlug )
|
||||
}
|
||||
|
||||
get apiFiles () {
|
||||
return this.pages.map( kindPage => {
|
||||
|
||||
// If we have a number, we need to add it to the file path
|
||||
const nextPage = kindPage.hasNextPage ? makeKindEndpoint({
|
||||
kindSlug: this.kindSlug,
|
||||
number: kindPage.number + 1
|
||||
}) : ''
|
||||
|
||||
const previousPage = kindPage.hasPreviousPage ? makeKindEndpoint({
|
||||
kindSlug: this.kindSlug,
|
||||
number: kindPage.number - 1
|
||||
}) : ''
|
||||
|
||||
return {
|
||||
path: makeKindFilePath({ kindSlug: this.kindSlug, number: kindPage.number }),
|
||||
content: {
|
||||
number: kindPage.number,
|
||||
previousPage,
|
||||
nextPage,
|
||||
summary: this.summary,
|
||||
items: excludeExtaKindData({
|
||||
rawKindPage: kindPage,
|
||||
}).items
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const KindListMemoized = memoizeGetters( KindList )
|
||||
76
helpers/api/pagination.js
Normal file
76
helpers/api/pagination.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
export const defaultPerPage = 20
|
||||
|
||||
|
||||
export class PaginatedList {
|
||||
constructor({ list, perPage = defaultPerPage }) {
|
||||
|
||||
// Catch errors if the list is not an array or a function
|
||||
if ( !Array.isArray( list ) && typeof list !== 'function' ) {
|
||||
throw new Error(`List must be an array or a function but is ${typeof list}`)
|
||||
}
|
||||
|
||||
this.listArg = list
|
||||
this.perPage = perPage
|
||||
}
|
||||
|
||||
get list () {
|
||||
// if our list is a function, call it
|
||||
if ( typeof this.listArg === 'function' ) {
|
||||
return this.listArg()
|
||||
}
|
||||
|
||||
return this.listArg
|
||||
}
|
||||
|
||||
get total () {
|
||||
// Catch errors if the list is not an array or a function
|
||||
if ( !Array.isArray( this.list ) ) {
|
||||
throw new Error(`List must be an array or a function but is ${typeof list}`)
|
||||
}
|
||||
|
||||
return this.list.length
|
||||
}
|
||||
|
||||
get pageCount () {
|
||||
return Math.ceil( this.total / this.perPage )
|
||||
}
|
||||
|
||||
makePageItems ( pageNumber ) {
|
||||
const start = (pageNumber - 1) * this.perPage
|
||||
const end = start + this.perPage
|
||||
|
||||
return this.list.slice(start, end)
|
||||
}
|
||||
|
||||
hasPage ( pageNumber ) {
|
||||
return pageNumber > 0 && pageNumber <= this.pageCount
|
||||
}
|
||||
|
||||
makePage ( pageNumber ) {
|
||||
const items = this.makePageItems( pageNumber )
|
||||
|
||||
return {
|
||||
number: pageNumber,
|
||||
items,
|
||||
hasPreviousPage: this.hasPage( pageNumber - 1 ),
|
||||
hasNextPage: this.hasPage( pageNumber + 1 ),
|
||||
get json() {
|
||||
return JSON.stringify( items )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get pages () {
|
||||
// Create an empty array of pages
|
||||
const pages = Array( this.pageCount ).fill({})
|
||||
|
||||
return pages.map( ( _, index ) => {
|
||||
const pageNumber = index + 1
|
||||
|
||||
return this.makePage( pageNumber )
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
54
helpers/api/sitemap/build.js
Normal file
54
helpers/api/sitemap/build.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import glob from 'fast-glob'
|
||||
import { simpleSitemapAndIndex } from 'sitemap'
|
||||
|
||||
|
||||
import {
|
||||
sitemapLocation
|
||||
} from '~/helpers/constants.js'
|
||||
import { getSiteUrl } from '~/helpers/url.js'
|
||||
|
||||
const astroPageTemplatePath = './src/pages'
|
||||
|
||||
export async function getUrlsForAstroDefinedPages () {
|
||||
const siteUrl = getSiteUrl()
|
||||
const filesPaths = await glob( `${ astroPageTemplatePath }/**/*.astro` )
|
||||
|
||||
const urls = []
|
||||
|
||||
for ( const filePath of filesPaths ) {
|
||||
const urlPath = filePath
|
||||
.replace( astroPageTemplatePath, '' )
|
||||
.replace( '.astro', '' )
|
||||
.replace( '/index', '/' )
|
||||
|
||||
// Skip any paths for templates that include '['
|
||||
if ( urlPath.includes( '[' ) ) continue
|
||||
|
||||
console.log( 'urlPath', urlPath )
|
||||
|
||||
// Create a new url object from the site url and the path
|
||||
const url = new URL( urlPath, siteUrl )
|
||||
|
||||
urls.push( url.pathname )
|
||||
}
|
||||
|
||||
return urls
|
||||
}
|
||||
|
||||
export async function saveSitemap ( sitemapUrls ) {
|
||||
|
||||
await simpleSitemapAndIndex({
|
||||
hostname: process.env.PUBLIC_URL,
|
||||
destinationDir: sitemapLocation,
|
||||
gzip: false,
|
||||
// [{ url: '/page-1/', changefreq: 'daily'}, ...],
|
||||
sourceData: sitemapUrls.map( url => {
|
||||
return {
|
||||
url,
|
||||
// Google doesn't care about changefreq and priority - https://www.seroundtable.com/google-priority-change-frequency-xml-sitemap-20273.html
|
||||
// changefreq: 'daily'
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
}
|
||||
73
helpers/api/sitemap/parse.js
Normal file
73
helpers/api/sitemap/parse.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import path from 'path'
|
||||
import fs from 'fs-extra'
|
||||
import axios from 'axios'
|
||||
import { parse } from 'fast-xml-parser'
|
||||
|
||||
import {
|
||||
sitemapLocation,
|
||||
sitemapIndexFileName,
|
||||
} from '~/helpers/constants.js'
|
||||
|
||||
const sitemapFilesToTry = [
|
||||
sitemapIndexFileName,
|
||||
'sitemap.xml'
|
||||
]
|
||||
|
||||
export function parseSitemapXml ( sitemapXml ) {
|
||||
// Get URLs from index
|
||||
const sitemapRoot = parse( sitemapXml )
|
||||
|
||||
const {
|
||||
sitemapindex = null,
|
||||
urlset = null,
|
||||
} = sitemapRoot
|
||||
|
||||
|
||||
if ( sitemapindex !== null ) {
|
||||
const {
|
||||
sitemap
|
||||
} = sitemapindex
|
||||
|
||||
const urlEntries = Array.isArray( sitemap ) ? sitemap : [ sitemap ]
|
||||
|
||||
return urlEntries
|
||||
}
|
||||
|
||||
// console.log( 'sitemapRoot', sitemapRoot )
|
||||
|
||||
return urlset.url
|
||||
}
|
||||
|
||||
export async function getAllUrlsFromLocalSitemap ( sitemapPath ) {
|
||||
// Get intial sitemap
|
||||
const sitemapXml = await fs.readFile( sitemapPath, 'utf8' )
|
||||
const sitemapDirectory = path.dirname( sitemapPath )
|
||||
|
||||
// Get URLs from index
|
||||
const urlEntries = parseSitemapXml( sitemapXml )
|
||||
|
||||
// Check if url entries are sitemaps
|
||||
const isSitemapIndex = !!urlEntries[0].loc && urlEntries[0].loc.includes('.xml')
|
||||
|
||||
if ( !isSitemapIndex ) return urlEntries
|
||||
|
||||
|
||||
// Get urls from our sitemap
|
||||
const sitemaps = await Promise.all( urlEntries.map( async entry => {
|
||||
// Build Sitemap Index URL
|
||||
const sitemapUrl = new URL( entry.loc )
|
||||
|
||||
const childSitemapPath = path.join( sitemapDirectory, sitemapUrl.pathname )
|
||||
|
||||
return await getAllUrlsFromLocalSitemap( childSitemapPath )
|
||||
}))
|
||||
|
||||
// Flatten array
|
||||
return sitemaps.flat()
|
||||
}
|
||||
|
||||
export async function fetchParsedSitemapXmlForDomain ( domain ) {
|
||||
for ( const sitemapFile of sitemapFilesToTry ) {
|
||||
|
||||
}
|
||||
}
|
||||
34
helpers/api/static.js
Normal file
34
helpers/api/static.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import fs from 'fs-extra'
|
||||
import axios from 'axios'
|
||||
import 'dotenv/config'
|
||||
|
||||
import {
|
||||
// storkVersion,
|
||||
// storkExecutableName,
|
||||
// storkExecutablePath,
|
||||
storkTomlPath,
|
||||
} from '~/helpers/stork/config.js'
|
||||
|
||||
export async function downloadStorkToml () {
|
||||
// Check if the toml file exists
|
||||
if (fs.existsSync(storkTomlPath)) {
|
||||
console.log(`Stork toml file already exists at ${storkTomlPath}`)
|
||||
return
|
||||
}
|
||||
|
||||
const apiUrl = new URL( process.env.PUBLIC_API_DOMAIN )
|
||||
|
||||
apiUrl.pathname = storkTomlPath.replace('static/', '')
|
||||
|
||||
const response = await axios({
|
||||
method: "get",
|
||||
url: apiUrl.toString(),
|
||||
})
|
||||
|
||||
await fs.writeFile( storkTomlPath, response.data, { encoding: null })
|
||||
|
||||
// Get toml file stats
|
||||
const stats = await fs.stat( storkTomlPath )
|
||||
console.log( stats.isFile() ? '✅' : '❌', 'TOML is file', storkTomlPath )
|
||||
// console.log('TOML Stats', stats)
|
||||
}
|
||||
172
helpers/api/youtube/build.js
Normal file
172
helpers/api/youtube/build.js
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import fs from 'fs-extra'
|
||||
import { google } from 'googleapis'
|
||||
|
||||
import { playlists, benchmarksPlaylistId } from './playlists.js'
|
||||
|
||||
|
||||
export const youtubeVideoPath = './static/api/youtube-videos.json'
|
||||
|
||||
|
||||
async function getPlaylistsItems ( { playlistId } = {} ) {
|
||||
const perPage = 50
|
||||
|
||||
// Setup Youtube API V3 Service instance
|
||||
const service = google.youtube('v3')
|
||||
|
||||
// Fetch data from the Youtube API
|
||||
const { errors = null, data = null } = await service.playlistItems.list({
|
||||
key: process.env.GOOGLE_API_KEY,
|
||||
part: 'snippet,contentDetails',
|
||||
playlistId,
|
||||
maxResults: perPage
|
||||
}).catch(({ errors }) => {
|
||||
|
||||
console.log('Error fetching playlist', errors)
|
||||
|
||||
return {
|
||||
errors
|
||||
}
|
||||
})
|
||||
|
||||
// Send an error response if something went wrong
|
||||
if (errors !== null) {
|
||||
throw new Error(errors)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const items = data.items
|
||||
|
||||
// If there are more results then push them to our playlist
|
||||
if (data.nextPageToken !== null) {
|
||||
|
||||
// Store the token for page #2 into our variable
|
||||
let pageToken = data.nextPageToken
|
||||
|
||||
while (pageToken !== null) {
|
||||
// Fetch data from the Youtube API
|
||||
const youtubePageResponse = await service.playlistItems.list({
|
||||
key: process.env.GOOGLE_API_KEY,
|
||||
part: 'snippet,contentDetails',
|
||||
playlistId,
|
||||
maxResults: perPage,
|
||||
pageToken: pageToken
|
||||
})
|
||||
|
||||
// Add the videos from this page on to our total items list
|
||||
youtubePageResponse.data.items.forEach(item => items.push(item))
|
||||
|
||||
// Now that we're done set up the next page token or empty out the pageToken variable so our loop will stop
|
||||
pageToken = ('nextPageToken' in youtubePageResponse.data) ? youtubePageResponse.data.nextPageToken : null
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Fetched ${items.length} videos from https://www.youtube.com/playlist?list=${ playlistId }`)
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
async function getYouTubeVideos ( options = {} ) {
|
||||
|
||||
const {
|
||||
// requestsDelay = 3600,
|
||||
} = options
|
||||
|
||||
// Fetch all videos from playlists
|
||||
const playlistSets = []
|
||||
|
||||
for ( const playlistToFetch of playlists ) {
|
||||
|
||||
// console.log('playlistJsonUrl', playlistJsonUrl)
|
||||
|
||||
const playlistItems = await getPlaylistsItems({
|
||||
playlistId: playlistToFetch.id
|
||||
})
|
||||
// console.log('playlistItems', playlistItems.length)
|
||||
|
||||
playlistSets.push( playlistItems )
|
||||
}
|
||||
|
||||
// Pull benchmarksPlaylist out of playlist sets
|
||||
// benchmarksPlaylistId
|
||||
const benchmarksVideoIds = playlistSets.find( playlist => {
|
||||
// Skip empty playlists
|
||||
if (playlist.length === 0) return false
|
||||
|
||||
// Get this playlist's ID from first video
|
||||
// and check against benchmarksPlaylistId
|
||||
return playlist[0].snippet.playlistId === benchmarksPlaylistId
|
||||
}).map( video => video.contentDetails.videoId)
|
||||
|
||||
// Creat an object to store playlist items
|
||||
const playlistItems = {}
|
||||
|
||||
|
||||
// Loop through the sets and store all the videos into a single array
|
||||
for (const playlistSet of playlistSets) {
|
||||
for (const playlistItem of playlistSet) {
|
||||
// If we've already stored this video
|
||||
// then skip
|
||||
if (playlistItems.hasOwnProperty(playlistItem.contentDetails.videoId)) continue
|
||||
|
||||
const tags = []
|
||||
|
||||
// If this video is in the benchmarks playlist
|
||||
// then add the benchmark tag
|
||||
if (benchmarksVideoIds.includes(playlistItem.contentDetails.videoId)) {
|
||||
tags.push('benchmark')
|
||||
}
|
||||
|
||||
// Store newly found video
|
||||
playlistItems[playlistItem.contentDetails.videoId] = {
|
||||
title: playlistItem.snippet.title,
|
||||
description: playlistItem.snippet.description,
|
||||
timestamps: [],
|
||||
rawData: playlistItem,
|
||||
tags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Loop through playlist items and store timestamp data
|
||||
for (const videoId in playlistItems) {
|
||||
// console.log('playlistItem', playlistItem)
|
||||
// If the description is empty
|
||||
// then skip
|
||||
if (playlistItems[videoId].description.trim().length === 0) continue
|
||||
|
||||
// Break up the description by line breaks
|
||||
const descriptionLines = playlistItems[videoId].description.split(/\r?\n/)
|
||||
|
||||
// console.log('descriptionLines', descriptionLines)
|
||||
|
||||
for (const line of descriptionLines) {
|
||||
// https://stackoverflow.com/a/11067610/1397641
|
||||
const matches = line.match(/(?:([0-5]?[0-9]):)?([0-5]?[0-9]):([0-5][0-9])/)
|
||||
|
||||
// If there are no timestamps on this line
|
||||
// then skip
|
||||
if (matches === null) continue
|
||||
|
||||
playlistItems[videoId].timestamps.push({
|
||||
time: matches[0],
|
||||
fullText: line
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return playlistItems
|
||||
}
|
||||
|
||||
|
||||
export async function saveYouTubeVideos () {
|
||||
//
|
||||
const youtubeVideos = await getYouTubeVideos()
|
||||
|
||||
//
|
||||
|
||||
// Save to JSON
|
||||
await fs.outputJson( youtubeVideoPath, youtubeVideos )
|
||||
|
||||
}
|
||||
221
helpers/api/youtube/playlists.js
Normal file
221
helpers/api/youtube/playlists.js
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
|
||||
|
||||
export const benchmarksPlaylistId = 'PLaa9cZC07ZPFqjoYLZRR3kbbnJRHhlmXq'
|
||||
|
||||
|
||||
|
||||
export const playlists = [
|
||||
// Awais Mirza - Apple Silicon Mac Software Testing
|
||||
// https://www.youtube.com/playlist?list=PLz5rnvLVJX5XF8cXAOQuarPIeP8Xr7b1_
|
||||
{
|
||||
id: 'PLz5rnvLVJX5XF8cXAOQuarPIeP8Xr7b1_'
|
||||
},
|
||||
// Andrew Tsai - M1 Apple Silicon Game Benchmarks
|
||||
// https://www.youtube.com/playlist?list=PLFbqxkNlqrHNK0i4WN99Jc8g-qSbKoZEy
|
||||
{
|
||||
id: 'PLFbqxkNlqrHNK0i4WN99Jc8g-qSbKoZEy'
|
||||
},
|
||||
// Andrew Tsai - Source Ports M1 Mac
|
||||
// https://www.youtube.com/playlist?list=PLYzT4cEhhFvg5BPfy5-cOUNq7PtqzPr5s
|
||||
{
|
||||
id: 'PLYzT4cEhhFvg5BPfy5-cOUNq7PtqzPr5s'
|
||||
},
|
||||
// Andrew Tsai - Emulators M1 Mac
|
||||
// https://www.youtube.com/playlist?list=PLYzT4cEhhFvh9IAsuic4paA_elCqNKs3J
|
||||
{
|
||||
id: 'PLYzT4cEhhFvh9IAsuic4paA_elCqNKs3J'
|
||||
},
|
||||
// Max Tech - Apple Silicon Macs Explained
|
||||
// https://www.youtube.com/playlist?list=PLo11Rczpzuj05que94HF80LWD217ToJht
|
||||
{
|
||||
id: 'PLo11Rczpzuj05que94HF80LWD217ToJht'
|
||||
},
|
||||
// MrMacRight - Gaming Performance Tests
|
||||
// https://www.youtube.com/playlist?list=PL9H5Z-IdZ8M0tUdSfS_rmdc8CRR_FD83e
|
||||
{
|
||||
id: 'PL9H5Z-IdZ8M0tUdSfS_rmdc8CRR_FD83e'
|
||||
},
|
||||
// Created Labs - New 2020 M1 MacBook
|
||||
// https://www.youtube.com/playlist?list=PLcbQnQIwz6qSt-qsD3jCJaEw9fK8yXarA
|
||||
{
|
||||
id: 'PLcbQnQIwz6qSt-qsD3jCJaEw9fK8yXarA'
|
||||
},
|
||||
// DaVinci Resolve + Apple M1 Tests - Learn Color Grading
|
||||
// https://www.youtube.com/playlist?list=PLRYMmqUFQ_cfETgJ_9tSHT1eD8c94y-qg
|
||||
{
|
||||
id: 'PLRYMmqUFQ_cfETgJ_9tSHT1eD8c94y-qg'
|
||||
},
|
||||
// Apple Silicon Macs — M1 & Beyond! - Rene Ritchie
|
||||
// https://www.youtube.com/playlist?list=PL3XJJi5sAjD1sCicSKd0irf5TnQ1KJvPm
|
||||
{
|
||||
id: 'PL3XJJi5sAjD1sCicSKd0irf5TnQ1KJvPm'
|
||||
},
|
||||
// M1 Pro / Max — High-Performance Macs! - Rene Ritchie
|
||||
// https://www.youtube.com/playlist?list=PL3XJJi5sAjD1U4yadF-wNFS-k34L11jqp
|
||||
{
|
||||
id: 'PL3XJJi5sAjD1U4yadF-wNFS-k34L11jqp'
|
||||
},
|
||||
// Apple Silicon Deep Dives — A15 to M1 Pro & Max! - Rene Ritchie
|
||||
// https://www.youtube.com/playlist?list=PL3XJJi5sAjD3IOyW5k_CObxQse3JlPI4A
|
||||
{
|
||||
id: 'PL3XJJi5sAjD3IOyW5k_CObxQse3JlPI4A'
|
||||
},
|
||||
// Apple Silicon Macs — M1, Pro, & Max! - Rene Ritchie
|
||||
// https://www.youtube.com/playlist?list=PL3XJJi5sAjD3kMEU_xUQLRHAt4_2r3UtF
|
||||
{
|
||||
id: 'PL3XJJi5sAjD3kMEU_xUQLRHAt4_2r3UtF'
|
||||
},
|
||||
// Apple M1 Silicon Benchmarks - Tonyisgaming
|
||||
// https://www.youtube.com/playlist?list=PLgz-h_Uy9AwOctqRcm6hEYEqTO9yIawoO
|
||||
{
|
||||
id: 'PLgz-h_Uy9AwOctqRcm6hEYEqTO9yIawoO'
|
||||
},
|
||||
// M1 - Jerry Schulze
|
||||
// https://www.youtube.com/playlist?list=PLFf7t8YdWQOgVmQdiEey2c_7ygDeZGBvf
|
||||
{
|
||||
id: 'PLFf7t8YdWQOgVmQdiEey2c_7ygDeZGBvf'
|
||||
},
|
||||
// Apple Silicon - DevChannel
|
||||
// https://www.youtube.com/playlist?list=PLEi3_qsqIyclc-3NqbEYlIvXEdcLXy1qX
|
||||
{
|
||||
id: 'PLEi3_qsqIyclc-3NqbEYlIvXEdcLXy1qX'
|
||||
},
|
||||
// Michael P. Schmidt
|
||||
// https://www.youtube.com/playlist?list=PLsT75DpPtn2N0RvXGdkjnwAYM0m9EMpb7
|
||||
{
|
||||
id: 'PLsT75DpPtn2N0RvXGdkjnwAYM0m9EMpb7'
|
||||
},
|
||||
// Ben G. Kaiser
|
||||
// https://www.youtube.com/playlist?list=PL_9qmWdi19yCOCzAdTfFxpZtXonyfWTYJ
|
||||
{
|
||||
id: 'PL_9qmWdi19yCOCzAdTfFxpZtXonyfWTYJ'
|
||||
},
|
||||
// Constant Geekery
|
||||
// https://youtube.com/playlist?list=PLQncOO7KICBIH-1Jv3fhOOU9b3-j6XoED
|
||||
{
|
||||
id: 'PLQncOO7KICBIH-1Jv3fhOOU9b3-j6XoED'
|
||||
},
|
||||
// Alexander Ziskind
|
||||
// https://youtube.com/playlist?list=PLPwbI_iIX3aR88msMh-cHoJiBqS6YMUUH
|
||||
{
|
||||
id: 'PLPwbI_iIX3aR88msMh-cHoJiBqS6YMUUH'
|
||||
},
|
||||
// Execute Automation
|
||||
// https://youtube.com/playlist?list=PL6tu16kXT9Pqwg2H8G3mROh5g7LISl8wT
|
||||
{
|
||||
id: 'PL6tu16kXT9Pqwg2H8G3mROh5g7LISl8wT'
|
||||
},
|
||||
// Portland CNC
|
||||
// https://youtube.com/playlist?list=PLlQPaN85gB1kRnc8RUnV-TEOXU_2iap8S
|
||||
{
|
||||
id: 'PLlQPaN85gB1kRnc8RUnV-TEOXU_2iap8S'
|
||||
},
|
||||
// Ben Designs
|
||||
// https://youtube.com/playlist?list=PLQgB1FZhNI7XAf9-2Gb88o6MxxbqDQIgj
|
||||
{
|
||||
id: 'PLQgB1FZhNI7XAf9-2Gb88o6MxxbqDQIgj'
|
||||
},
|
||||
// Ben Aqua
|
||||
// https://youtube.com/playlist?list=PLvswiYwf1PZR_V1cXgKu0fKqN690LXRal
|
||||
{
|
||||
id: 'PLvswiYwf1PZR_V1cXgKu0fKqN690LXRal'
|
||||
},
|
||||
// Tech Gear Talk
|
||||
// https://youtube.com/playlist?list=PL9Y8hNm43poLPdLAfFikiO_c4ZvpB4Wi8
|
||||
{
|
||||
id: 'PL9Y8hNm43poLPdLAfFikiO_c4ZvpB4Wi8'
|
||||
},
|
||||
// c0pist
|
||||
// https://youtube.com/playlist?list=PLmEmhiFYCJygSGcPGNRgiJ9T1AKxJjERI
|
||||
{
|
||||
id: 'PLmEmhiFYCJygSGcPGNRgiJ9T1AKxJjERI'
|
||||
},
|
||||
// BilValentine
|
||||
// https://youtube.com/playlist?list=PLj__zPOcS7bkamYx2oe9p6YOn-63Imexd
|
||||
{
|
||||
id: 'PLj__zPOcS7bkamYx2oe9p6YOn-63Imexd'
|
||||
},
|
||||
// Techkhamun
|
||||
// https://youtube.com/playlist?list=PLprAMxVeEzkb4-19ncZ6GNWWL4QMxZIQK
|
||||
{
|
||||
id: 'PLprAMxVeEzkb4-19ncZ6GNWWL4QMxZIQK'
|
||||
},
|
||||
// iCave
|
||||
// https://youtube.com/playlist?list=PLXHx58X9BZxJfSmttXQJv1zmNi0f7ANhq
|
||||
{
|
||||
id: 'PLXHx58X9BZxJfSmttXQJv1zmNi0f7ANhq'
|
||||
},
|
||||
// Douglas Hewitt
|
||||
// https://youtube.com/playlist?list=PLugJ1ygKKMlI4WwyCbdLJOOHUZkFwvYbK
|
||||
{
|
||||
id: 'PLugJ1ygKKMlI4WwyCbdLJOOHUZkFwvYbK'
|
||||
},
|
||||
// Painfully Honest Tech
|
||||
// https://youtube.com/playlist?list=PLWrpcd0vRa5StCaJoGqT4NsmvOPjF-VZu
|
||||
{
|
||||
id: 'PLWrpcd0vRa5StCaJoGqT4NsmvOPjF-VZu'
|
||||
},
|
||||
// IrixGuy
|
||||
// https://youtube.com/playlist?list=PLzbToXWOz_ZiL5rUDKGaTQS5-eeWfMq7T
|
||||
{
|
||||
id: 'PLzbToXWOz_ZiL5rUDKGaTQS5-eeWfMq7T'
|
||||
},
|
||||
// sand0m1ze gaming
|
||||
// https://youtube.com/playlist?list=PLPPMLgyyaMusoENI6yomC6DsYUymf2ey5
|
||||
{
|
||||
id: 'PLPPMLgyyaMusoENI6yomC6DsYUymf2ey5'
|
||||
},
|
||||
// The Dev
|
||||
// https://youtube.com/playlist?list=PLtOOsLeNlKexKL9yzwp7vpbXaaynmfuQb
|
||||
{
|
||||
id: 'PLtOOsLeNlKexKL9yzwp7vpbXaaynmfuQb'
|
||||
},
|
||||
// Luke Barousse - Data Science
|
||||
// https://youtube.com/playlist?list=PL_CkpxkuPiT9eGORxdWVWn0J58AUki_0W
|
||||
{
|
||||
id: 'PL_CkpxkuPiT9eGORxdWVWn0J58AUki_0W'
|
||||
},
|
||||
// AudioMap
|
||||
// https://youtube.com/playlist?list=PL2xBqm1csOKoBkzu4GCnlHe_KiTGjzlNL
|
||||
{
|
||||
id: 'PL2xBqm1csOKoBkzu4GCnlHe_KiTGjzlNL'
|
||||
},
|
||||
// Mark Payne
|
||||
// https://youtube.com/playlist?list=PL_NaeOyKxj28b_iVaiXfsyPaKwVeht-mG
|
||||
{
|
||||
id: 'PL_NaeOyKxj28b_iVaiXfsyPaKwVeht-mG'
|
||||
},
|
||||
// White Sea Studio
|
||||
// https://youtube.com/playlist?list=PLil8shFjsBGaDDxkXhXPYa937D3LcMJDx
|
||||
{
|
||||
id: 'PLil8shFjsBGaDDxkXhXPYa937D3LcMJDx'
|
||||
},
|
||||
// Pete Herro
|
||||
// https://youtube.com/playlist?list=PLy7gjSbRTE7M8QuYN6Uxp81whm2gAfwK9
|
||||
{
|
||||
id: 'PLy7gjSbRTE7M8QuYN6Uxp81whm2gAfwK9'
|
||||
},
|
||||
// ProgramHub - M1 Pro / M1 Max Apple Silicon
|
||||
// https://www.youtube.com/playlist?list=PLv-goqr3JHiT1_th-4G7KYt2ANT_nhN14
|
||||
{
|
||||
id: 'PLv-goqr3JHiT1_th-4G7KYt2ANT_nhN14'
|
||||
},
|
||||
// ProgramHub - MacBook M2
|
||||
// https://www.youtube.com/playlist?list=PLv-goqr3JHiRlly49iSp69J-0eEeIBEXu
|
||||
{
|
||||
id: 'PLv-goqr3JHiRlly49iSp69J-0eEeIBEXu'
|
||||
},
|
||||
|
||||
|
||||
|
||||
// My Personal Benchmarks Playlist
|
||||
// https://www.youtube.com/playlist?list=PLaa9cZC07ZPFqjoYLZRR3kbbnJRHhlmXq
|
||||
{
|
||||
id: benchmarksPlaylistId
|
||||
},
|
||||
// My Personal Playlist (For odds and ends)
|
||||
// https://www.youtube.com/playlist?list=PLaa9cZC07ZPGM1f6A3F72qXNtPbVLl4V7
|
||||
{
|
||||
id: 'PLaa9cZC07ZPGM1f6A3F72qXNtPbVLl4V7'
|
||||
},
|
||||
]
|
||||
|
|
@ -1,5 +1,10 @@
|
|||
// App Data that is derived from other app data
|
||||
|
||||
import {
|
||||
categories,
|
||||
categoryTemplate
|
||||
} from '~/helpers/categories.js'
|
||||
|
||||
export function isDevice ( listing ) {
|
||||
if ( !listing.hasOwnProperty('endpoint') ) return false
|
||||
|
||||
|
|
@ -57,9 +62,32 @@ export function getVideoEndpoint ( video ) {
|
|||
}
|
||||
|
||||
export function getRouteType ( routeString ) {
|
||||
// Catch non-string routes
|
||||
if ( typeof routeString !== 'string' ) {
|
||||
console.warn( 'routeString is not a string', routeString )
|
||||
throw new Error('Route is not a string')
|
||||
}
|
||||
|
||||
// Remove first slash and split by remaining
|
||||
// slashes to get first part of route
|
||||
const [ routeType ] = routeString.substring(1).split('/')
|
||||
const [ routeType, , subType = null ] = routeString.substring(1).split('/')
|
||||
|
||||
if ( subType === 'benchmarks' ) return 'benchmarks'
|
||||
|
||||
return routeType
|
||||
}
|
||||
|
||||
export function getIconForListing ( listing ) {
|
||||
const routeType = getRouteType( listing.endpoint )
|
||||
|
||||
if ( routeType === 'tv' || routeType === 'benchmarks' ) return '▶️'
|
||||
|
||||
if ( routeType === 'device' ) return '🖥'
|
||||
|
||||
if ( routeType === 'formula' ) return categories.homebrew.icon
|
||||
|
||||
if ( routeType === 'game' ) return categories.games.icon
|
||||
|
||||
// Just use default icon
|
||||
return categoryTemplate.icon
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ import axios from 'axios'
|
|||
|
||||
import { isString } from './check-types.js'
|
||||
import parseMacho from './macho/index.js'
|
||||
|
||||
const prettyBytes = require('pretty-bytes')
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
|
||||
|
||||
const knownArchiveExtensions = new Set([
|
||||
|
|
@ -38,31 +37,55 @@ function callWithTimeout(timeout, func) {
|
|||
})
|
||||
}
|
||||
|
||||
let zip
|
||||
|
||||
// https://stackoverflow.com/a/35610685/1397641
|
||||
const arrayChangeHandler = {
|
||||
get: function( target, property ) {
|
||||
console.log('getting ' + property + ' for ' + target)
|
||||
// property is index in this case
|
||||
return target[property]
|
||||
},
|
||||
set: function( target, property, value, receiver ) {
|
||||
console.log('setting ' + property + ' for ' + target + ' with value ' + value)
|
||||
target[property] = value
|
||||
// you have to return true to accept the changes
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export function makeObservableArray () {
|
||||
const originalArray = []
|
||||
const proxyToArray = new Proxy( originalArray, arrayChangeHandler )
|
||||
|
||||
return {
|
||||
originalArray,
|
||||
proxyToArray
|
||||
}
|
||||
}
|
||||
|
||||
let zip = null
|
||||
|
||||
export default class AppFilesScanner {
|
||||
|
||||
constructor( {
|
||||
observableFilesArray,
|
||||
testResultStore
|
||||
testResultStore,
|
||||
zipModule = null
|
||||
} ) {
|
||||
// Files to process
|
||||
this.files = observableFilesArray
|
||||
|
||||
this.testResultStore = testResultStore
|
||||
|
||||
// https://gildas-lormeau.github.io/zip.js/
|
||||
zip = require('@zip.js/zip.js')
|
||||
|
||||
// https://gildas-lormeau.github.io/zip.js/core-api.html#configuration
|
||||
zip.configure({
|
||||
workerScripts: true,
|
||||
// workerScripts: {
|
||||
// inflate: ["lib/z-worker-pako.js", "pako_inflate.min.js"]
|
||||
// }
|
||||
})
|
||||
this.zipModule = zipModule
|
||||
}
|
||||
|
||||
get zip () {
|
||||
|
||||
if ( this.zipModule ) return this.zipModule
|
||||
|
||||
return zip
|
||||
}
|
||||
|
||||
isApp ( file ) {
|
||||
|
||||
|
|
@ -116,7 +139,9 @@ export default class AppFilesScanner {
|
|||
// }
|
||||
|
||||
async unzipFile ( file ) {
|
||||
const fileReader = new zip.BlobReader( file.instance )//new FileReader()
|
||||
if ( !this.zip ) throw new Error('Zip module not loaded')
|
||||
|
||||
const fileReader = new this.zip.BlobReader( file.instance )//new FileReader()
|
||||
|
||||
fileReader.onload = function() {
|
||||
|
||||
|
|
@ -146,7 +171,7 @@ export default class AppFilesScanner {
|
|||
// console.log('fileReader', fileReader)
|
||||
|
||||
// https://gildas-lormeau.github.io/zip.js/core-api.html#zip-reading
|
||||
const zipReader = new zip.ZipReader( fileReader )
|
||||
const zipReader = new this.zip.ZipReader( fileReader )
|
||||
|
||||
// zipReader.onprogress = console.log
|
||||
|
||||
|
|
@ -404,7 +429,7 @@ export default class AppFilesScanner {
|
|||
const infoXml = await rootInfoEntry.getData(
|
||||
// writer
|
||||
// https://gildas-lormeau.github.io/zip.js/core-api.html#zip-writing
|
||||
new zip.TextWriter(),
|
||||
new this.zip.TextWriter(),
|
||||
// options
|
||||
{
|
||||
useWebWorkers: true,
|
||||
|
|
@ -478,7 +503,7 @@ export default class AppFilesScanner {
|
|||
const bundleExecutableBlob = await bundleExecutable.getData(
|
||||
// writer
|
||||
// https://gildas-lormeau.github.io/zip.js/core-api.html#zip-writing
|
||||
new zip.BlobWriter(),
|
||||
new this.zip.BlobWriter(),
|
||||
// options
|
||||
{
|
||||
useWebWorkers: true
|
||||
|
|
@ -595,4 +620,32 @@ export default class AppFilesScanner {
|
|||
return
|
||||
}
|
||||
|
||||
async setupZipReader () {
|
||||
// https://gildas-lormeau.github.io/zip.js/
|
||||
zip = await import('@zip.js/zip.js')
|
||||
|
||||
// console.log( 'zip', zip )
|
||||
|
||||
// https://gildas-lormeau.github.io/zip.js/core-api.html#configuration
|
||||
zip.configure({
|
||||
workerScripts: true,
|
||||
// workerScripts: {
|
||||
// inflate: ["lib/z-worker-pako.js", "pako_inflate.min.js"]
|
||||
// }
|
||||
})
|
||||
}
|
||||
|
||||
get isSetup () {
|
||||
return this.zip === null
|
||||
}
|
||||
|
||||
async setup () {
|
||||
|
||||
// Setup zip reader if not already done
|
||||
if ( !this.zipModule && !zip ) {
|
||||
await this.setupZipReader()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
21
helpers/array.js
Normal file
21
helpers/array.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
export function getSymmetricDifference (a, b) {
|
||||
return [
|
||||
a.filter(x => !b.includes(x)),
|
||||
b.filter(x => !a.includes(x))
|
||||
]
|
||||
}
|
||||
|
||||
export function logArraysDifference (a, b) {
|
||||
const [ aOnly, bOnly ] = getSymmetricDifference(a, b)
|
||||
|
||||
|
||||
console.log( 'Missing from first list:', aOnly )
|
||||
console.log( 'Missing from second list:', bOnly )
|
||||
|
||||
console.log( `List difference Count ${ aOnly.length } / ${ bOnly.length }`, )
|
||||
|
||||
return {
|
||||
aOnly,
|
||||
bOnly,
|
||||
}
|
||||
}
|
||||
17
helpers/astro/request.js
Normal file
17
helpers/astro/request.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { getNetlifyRedirect } from '~/helpers/config-node.js'
|
||||
|
||||
export async function catchRedirectResponse ( Astro ) {
|
||||
const requestUrl = new URL( Astro.request.url )
|
||||
|
||||
const netlifyRedirectUrl = await getNetlifyRedirect( requestUrl.pathname )
|
||||
|
||||
// console.log('netlifyRedirectUrl', netlifyRedirectUrl)
|
||||
|
||||
if ( netlifyRedirectUrl !== null ) {
|
||||
return Astro.redirect( netlifyRedirectUrl.to )
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,6 +1,3 @@
|
|||
|
||||
// import { promises as fs } from 'fs'
|
||||
|
||||
import fs from 'fs-extra'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import axios from 'axios'
|
||||
|
|
@ -12,7 +9,11 @@ import parseDate from './parse-date'
|
|||
import { eitherMatches } from './matching.js'
|
||||
import { getAppEndpoint } from './app-derived'
|
||||
import { makeSlug } from './slug.js'
|
||||
import { byTimeThenNull } from './sort-list.js'
|
||||
|
||||
import {
|
||||
cliOptions
|
||||
} from '~/helpers/cli-options.js'
|
||||
|
||||
const md = new MarkdownIt()
|
||||
|
||||
|
|
@ -156,7 +157,9 @@ export function buildReadmeAppList ({ readmeContent, scanListMap, commits }) {
|
|||
} )
|
||||
}
|
||||
|
||||
console.log(`Merged ${alias} (${scannedApp.bundleIds[0]}) from scanned apps into ${name} from README`)
|
||||
if ( cliOptions.showMerges || cliOptions.verbose ) {
|
||||
console.log(`Merged ${alias} (${scannedApp.bundleIds[0]}) from scanned apps into ${name} from README`)
|
||||
}
|
||||
scanListMap.delete( key )
|
||||
}
|
||||
}
|
||||
|
|
@ -435,16 +438,8 @@ export default async function () {
|
|||
// console.log('appList', appList)
|
||||
|
||||
|
||||
return [
|
||||
return ([
|
||||
...appList,
|
||||
...Array.from( scanListMap, ([name, value]) => value )
|
||||
]
|
||||
|
||||
// fs.readFile('../README.md', 'utf8')
|
||||
// .then((err, data) => {
|
||||
// const result = md.parse(data)
|
||||
// console.log('result', result)
|
||||
|
||||
// return result
|
||||
// })
|
||||
]).sort( byTimeThenNull )
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
|
||||
import axios from 'axios'
|
||||
import fs from 'fs-extra'
|
||||
import { PromisePool } from '@supercharge/promise-pool'
|
||||
|
||||
import { fuzzyMatchesWholeWord } from './matching.js'
|
||||
|
|
@ -7,6 +6,7 @@ import { byTimeThenNull } from './sort-list.js'
|
|||
import { getVideoEndpoint } from './app-derived.js'
|
||||
import parseDate from './parse-date'
|
||||
import { makeSlug } from './slug.js'
|
||||
import { youtubeVideoPath } from '~/helpers/api/youtube/build.js'
|
||||
|
||||
|
||||
const inTimestamps = ( name, video ) => {
|
||||
|
|
@ -140,13 +140,16 @@ async function handleFetchedVideo ( fetchedVideo, videoId, applist ) {
|
|||
// Build video slug
|
||||
const slug = makeSlug( `${fetchedVideo.title}-i-${videoId}` )
|
||||
|
||||
const apps = []
|
||||
const appLinks = []
|
||||
// Generate new tag set based on api data
|
||||
const tags = generateVideoTags(fetchedVideo)
|
||||
|
||||
for ( const app of applist ) {
|
||||
if (videoFeaturesApp(app, fetchedVideo)) {
|
||||
apps.push(app.slug)
|
||||
appLinks.push({
|
||||
name: app.name,
|
||||
endpoint: app.endpoint
|
||||
})
|
||||
|
||||
tags.add(app.category.slug)
|
||||
}
|
||||
|
|
@ -165,7 +168,7 @@ async function handleFetchedVideo ( fetchedVideo, videoId, applist ) {
|
|||
name: fetchedVideo.title,
|
||||
id: videoId,
|
||||
lastUpdated,
|
||||
apps,
|
||||
appLinks,
|
||||
slug,
|
||||
channel: {
|
||||
name: fetchedVideo.rawData.snippet.channelTitle,
|
||||
|
|
@ -184,12 +187,12 @@ async function handleFetchedVideo ( fetchedVideo, videoId, applist ) {
|
|||
|
||||
export default async function ( applist ) {
|
||||
|
||||
const videosJsonUrl = process.env.VIDEO_SOURCE || `${process.env.VFUNCTIONS_URL}/videos.json`
|
||||
// const videosJsonUrl = process.env.VIDEO_SOURCE || `${process.env.VFUNCTIONS_URL}/videos.json`
|
||||
|
||||
// Fetch Commits
|
||||
const response = await axios.get( videosJsonUrl )
|
||||
// const response = await axios.get( videosJsonUrl )
|
||||
// Extract commit from response data
|
||||
const fetchedVideos = response.data
|
||||
const fetchedVideos = await fs.readJson( youtubeVideoPath )//response.data
|
||||
|
||||
const videos = []
|
||||
|
||||
|
|
|
|||
13
helpers/bundles.js
Normal file
13
helpers/bundles.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export function supportedArchitectures ( appScan ) {
|
||||
// if ( Array.isArray(appScan['Macho Meta']) ) {
|
||||
// return appScan['Macho Meta'].map( architecture => architecture.processorType)
|
||||
// }
|
||||
|
||||
// console.log('meta', appScan['Macho Meta'])
|
||||
|
||||
if ( appScan['Macho Meta'].architectures === undefined ) return []
|
||||
|
||||
return appScan['Macho Meta'].architectures
|
||||
.map( architecture => architecture.processorType)
|
||||
.filter(processorType => Number(processorType) !== 0)
|
||||
}
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
// Universal JS imports only
|
||||
import shuffle from 'just-shuffle'
|
||||
|
||||
import { makeSlug } from './slug.js'
|
||||
|
||||
export function makeCategorySlug ( categoryName ) {
|
||||
return makeSlug( categoryName )
|
||||
}
|
||||
|
||||
|
||||
// Maps iTunes Genres titles to Category IDs
|
||||
const categoryMap = new Map([
|
||||
[ 'Business', 2 ],
|
||||
[ 'Entertainment', 5 ],
|
||||
|
|
@ -18,7 +22,12 @@ const categoryMap = new Map([
|
|||
// [ 'Name', 1 ],
|
||||
|
||||
// Needs work before apps can be assigned games category
|
||||
// so for now 'Games' genre is Entertainment
|
||||
// Games will be assigned to the "Games" category
|
||||
// but need to be put into games kind/list
|
||||
// so the solution may be to separate games from apps
|
||||
// during app list build and then merge "Game from Apps"
|
||||
// into the "Games" list
|
||||
// but for now 'Games' genre is Entertainment
|
||||
// [ 'Games', 100 ],
|
||||
[ 'Games', 5 ],
|
||||
])
|
||||
|
|
@ -42,7 +51,8 @@ export const categoryTemplate = {
|
|||
pluralLabel: null,
|
||||
itemSuffixLabel: null,
|
||||
icon: null,
|
||||
requestLinks: null
|
||||
requestLinks: null,
|
||||
icon: '💻',
|
||||
}
|
||||
|
||||
export const categories = {
|
||||
|
|
@ -59,6 +69,7 @@ export const categories = {
|
|||
label: 'Developer Tools',
|
||||
pluralLabel: 'Developer Tools',
|
||||
slug: 'developer-tools',
|
||||
snakeSlug: 'developer_tools',
|
||||
},
|
||||
|
||||
'productivity-tools': {
|
||||
|
|
@ -67,6 +78,7 @@ export const categories = {
|
|||
label: 'Productivity Tools',
|
||||
pluralLabel: 'Productivity Tools',
|
||||
slug: 'productivity-tools',
|
||||
snakeSlug: 'productivity_tools',
|
||||
},
|
||||
|
||||
'video-and-motion-tools': {
|
||||
|
|
@ -75,6 +87,7 @@ export const categories = {
|
|||
label: 'Video and Motion Tools',
|
||||
pluralLabel: 'Video and Motion Tools',
|
||||
slug: 'video-and-motion-tools',
|
||||
snakeSlug: 'video_and_motion_tools',
|
||||
},
|
||||
|
||||
'social-and-communication': {
|
||||
|
|
@ -83,6 +96,7 @@ export const categories = {
|
|||
label: 'Social and Communication',
|
||||
pluralLabel: 'Social and Communication Apps',
|
||||
slug: 'social-and-communication',
|
||||
snakeSlug: 'social_and_communication',
|
||||
},
|
||||
|
||||
'entertainment-and-media-apps': {
|
||||
|
|
@ -91,6 +105,7 @@ export const categories = {
|
|||
label: 'Entertainment and Media Apps',
|
||||
pluralLabel: 'Entertainment and Media Apps',
|
||||
slug: 'entertainment-and-media-apps',
|
||||
snakeSlug: 'entertainment_and_media_apps',
|
||||
},
|
||||
|
||||
'music-and-audio-tools': {
|
||||
|
|
@ -99,6 +114,7 @@ export const categories = {
|
|||
label: 'Music and Audio Tools',
|
||||
pluralLabel: 'Music and Audio Tools',
|
||||
slug: 'music-and-audio-tools',
|
||||
snakeSlug: 'music_and_audio_tools',
|
||||
},
|
||||
|
||||
'photo-and-graphic-tools': {
|
||||
|
|
@ -107,6 +123,7 @@ export const categories = {
|
|||
label: 'Photo and Graphic Tools',
|
||||
pluralLabel: 'Photo and Graphic Tools',
|
||||
slug: 'photo-and-graphic-tools',
|
||||
snakeSlug: 'photo_and_graphic_tools',
|
||||
},
|
||||
|
||||
'science-and-research-software': {
|
||||
|
|
@ -115,6 +132,7 @@ export const categories = {
|
|||
label: 'Science and Research Software',
|
||||
pluralLabel: 'Science and Research Software',
|
||||
slug: 'science-and-research-software',
|
||||
snakeSlug: 'science_and_research_software',
|
||||
},
|
||||
|
||||
'3d-and-architecture': {
|
||||
|
|
@ -123,6 +141,7 @@ export const categories = {
|
|||
label: '3D and Architecture',
|
||||
pluralLabel: '3D and Architecture Applications',
|
||||
slug: '3d-and-architecture',
|
||||
snakeSlug: '3d_and_architecture',
|
||||
},
|
||||
|
||||
'vpns-security-and-privacy': {
|
||||
|
|
@ -131,6 +150,7 @@ export const categories = {
|
|||
label: 'VPNs, Security, and Privacy',
|
||||
pluralLabel: 'VPN, Security, and Privacy Applications',
|
||||
slug: 'vpns-security-and-privacy',
|
||||
snakeSlug: 'vpns_security_and_privacy',
|
||||
},
|
||||
|
||||
'live-production-and-performance': {
|
||||
|
|
@ -139,6 +159,7 @@ export const categories = {
|
|||
label: 'Live Production and Performance',
|
||||
pluralLabel: 'Live Production and Performance Software',
|
||||
slug: 'live-production-and-performance',
|
||||
snakeSlug: 'live_production_and_performance',
|
||||
},
|
||||
|
||||
'system-tools': {
|
||||
|
|
@ -147,6 +168,7 @@ export const categories = {
|
|||
label: 'System Tools',
|
||||
pluralLabel: 'System Tools',
|
||||
slug: 'system-tools',
|
||||
snakeSlug: 'system_tools',
|
||||
},
|
||||
|
||||
// Special Lists
|
||||
|
|
@ -156,6 +178,7 @@ export const categories = {
|
|||
label: 'Games',
|
||||
pluralLabel: 'Games',
|
||||
slug: 'games',
|
||||
snakeSlug: 'games',
|
||||
icon: '🎮',
|
||||
requestLinks: [
|
||||
{
|
||||
|
|
@ -171,6 +194,7 @@ export const categories = {
|
|||
pluralLabel: 'Homebrew Formulae',
|
||||
itemSuffixLabel: 'via Homebrew',
|
||||
slug: 'homebrew',
|
||||
snakeSlug: 'homebrew',
|
||||
icon: '🍺'
|
||||
},
|
||||
|
||||
|
|
@ -182,9 +206,25 @@ export const categories = {
|
|||
label: 'Uncategorized',
|
||||
pluralLabel: 'Uncategorized Software',
|
||||
slug: 'uncategorized',
|
||||
snakeSlug: 'uncategorized',
|
||||
},
|
||||
}
|
||||
|
||||
// Maps categories to kinds and vice versa
|
||||
const categoryToKind = {
|
||||
...Object.fromEntries( Object.keys( categories ).map( key => [ key, key ] ) ),
|
||||
'homebrew': 'formula',
|
||||
'games': 'game',
|
||||
}
|
||||
|
||||
// Respective directory for each category
|
||||
export function getCategoryKindName ( categorySlug ) {
|
||||
return categoryToKind[ categorySlug ]
|
||||
}
|
||||
|
||||
export function getKindToCategorySlug ( kindSlug ) {
|
||||
return Object.keys( categories ).find( key => categoryToKind[ key ] === kindSlug )
|
||||
}
|
||||
|
||||
export const categoriesById = Object.fromEntries( Object.entries( categories ).map( ([ key, category ]) => [category.id, { ...category, key } ] ) )
|
||||
|
||||
|
|
@ -211,6 +251,16 @@ export function getAppCategory (app) {
|
|||
return categories[app.category.slug]
|
||||
}
|
||||
|
||||
const categoryFilterPrefix = 'category_'
|
||||
|
||||
export function makeCategoryFilterFromListing ( listing ) {
|
||||
return `${ categoryFilterPrefix }${ getAppCategory( listing ).snakeSlug }`
|
||||
}
|
||||
|
||||
export function makeCategoryFilterFromCategorySlug ( categorySlug ) {
|
||||
const category = categories[ categorySlug ]
|
||||
return `${ categoryFilterPrefix }${ category.snakeSlug }`
|
||||
}
|
||||
|
||||
export function findCategoryForTagsSet ( tags ) {
|
||||
|
||||
|
|
@ -230,3 +280,20 @@ export function findCategoryForTagsSet ( tags ) {
|
|||
|
||||
return null
|
||||
}
|
||||
|
||||
const sampleListFormatter = new Intl.ListFormat( 'en', { style: 'long', type: 'unit' })
|
||||
|
||||
export function makeSummaryOfListings ({
|
||||
list,
|
||||
length = 25,
|
||||
random = false,
|
||||
suffix = ', etc...',
|
||||
} = {}) {
|
||||
const listToSample = random ? shuffle( list ) : list
|
||||
|
||||
const samples = listToSample
|
||||
.slice(0, length)
|
||||
.map( listing => listing.name )
|
||||
|
||||
return sampleListFormatter.format( samples ) + suffix
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,25 @@ export function isString( maybeString ) {
|
|||
return (typeof maybeString === 'string' || maybeString instanceof String)
|
||||
}
|
||||
|
||||
export function isNonEmptyString ( maybeString ) {
|
||||
if ( !isString( maybeString ) ) return false
|
||||
|
||||
return maybeString.length > 0
|
||||
}
|
||||
|
||||
export function isNonEmptyArray ( maybeArray ) {
|
||||
if ( !Array.isArray( maybeArray ) ) return false
|
||||
|
||||
return maybeArray.length > 0
|
||||
}
|
||||
|
||||
export function isPositiveNumberString ( maybeNumber ) {
|
||||
if ( !isString( maybeNumber ) ) return false
|
||||
|
||||
return /\d+$/.test( maybeNumber )
|
||||
}
|
||||
|
||||
|
||||
export function isValidHttpUrl( maybeUrl, allowUnsecure = false ) {
|
||||
if ( !isString( maybeUrl ) ) return false
|
||||
|
||||
|
|
@ -22,6 +41,17 @@ export function isValidHttpUrl( maybeUrl, allowUnsecure = false ) {
|
|||
return url.protocol === "https:"
|
||||
}
|
||||
|
||||
export function isValidImageUrl ( maybeUrl ) {
|
||||
if ( !isValidHttpUrl( maybeUrl ) ) return false
|
||||
|
||||
// Check if url has a file extension
|
||||
const url = new URL(maybeUrl)
|
||||
const fileExtension = url.pathname.split('.').pop()
|
||||
|
||||
return isNonEmptyString( fileExtension )
|
||||
}
|
||||
|
||||
|
||||
export function isObject( maybeObject ) {
|
||||
return maybeObject === Object( maybeObject )
|
||||
}
|
||||
|
|
|
|||
8
helpers/cli-options.js
Normal file
8
helpers/cli-options.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export const commandArguments = process.argv
|
||||
export const cliOptions = {
|
||||
verbose: commandArguments.includes('--verbose'),
|
||||
|
||||
withApi: commandArguments.includes('--with-api'),
|
||||
noLists: commandArguments.includes('--no-lists'),
|
||||
showMerges: commandArguments.includes('--show-merges'),
|
||||
}
|
||||
323
helpers/config-node.js
Normal file
323
helpers/config-node.js
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
import TOML from '@iarna/toml'
|
||||
import fs from 'fs-extra'
|
||||
|
||||
import pkg from '~/package.json'
|
||||
import { getSiteUrl } from '~/helpers/url.js'
|
||||
|
||||
|
||||
export const siteUrl = getSiteUrl()
|
||||
|
||||
export const nuxtHead = {
|
||||
// this htmlAttrs you need
|
||||
htmlAttrs: {
|
||||
lang: 'en',
|
||||
},
|
||||
title: 'Does It ARM',
|
||||
description: pkg.description,
|
||||
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width, initial-scale=1'
|
||||
},
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: pkg.description
|
||||
},
|
||||
{
|
||||
'property': 'og:image',
|
||||
'content': `${ siteUrl }/images/og-image.png`
|
||||
},
|
||||
{
|
||||
'property': 'og:image:width',
|
||||
'content': '1200'
|
||||
},
|
||||
{
|
||||
'property': 'og:image:height',
|
||||
'content': '627'
|
||||
},
|
||||
{
|
||||
'property': 'og:image:alt',
|
||||
'content': 'Does It ARM Logo'
|
||||
},
|
||||
|
||||
// Twitter Card
|
||||
{
|
||||
'property': 'twitter:card',
|
||||
'content': 'summary'
|
||||
},
|
||||
{
|
||||
'property': 'twitter:title',
|
||||
'content': 'Does It ARM'
|
||||
},
|
||||
{
|
||||
'property': 'twitter:description',
|
||||
'content': pkg.description
|
||||
},
|
||||
{
|
||||
'property': 'twitter:url',
|
||||
'content': `${ siteUrl }`
|
||||
},
|
||||
{
|
||||
'property': 'twitter:image',
|
||||
'content': `${ siteUrl }/images/mark.png`
|
||||
}
|
||||
],
|
||||
link: [
|
||||
// Favicon
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/x-icon',
|
||||
href: '/favicon.ico'
|
||||
},
|
||||
|
||||
// Gtag Preconnect
|
||||
{
|
||||
rel: 'preconnect',
|
||||
href: 'https://www.googletagmanager.com'
|
||||
},
|
||||
|
||||
// Carbon Preconnects
|
||||
{
|
||||
rel: 'preconnect',
|
||||
href: 'https://cdn.carbonads.com'
|
||||
},
|
||||
{
|
||||
rel: 'preconnect',
|
||||
href: 'https://srv.carbonads.net'
|
||||
},
|
||||
{
|
||||
rel: 'preconnect',
|
||||
href: 'https://cdn4.buysellads.net'
|
||||
},
|
||||
],
|
||||
|
||||
script: [
|
||||
// // Carbon Ads
|
||||
// // https://sell.buysellads.com/zones/1294/ad-tags#z=js
|
||||
// {
|
||||
// // <script async type="text/javascript" src="//cdn.carbonads.com/carbon.js?serve=CK7DVK3M&placement=doesitarmcom" id="_carbonads_js"></script>
|
||||
// src: 'https://cdn.carbonads.com/carbon.js?serve=CK7DVK3M&placement=doesitarmcom',
|
||||
// async: true,
|
||||
// type: 'text/javascript',
|
||||
// id: '_carbonads_js',
|
||||
// class: 'include-on-static carbon-inline-wide',
|
||||
// body: true
|
||||
// }
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function getNetlifyConfig () {
|
||||
const configPath = './netlify.toml'
|
||||
const tomlContent = await fs.readFile(configPath, 'utf-8')
|
||||
const netlifyConfig = TOML.parse(tomlContent)
|
||||
|
||||
// console.log('netlifyConfig', netlifyConfig)
|
||||
// console.log('tomlContent', tomlContent)
|
||||
|
||||
return netlifyConfig
|
||||
}
|
||||
|
||||
export async function getNetlifyRedirect ( path ) {
|
||||
// Check if the path is valid
|
||||
// by checking if it starts with a slash
|
||||
// and does not end with a slash
|
||||
// if ( !path.startsWith('/') || path.endsWith('/') ) {
|
||||
// throw new Error(`Invalid Netlify redirect path: ${ path }`)
|
||||
// }
|
||||
|
||||
const netlifyConfig = await getNetlifyConfig()
|
||||
const redirects = netlifyConfig.redirects
|
||||
|
||||
for ( const redirect of redirects ) {
|
||||
// Check if the from path is valid
|
||||
// by checking if it starts with a slash
|
||||
// and does not end with a slash
|
||||
if ( !redirect.from.startsWith('/') || redirect.from.endsWith('/') ) {
|
||||
throw new Error(`Invalid Netlify redirect.from path: ${ redirect.from }`)
|
||||
}
|
||||
|
||||
if ( redirect.from === path ) {
|
||||
return redirect
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function makeTitle ( listing ) {
|
||||
return `Does ${ listing.name } work on Apple Silicon? - ${ nuxtHead.title }`
|
||||
}
|
||||
|
||||
export function makeDescription ( listing ) {
|
||||
return `Latest reported support status of ${ listing.name } on Apple Silicon and ${ this.$config.processorsVerbiage } Processors.`
|
||||
}
|
||||
|
||||
function makeTag ( tag, tagName = 'meta' ) {
|
||||
|
||||
const attributes = Object.entries(tag).map( ([ name, value ]) => `${name}="${value}"` ).join(' ')
|
||||
|
||||
return `<${tagName} ${attributes}>`
|
||||
}
|
||||
|
||||
function mapMetaTag ( tag ) {
|
||||
|
||||
if ( tag.hasOwnProperty('property') ) {
|
||||
return [
|
||||
`property-${tag.property}`,
|
||||
makeTag(tag)
|
||||
]
|
||||
}
|
||||
|
||||
if ( tag.hasOwnProperty('name') ) {
|
||||
return [
|
||||
`name-${tag.name}`,
|
||||
makeTag(tag)
|
||||
]
|
||||
}
|
||||
|
||||
if ( tag.hasOwnProperty('charset') ) {
|
||||
return [
|
||||
'charset',
|
||||
makeTag(tag)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function mapLinkTag ( tag ) {
|
||||
return [
|
||||
`type-${tag.type}`,
|
||||
makeTag(tag, 'link')
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
export class PageHead {
|
||||
|
||||
constructor ( options = {} ) {
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
meta = [],
|
||||
link = [],
|
||||
structuredData = null,
|
||||
|
||||
domain = getSiteUrl(),
|
||||
pathname,
|
||||
} = options
|
||||
|
||||
this.title = title
|
||||
this.description = description
|
||||
this.meta = meta
|
||||
this.link = link
|
||||
this.structuredData = structuredData
|
||||
|
||||
this.domain = domain
|
||||
this.pathname = pathname
|
||||
}
|
||||
|
||||
get pageUrl () {
|
||||
const urlInstance = new URL( this.domain )
|
||||
|
||||
urlInstance.pathname = this.pathname
|
||||
|
||||
return urlInstance
|
||||
}
|
||||
|
||||
get pageUrlString () {
|
||||
return this.pageUrl.toString()
|
||||
}
|
||||
|
||||
get defaultMeta () {
|
||||
return nuxtHead.meta
|
||||
}
|
||||
|
||||
get defaultMetaTags () {
|
||||
return Object.fromEntries( nuxtHead.meta.map( mapMetaTag ) )
|
||||
}
|
||||
|
||||
get defaultLinkTags () {
|
||||
return Object.fromEntries( nuxtHead.link.map( mapLinkTag ) )
|
||||
}
|
||||
|
||||
get pageMeta () {
|
||||
// console.log('this.defaultMeta', this.defaultMeta)
|
||||
return [
|
||||
...this.defaultMeta,
|
||||
|
||||
{
|
||||
'property': 'twitter:url',
|
||||
'content': this.pageUrlString
|
||||
},
|
||||
|
||||
...this.meta
|
||||
]
|
||||
}
|
||||
|
||||
get metaTags () {
|
||||
|
||||
const metaTags = {
|
||||
// ...this.defaultMeta,
|
||||
// 'property-twitter:url': `<meta property="twitter:url" content="${ this.pageUrlString }">`,
|
||||
...Object.fromEntries( this.pageMeta.map(mapMetaTag) )
|
||||
}
|
||||
|
||||
// Get description from data
|
||||
if ( this.description ) {
|
||||
// Set meta description
|
||||
metaTags['name-description'] = `<meta hid="description" name="description" content="${ this.description }">`
|
||||
// Set twitter description
|
||||
metaTags['property-twitter:description'] = `<meta hid="twitter:description" property="twitter:description" content="${ this.description }">`
|
||||
}
|
||||
|
||||
// Get title from data
|
||||
if ( this.title ) {
|
||||
// Set twitter title
|
||||
metaTags['property-twitter:title'] = `<meta hid="twitter:title" property="twitter:title" content="${ this.title }">`
|
||||
}
|
||||
|
||||
|
||||
return metaTags
|
||||
}
|
||||
|
||||
|
||||
get metaMarkup () {
|
||||
return Object.values( this.metaTags ).join('')
|
||||
}
|
||||
|
||||
get linkTags () {
|
||||
|
||||
const linkTags = {
|
||||
...this.defaultLinkTags,
|
||||
...Object.fromEntries( this.link.map( mapLinkTag ) )
|
||||
}
|
||||
|
||||
return linkTags
|
||||
}
|
||||
|
||||
get linkMarkup () {
|
||||
return Object.values( this.linkTags ).join('')
|
||||
}
|
||||
|
||||
get metaAndLinkMarkup () {
|
||||
return [
|
||||
this.metaMarkup,
|
||||
this.linkMarkup
|
||||
].join('')
|
||||
}
|
||||
|
||||
get structuredDataMarkup () {
|
||||
|
||||
if ( structuredData === null ) return ''
|
||||
|
||||
const structuredDataJson = JSON.stringify( structuredData )
|
||||
|
||||
return `<script type="application/ld+json">${ structuredDataJson }</script>`
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
9
helpers/constants.js
Normal file
9
helpers/constants.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
export const filterSeparator = '_'
|
||||
|
||||
export const sitemapLocation = './static/'
|
||||
|
||||
export const sitemapIndexFileName = 'sitemap-index.xml'
|
||||
|
||||
// https://analytics.google.com/analytics/web/#/a28434239p302384837/admin/streams/table/3228170828
|
||||
export const gaMeasurementId = 'G-0WLH5YTTB0'
|
||||
15
helpers/environment.js
Normal file
15
helpers/environment.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import has from 'just-has'
|
||||
|
||||
|
||||
export function isNuxt( VueThis ) {
|
||||
return has( VueThis, [ '$nuxt' ])
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/8684009/1397641
|
||||
export function isDarwin () {
|
||||
if ( typeof navigator !== 'undefined' ) return false
|
||||
|
||||
if ( typeof process === 'undefined' ) return false
|
||||
|
||||
return process.platform === 'darwin'
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
import statuses from '~/helpers/statuses'
|
||||
|
||||
export default function ( appList ) {
|
||||
if ( !Array.isArray( appList ) ) {
|
||||
throw new Error(`List must be an array but is ${typeof appList}`)
|
||||
}
|
||||
|
||||
const totalApps = appList.length
|
||||
|
||||
|
|
|
|||
214
helpers/listing-page.js
Normal file
214
helpers/listing-page.js
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
|
||||
import has from 'just-has'
|
||||
// https://anguscroll.com/just/just-replace-all
|
||||
import replaceAll from 'just-replace-all'
|
||||
|
||||
import {
|
||||
getAppType
|
||||
} from './app-derived.js'
|
||||
import { buildVideoStructuredData } from './structured-data.js'
|
||||
import { nuxtHead } from '~/helpers/config-node.js'
|
||||
import {
|
||||
getPartPartsFromUrl
|
||||
} from './url.js'
|
||||
|
||||
|
||||
export const samChannelId = 'UCB3jOb5QVjX7lYecvyCoTqQ'
|
||||
|
||||
function makeTitle ( listing ) {
|
||||
return `Does ${ listing.name } work on Apple Silicon? - ${ nuxtHead.title }`
|
||||
}
|
||||
|
||||
function makeDescription ( listing ) {
|
||||
// const processorsVerbiage = process.env.npm_package_config_verbiage_processors || this.$config.processorsVerbiage
|
||||
|
||||
const processorsVerbiage = 'Apple M2 and M1 Ultra'
|
||||
|
||||
return `Latest reported support status of ${ listing.name } on Apple Silicon and ${ processorsVerbiage } Processors.`
|
||||
}
|
||||
|
||||
function convertYoutubeImageUrl ( stringWithUrls, extension ) {
|
||||
let workingString = stringWithUrls
|
||||
|
||||
workingString = replaceAll( stringWithUrls, 'ytimg.com/vi/', `ytimg.com/vi_${ extension }/`)
|
||||
|
||||
workingString = workingString.replace(/.png|.jpg|.jpeg/g, `.${ extension }`)
|
||||
|
||||
return workingString
|
||||
}
|
||||
|
||||
export function getVideoImages ( video ) {
|
||||
|
||||
// Catch the case where the video has no thumbnails
|
||||
if ( !has( video, 'thumbnail' ) ) throw new Error('No thumbnail found')
|
||||
|
||||
const webpSource = {
|
||||
...video.thumbnail,
|
||||
srcset: convertYoutubeImageUrl( video.thumbnail.srcset, 'webp' ),
|
||||
src: convertYoutubeImageUrl( video.thumbnail.src, 'webp' ),
|
||||
type: 'image/webp'
|
||||
}
|
||||
|
||||
const jpgSource = {
|
||||
...video.thumbnail,
|
||||
type: 'image/jpeg'
|
||||
}
|
||||
|
||||
const sources = {
|
||||
webp: webpSource,
|
||||
jpeg: jpgSource
|
||||
}
|
||||
|
||||
// Responsive Preloads - https://web.dev/preload-responsive-images/
|
||||
// Responsive Preloads with image types - https://blog.laurenashpole.com/post/658079409151016960/preloading-images-in-a-responsive-webp-world
|
||||
// <link rel="preload" as="image" href="large-image.webp" media="(min-width: 768px)" imagesrcset="large-image.webp, large-image-2x.webp 2x" type="image/webp" />
|
||||
const preloads = Object.entries( sources ).map( ([ typeKey, typeSource ]) => {
|
||||
return {
|
||||
'rel': 'preload',
|
||||
'as': 'image',
|
||||
'href': typeSource.src,
|
||||
'media': typeSource.sizes,
|
||||
'imagesrcset': typeSource.srcset,
|
||||
'type': typeSource.type
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
imgSrc: video.thumbnail.src,
|
||||
sources,
|
||||
preloads
|
||||
}
|
||||
}
|
||||
|
||||
export function makeApiPathFromEndpoint ( endpoint ) {
|
||||
const [
|
||||
kind,
|
||||
listingSlug
|
||||
] = getPartPartsFromUrl( endpoint )
|
||||
|
||||
return `/api/${ kind }/${ listingSlug }.json`
|
||||
}
|
||||
export class ListingDetails {
|
||||
constructor ( listing ) {
|
||||
this.api = listing
|
||||
|
||||
this.type = getAppType( listing )
|
||||
}
|
||||
|
||||
type = ''
|
||||
|
||||
get isGame () {
|
||||
return this.type === 'game'
|
||||
}
|
||||
|
||||
isListingDetails = true
|
||||
|
||||
get mainHeading () {
|
||||
// Use the video title for videos
|
||||
if ( this.type === 'video' ) {
|
||||
return this.api.name
|
||||
}
|
||||
|
||||
if ( this.type === 'formula' ) {
|
||||
return `Does <code class="bg-darkest rounded px-2 py-1">${ this.api.name }</code> work on Apple Silicon when installed via Homebrew?`
|
||||
}
|
||||
|
||||
return `Does ${ this.api.name } work on Apple Silicon?`
|
||||
}
|
||||
|
||||
get subtitle () {
|
||||
return this.api.text
|
||||
}
|
||||
|
||||
get pageTitle () {
|
||||
return makeTitle( this.api )
|
||||
}
|
||||
|
||||
get pageDescription () {
|
||||
return makeDescription( this.api )
|
||||
}
|
||||
|
||||
get endpointParts () {
|
||||
return getPartPartsFromUrl( this.api.endpoint )
|
||||
}
|
||||
|
||||
get apiEndpointPath () {
|
||||
return makeApiPathFromEndpoint( this.api.endpoint )
|
||||
}
|
||||
|
||||
get relatedVideos () {
|
||||
if ( Array.isArray( this.api.relatedVideos ) ) {
|
||||
return this.api.relatedVideos
|
||||
}
|
||||
|
||||
if ( !!this.api.payload && Array.isArray( this.api.payload.relatedVideos ) ) {
|
||||
return this.api.payload.relatedVideos
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
get hasRelatedVideos () {
|
||||
return this.relatedVideos.length > 0
|
||||
}
|
||||
|
||||
get hasRelatedApps () {
|
||||
return Array.isArray( this.api.appLinks ) && this.api.appLinks.length > 0
|
||||
}
|
||||
|
||||
get hasBenchmarksPage () {
|
||||
return this.hasRelatedVideos
|
||||
}
|
||||
|
||||
get shouldHaveSubscribeButton () {
|
||||
if ( this.initialVideo === null ) return false
|
||||
|
||||
return this.initialVideo.channel.id !== samChannelId
|
||||
}
|
||||
|
||||
get initialVideo () {
|
||||
if ( this.type === 'video' ) {
|
||||
return this.api
|
||||
}
|
||||
|
||||
if ( this.hasRelatedVideos ) {
|
||||
return this.api.relatedVideos[0]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
get hasInitialVideo () {
|
||||
return this.initialVideo !== null
|
||||
}
|
||||
|
||||
get structuredData () {
|
||||
if ( this.type === 'video' ) {
|
||||
return buildVideoStructuredData( this.api, this.api.appLinks )
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
get headOptions () {
|
||||
return {
|
||||
title: this.pageTitle,
|
||||
description: this.pageDescription,
|
||||
// meta,
|
||||
link: [],
|
||||
structuredData: this.structuredData,
|
||||
|
||||
// domain,
|
||||
pathname: this.api.endpoint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function ensureListingDetails ( listing ) {
|
||||
if ( listing.isListingDetails ) {
|
||||
return listing
|
||||
}
|
||||
|
||||
return new ListingDetails( listing )
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// https://github.com/paulirish/lite-youtube-embed/blob/master/src/lite-yt-embed.js
|
||||
|
||||
// import canAutoPlay from 'can-autoplay'
|
||||
import canAutoPlay from 'can-autoplay'
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -47,10 +47,10 @@ class LiteYTEmbed extends HTMLElement {
|
|||
this.playerContainer = this.querySelector('.player-container')
|
||||
this.playerPoster = this.querySelector('.player-poster')
|
||||
|
||||
// console.log('canAutoplay from connectedCallback', canAutoplay)
|
||||
// console.log('canAutoPlay from connectedCallback', canAutoPlay)
|
||||
|
||||
console.log('video', this.video)
|
||||
console.log('this.playerContainer', this.playerContainer)
|
||||
// console.log('video', this.video)
|
||||
// console.log('this.playerContainer', this.playerContainer)
|
||||
|
||||
|
||||
// Start watchers here
|
||||
|
|
@ -83,7 +83,7 @@ class LiteYTEmbed extends HTMLElement {
|
|||
|
||||
this.detectAutoplay()
|
||||
.then( ({ willAutoplay }) => {
|
||||
console.log('willAutoplay', willAutoplay)
|
||||
// console.log('willAutoplay', willAutoplay)
|
||||
|
||||
// If we're allowed to autoplay
|
||||
// then start loading the player
|
||||
|
|
@ -286,7 +286,7 @@ class LiteYTEmbed extends HTMLElement {
|
|||
|
||||
// const { default: canAutoPlay } = await import('can-autoplay')
|
||||
|
||||
const willAutoplay = await canAutoplay.video()
|
||||
const willAutoplay = await canAutoPlay.video()
|
||||
// const willAutoplayMuted = await canAutoPlay.video({ muted: true, inline: true })
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export function MachoParser(file, callback) {
|
|||
var cmd = null;
|
||||
if(type == LOAD_COMMAND_TYPE.LC_SEGMENT) {
|
||||
if(data.length < 48) {
|
||||
if(process.env.VERBOSE){ console.log('Segment command OOB'); }
|
||||
// if(process.env.VERBOSE){ console.log('Segment command OOB'); }
|
||||
return new LoadCommand(type, data, size, off);
|
||||
}
|
||||
let name = new Cstr(data.slice(0, (4*uint32_t)));
|
||||
|
|
@ -173,7 +173,7 @@ export function MachoParser(file, callback) {
|
|||
let data = new Uint8Array(e.target.result);
|
||||
let magics = FindMagic(data, false); //Try to find all Mach-O magics in the byte array
|
||||
|
||||
if ( process.env.DEBUG ) { console.log('Parsing all magics...'); }
|
||||
// if ( process.env.DEBUG ) { console.log('Parsing all magics...'); }
|
||||
|
||||
//If magics where found, parse the binary.
|
||||
if(magics.length > 0) {
|
||||
|
|
|
|||
25
helpers/public-runtime-config.mjs
Normal file
25
helpers/public-runtime-config.mjs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
|
||||
|
||||
export const publicRuntimeConfig = {
|
||||
allUpdateSubscribe: process.env.ALL_UPDATE_SUBSCRIBE,
|
||||
testResultStore: process.env.TEST_RESULT_STORE,
|
||||
siteUrl: process.env.URL,
|
||||
macsVerbiage: process.env.npm_package_config_verbiage_macs,
|
||||
processorsVerbiage: process.env.npm_package_config_verbiage_processors,
|
||||
}
|
||||
|
||||
export function makeViteDefinitions () {
|
||||
const definitions = {}
|
||||
|
||||
for ( const key in publicRuntimeConfig ) {
|
||||
definitions[`this.$config.${key}`] = JSON.stringify( publicRuntimeConfig[key] )
|
||||
definitions[`global.$config.${key}`] = JSON.stringify( publicRuntimeConfig[key] )
|
||||
definitions[`global.$config`] = JSON.stringify( publicRuntimeConfig )
|
||||
}
|
||||
|
||||
return definitions
|
||||
}
|
||||
|
|
@ -1,8 +1,19 @@
|
|||
// import { allVideoAppsListSet } from '~/helpers/get-list.js'
|
||||
// import videoList from '~/static/video-list.json'
|
||||
|
||||
import { getAppEndpoint, getAppType } from '~/helpers/app-derived.js'
|
||||
|
||||
function videoHasAppEndpoint ( video, appEndpoint ) {
|
||||
for (const appLink of video.appLinks) {
|
||||
if ( appLink.endpoint === appEndpoint ) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function appsRelatedToVideo ( video, allVideoAppsListSet ) {
|
||||
// console.log('allVideoAppsListSet', allVideoAppsListSet.length)
|
||||
|
||||
const relatedApps = []
|
||||
|
||||
|
|
@ -10,7 +21,7 @@ export function appsRelatedToVideo ( video, allVideoAppsListSet ) {
|
|||
for (const app of allVideoAppsListSet) {
|
||||
// console.log('video', video)
|
||||
// Skip this app if it's not listed in the videos apps
|
||||
if (!video.apps.includes(app.slug)) continue
|
||||
if (!videoHasAppEndpoint( video, app.endpoint )) continue
|
||||
|
||||
// Add this app to our featured app list
|
||||
relatedApps.push(app)
|
||||
|
|
@ -22,9 +33,6 @@ export function appsRelatedToVideo ( video, allVideoAppsListSet ) {
|
|||
export function videosRelatedToVideo ( video, allVideoAppsListSet, videoListSet ) {
|
||||
const relatedVideos = {}
|
||||
|
||||
// console.log('videoList', videoList[0])
|
||||
// console.log('allVideoAppsListSet', allVideoAppsListSet[0])
|
||||
|
||||
const featuredApps = appsRelatedToVideo( video, allVideoAppsListSet )
|
||||
|
||||
// Find other videos that also feature this video's app
|
||||
|
|
@ -32,7 +40,7 @@ export function videosRelatedToVideo ( video, allVideoAppsListSet, videoListSet
|
|||
for (const app of featuredApps) {
|
||||
// console.log('otherVideo', otherVideo)
|
||||
// Skip if this app is not in the other video's apps
|
||||
if (!otherVideo.apps.includes(app.slug)) continue
|
||||
if (!videoHasAppEndpoint( otherVideo, app.endpoint )) continue
|
||||
|
||||
// Skip if the other video is, in fact, this video
|
||||
if (otherVideo.slug === video.slug) continue
|
||||
|
|
@ -50,14 +58,38 @@ export function videosRelatedToApp ( app, videoListSet ) {
|
|||
|
||||
// console.log('videoListSet', videoListSet)
|
||||
|
||||
const relatedVideos = {}
|
||||
const relatedVideos = []
|
||||
|
||||
// Find other videos that also feature this video's app
|
||||
for (const video of videoListSet) {
|
||||
if (!video.apps.includes(app.slug)) continue
|
||||
|
||||
relatedVideos[video.id] = video
|
||||
if (!videoHasAppEndpoint( video, app.endpoint )) continue
|
||||
|
||||
relatedVideos.push( video )
|
||||
|
||||
if ( relatedVideos.length > 20 ) break
|
||||
}
|
||||
|
||||
return Object.values(relatedVideos)
|
||||
return relatedVideos
|
||||
}
|
||||
|
||||
export function videoBenchmarksRelatedToApp ( app, videoListSet ) {
|
||||
return videosRelatedToApp( app, videoListSet ).map(video => {
|
||||
return {
|
||||
...video,
|
||||
endpoint: `${getAppEndpoint( app )}/benchmarks#${video.id}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export function getRelatedVideos ( { listing, videoListSet, appListSet } = {} ) {
|
||||
const listingType = getAppType( listing )
|
||||
|
||||
if ( listingType === 'video' ) {
|
||||
return videosRelatedToVideo( listing, appListSet, videoListSet )
|
||||
}
|
||||
|
||||
return videoBenchmarksRelatedToApp( listing, videoListSet )
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
|
||||
function scrollHorizontalCarousel ( event ) {
|
||||
export function scrollHorizontalCarousel ( event ) {
|
||||
event.stopPropagation()
|
||||
|
||||
// console.log('event.target', event.currentTarget)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,65 @@
|
|||
|
||||
import { filterSeparator } from '~/helpers/constants.js'
|
||||
|
||||
|
||||
const statuses = {
|
||||
'✅': 'native',
|
||||
'✳️': 'rosetta',
|
||||
'⏹': 'no-in-progress',
|
||||
'🚫': 'no',
|
||||
'🔶': 'unreported',
|
||||
'native': {
|
||||
icon: '✅',
|
||||
filterLabel: 'Native Support',
|
||||
snakeSlug: 'native',
|
||||
},
|
||||
'rosetta': {
|
||||
icon: '✳️',
|
||||
filterLabel: 'Rosetta',
|
||||
snakeSlug: 'rosetta',
|
||||
},
|
||||
'no-in-progress': {
|
||||
icon: '⏹',
|
||||
filterLabel: 'In Progress',
|
||||
snakeSlug: 'no_in_progress',
|
||||
},
|
||||
'no': {
|
||||
icon: '🚫',
|
||||
filterLabel: 'Unsupported',
|
||||
snakeSlug: 'no',
|
||||
},
|
||||
'unreported': {
|
||||
icon: '🔶',
|
||||
filterLabel: 'Unreported',
|
||||
snakeSlug: 'unreported',
|
||||
}
|
||||
}
|
||||
|
||||
const statusesByIcon = Object.keys( statuses ).reduce( ( acc, key ) => {
|
||||
const status = statuses[ key ]
|
||||
acc[ status.icon ] = key
|
||||
return acc
|
||||
}, {} )
|
||||
|
||||
|
||||
export const statusFilterPrefix = 'status'
|
||||
|
||||
|
||||
// Example:
|
||||
// {
|
||||
// label: '✅ Native Support',
|
||||
// query: 'status_native'
|
||||
// },
|
||||
export const defaultStatusFilters = Object.keys( statuses ).reduce( ( acc, key ) => {
|
||||
const status = statuses[ key ]
|
||||
acc[ statusFilterPrefix + filterSeparator + key ] = status.filterLabel
|
||||
|
||||
acc = [...acc, {
|
||||
label: `${ status.icon } ${ status.filterLabel }`,
|
||||
query: statusFilterPrefix + filterSeparator + status.snakeSlug
|
||||
}]
|
||||
return acc
|
||||
}, [] )
|
||||
|
||||
|
||||
|
||||
export function getStatusName ( status ) {
|
||||
for (const key in statuses) {
|
||||
if (status.trim().startsWith( key )) return statuses[key]
|
||||
for (const key in statusesByIcon) {
|
||||
if (status.trim().startsWith( key )) return statusesByIcon[key]
|
||||
}
|
||||
|
||||
throw new Error('Non status matched')
|
||||
|
|
@ -33,4 +81,4 @@ export function getStatusOfScan ( appScan, includeVersion = true ) {
|
|||
}
|
||||
|
||||
|
||||
export default statuses
|
||||
export default statusesByIcon
|
||||
|
|
|
|||
396
helpers/stork/browser.js
Normal file
396
helpers/stork/browser.js
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
import { filterSeparator } from '~/helpers/constants.js'
|
||||
|
||||
import { isString } from '~/helpers/check-types.js'
|
||||
|
||||
import {
|
||||
storkIndexRelativeURL,
|
||||
storkScriptURL
|
||||
} from '~/helpers/stork/config.js'
|
||||
|
||||
export function makeHighlightedMarkup ( options = {} ) {
|
||||
const {
|
||||
text,
|
||||
highlight_ranges,
|
||||
withElipsis = true,
|
||||
} = options
|
||||
|
||||
if ( highlight_ranges.length === 0 ) return [ text ]
|
||||
|
||||
const highlighted_text = highlight_ranges.map( range => {
|
||||
const {
|
||||
beginning,
|
||||
end
|
||||
} = range
|
||||
|
||||
const before = text.slice( 0, beginning )
|
||||
const target = text.slice( beginning, end + 1 )
|
||||
const after = text.slice( end + 1 )
|
||||
|
||||
// console.log({
|
||||
// before,
|
||||
// target,
|
||||
// after
|
||||
// })
|
||||
|
||||
// `<span class="stork-highlighted-text">${ highlighted_text }</span>`
|
||||
|
||||
return [
|
||||
withElipsis ? '...' : '',
|
||||
before.trim(),
|
||||
/* html */` <span class="stork-highlighted-text font-bold text-white bg-green-800 rounded px-1">${ target.trim() }</span> `,
|
||||
after.trim(),
|
||||
withElipsis ? '...' : '',
|
||||
].join('')
|
||||
} )
|
||||
|
||||
return highlighted_text
|
||||
}
|
||||
|
||||
export function makeHighlightedResultTitle ( result ) {
|
||||
const [ highlightedTitleMarkup ] = makeHighlightedMarkup({
|
||||
text: result.entry.title,
|
||||
highlight_ranges: result.title_highlight_ranges,
|
||||
withElipsis: false
|
||||
})
|
||||
|
||||
// console.log('highlightedTitleMarkup', highlightedTitleMarkup)
|
||||
// console.log('result', result)
|
||||
|
||||
if ( !isString( highlightedTitleMarkup ) ) throw new Error('highlightedTitleMarkup is not a string')
|
||||
|
||||
return highlightedTitleMarkup
|
||||
}
|
||||
|
||||
export class StorkClient {
|
||||
constructor ( options = {} ) {
|
||||
|
||||
this.name = options.name || 'index'
|
||||
this.url = options.url || storkIndexRelativeURL
|
||||
|
||||
// Configuration Reference - https://stork-search.net/docs/js-ref#showProgress
|
||||
// Example - https://github.com/jameslittle230/stork/blob/ff49f163db06734e18ab690c188b45a3c68442ae/js/config.ts#L4
|
||||
this.config = {
|
||||
minimumQueryLength: 1,
|
||||
showScores: true,
|
||||
...options.config || {}
|
||||
}
|
||||
|
||||
// Stork instance
|
||||
this.stork = options.stork || null
|
||||
|
||||
this.cancelCurrentQuery = null
|
||||
}
|
||||
|
||||
setupState = 'not-setup'
|
||||
|
||||
get isSetup () {
|
||||
return this.setupState === 'complete'
|
||||
}
|
||||
|
||||
resultHasTerm ( result, term ) {
|
||||
const {
|
||||
entry: {
|
||||
url,
|
||||
title
|
||||
},
|
||||
excerpts
|
||||
} = result
|
||||
|
||||
if ( title.toLowerCase().includes( term.toLowerCase() ) ) return true
|
||||
|
||||
if ( url.toLowerCase().includes( term.toLowerCase() ) ) return true
|
||||
|
||||
for ( const excerpt of excerpts ) {
|
||||
if ( excerpt.text.toLowerCase().includes( term.toLowerCase() ) ) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
resultHasAnyTerm ( result, terms ) {
|
||||
for ( const term of terms ) {
|
||||
if ( this.resultHasTerm( result, term ) ) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
resultHasAllTerms ( result, terms ) {
|
||||
for ( const term of terms ) {
|
||||
if ( !this.resultHasTerm( result, term ) ) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
search ( query ) {
|
||||
if ( !this.isSetup ) throw new Error('Not setup')
|
||||
|
||||
// search() - https://github.com/jameslittle230/stork/blob/ff49f163db06734e18ab690c188b45a3c68442ae/js/main.ts#L55
|
||||
return this.stork.search( this.name, query )
|
||||
}
|
||||
|
||||
// Loads the Stork WASM and Index into the browser on first query
|
||||
// so that we don't have to load them initially.
|
||||
async lazyQuery ( query, requiredTerms = [] ) {
|
||||
|
||||
// Sleep
|
||||
// await new Promise( resolve => setTimeout( resolve, 50000000 ) )
|
||||
|
||||
const result = await new Promise( async ( resolve, reject ) => {
|
||||
|
||||
// If there an existing query to cancel
|
||||
// then cancel it
|
||||
// so that we don't race bad conditions
|
||||
// such as earrly queries beating the final one
|
||||
if ( this.cancelCurrentQuery !== null ) {
|
||||
this.cancelCurrentQuery()
|
||||
}
|
||||
|
||||
// Plugin this promise to our cancel method
|
||||
this.cancelCurrentQuery = () => { reject({ message: `Cancelled previous query for ${ query }`, canceled: true }) }
|
||||
|
||||
if ( !this.isSetup ) await this.setup()
|
||||
|
||||
// console.log('debounce', this.query)
|
||||
|
||||
const queryResponse = this.search( query )
|
||||
|
||||
if ( requiredTerms.length !== 0 ) {
|
||||
// Filter out results that don't have the required terms
|
||||
const filteredResults = queryResponse.results.filter( result => {
|
||||
return this.resultHasAllTerms( result, requiredTerms )
|
||||
})
|
||||
|
||||
|
||||
resolve( {
|
||||
...queryResponse,
|
||||
results: filteredResults
|
||||
} )
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
resolve( queryResponse )
|
||||
|
||||
}).catch( err => {
|
||||
console.log('Query rejected', err)
|
||||
return null
|
||||
})
|
||||
|
||||
console.log( 'result', result )
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
waitForSetup () {
|
||||
return new Promise( resolve => {
|
||||
if ( this.isSetup ) resolve()
|
||||
|
||||
// Start timer to check for setup
|
||||
const timer = setInterval( () => {
|
||||
if ( this.isSetup ) {
|
||||
clearInterval( timer )
|
||||
resolve()
|
||||
}
|
||||
}, 50 )
|
||||
})
|
||||
}
|
||||
|
||||
loadStorkScript () {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if ( !!this.stork ) resolve()
|
||||
|
||||
if ( !!window.stork ) {
|
||||
this.stork = window.stork
|
||||
resolve()
|
||||
}
|
||||
|
||||
const s = document.createElement('script')
|
||||
let r = false
|
||||
s.type = 'text/javascript'
|
||||
s.src = storkScriptURL
|
||||
s.async = true
|
||||
s.onerror = function(err) {
|
||||
reject(err, s)
|
||||
}
|
||||
|
||||
s.onload = s.onreadystatechange = () => {
|
||||
// console.log(this.readyState); // uncomment this line to see which ready states are called.
|
||||
if (!r && (!this.readyState || this.readyState == 'complete')) {
|
||||
r = true
|
||||
|
||||
this.stork = window.stork
|
||||
|
||||
// console.log('window.stork', typeof window.stork)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
const t = document.getElementsByTagName('script')[0]
|
||||
t.parentElement.insertBefore(s, t)
|
||||
})
|
||||
}
|
||||
|
||||
// https://github.com/jameslittle230/stork/blob/ff49f163db06734e18ab690c188b45a3c68442ae/js/main.ts#L40
|
||||
async setup () {
|
||||
// Prevent multiple setups
|
||||
if ( this.setupState !== 'not-setup' ) {
|
||||
await this.waitForSetup()
|
||||
return
|
||||
}
|
||||
|
||||
// We're the first to setup
|
||||
// so let's set the state to prevent duplicates
|
||||
this.setupState = 'pending'
|
||||
|
||||
// Load Stork Script
|
||||
if ( !this.stork ) {
|
||||
// console.log('Loading stork script...')
|
||||
await this.loadStorkScript()
|
||||
}
|
||||
|
||||
const {
|
||||
initialize,
|
||||
downloadIndex,
|
||||
} = this.stork
|
||||
|
||||
// Stork JavaScript Reference - https://stork-search.net/docs/js-ref
|
||||
|
||||
// initialize() - https://github.com/jameslittle230/stork/blob/ff49f163db06734e18ab690c188b45a3c68442ae/js/main.ts#L14
|
||||
const initPromise = initialize()
|
||||
|
||||
// downloadIndex() - https://github.com/jameslittle230/stork/blob/ff49f163db06734e18ab690c188b45a3c68442ae/js/main.ts#L20
|
||||
const downloadPromise = downloadIndex( this.name, this.url, this.config )
|
||||
|
||||
// This silly `then` call turns a [(void), (void)] into a (void), which is
|
||||
// only necessary to make Typescript happy.
|
||||
// You begin to wonder if you write Typescript code, or if Typescript code writes you.
|
||||
await Promise.all([ initPromise, downloadPromise ])
|
||||
|
||||
// Mark setup as complete
|
||||
this.setupState = 'complete'
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export class StorkFilters {
|
||||
constructor({
|
||||
initialFilters = {}
|
||||
} = {}) {
|
||||
this.initialFilters = initialFilters
|
||||
|
||||
this.filters = {
|
||||
...initialFilters
|
||||
}
|
||||
}
|
||||
|
||||
get list () {
|
||||
return Object.entries( this.filters ).map( ([ filterKey, filterValue ]) => {
|
||||
return `${ filterKey }${ filterSeparator }${ filterValue }`
|
||||
} )
|
||||
}
|
||||
|
||||
get asQuery () {
|
||||
return this.list.join(' ')
|
||||
}
|
||||
|
||||
getByKey ( key ) {
|
||||
return `${ key }${ filterSeparator }${ this.filters[ key ] }`
|
||||
}
|
||||
|
||||
isQueryValue ( filterNameOrQueryValue ) {
|
||||
return filterNameOrQueryValue.includes( filterSeparator )
|
||||
}
|
||||
|
||||
getKeyAndValue ( filterQueryValue ) {
|
||||
const key = filterQueryValue.substring(0, filterQueryValue.indexOf( filterSeparator ))
|
||||
const value = filterQueryValue.substring(filterQueryValue.indexOf( filterSeparator )+1)
|
||||
|
||||
return { key, value }
|
||||
}
|
||||
|
||||
getFilterNameAndValueFromString ( filterNameOrQueryValue ) {
|
||||
if ( this.isQueryValue( filterNameOrQueryValue ) ) {
|
||||
return this.getKeyAndValue( filterNameOrQueryValue )
|
||||
}
|
||||
|
||||
return {
|
||||
key: filterNameOrQueryValue,
|
||||
value: null
|
||||
}
|
||||
}
|
||||
|
||||
remove ( filterName ) {
|
||||
// Throw error if it's not a valid filter name
|
||||
if ( this.isQueryValue( filterName ) ) throw new Error(`${ filterName } is not a valid filter name`)
|
||||
|
||||
delete this.filters[ filterName ]
|
||||
}
|
||||
|
||||
setFromStringArray ( filterStringArray ) {
|
||||
filterStringArray.forEach( filterString => {
|
||||
const { key, value } = this.getFilterNameAndValueFromString( filterString )
|
||||
|
||||
this.filters[ key ] = value
|
||||
})
|
||||
}
|
||||
|
||||
setFromString ( filterNameOrQueryValue ) {
|
||||
const {
|
||||
key,
|
||||
value = ''
|
||||
} = this.getFilterNameAndValueFromString( filterNameOrQueryValue )
|
||||
|
||||
// Throw for empty values
|
||||
if ( value.trim().length === 0 ) throw new Error(`${ filterNameOrQueryValue } is not a valid filter value`)
|
||||
|
||||
this.set( key, value )
|
||||
}
|
||||
|
||||
set ( filterName, filterValue ) {
|
||||
// Throw error if it's not a valid filter name
|
||||
if ( this.isQueryValue( filterName ) ) throw new Error(`${ filterName } is not a valid filter name`)
|
||||
|
||||
this.filters[ filterName ] = filterValue
|
||||
}
|
||||
|
||||
toggleFilter ( filterNameOrQueryValue, filterValue = null ) {
|
||||
|
||||
const fromString = this.getFilterNameAndValueFromString( filterNameOrQueryValue )
|
||||
|
||||
const filterName = fromString.key
|
||||
filterValue = filterValue || fromString.value
|
||||
|
||||
// If the filter is already set, remove it
|
||||
if ( this.has( filterNameOrQueryValue ) ) {
|
||||
this.remove( filterName )
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Throw error if filter value is not a string
|
||||
if ( typeof filterValue !== 'string' ) {
|
||||
throw new Error(`Filter value must be a string. Got ${ typeof filterValue }`)
|
||||
}
|
||||
|
||||
this.set( filterName, filterValue )
|
||||
}
|
||||
|
||||
has ( filterNameOrQueryValue ) {
|
||||
|
||||
const {
|
||||
key : filterName,
|
||||
value : filterValue = null
|
||||
} = this.getFilterNameAndValueFromString( filterNameOrQueryValue )
|
||||
|
||||
// If this filter is a name and value, check if it's set
|
||||
if ( isString( filterValue ) ) {
|
||||
return !!this.filters[ filterName ] && this.filters[ filterName ] === filterValue
|
||||
}
|
||||
|
||||
return !!this.filters[ filterName ]
|
||||
}
|
||||
}
|
||||
13
helpers/stork/config.js
Normal file
13
helpers/stork/config.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { config } from '~/package.json'
|
||||
|
||||
|
||||
export const storkOptions = config.stork
|
||||
export const storkVersion = '1.4.2'
|
||||
|
||||
export const storkExecutableName = storkOptions.executable
|
||||
export const storkExecutablePath = `./${ storkExecutableName }`
|
||||
export const storkTomlPath = storkOptions.toml
|
||||
export const storkIndexPath = storkOptions.index
|
||||
|
||||
export const storkIndexRelativeURL = storkIndexPath.replace('static/', '/')
|
||||
export const storkScriptURL = `https://files.stork-search.net/releases/v${ storkVersion }/stork.js`
|
||||
95
helpers/stork/executable.js
Normal file
95
helpers/stork/executable.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// Stork and Netlify - https://stork-search.net/docs/stork-and-netlify
|
||||
import fs from 'fs-extra'
|
||||
import execa from 'execa'
|
||||
|
||||
import { isDarwin } from '~/helpers/environment.js'
|
||||
import {
|
||||
storkVersion,
|
||||
storkExecutableName,
|
||||
storkExecutablePath,
|
||||
storkTomlPath,
|
||||
storkIndexPath
|
||||
} from '~/helpers/stork/config.js'
|
||||
|
||||
// https://stork-search.net/docs/install
|
||||
const execDownloadUrls = {
|
||||
darwin: `https://files.stork-search.net/releases/v${ storkVersion }/stork-macos-10-15`,
|
||||
default: `https://files.stork-search.net/releases/v${ storkVersion }/stork-ubuntu-20-04`
|
||||
// default: `https://files.stork-search.net/releases/v${ storkVersion }/stork-amazon-linux`
|
||||
}
|
||||
|
||||
// Check if a file is executable
|
||||
// https://stackoverflow.com/a/69897809/1397641
|
||||
async function isExecutable ( path ) {
|
||||
const stats = await fs.stat( path )
|
||||
const isExecutable = !!(stats.mode & fs.constants.S_IXUSR)
|
||||
|
||||
return isExecutable
|
||||
}
|
||||
|
||||
// 👩💻 Bash Download example - https://github.com/jmooring/hugo-stork/blob/main/build.sh
|
||||
export async function downloadStorkExecutable () {
|
||||
const envKey = isDarwin() ? 'darwin' : 'default'
|
||||
|
||||
const execDownloadUrl = execDownloadUrls[ envKey ]
|
||||
|
||||
// console.log( 'execDownloadUrl', execDownloadUrl )
|
||||
|
||||
// Delete any existing executable
|
||||
// so we don't get write errors
|
||||
// or false positives from preexisting executable files
|
||||
await fs.remove( storkExecutablePath )
|
||||
|
||||
// Download the binary
|
||||
await execa( `curl`, [
|
||||
execDownloadUrl,
|
||||
|
||||
// Set filename
|
||||
'-o',
|
||||
storkExecutableName
|
||||
])
|
||||
|
||||
|
||||
// Set the downloaded binary as executable
|
||||
await fs.chmod( storkExecutablePath, '755' )
|
||||
// Check that our downloaded binary is executable
|
||||
|
||||
|
||||
// console.log( 'isExecutable', isExecutable )
|
||||
if ( !isExecutable( storkExecutablePath ) ) throw new Error( `Downloaded binary at ${ storkExecutablePath } is not executable.` )
|
||||
|
||||
|
||||
// Check Stork version
|
||||
// so we know our binary is working
|
||||
const { stdout } = await execa( storkExecutablePath, [
|
||||
'--version'
|
||||
])
|
||||
|
||||
console.log( 'Stork Version', stdout )
|
||||
if ( !stdout.includes( storkVersion ) ) throw new Error( 'Stork --version command failed.' )
|
||||
|
||||
return stdout
|
||||
}
|
||||
|
||||
|
||||
export async function buildIndex () {
|
||||
|
||||
if ( !isExecutable( storkExecutablePath ) ) throw new Error( `Binary at ${ storkExecutablePath } is not executable.` )
|
||||
|
||||
// Check Stork version
|
||||
// so we know our binary is working
|
||||
const { stdout } = await execa( storkExecutablePath, [
|
||||
'build',
|
||||
|
||||
'--input',
|
||||
storkTomlPath,
|
||||
|
||||
'--output',
|
||||
storkIndexPath
|
||||
])
|
||||
|
||||
console.log( 'Stork Build', stdout )
|
||||
if ( !stdout.includes( storkVersion ) ) throw new Error( 'Stork --version command failed.' )
|
||||
|
||||
return stdout
|
||||
}
|
||||
138
helpers/stork/toml.js
Normal file
138
helpers/stork/toml.js
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
// import execa from 'execa'
|
||||
import fs from 'fs-extra'
|
||||
import has from 'just-has'
|
||||
import TOML from '@iarna/toml'
|
||||
import * as matter from 'gray-matter'
|
||||
|
||||
|
||||
import {
|
||||
isNonEmptyString,
|
||||
isNonEmptyArray
|
||||
} from '~/helpers/check-types.js'
|
||||
import {
|
||||
getRouteType
|
||||
} from '~/helpers/app-derived.js'
|
||||
import {
|
||||
makeCategoryFilterFromListing
|
||||
} from '~/helpers/categories.js'
|
||||
import {
|
||||
storkTomlPath,
|
||||
} from '~/helpers/stork/config.js'
|
||||
|
||||
|
||||
|
||||
|
||||
function makeDetailsFromListing ({ listing, route }) {
|
||||
|
||||
const propertiesToCheck = {
|
||||
text: isNonEmptyString,
|
||||
content: isNonEmptyString,
|
||||
description: isNonEmptyString,
|
||||
// status: isNonEmptyString,
|
||||
aliases: isNonEmptyArray,
|
||||
tags: isNonEmptyArray,
|
||||
}
|
||||
|
||||
const contents = {}
|
||||
|
||||
for ( const [ property, isValid ] of Object.entries( propertiesToCheck ) ) {
|
||||
if ( !has( listing, property ) ) continue
|
||||
|
||||
if ( !isValid( listing[ property ] ) ) continue
|
||||
|
||||
let value = listing[ property ]
|
||||
|
||||
// Convert arrays to string
|
||||
if ( Array.isArray( value ) ) {
|
||||
value = value.join(', ')
|
||||
}
|
||||
|
||||
// Property can be added to content
|
||||
contents[ property ] = value
|
||||
}
|
||||
|
||||
|
||||
return [
|
||||
listing.content || '∅', // Null Symbol
|
||||
has( listing, 'status' ) ? `status_${ listing.status }` : '',
|
||||
has( listing, 'category' ) ? makeCategoryFilterFromListing( listing ) : '',
|
||||
`type_${ getRouteType( route ) }`,
|
||||
// Brownmatter
|
||||
matter.stringify( '', contents ),
|
||||
].join('\r\n')
|
||||
}
|
||||
|
||||
|
||||
function mapSitemapEndpointsToToml ( sitemap ) {
|
||||
|
||||
const files = sitemap.map( sitemapEntry => {
|
||||
const {
|
||||
payload,
|
||||
route
|
||||
} = sitemapEntry
|
||||
|
||||
const routeType = getRouteType( route )
|
||||
|
||||
// console.log( 'payload', route, payload )
|
||||
|
||||
const listing = payload.app || payload.listing || payload.video || {}
|
||||
|
||||
const contents = makeDetailsFromListing({ listing, route })
|
||||
|
||||
let title = listing.name || route
|
||||
|
||||
// If this route is a benchmark route, add the benchmark name
|
||||
if ( routeType === 'benchmarks' ) {
|
||||
title = `${ title } Benchmarks`
|
||||
}
|
||||
|
||||
// console.log( 'listing', listing )
|
||||
// console.log( 'contents', contents )
|
||||
// console.log( 'name', listing.name )
|
||||
|
||||
if ( contents.trim().length === 0 ) {
|
||||
console.log( 'listing', listing )
|
||||
throw new Error('Empty Content')
|
||||
}
|
||||
|
||||
return {
|
||||
// https://stork-search.net/docs/config-ref#title
|
||||
title,
|
||||
url: route,
|
||||
contents
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
input: {
|
||||
// https://stork-search.net/docs/config-ref#base_directory
|
||||
base_directory: '.',
|
||||
url_prefix: 'https://doesitarm.com',
|
||||
|
||||
// https://stork-search.net/docs/config-ref#files
|
||||
files
|
||||
},
|
||||
output: {
|
||||
// debug: true,
|
||||
// save_nearest_html_id: false,
|
||||
// excerpt_buffer: 8,
|
||||
// excerpts_per_result: 5,
|
||||
displayed_results_count: 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function writeStorkToml ( sitemap ) {
|
||||
|
||||
const indexToml = mapSitemapEndpointsToToml( sitemap )
|
||||
|
||||
// Build Stork Index TOML
|
||||
// https://stork-search.net/docs/config-ref
|
||||
const indexString = TOML.stringify( indexToml )
|
||||
|
||||
// Save to file
|
||||
await fs.outputFile( storkTomlPath, indexString )
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -1,12 +1,20 @@
|
|||
import { getSiteUrl } from './url'
|
||||
|
||||
function makeFeaturedAppsString ( featuredApps ) {
|
||||
return featuredApps.slice(0, 5).map(app => app.name).join(', ')
|
||||
}
|
||||
|
||||
export function buildVideoStructuredData ( video, featuredApps, options ) {
|
||||
export function buildVideoStructuredData ( video, featuredApps, options = {} ) {
|
||||
// console.log('video', video)
|
||||
|
||||
// Throw for missing featured apps
|
||||
if ( Array.isArray(featuredApps) === false ) {
|
||||
console.warn( 'featuredApps not array', featuredApps )
|
||||
throw new Error('featuredApps must be an array of objects')
|
||||
}
|
||||
|
||||
const {
|
||||
siteUrl
|
||||
siteUrl = getSiteUrl(),
|
||||
} = options
|
||||
|
||||
const thumbnailUrls = video.thumbnail.srcset.split(',').map( srcSetImage => {
|
||||
|
|
|
|||
92
helpers/url.js
Normal file
92
helpers/url.js
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
|
||||
export function getSiteUrl () {
|
||||
|
||||
// console.log( 'import.meta.site', import.meta.env )
|
||||
|
||||
const hasImportMeta = typeof import.meta !== 'undefined'
|
||||
const hasImportMetaEnv = hasImportMeta && typeof import.meta.env !== 'undefined'
|
||||
|
||||
// Try PUBLIC_URL
|
||||
if ( typeof process.env.PUBLIC_URL !== 'undefined' ) {
|
||||
console.log('Has env.PUBLIC_URL')
|
||||
return process.env.PUBLIC_URL
|
||||
}
|
||||
|
||||
if ( hasImportMetaEnv && typeof import.meta.env.PUBLIC_URL !== 'undefined' ) {
|
||||
console.log('Has PUBLIC_URL')
|
||||
return import.meta.env.PUBLIC_URL
|
||||
}
|
||||
|
||||
|
||||
// Try process.env.URL
|
||||
if ( typeof process.env.URL !== 'undefined' ) {
|
||||
console.log('Has env.URL')
|
||||
return process.env.URL
|
||||
}
|
||||
|
||||
// Try Astro.site.origin
|
||||
if ( typeof Astro !== 'undefined' ) {
|
||||
console.log('Has Astro')
|
||||
return Astro.site.origin
|
||||
}
|
||||
|
||||
// Try URL
|
||||
if ( hasImportMetaEnv && typeof import.meta.env.URL !== 'undefined' ) {
|
||||
console.log('Has URL')
|
||||
return import.meta.env.URL
|
||||
}
|
||||
|
||||
throw new Error('Could not find site URL')
|
||||
}
|
||||
|
||||
export function getApiUrl () {
|
||||
|
||||
const hasImportMeta = typeof import.meta !== 'undefined'
|
||||
const hasImportMetaEnv = hasImportMeta && typeof import.meta.env !== 'undefined'
|
||||
|
||||
// Try PUBLIC_API_DOMAIN
|
||||
if ( typeof process.env.PUBLIC_API_DOMAIN !== 'undefined' ) {
|
||||
// console.log('Has env.PUBLIC_API_DOMAIN')
|
||||
return process.env.PUBLIC_API_DOMAIN
|
||||
}
|
||||
|
||||
if ( hasImportMetaEnv && typeof import.meta.env.PUBLIC_API_DOMAIN !== 'undefined' ) {
|
||||
// console.log('Has PUBLIC_API_DOMAIN')
|
||||
return import.meta.env.PUBLIC_API_DOMAIN
|
||||
}
|
||||
|
||||
throw new Error('Could not find API URL')
|
||||
}
|
||||
|
||||
export function getPartPartsFromUrl ( urlString ) {
|
||||
if ( typeof urlString !== 'string' ) throw new Error('urlString must be a string')
|
||||
|
||||
const url = new URL( urlString, 'https://doesitarm.com' )
|
||||
|
||||
const pathParts = url.pathname
|
||||
.replace(/^\/+/, '') // Trim slashes from the beginning
|
||||
.replace(/\/+$/, '') // Trim slashes from the end
|
||||
.split('/')
|
||||
|
||||
return pathParts
|
||||
}
|
||||
|
||||
export function getPathPartsFromAstroRequest ( AstroRequest ) {
|
||||
// Parse the request url
|
||||
|
||||
const url = new URL( AstroRequest.url, 'https://doesitarm.com' )
|
||||
|
||||
const [
|
||||
routeType,
|
||||
pathSlug,
|
||||
subSlug
|
||||
] = getPartPartsFromUrl ( AstroRequest.url )
|
||||
|
||||
return {
|
||||
pathname: url.pathname,
|
||||
routeType,
|
||||
pathSlug,
|
||||
subSlug,
|
||||
params: Object.fromEntries( url.searchParams )
|
||||
}
|
||||
}
|
||||
74
layouts/base.vue
Normal file
74
layouts/base.vue
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<div class="app-wrapper text-gray-300 bg-gradient-to-bl from-dark to-darker bg-fixed">
|
||||
<Navbar />
|
||||
<div class="app-main min-h-screen flex items-center">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<footer class="">
|
||||
<div class="max-w-screen-xl mx-auto py-12 px-4 overflow-hidden space-y-24 sm:px-6 lg:px-8">
|
||||
<!-- <nav class="-mx-5 -my-2 flex flex-wrap justify-center">
|
||||
<div class="px-5 py-2">
|
||||
<a href="#" class="text-base leading-6 text-gray-500 hover:text-gray-900">
|
||||
About
|
||||
</a>
|
||||
</div>
|
||||
<div class="px-5 py-2">
|
||||
<a href="#" class="text-base leading-6 text-gray-500 hover:text-gray-900">
|
||||
Blog
|
||||
</a>
|
||||
</div>
|
||||
</nav> -->
|
||||
<div class="flex justify-center space-x-6">
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<AllUpdatesSubscribe />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-center text-base leading-6 text-gray-400">
|
||||
<span>Built by </span>
|
||||
<a
|
||||
href="https://samcarlton.com/"
|
||||
rel="noopener"
|
||||
class="underline"
|
||||
>Sam Carlton</a>
|
||||
<span> and the awesome </span>
|
||||
<a
|
||||
href="https://github.com/ThatGuySam/doesitarm/graphs/contributors"
|
||||
rel="noopener"
|
||||
class="underline"
|
||||
>🦾 Does It ARM Contributors. </a>
|
||||
</p>
|
||||
<p class="mt-8 text-center text-base leading-6 text-gray-400">
|
||||
© {{ currentYear }} Does It ARM All rights reserved. This site is supported by Affiliate links.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
// import '@fontsource/inter/latin-100.css'
|
||||
// import '@fontsource/inter/latin-400.css'
|
||||
// import '@fontsource/inter/latin-700.css'
|
||||
|
||||
import '@fontsource/inter/variable.css'
|
||||
|
||||
import Navbar from '~/components/navbar.vue'
|
||||
// import TwitterFollow from '~/components/twitter-follow.vue'
|
||||
import AllUpdatesSubscribe from '~/components/all-updates-subscribe.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Navbar,
|
||||
AllUpdatesSubscribe
|
||||
},
|
||||
computed: {
|
||||
currentYear () {
|
||||
return new Date().getFullYear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
@ -1,74 +1,17 @@
|
|||
<template>
|
||||
<div class="app-wrapper text-gray-300 bg-gradient-to-bl from-dark to-darker bg-fixed">
|
||||
<Navbar />
|
||||
<div class="app-main min-h-screen flex items-center">
|
||||
<nuxt />
|
||||
</div>
|
||||
|
||||
<footer class="">
|
||||
<div class="max-w-screen-xl mx-auto py-12 px-4 overflow-hidden space-y-24 sm:px-6 lg:px-8">
|
||||
<!-- <nav class="-mx-5 -my-2 flex flex-wrap justify-center">
|
||||
<div class="px-5 py-2">
|
||||
<a href="#" class="text-base leading-6 text-gray-500 hover:text-gray-900">
|
||||
About
|
||||
</a>
|
||||
</div>
|
||||
<div class="px-5 py-2">
|
||||
<a href="#" class="text-base leading-6 text-gray-500 hover:text-gray-900">
|
||||
Blog
|
||||
</a>
|
||||
</div>
|
||||
</nav> -->
|
||||
<div class="flex justify-center space-x-6">
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<AllUpdatesSubscribe />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-center text-base leading-6 text-gray-400">
|
||||
<span>Built by </span>
|
||||
<a
|
||||
href="https://samcarlton.com/"
|
||||
rel="noopener"
|
||||
class="underline"
|
||||
>Sam Carlton</a>
|
||||
<span> and the awesome </span>
|
||||
<a
|
||||
href="https://github.com/ThatGuySam/doesitarm/graphs/contributors"
|
||||
rel="noopener"
|
||||
class="underline"
|
||||
>🦾 Does It ARM Contributors. </a>
|
||||
</p>
|
||||
<p class="mt-8 text-center text-base leading-6 text-gray-400">
|
||||
© {{ currentYear }} Does It ARM All rights reserved. This site is supported by Affiliate links.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div>
|
||||
<Base>
|
||||
<nuxt />
|
||||
</Base>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
// import '@fontsource/inter/latin-100.css'
|
||||
// import '@fontsource/inter/latin-400.css'
|
||||
// import '@fontsource/inter/latin-700.css'
|
||||
|
||||
import '@fontsource/inter/variable.css'
|
||||
|
||||
import Navbar from '~/components/navbar.vue'
|
||||
// import TwitterFollow from '~/components/twitter-follow.vue'
|
||||
import AllUpdatesSubscribe from '~/components/all-updates-subscribe.vue'
|
||||
import Base from '~/layouts/base.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Navbar,
|
||||
AllUpdatesSubscribe
|
||||
},
|
||||
computed: {
|
||||
currentYear () {
|
||||
return new Date().getFullYear()
|
||||
}
|
||||
Base
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
|
|||
30
netlify.toml
30
netlify.toml
|
|
@ -1,7 +1,26 @@
|
|||
[build]
|
||||
publish = "dist/"
|
||||
command = "npm run generate --quiet"
|
||||
# functions = "functions/"
|
||||
command = "npm run netlify-build"
|
||||
|
||||
|
||||
# https://docs.netlify.com/configure-builds/file-based-configuration/#functions
|
||||
[functions]
|
||||
# Sets a custom directory for Netlify Functions
|
||||
directory = "dist/functions"
|
||||
|
||||
# Specifies `esbuild` for functions bundling
|
||||
# node_bundler = "esbuild"
|
||||
|
||||
# Flags "astro" as an external node module for all functions
|
||||
# external_node_modules = ["astro"]
|
||||
|
||||
# Includes all Markdown files inside the "files/" directory.
|
||||
# included_files = ["files/*.md"]
|
||||
|
||||
# Astro Entry function
|
||||
[functions."entry"]
|
||||
# https://www.netlify.com/blog/2021/08/12/how-to-include-files-in-netlify-serverless-functions/
|
||||
included_files = ["netlify.toml"]
|
||||
|
||||
|
||||
|
||||
|
|
@ -9,9 +28,14 @@
|
|||
NPM_FLAGS = "--no-optional"
|
||||
CI = "1"
|
||||
|
||||
[[headers]]
|
||||
for = "/search-index.st"
|
||||
[headers.values]
|
||||
Content-Type = "application/wasm"
|
||||
Cache-Control = "max-age=43200, must-revalidate"
|
||||
|
||||
# https://docs.netlify.com/configure-builds/file-based-configuration/#redirects
|
||||
|
||||
# These redirect are lower priority than _redirects in the root or dist
|
||||
# old node redirect
|
||||
[[redirects]]
|
||||
from = "/app/node"
|
||||
|
|
|
|||
117
nuxt.config.js
117
nuxt.config.js
|
|
@ -1,17 +1,18 @@
|
|||
import { promises as fs } from 'fs'
|
||||
|
||||
import pkg from './package'
|
||||
import pkg from './package.json'
|
||||
import { getSiteUrl } from '~/helpers/url.js'
|
||||
import { publicRuntimeConfig } from '~/helpers/public-runtime-config.mjs'
|
||||
|
||||
|
||||
|
||||
const siteUrl = getSiteUrl()
|
||||
|
||||
|
||||
export default {
|
||||
target: 'static',
|
||||
|
||||
publicRuntimeConfig: {
|
||||
allUpdateSubscribe: process.env.ALL_UPDATE_SUBSCRIBE,
|
||||
testResultStore: process.env.TEST_RESULT_STORE,
|
||||
siteUrl: process.env.URL,
|
||||
...pkg.config
|
||||
},
|
||||
publicRuntimeConfig,
|
||||
|
||||
/*
|
||||
** Hooks
|
||||
|
|
@ -42,107 +43,7 @@ export default {
|
|||
/*
|
||||
** Headers of the page
|
||||
*/
|
||||
head: {
|
||||
// this htmlAttrs you need
|
||||
htmlAttrs: {
|
||||
lang: 'en',
|
||||
},
|
||||
title: 'Does It ARM',
|
||||
description: process.env.npm_package_config_verbiage_description,
|
||||
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width, initial-scale=1'
|
||||
},
|
||||
{
|
||||
hid: 'description',
|
||||
name: 'description',
|
||||
content: process.env.npm_package_config_verbiage_description
|
||||
},
|
||||
{
|
||||
'property': 'og:image',
|
||||
'content': `${process.env.URL}/images/og-image.png`
|
||||
},
|
||||
{
|
||||
'property': 'og:image:width',
|
||||
'content': '1200'
|
||||
},
|
||||
{
|
||||
'property': 'og:image:height',
|
||||
'content': '627'
|
||||
},
|
||||
{
|
||||
'property': 'og:image:alt',
|
||||
'content': 'Does It ARM Logo'
|
||||
},
|
||||
|
||||
// Twitter Card
|
||||
{
|
||||
'property': 'twitter:card',
|
||||
'content': 'summary'
|
||||
},
|
||||
{
|
||||
'property': 'twitter:title',
|
||||
'content': 'Does It ARM'
|
||||
},
|
||||
// {
|
||||
// 'property': 'twitter:description',
|
||||
// 'content': process.env.npm_package_config_verbiage_description
|
||||
// },
|
||||
{
|
||||
'property': 'twitter:url',
|
||||
'content': `${process.env.URL}`
|
||||
},
|
||||
{
|
||||
'property': 'twitter:image',
|
||||
'content': `${process.env.URL}/images/mark.png`
|
||||
}
|
||||
],
|
||||
link: [
|
||||
// Favicon
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/x-icon',
|
||||
href: '/favicon.ico'
|
||||
},
|
||||
|
||||
// Gtag Preconnect
|
||||
{
|
||||
rel: 'preconnect',
|
||||
href: 'https://www.googletagmanager.com'
|
||||
},
|
||||
|
||||
// Carbon Preconnects
|
||||
{
|
||||
rel: 'preconnect',
|
||||
href: 'https://cdn.carbonads.com'
|
||||
},
|
||||
{
|
||||
rel: 'preconnect',
|
||||
href: 'https://srv.carbonads.net'
|
||||
},
|
||||
{
|
||||
rel: 'preconnect',
|
||||
href: 'https://cdn4.buysellads.net'
|
||||
},
|
||||
],
|
||||
|
||||
script: [
|
||||
// // Carbon Ads
|
||||
// // https://sell.buysellads.com/zones/1294/ad-tags#z=js
|
||||
// {
|
||||
// // <script async type="text/javascript" src="//cdn.carbonads.com/carbon.js?serve=CK7DVK3M&placement=doesitarmcom" id="_carbonads_js"></script>
|
||||
// src: 'https://cdn.carbonads.com/carbon.js?serve=CK7DVK3M&placement=doesitarmcom',
|
||||
// async: true,
|
||||
// type: 'text/javascript',
|
||||
// id: '_carbonads_js',
|
||||
// class: 'include-on-static carbon-inline-wide',
|
||||
// body: true
|
||||
// }
|
||||
]
|
||||
},
|
||||
head: ,
|
||||
|
||||
/*
|
||||
** Customize the progress-bar color
|
||||
|
|
|
|||
14030
package-lock.json
generated
14030
package-lock.json
generated
File diff suppressed because it is too large
Load diff
70
package.json
70
package.json
|
|
@ -4,20 +4,25 @@
|
|||
"description": "Find out the latest app support for Apple Silicon and the Apple M2 and M1 Ultra Processors",
|
||||
"author": "Sam Carlton",
|
||||
"private": true,
|
||||
"ava": {
|
||||
"require": [
|
||||
"esm"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"verbiage": {
|
||||
"processors": "Apple M2 and M1 Ultra",
|
||||
"macs": "Apple M2 or M1 Ultra Mac",
|
||||
"description": "Find out the latest app support for Apple Silicon and the Apple M2 and M1 Ultra Processors"
|
||||
},
|
||||
"stork": {
|
||||
"executable": "stork-executable",
|
||||
"toml": "static/stork.toml",
|
||||
"index": "static/search-index.st"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test-prebuild": "ava ./test/prebuild.js --verbose",
|
||||
"test-prebuild": "ava ./test/prebuild/**/*.js --verbose",
|
||||
"test-api-client": "ava ./test/api/client.js --verbose",
|
||||
"test-listings": "ava ./test/listings/**/*.js --verbose",
|
||||
"test-prebuild-functions": "run-s test-prebuild test-api-client test-listings",
|
||||
"test-postbuild-api": "run-s test-listings",
|
||||
"test-postbuild-functions": "ava ./test/main.js --verbose",
|
||||
"test": "ava --timeout=1m --verbose",
|
||||
"dev": "nuxt",
|
||||
"build": "nuxt build",
|
||||
|
|
@ -26,7 +31,18 @@
|
|||
"start": "nuxt start",
|
||||
"generate-dev": "npm run generate && npm test",
|
||||
"generate": "npm run clone-readme && npm run build-lists && npm run generate-nuxt && npm run generate-eleventy",
|
||||
"build-lists": "npm run test-prebuild && node -r esm build-lists.js",
|
||||
"build-lists": "npm run test-prebuild && node -r esm -r tsconfig-paths/register build-lists.js",
|
||||
"setup-stork": "run-s download-stork-toml download-stork-executable",
|
||||
"build-stork-index": "./$npm_package_config_stork_executable build --input $npm_package_config_stork_toml --output $npm_package_config_stork_index",
|
||||
"build-stork-index-js": "node -r esm -r tsconfig-paths/register scripts/build-stork-index.js",
|
||||
"stork-search": "./$npm_package_config_stork_executable search --index $npm_package_config_stork_index --query $1",
|
||||
"stork-index": "run-s setup-stork build-stork-index",
|
||||
"download-stork-toml": "node -r esm -r tsconfig-paths/register scripts/download-stork-toml.js",
|
||||
"download-stork-executable": "node -r esm -r tsconfig-paths/register scripts/download-stork-executable.js",
|
||||
"download-sitemaps": "node -r esm -r tsconfig-paths/register scripts/download-sitemaps.js",
|
||||
"stork-netlify": "chmod +x scripts/stork-netlify.sh && ./scripts/stork-netlify.sh",
|
||||
"dev-astro": "astro dev",
|
||||
"generate-astro": "astro build --experimental-ssr",
|
||||
"generate-nuxt": "NODE_OPTIONS=--max-old-space-size=60000 nuxt generate",
|
||||
"generate-eleventy": "node --max-old-space-size=60000 -r esm node_modules/.bin/eleventy --quiet",
|
||||
"generate-postcss": "ENV=production postcss assets/css/tailwind.css --o static/tailwind.css",
|
||||
|
|
@ -39,36 +55,60 @@
|
|||
"precommit": "npm run lint",
|
||||
"clone-readme": "cp ./README.md README-temp.md",
|
||||
"cloudflare-deploy": "npm run build-api",
|
||||
"vercel-build": "npm run build-lists-and-api"
|
||||
"vercel-build": "run-s build-lists-and-api test-postbuild-api",
|
||||
"netlify-build": "run-s test-prebuild-functions download-sitemaps stork-index generate-astro test-postbuild-functions"
|
||||
},
|
||||
"dependencies": {
|
||||
"@11ty/eleventy-assets": "^1.0.5",
|
||||
"@astrojs/partytown": "^0.1.4",
|
||||
"@astrojs/vue": "^0.1.3",
|
||||
"@fontsource/inter": "^4.0.1",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@nuxtjs/sitemap": "^2.4.0",
|
||||
"@open-wc/webpack-import-meta-loader": "^0.4.7",
|
||||
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
||||
"@supercharge/promise-pool": "^2.1.0",
|
||||
"@zip.js/zip.js": "^2.2.6",
|
||||
"astro": "^1.0.0-beta.27",
|
||||
"axios": "^0.21.0",
|
||||
"can-autoplay": "^3.0.0",
|
||||
"chance": "^1.1.7",
|
||||
"cross-env": "^5.2.0",
|
||||
"esbuild": "^0.11.20",
|
||||
"execa": "^5.1.1",
|
||||
"fast-glob": "^3.2.11",
|
||||
"fast-memoize": "^2.5.2",
|
||||
"fs-extra": "^8.1.0",
|
||||
"googleapis": "^100.0.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"jsdom": "^16.4.0",
|
||||
"just-has": "^2.1.0",
|
||||
"just-map-values": "^3.0.2",
|
||||
"just-replace-all": "^2.0.1",
|
||||
"just-shuffle": "^4.0.1",
|
||||
"lazysizes": "^5.3.0-beta1",
|
||||
"markdown-it": "^11.0.1",
|
||||
"marked": "^1.2.7",
|
||||
"memoize-getters": "^1.1.0",
|
||||
"node-html-parser": "^2.0.0",
|
||||
"observe-element-in-viewport": "0.0.15",
|
||||
"plist": "^3.0.1",
|
||||
"pretty-bytes": "^5.5.0",
|
||||
"scroll-into-view-if-needed": "^2.2.26",
|
||||
"semver": "^7.3.7",
|
||||
"sitemap": "^7.1.1",
|
||||
"slugify": "^1.4.6",
|
||||
"stork-search": "^1.0.4",
|
||||
"terser": "^4.8.0",
|
||||
"vue-gtag": "^1.16.1"
|
||||
"uuid": "^8.3.2",
|
||||
"vue": "^3.2.30",
|
||||
"vue-gtag": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@11ty/eleventy": "^0.11.1",
|
||||
"@astrojs/netlify": "0.3.2",
|
||||
"@astrojs/sitemap": "^0.1.0",
|
||||
"@astrojs/tailwind": "^0.2.0",
|
||||
"@nuxt/postcss8": "^1.1.3",
|
||||
"@nuxtjs/tailwindcss": "^3.3.4",
|
||||
"autoprefixer": "^8.6.4",
|
||||
|
|
@ -76,21 +116,19 @@
|
|||
"babel-eslint": "^8.2.1",
|
||||
"cssnano": "^4.1.10",
|
||||
"dotenv": "^8.2.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-prettier": "^3.1.0",
|
||||
"eslint-loader": "^2.0.0",
|
||||
"eslint-plugin-prettier": "2.6.2",
|
||||
"eslint-plugin-vue": "^4.0.0",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-plugin-vue": "^8.7.1",
|
||||
"esm": "^3.2.25",
|
||||
"fast-xml-parser": "^3.19.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"nodemon": "^1.11.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"nuxt": "^2.14.11",
|
||||
"postcss": "^8.2.4",
|
||||
"postcss-cli": "^8.3.1",
|
||||
"prettier": "1.14.3",
|
||||
"replace-css-url": "^1.2.6",
|
||||
"structured-data-testing-tool": "^4.5.0",
|
||||
"tailwindcss": "^1.9.6"
|
||||
"tailwindcss": "^1.9.6",
|
||||
"tsconfig-paths": "^3.14.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@
|
|||
|
||||
// import AppFilesScanner from '~/helpers/app-files-scanner.js'
|
||||
|
||||
import { isNuxt } from '~/helpers/environment.js'
|
||||
|
||||
import LinkButton from '~/components/link-button.vue'
|
||||
import AllUpdatesSubscribe from '~/components/all-updates-subscribe.vue'
|
||||
|
|
@ -191,6 +192,12 @@ export default {
|
|||
LinkButton,
|
||||
AllUpdatesSubscribe
|
||||
},
|
||||
props: {
|
||||
config: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
query: '',
|
||||
|
|
@ -200,7 +207,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
npm_package_config_verbiage_macs () {
|
||||
return process.env.npm_package_config_verbiage_macs
|
||||
return this.config.macsVerbiage //process.env.npm_package_config_verbiage_macs
|
||||
},
|
||||
foundFiles () {
|
||||
return this.appsBeingScanned.filter( appScan => {
|
||||
|
|
@ -264,7 +271,7 @@ export default {
|
|||
return `Apple Silicon Compatibility Test Online`
|
||||
},
|
||||
description () {
|
||||
return `Check for Apple Silicon compatibility for any of your apps instantly before you buy an ${ process.env.npm_package_config_verbiage_macs }. `
|
||||
return `Check for Apple Silicon compatibility for any of your apps instantly before you buy an ${ this.$config.macsVerbiage }. `
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
|
|
@ -295,13 +302,18 @@ export default {
|
|||
console.log('Initializing scanner instance')
|
||||
|
||||
// Bring in code
|
||||
const { default: AppFilesScanner} = await import('~/helpers/app-files-scanner.js')
|
||||
const { default: AppFilesScanner } = await import('~/helpers/app-files-scanner.js')
|
||||
|
||||
const testResultStore = this.config ? this.$config.testResultStore : this.$config.testResultStore
|
||||
|
||||
// Initialize instance
|
||||
this.scanner = new AppFilesScanner({
|
||||
observableFilesArray: this.appsBeingScanned,
|
||||
testResultStore: this.$config.testResultStore
|
||||
testResultStore
|
||||
})
|
||||
|
||||
// Setup scanner
|
||||
await this.scanner.setup()
|
||||
}
|
||||
|
||||
// console.log('fileInputChanged files', fileList)
|
||||
|
|
|
|||
11
sandbox.config.json
Normal file
11
sandbox.config.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"infiniteLoopProtection": true,
|
||||
"hardReloadOnChange": false,
|
||||
"view": "browser",
|
||||
"template": "node",
|
||||
"container": {
|
||||
"port": 3000,
|
||||
"startScript": "start",
|
||||
"node": "14"
|
||||
}
|
||||
}
|
||||
12
scripts/build-stork-index.js
Normal file
12
scripts/build-stork-index.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import {
|
||||
downloadStorkExecutable,
|
||||
buildIndex
|
||||
} from '~/helpers/stork/executable.js'
|
||||
|
||||
;(async () => {
|
||||
await downloadStorkExecutable()
|
||||
|
||||
await buildIndex()
|
||||
|
||||
process.exit()
|
||||
})()
|
||||
55
scripts/download-sitemaps.js
Normal file
55
scripts/download-sitemaps.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import fs from 'fs-extra'
|
||||
import 'dotenv/config'
|
||||
import axios from 'axios'
|
||||
|
||||
import {
|
||||
sitemapLocation,
|
||||
sitemapIndexFileName,
|
||||
} from '~/helpers/constants.js'
|
||||
|
||||
import { parseSitemapXml } from '~/helpers/api/sitemap/parse.js'
|
||||
|
||||
|
||||
;(async () => {
|
||||
|
||||
// Build Sitemap Index URL
|
||||
const sitemapIndexUrl = new URL( `${ sitemapLocation.split('static')[1] }${ sitemapIndexFileName }`, process.env.PUBLIC_API_DOMAIN )
|
||||
|
||||
// Fetch Sitemap Index
|
||||
const sitemapIndexXML = await axios.get( sitemapIndexUrl.href ).then( response => response.data )
|
||||
|
||||
// Save Sitemap Index
|
||||
const sitemapIndexFilePath = `${ sitemapLocation }${ sitemapIndexFileName }`
|
||||
await fs.writeFile( sitemapIndexFilePath, sitemapIndexXML )
|
||||
|
||||
const urlEntries = parseSitemapXml( sitemapIndexXML )
|
||||
|
||||
|
||||
// Fetch each sitemap
|
||||
for ( const entry of urlEntries ) {
|
||||
|
||||
// Build Sitemap Index URL
|
||||
const sitemapUrl = new URL( entry.loc )
|
||||
const apiSitemapUrl = new URL( sitemapUrl.pathname, process.env.PUBLIC_API_DOMAIN )
|
||||
|
||||
// sitemapUrl.origin = process.env.PUBLIC_API_DOMAIN
|
||||
|
||||
// Fetch Sitemap Index
|
||||
const sitemapXML = await axios.get( apiSitemapUrl.href ).then( response => response.data )
|
||||
|
||||
// const sitemap = parse( sitemapXML )
|
||||
|
||||
// console.log( 'sitemap', sitemap )
|
||||
|
||||
// console.log( 'apiSitemapUrl', apiSitemapUrl )
|
||||
|
||||
const sitemapFileName = apiSitemapUrl.pathname.split('/')[1]
|
||||
const sitemapIndexFilePath = `${ sitemapLocation }${ sitemapFileName }`
|
||||
|
||||
// Save file
|
||||
await fs.writeFile( sitemapIndexFilePath, sitemapXML )
|
||||
}
|
||||
|
||||
|
||||
process.exit()
|
||||
})()
|
||||
7
scripts/download-stork-executable.js
Normal file
7
scripts/download-stork-executable.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { downloadStorkExecutable } from '~/helpers/stork/executable.js'
|
||||
|
||||
;(async () => {
|
||||
await downloadStorkExecutable()
|
||||
|
||||
process.exit()
|
||||
})()
|
||||
9
scripts/download-stork-toml.js
Normal file
9
scripts/download-stork-toml.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import {
|
||||
downloadStorkToml
|
||||
} from '~/helpers/api/static.js'
|
||||
|
||||
;(async () => {
|
||||
await downloadStorkToml()
|
||||
|
||||
process.exit()
|
||||
})()
|
||||
12
scripts/stork-netlify.sh
Executable file
12
scripts/stork-netlify.sh
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Hugo Bash Example https://github.com/jmooring/hugo-stork/blob/main/build.sh
|
||||
|
||||
# curl https://files.stork-search.net/releases/latest/stork-amazon-linux -o stork-executable
|
||||
# curl https://files.stork-search.net/releases/v1.4.3/stork-macos-latest -o stork-executable
|
||||
|
||||
curl https://files.stork-search.net/releases/v1.4.2/stork-amazon-linux -o stork-executable
|
||||
# curl https://files.stork-search.net/releases/v1.4.2/stork-macos-10-15 -o stork-executable
|
||||
|
||||
chmod +x stork-executable
|
||||
./stork-executable build --input static/stork.toml --output static/search-index.st
|
||||
91
src/components/default-listing.astro
Normal file
91
src/components/default-listing.astro
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
---
|
||||
// Default Listing template for Apps, Games, and formulas
|
||||
|
||||
import {
|
||||
ListingDetails
|
||||
} from '~/helpers/listing-page.js'
|
||||
|
||||
import Aliases from '~/src/components/listing-parts/aliases.astro'
|
||||
import ThomasCredit from '~/components/thomas-credit.vue'
|
||||
import RelatedLinks from '~/src/components/listing-parts/related-links.astro'
|
||||
import Virtualization from './listing-parts/virtualization.astro'
|
||||
import CarbonInline from '~/components/carbon-inline.vue'
|
||||
import Devices from '~/src/components/listing-parts/devices.astro'
|
||||
import RelatedVideos from '~/src/components/listing-parts/related-videos.astro'
|
||||
import Bundles from '~/src/components/listing-parts/bundles.astro'
|
||||
import GameReports from '~/src/components/listing-parts/game-reports.astro'
|
||||
import LastUpdated from '~/src/components/listing-parts/last-updated.astro'
|
||||
|
||||
import AllUpdatesSubscribe from '~/components/all-updates-subscribe.vue'
|
||||
|
||||
// import { makeLastUpdatedFriendly } from '~/helpers/parse-date'
|
||||
// import { getAppEndpoint } from '~/helpers/app-derived.js'
|
||||
|
||||
// import LinkButton from '~/components/link-button.vue'
|
||||
// import VideoRow from '~/components/video/row.vue'
|
||||
|
||||
|
||||
const {
|
||||
listing
|
||||
} = Astro.props
|
||||
|
||||
const details = new ListingDetails( listing )
|
||||
|
||||
---
|
||||
<section class="container space-y-8 py-32">
|
||||
|
||||
<div class="intro-content flex flex-col items-center text-center min-h-3/4-screen md:min-h-0 gap-8">
|
||||
|
||||
<div
|
||||
class="title text-sm md:text-xl font-bold"
|
||||
set:html={ details.mainHeading }
|
||||
/>
|
||||
|
||||
<h2 class="subtitle text-2xl md:text-5xl font-bold">
|
||||
{ details.subtitle }
|
||||
</h2>
|
||||
|
||||
{ details.isGame && (
|
||||
<ThomasCredit />
|
||||
)}
|
||||
|
||||
<Aliases listing={ listing } />
|
||||
|
||||
<!-- <AllUpdatesSubscribe
|
||||
client:visible
|
||||
/> -->
|
||||
|
||||
<div class="links space-y-6 sm:space-x-6">
|
||||
<RelatedLinks
|
||||
listing={ listing }
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<Virtualization listing={ listing } />
|
||||
|
||||
<CarbonInline class="carbon-inline-wide w-full" />
|
||||
|
||||
<Devices
|
||||
listing={ listing }
|
||||
/>
|
||||
|
||||
<RelatedVideos
|
||||
listing={ listing }
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<Bundles
|
||||
listing={ listing }
|
||||
/>
|
||||
|
||||
<GameReports
|
||||
listing={ listing }
|
||||
/>
|
||||
|
||||
<LastUpdated
|
||||
listing={ listing }
|
||||
/>
|
||||
|
||||
</section>
|
||||
28
src/components/google-analytics.astro
Normal file
28
src/components/google-analytics.astro
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
|
||||
import { gaMeasurementId } from '~/helpers/constants.js'
|
||||
// const gaMeasurementId = 'G-0WLH5YTTB0'
|
||||
|
||||
---
|
||||
<!-- Set gaMeasurementId so it's available within the browser/window context -->
|
||||
<script
|
||||
type="text/javascript"
|
||||
set:html={ `window.gaMeasurementId = '${ gaMeasurementId }'` }
|
||||
></script>
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script
|
||||
type="text/partytown"
|
||||
async
|
||||
src={ `https://www.googletagmanager.com/gtag/js?id=${ gaMeasurementId }` }
|
||||
></script>
|
||||
<script type="text/partytown">
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag () {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', gaMeasurementId);
|
||||
|
||||
// console.log('I wanna to shake your hand', gaMeasurementId )
|
||||
</script>
|
||||
14
src/components/listing-parts/aliases.astro
Normal file
14
src/components/listing-parts/aliases.astro
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
const {
|
||||
listing
|
||||
} = Astro.props
|
||||
|
||||
const hasMultipleAliases = Array.isArray( listing.aliases ) && listing.aliases.length > 1
|
||||
|
||||
|
||||
const listFormatter = new Intl.ListFormat('en', { style: 'short', type: 'disjunction' })
|
||||
|
||||
---
|
||||
{ hasMultipleAliases ?
|
||||
<small class="text-xs opacity-75">May also be known as { listFormatter.format( listing.aliases, 'or' ) }</small>
|
||||
: '' }
|
||||
105
src/components/listing-parts/bundles.astro
Normal file
105
src/components/listing-parts/bundles.astro
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
---
|
||||
// https://docs.astro.build/en/reference/api-reference/#code-
|
||||
// import { Code } from 'astro/components'
|
||||
|
||||
import { getStatusOfScan } from '~/helpers/statuses.js'
|
||||
import { supportedArchitectures } from '~/helpers/bundles.js'
|
||||
|
||||
import Heading from './heading.astro'
|
||||
|
||||
const {
|
||||
listing
|
||||
} = Astro.props
|
||||
|
||||
const hasBundleIdentifiers = Array.isArray( listing.bundles ) && listing.bundles.length > 0
|
||||
---
|
||||
{ hasBundleIdentifiers && (
|
||||
|
||||
<div class="app-bundles w-full">
|
||||
|
||||
<Heading text='Bundle Version History' />
|
||||
|
||||
<div class="app-bundles-container border rounded-lg">
|
||||
|
||||
<div class="app-bundles-list md:inline-flex w-full overflow-x-auto overflow-y-visible md:whitespace-no-wrap divide-y md:divide-y-0 md:divide-x divide-gray-700 space-y-3 md:space-y-0 py-4 px-3">
|
||||
|
||||
{ listing.bundles.map( ( [ bundleIdentifier ] ) => (
|
||||
<div class="bundle-listing-container w-full md:w-auto inline-flex flex-col space-y-2 px-2">
|
||||
<a
|
||||
href={ `#bundle_identifier=${bundleIdentifier}` }
|
||||
role="button"
|
||||
class="bundle-link block rounded-md text-sm font-medium leading-5 focus:outline-none focus:text-white focus:bg-gray-700 transition duration-150 ease-in-out text-gray-300 hover:bg-darker hover:neumorphic-shadow p-2"
|
||||
aria-label={ bundleIdentifier }
|
||||
>{ bundleIdentifier }</a>
|
||||
|
||||
</div>
|
||||
)) }
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="app-bundle-detail-view space-y-12 py-6 md:px-5">
|
||||
{ listing.bundles.map( ( [ bundleIdentifier, versions ] ) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
id={ `bundle_identifier=${bundleIdentifier}` }
|
||||
class="bundle-detail-container w-full overflow-hidden space-y-2 px-2"
|
||||
>
|
||||
<h3
|
||||
class="md:text-2xl font-bold"
|
||||
>{ bundleIdentifier }</h3>
|
||||
|
||||
<div class="bundle-versions-container border rounded-lg bg-black bg-opacity-10">
|
||||
|
||||
<div class="app-bundles-list md:inline-flex w-full overflow-x-auto overflow-y-visible md:whitespace-no-wrap divide-y md:divide-y-0 md:divide-x divide-gray-700 space-y-3 md:space-y-0 py-4 px-3">
|
||||
|
||||
{ versions.map( ( [ version, report ] ) => (
|
||||
<div
|
||||
class="bundle-listing-container w-full md:w-auto inline-flex flex-col p-4"
|
||||
style="min-width: 300px;"
|
||||
>
|
||||
<div class="version-heading font-bold text-xl">v{ version }</div>
|
||||
<div class="version-body divide-y-0 py-2">
|
||||
<div class="version-status">
|
||||
{ getStatusOfScan( report, false ) }
|
||||
</div>
|
||||
<div class="version-architecture">
|
||||
🖥 Supported Architectures <span class="rounded bg-black bg-opacity-50 p-1">{ supportedArchitectures( report ).join(', ') }</span>
|
||||
</div>
|
||||
</div>
|
||||
<details>
|
||||
<summary
|
||||
class="text-xs cursor-pointer hover:bg-black-7 rounded px-2 py-1"
|
||||
>Full Info Plist</summary>
|
||||
<pre
|
||||
class="inline-block border-dashed border rounded-lg space-y-4 p-4 mt-4"
|
||||
style="background-color: #0d1117"
|
||||
>{ JSON.stringify( report['Info Plist'], undefined, 2) }</pre>
|
||||
</details>
|
||||
<details>
|
||||
<summary
|
||||
class="text-xs cursor-pointer hover:bg-black-7 rounded px-2 py-1"
|
||||
>Full Meta Details</summary>
|
||||
<pre
|
||||
class="inline-block border-dashed border rounded-lg space-y-4 p-4 mt-4"
|
||||
style="background-color: #0d1117"
|
||||
>{ JSON.stringify( report['Macho Meta'], undefined, 2) }</pre>
|
||||
</details>
|
||||
</div>
|
||||
)) }
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}) }
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
)}
|
||||
33
src/components/listing-parts/devices.astro
Normal file
33
src/components/listing-parts/devices.astro
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
import Heading from './heading.astro'
|
||||
|
||||
const {
|
||||
listing
|
||||
} = Astro.props
|
||||
|
||||
const hasDeviceSupport = Array.isArray( listing.deviceSupport )
|
||||
---
|
||||
{ hasDeviceSupport && (
|
||||
<div class="device-support w-full">
|
||||
|
||||
<Heading text='Device Support' />
|
||||
|
||||
<div class="device-support-apps md:inline-flex md:w-full max-w-4xl overflow-x-auto overflow-y-visible md:whitespace-no-wrap border rounded-lg divide-y md:divide-y-0 md:divide-x divide-gray-700 space-y-3 md:space-y-0 py-4 px-3">
|
||||
|
||||
{ listing.deviceSupport.map( device => (
|
||||
<div class="device-container w-full md:w-auto inline-flex flex-col space-y-2 px-2">
|
||||
<a
|
||||
href={ device.endpoint }
|
||||
role="button"
|
||||
class="device-link block rounded-md text-sm font-medium leading-5 focus:outline-none focus:text-white focus:bg-gray-700 transition duration-150 ease-in-out text-gray-300 hover:bg-darker hover:neumorphic-shadow p-2"
|
||||
aria-label={ device.ariaLabel }
|
||||
>{ device.emoji } { device.name }</a>
|
||||
|
||||
<a href={ device.amazonUrl } target="_blank" class="underline text-xs pb-3" rel="noopener">Check Pricing</a>
|
||||
</div>
|
||||
)) }
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
89
src/components/listing-parts/game-reports.astro
Normal file
89
src/components/listing-parts/game-reports.astro
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
---
|
||||
// import { getStatusOfScan } from '~/helpers/statuses.js'
|
||||
// import { supportedArchitectures } from '~/helpers/bundles.js'
|
||||
import { ensureListingDetails } from '~/helpers/listing-page'
|
||||
import Heading from './heading.astro'
|
||||
|
||||
const {
|
||||
listing
|
||||
} = Astro.props
|
||||
|
||||
const details = ensureListingDetails(listing)
|
||||
|
||||
const hasReports = Array.isArray( details.api.reports ) && details.api.reports.length > 0
|
||||
|
||||
const defaultSourceUrl = 'https://applesilicongames.com/'
|
||||
|
||||
function getSourceUrl ( report ) {
|
||||
// Try to get the source url from the report first
|
||||
if ( report['Source'].includes('https://') ) {
|
||||
return report['Source']
|
||||
}
|
||||
|
||||
// Otherwise, fall back to the default source url
|
||||
return defaultSourceUrl
|
||||
}
|
||||
|
||||
---
|
||||
{ hasReports && (
|
||||
|
||||
<Heading
|
||||
text='Reports'
|
||||
/>
|
||||
|
||||
<ul class="flex flex-col md:flex-row space-x-0 space-y-4 md:space-y-0 md:space-x-4 mb-4">
|
||||
|
||||
{ listing.reports.map( report => (
|
||||
<li
|
||||
class="col-span-1 rounded-lg border w-full md:w-64"
|
||||
>
|
||||
<div class="w-full flex items-center justify-between p-6">
|
||||
<div class="flex-1">
|
||||
<div class="space-x-3">
|
||||
<h3 class="text-sm leading-5 font-bold">{ report['Specs'] }</h3>
|
||||
<span class="flex-shrink-0 inline-block px-2 py-0.5 text-teal-800 text-xs leading-4 font-bold bg-teal-100 rounded-full">{ report['FPS'] }</span>
|
||||
</div>
|
||||
<p class="mt-1 text-sm leading-5">{ report['Notes'] }</p>
|
||||
<p
|
||||
v-if="report['Resolution'].length !== 0"
|
||||
class="mt-1 text-sm leading-5"
|
||||
>
|
||||
🖥 { report['Resolution'] }
|
||||
</p>
|
||||
{ report['Settings'].length !== 0 && (
|
||||
<p class="mt-1 text-sm leading-5">
|
||||
⚙️ { report['Settings'] }
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="border-t border-gray-200">
|
||||
<div class="-mt-px flex">
|
||||
<div class="w-0 flex-1 flex border-r border-gray-200">
|
||||
<a
|
||||
href={ getSourceUrl( report ) }
|
||||
class="relative -mr-px w-0 flex-1 inline-flex items-center justify-center py-4 text-sm leading-5 font-bold border border-transparent rounded-bl-lg hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 transition ease-in-out duration-150"
|
||||
>
|
||||
<!-- Heroicon name: mail -->
|
||||
<svg
|
||||
class="w-5 h-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
|
||||
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
|
||||
</svg>
|
||||
<span class="ml-3 opacity-75">Source</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
||||
</ul>
|
||||
|
||||
)}
|
||||
13
src/components/listing-parts/heading.astro
Normal file
13
src/components/listing-parts/heading.astro
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
import { makeSlug } from '~/helpers/slug.js'
|
||||
|
||||
const {
|
||||
text
|
||||
} = Astro.props
|
||||
---
|
||||
<h2
|
||||
id={ makeSlug( text ) }
|
||||
class="section-heading text-xl md:text-2xl text-center font-bold mb-3"
|
||||
>
|
||||
{ text }
|
||||
</h2>
|
||||
37
src/components/listing-parts/last-updated.astro
Normal file
37
src/components/listing-parts/last-updated.astro
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
import { ensureListingDetails } from '~/helpers/listing-page.js'
|
||||
import { makeLastUpdatedFriendly } from '~/helpers/parse-date.js'
|
||||
|
||||
const {
|
||||
listing
|
||||
} = Astro.props
|
||||
|
||||
const details = ensureListingDetails( listing )
|
||||
|
||||
const lastUpdatedFriendly = makeLastUpdatedFriendly( listing.lastUpdated )
|
||||
|
||||
const gameReportUrl = 'https://forms.gle/29GWt85i1G1L7Ttj8'
|
||||
const defaultReportUrl = `https://github.com/ThatGuySam/doesitarm/issues?q=is%3Aissue+${ listing.name }`
|
||||
|
||||
const reportUrl = details.isGame ? gameReportUrl : defaultReportUrl
|
||||
|
||||
---
|
||||
<div class="report-update text-xs text-center w-full shadow-none py-24">
|
||||
{ lastUpdatedFriendly !== null &&
|
||||
<div>
|
||||
<time
|
||||
datetime={ listing.lastUpdated.raw }
|
||||
>
|
||||
Last Updated { lastUpdatedFriendly }
|
||||
</time>
|
||||
</div>
|
||||
}
|
||||
<!-- https://eric.blog/2016/01/08/prefilling-github-issues/ -->
|
||||
<a
|
||||
href={ reportUrl }
|
||||
target="_blank"
|
||||
class="underline"
|
||||
rel="noopener"
|
||||
>Report Update</a>
|
||||
</div>
|
||||
|
||||
23
src/components/listing-parts/related-links.astro
Normal file
23
src/components/listing-parts/related-links.astro
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
const {
|
||||
listing
|
||||
} = Astro.props
|
||||
|
||||
const hasRelatedLinks = Array.isArray(listing.relatedLinks)
|
||||
---
|
||||
{ hasRelatedLinks && listing.relatedLinks.map( (link, i) => {
|
||||
|
||||
const notAppTestLink = !link.label.includes('🧪')
|
||||
|
||||
const isMainLink = (i === 0) && notAppTestLink
|
||||
|
||||
return (
|
||||
<a
|
||||
class="relative inline-flex items-center rounded-md px-4 py-2 leading-5 font-bold text-white border border-transparent focus:outline-none focus:border-indigo-600 neumorphic-shadow focus:shadow-outline-indigo bg-darker hover:bg-indigo-400 active:bg-indigo-600 transition duration-150 ease-in-out"
|
||||
href={ link.href }
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
role="button"
|
||||
>{ isMainLink ? 'View' : link.label }</a>
|
||||
)
|
||||
} ) }
|
||||
24
src/components/listing-parts/related-videos.astro
Normal file
24
src/components/listing-parts/related-videos.astro
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
import { ensureListingDetails } from '~/helpers/listing-page.js'
|
||||
import Heading from './heading.astro'
|
||||
import VideoRow from '~/src/components/video/row.astro'
|
||||
|
||||
const {
|
||||
listing
|
||||
} = Astro.props
|
||||
|
||||
const details = ensureListingDetails(listing)
|
||||
|
||||
---
|
||||
{ details.hasRelatedVideos && (
|
||||
<div
|
||||
class="related-videos w-full"
|
||||
>
|
||||
<Heading text="Related Videos" />
|
||||
|
||||
<VideoRow
|
||||
videos={details.relatedVideos}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
79
src/components/listing-parts/virtualization.astro
Normal file
79
src/components/listing-parts/virtualization.astro
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
---
|
||||
import { ensureListingDetails } from '~/helpers/listing-page.js'
|
||||
|
||||
// import LinkButton from '~/components/link-button.js'
|
||||
import Heading from './heading.astro'
|
||||
|
||||
const {
|
||||
listing
|
||||
} = Astro.props
|
||||
|
||||
|
||||
const details = ensureListingDetails( listing )
|
||||
|
||||
const isNonNativeGame = listing.status !== 'native' && details.isGame
|
||||
|
||||
const links = [
|
||||
{
|
||||
label: '🔄 CrossOver Compatibility',
|
||||
href: 'https://www.codeweavers.com/compatibility?ad=836'
|
||||
},
|
||||
{
|
||||
label: '🔄 CrossOver Performance',
|
||||
href: 'https://www.codeweavers.com/blog/jnewman/2020/11/23/more-crossover-m1-goodness-see-3-different-windows-games-running?ad=836'
|
||||
},
|
||||
{
|
||||
label: '⏸ Parallels Compatibility',
|
||||
href: 'https://prf.hn/l/pRelBQ5'
|
||||
},
|
||||
{
|
||||
label: '⏸ Parallels Performance',
|
||||
href: 'https://prf.hn/l/J9G0JeM'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
|
||||
const totalLinks = links.length
|
||||
---
|
||||
{ isNonNativeGame && (
|
||||
<div
|
||||
class="related-videos w-full"
|
||||
>
|
||||
<Heading text="Virtualization Support" />
|
||||
|
||||
<div class="text-xs opacity-75 mb-4">With Virtualization you can run apps on Apple Silicon Macs even if they are normally completely unsupported, such as Windows-only Apps, at the cost of some performance drop vs Native support. </div>
|
||||
|
||||
<span class="relative z-0 inline-flex text-center md:flex-row flex-col shadow-sm md:divide-x md:divide-y-0 divide-y divide-gray-700 border border-gray-300 rounded-md bg-darker md:py-3 md:px-0 px-4">
|
||||
{ links.map( (link, i) => {
|
||||
|
||||
return (
|
||||
<a
|
||||
type="button"
|
||||
href={ link.href }
|
||||
class={ [
|
||||
'relative inline-flex justify-center items-center font-medium focus:z-10 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500',
|
||||
'text-white group',
|
||||
].join(' ')}
|
||||
>
|
||||
<div
|
||||
class={[
|
||||
'inner-link group-hover:bg-indigo-400 group-active:bg-indigo-600 rounded-md px-4 md:py-2 md:mx-0 md:-my-3',
|
||||
'py-3 -mx-4',
|
||||
// First Link
|
||||
// i === 0 && 'rounded-l-md',
|
||||
// Not first Link
|
||||
i !== 0 ? 'md:-ml-px' : '',
|
||||
// Last Link
|
||||
// i === totalLinks - 1 ? 'rounded-r-md' : ''
|
||||
].join(' ')}
|
||||
>
|
||||
{ link.label }
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
35
src/components/simple-list.astro
Normal file
35
src/components/simple-list.astro
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
// Simple list for listing thinngs like categories, tags, etc.
|
||||
|
||||
const {
|
||||
items
|
||||
} = Astro.props
|
||||
---
|
||||
<ul class="simple-list space-y-3">
|
||||
{ items.map( item => (
|
||||
<li
|
||||
class="relative"
|
||||
>
|
||||
<a
|
||||
href={ item.href }
|
||||
class={ [
|
||||
'flex justify-start items-center inset-x-0 text-3xl md:text-4xl hover:bg-darkest focus:bg-gray-50 rounded-lg',
|
||||
'border-2 border-white border-opacity-0 hover:border-opacity-50 focus:outline-none',
|
||||
'duration-300 ease-in-out',
|
||||
// Spacing
|
||||
'space-x-3 -mx-5 px-5 md:pr-64 py-3'
|
||||
].join(' ') }
|
||||
style="transition-property: border;"
|
||||
>
|
||||
<div
|
||||
class="font-hairline flex flex-col gap-3"
|
||||
>
|
||||
<h2>{ item.heading }</h2>
|
||||
<div class="text-xs opacity-75 mb-3">{ item.description }</div>
|
||||
</div>
|
||||
<div>➔</div>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
)) }
|
||||
</ul>
|
||||
46
src/components/stork-vanilla.astro
Normal file
46
src/components/stork-vanilla.astro
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
// Stork Vanilla search
|
||||
// Stork Eleventy Example - https://github.com/stork-search/netlify-11ty-example
|
||||
|
||||
import {
|
||||
storkVersion,
|
||||
// storkExecutableName,
|
||||
// storkExecutablePath,
|
||||
// storkTomlPath,
|
||||
// storkIndexPath
|
||||
} from '~/helpers/stork/config.js'
|
||||
|
||||
// const {
|
||||
// listing
|
||||
// } = Astro.props
|
||||
|
||||
const storkStylesheetURL = `https://files.stork-search.net/releases/v${ storkVersion }/basic.css`
|
||||
const storkScriptURL = `https://files.stork-search.net/releases/v${ storkVersion }/stork.js`
|
||||
|
||||
---
|
||||
<h1>Search</h1>
|
||||
<link rel="stylesheet" href={ storkStylesheetURL } />
|
||||
|
||||
<div class="stork-wrapper">
|
||||
<input data-stork="index" class="stork-input" />
|
||||
<div data-stork="index-output" class="stork-output"></div>
|
||||
</div>
|
||||
|
||||
<script is:inline src={ storkScriptURL }></script>
|
||||
|
||||
<script>
|
||||
import { StorkClient } from '~/helpers/stork/browser.js'
|
||||
|
||||
const stork = new StorkClient({
|
||||
name: 'index',
|
||||
url: '/search-index.st',
|
||||
// config: {}
|
||||
|
||||
stork: window.stork
|
||||
})
|
||||
|
||||
const queryResult = await stork.lazyQuery( 'spo' )
|
||||
|
||||
console.log('queryResult', queryResult)
|
||||
|
||||
</script>
|
||||
79
src/components/video-listing.astro
Normal file
79
src/components/video-listing.astro
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
---
|
||||
// Video Listing template for Benchmarks and Videos
|
||||
|
||||
import {
|
||||
ensureListingDetails
|
||||
} from '~/helpers/listing-page.js'
|
||||
|
||||
// import Devices from '~/src/components/listing-parts/devices.astro'
|
||||
import RelatedVideos from '~/src/components/listing-parts/related-videos.astro'
|
||||
import HtmlPlayer from '~/src/components/video/player.astro'
|
||||
|
||||
|
||||
const {
|
||||
listing
|
||||
} = Astro.props
|
||||
|
||||
const details = ensureListingDetails( listing )
|
||||
|
||||
---
|
||||
<section class="container pb-16">
|
||||
<div class="flex flex-col items-center text-center space-y-6">
|
||||
|
||||
<HtmlPlayer
|
||||
video={ details.initialVideo }
|
||||
>
|
||||
|
||||
<div
|
||||
slot="cover-bottom"
|
||||
class="page-heading h-full flex items-end md:p-4"
|
||||
>
|
||||
<h1 class="title text-xs text-left md:text-2xl font-bold">{ details.mainHeading }</h1>
|
||||
</div>
|
||||
|
||||
</HtmlPlayer>
|
||||
|
||||
<div class="md:flex w-full justify-between space-y-4 md:space-y-0 md:px-10">
|
||||
|
||||
{ details.shouldHaveSubscribeButton &&
|
||||
<div
|
||||
class="channel-credit"
|
||||
>
|
||||
<a
|
||||
href={`https://www.youtube.com/channel/${ details.initialVideo.channel.id }?sub_confirmation=1`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
role="button"
|
||||
class="relative inline-flex items-center rounded-md px-4 py-2 leading-5 font-bold text-white border border-transparent focus:outline-none focus:border-indigo-600 neumorphic-shadow focus:shadow-outline-indigo bg-darker hover:bg-indigo-400 active:bg-indigo-600 transition duration-150 ease-in-out"
|
||||
>Subscribe to { details.initialVideo.channel.name }</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<hr class="w-full">
|
||||
|
||||
{ details.hasRelatedApps &&
|
||||
<div
|
||||
class="related-apps w-full"
|
||||
>
|
||||
<h2 class="subtitle text-xl md:text-2xl font-bold mb-3">
|
||||
Related Apps
|
||||
</h2>
|
||||
<div class="featured-apps overflow-x-auto overflow-y-visible whitespace-no-wrap py-2 space-x-2">
|
||||
{ details.initialVideo.appLinks.map( appLink => (
|
||||
<a
|
||||
href={ appLink.endpoint }
|
||||
role="button"
|
||||
class="relative 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 inline-block text-xs rounded-md px-4 py-2"
|
||||
>{ appLink.name }</a>
|
||||
) ) }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<RelatedVideos
|
||||
listing={ listing }
|
||||
/>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
33
src/components/video/bg-player.astro
Normal file
33
src/components/video/bg-player.astro
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
const {
|
||||
video,
|
||||
classes = ''
|
||||
} = Astro.props
|
||||
|
||||
//
|
||||
---
|
||||
<div
|
||||
class={[
|
||||
'video-canvas w-screen flex justify-center inset-x-1/2 bg-black transition-opacity duration-500 ease-in-out',
|
||||
classes
|
||||
].join(' ')}
|
||||
style="
|
||||
margin-left: -50vw;
|
||||
margin-right: -50vw;
|
||||
-webkit-mask-image: linear-gradient(to top, rgba(0, 0, 0, 0) 25%, rgba(0, 0, 0, 0.25));
|
||||
"
|
||||
>
|
||||
<div class="ratio-wrapper w-full">
|
||||
<div class="relative overflow-hidden w-full pb-16/9">
|
||||
<video
|
||||
src={`https://vumbnail.com/${ video.id }.mp4`}
|
||||
class="absolute w-full object-cover inset-0 blur-sm"
|
||||
style="height: 200%;"
|
||||
muted
|
||||
autoplay
|
||||
loop
|
||||
playsinline
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
63
src/components/video/card.astro
Normal file
63
src/components/video/card.astro
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
import Poster from './poster.astro'
|
||||
|
||||
const {
|
||||
video,
|
||||
width = '325px',
|
||||
classes = 'w-full flex-shrink-0 flex-grow-0 border-2 border-transparent rounded-2xl overflow-hidden'
|
||||
} = Astro.props
|
||||
|
||||
const cardClasses = `video-card ${ classes }`
|
||||
|
||||
---
|
||||
<div
|
||||
class={ cardClasses }
|
||||
style={ `max-width: ${ width }; flex-basis: ${ width }; scroll-snap-align: start;` }
|
||||
>
|
||||
<a
|
||||
href={video.endpoint}
|
||||
class=""
|
||||
>
|
||||
<div class="video-card-container relative overflow-hidden bg-black">
|
||||
<div class="video-card-image ratio-wrapper">
|
||||
<div class="relative overflow-hidden w-full pb-16/9">
|
||||
<Poster video={video} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="--gradient-from-color: rgba(0, 0, 0, 1); --gradient-to-color: rgba(0, 0, 0, 0.7)"
|
||||
class="video-card-overlay absolute inset-0 flex justify-between items-start bg-gradient-to-tr from-black to-transparent p-4"
|
||||
>
|
||||
<div class="play-circle w-8 h-8 bg-white-2 flex justify-center items-center outline-0 rounded-full ease">
|
||||
<svg
|
||||
viewBox="0 0 18 18"
|
||||
style="width:18px;height:18px;margin-left:3px"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M15.562 8.1L3.87.225c-.818-.562-1.87 0-1.87.9v15.75c0 .9 1.052 1.462 1.87.9L15.563 9.9c.584-.45.584-1.35 0-1.8z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{ ( video.tags.includes('benchmark') ) ?
|
||||
<div
|
||||
class="video-pill h-5 text-xs bg-white-2 flex justify-center items-center outline-0 rounded-full ease px-2"
|
||||
>
|
||||
Benchmark
|
||||
</div> : ''
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Video Text Content -->
|
||||
<div class="video-card-content absolute inset-0 flex items-end py-4 px-6">
|
||||
<div class="w-full text-sm text-left whitespace-normal">{ video.name }</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import 'lazysizes'
|
||||
</script>
|
||||
62
src/components/video/player.astro
Normal file
62
src/components/video/player.astro
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
---
|
||||
import Poster from './poster.astro'
|
||||
import Timestamps from './timestamps.astro'
|
||||
|
||||
const {
|
||||
video,
|
||||
width = '325px',
|
||||
classes = 'w-full flex-shrink-0 flex-grow-0 border-2 border-transparent rounded-2xl overflow-hidden'
|
||||
} = Astro.props
|
||||
|
||||
---
|
||||
<lite-youtube
|
||||
class="video-canvas w-screen flex flex-col justify-center items-center bg-black pt-16"
|
||||
style="left:50%;right:50%;margin-left:-50vw;margin-right:-50vw;"
|
||||
>
|
||||
<script
|
||||
class="video-data"
|
||||
type="application/json"
|
||||
set:html={ JSON.stringify( video ) }
|
||||
/>
|
||||
|
||||
<div class="ratio-wrapper w-full max-w-4xl">
|
||||
<div class="player-container relative overflow-hidden w-full pb-16/9">
|
||||
<div class="player-poster cursor-pointer">
|
||||
|
||||
<Poster
|
||||
video={ video }
|
||||
/>
|
||||
|
||||
<div
|
||||
class="video-card-overlay absolute inset-0 flex flex-col justify-center items-center bg-gradient-to-tr p-4"
|
||||
style="
|
||||
--tw-gradient-stops:
|
||||
rgba(0, 0, 0, 1),
|
||||
rgba(0, 0, 0, 0.6)
|
||||
;
|
||||
"
|
||||
>
|
||||
<div class="cover-top h-full"></div>
|
||||
<div class="play-circle bg-white-2 bg-blur flex justify-center items-center outline-0 rounded-full ease p-4">
|
||||
<svg viewBox="0 0 18 18" style="width:18px;height:18px;margin-left:3px">
|
||||
<path fill="currentColor" d="M15.562 8.1L3.87.225c-.818-.562-1.87 0-1.87.9v15.75c0 .9 1.052 1.462 1.87.9L15.563 9.9c.584-.45.584-1.35 0-1.8z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="cover-bottom h-full">
|
||||
<slot name="cover-bottom"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Timestamps
|
||||
video={ video }
|
||||
/>
|
||||
|
||||
</lite-youtube>
|
||||
|
||||
<script>
|
||||
import 'lazysizes'
|
||||
import '~/helpers/lite-youtube.js'
|
||||
</script>
|
||||
24
src/components/video/poster.astro
Normal file
24
src/components/video/poster.astro
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
import { getVideoImages } from '~/helpers/listing-page.js'
|
||||
const {
|
||||
video
|
||||
} = Astro.props
|
||||
|
||||
const images = getVideoImages( video )
|
||||
---
|
||||
<picture>
|
||||
|
||||
{ Object.entries( images.sources ).map( ([ key, source ]) => (
|
||||
<source
|
||||
sizes={ source.sizes }
|
||||
data-srcset={ source.srcset }
|
||||
type={ `image/${ key }` }
|
||||
>
|
||||
) ) }
|
||||
|
||||
<img
|
||||
data-src={ images.imgSrc }
|
||||
alt={ video.name }
|
||||
class="absolute inset-0 h-full w-full object-cover lazyload"
|
||||
>
|
||||
</picture>
|
||||
70
src/components/video/row.astro
Normal file
70
src/components/video/row.astro
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
---
|
||||
import Card from './card.astro'
|
||||
|
||||
const {
|
||||
videos,
|
||||
cardWidth = '325',
|
||||
classes = ''
|
||||
} = Astro.props
|
||||
|
||||
// Math.random should be unique because of its seeding algorithm.
|
||||
// Convert it to base 36 (numbers + letters), and grab the first 9 characters
|
||||
// after the decimal.
|
||||
const uid = Math.random().toString(36).substr(2, 9)
|
||||
const rowId = `row-${ uid }`
|
||||
const rowSelector = `#${ rowId }`
|
||||
|
||||
---
|
||||
<div class="video-row relative w-full ${ classes }">
|
||||
|
||||
<div
|
||||
id={ rowId }
|
||||
class="video-row-contents flex overflow-x-auto whitespace-no-wrap py-2 space-x-6"
|
||||
style="scroll-snap-type:x mandatory;"
|
||||
>
|
||||
{ videos.map(video => (
|
||||
<Card
|
||||
key={ video.id }
|
||||
video={ video }
|
||||
cardWidth={ cardWidth }
|
||||
/>
|
||||
)) }
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="scroll-button absolute left-0 h-10 w-10 flex justify-center items-center transform -translate-y-1/2 -translate-x-1/2 bg-darker rounded-full"
|
||||
style="top:50%;"
|
||||
distance={ cardWidth * -1 }
|
||||
scroll-target={ rowSelector }
|
||||
aria-label="Scroll to previous videos"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" class="h-5 w-5 text-gray-400" style="transform: scaleX(-1);">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="scroll-button absolute right-0 h-10 w-10 flex justify-center items-center transform -translate-y-1/2 translate-x-1/2 bg-darker rounded-full"
|
||||
style="top:50%;"
|
||||
distance={ cardWidth }
|
||||
scroll-target={ rowSelector }
|
||||
aria-label="Scroll to next videos"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" class="h-5 w-5 text-gray-400">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import { scrollHorizontalCarousel } from '~/helpers/scroll.js'
|
||||
|
||||
|
||||
// Add Click listeners to all buttons
|
||||
Array.from( document.querySelectorAll(`.video-row button.scroll-button`) ).forEach( button => {
|
||||
// console.log('button', button)
|
||||
button.addEventListener('click', scrollHorizontalCarousel)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
26
src/components/video/timestamps.astro
Normal file
26
src/components/video/timestamps.astro
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
const {
|
||||
video
|
||||
} = Astro.props
|
||||
|
||||
const hasTimeStamps = video.timestamps.length > 0
|
||||
|
||||
---
|
||||
{ hasTimeStamps &&
|
||||
<div class="player-timestamps w-full max-w-4xl">
|
||||
<div class="player-timestamps-wrapper md:inline-flex md:w-full max-w-4xl overflow-x-auto overflow-y-visible md:whitespace-nowrap rounded-xl py-3">
|
||||
{ video.timestamps.map( timestamp => {
|
||||
// const inSeconds = (minutes * 60) + Number(seconds)
|
||||
return (
|
||||
<button
|
||||
time={ timestamp.time }
|
||||
aria-label={`Jump to ${ timestamp.fullText }`}
|
||||
class="inline-block text-xs rounded-lg border-2 border-white focus:outline-none border-opacity-0 neumorphic-shadow-inner px-3 py-2"
|
||||
>
|
||||
{ timestamp.fullText }
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
44
src/layouts/default.astro
Normal file
44
src/layouts/default.astro
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
import '~/assets/css/tailwind.css'
|
||||
|
||||
import { PageHead } from '~/helpers/config-node.js'
|
||||
|
||||
import VueBaseLayout from '../../layouts/base.vue'
|
||||
import GoogleAnalytics from '~/src/components/google-analytics.astro'
|
||||
|
||||
const {
|
||||
// headTitle,
|
||||
// headDescription,
|
||||
headOptions = {}
|
||||
} = Astro.props
|
||||
|
||||
// console.log('Astro.site', Astro.site )
|
||||
|
||||
const pageHead = new PageHead({
|
||||
domain: Astro.site.origin,
|
||||
|
||||
...headOptions
|
||||
})
|
||||
|
||||
---
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{ pageHead.title }</title>
|
||||
<Fragment set:html={ pageHead.metaAndLinkMarkup } />
|
||||
|
||||
<GoogleAnalytics />
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<VueBaseLayout
|
||||
client:load
|
||||
>
|
||||
<slot />
|
||||
</VueBaseLayout>
|
||||
|
||||
<!-- <script defer src="/_nuxt/static/1650919862/state.js"></script> -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
36
src/layouts/embed.astro
Normal file
36
src/layouts/embed.astro
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
import '~/assets/css/tailwind.css'
|
||||
|
||||
import { PageHead } from '~/helpers/config-node.js'
|
||||
|
||||
import GoogleAnalytics from '~/src/components/google-analytics.astro'
|
||||
|
||||
const {
|
||||
// headTitle,
|
||||
// headDescription,
|
||||
headOptions = {}
|
||||
} = Astro.props
|
||||
|
||||
// console.log('Astro.site', Astro.site )
|
||||
|
||||
const pageHead = new PageHead({
|
||||
domain: Astro.site.origin,
|
||||
|
||||
...headOptions
|
||||
})
|
||||
|
||||
---
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{ pageHead.title }</title>
|
||||
<Fragment set:html={ pageHead.metaAndLinkMarkup } />
|
||||
|
||||
<GoogleAnalytics />
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
72
src/pages/[...page].astro
Normal file
72
src/pages/[...page].astro
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
// import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
|
||||
import { catchRedirectResponse } from '~/helpers/astro/request.js'
|
||||
|
||||
import Layout from '../layouts/default.astro'
|
||||
import LinkButton from '~/components/link-button.vue'
|
||||
|
||||
// Component Script:
|
||||
// You can write any JavaScript/TypeScript that you'd like here.
|
||||
// It will run during the build, but never in the browser.
|
||||
// All variables are available to use in the HTML template below.
|
||||
|
||||
// Full Astro Component Syntax:
|
||||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
|
||||
const redirectResponse = await catchRedirectResponse( Astro )
|
||||
|
||||
if ( redirectResponse !== null ) {
|
||||
return redirectResponse
|
||||
}
|
||||
|
||||
// https://docs.astro.build/en/reference/api-reference/#astroresponse
|
||||
Astro.response.status = 404
|
||||
Astro.response.statusText = 'Not found'
|
||||
---
|
||||
<Layout
|
||||
headOptions={ {
|
||||
title: `Page is not compatible with Apple Silicon - Does It ARM`,
|
||||
description: `Check for Apple Silicon compatibility for any of your apps instantly before you buy an ${ global.$config.processorsVerbiage } Mac. `,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
||||
// domain,
|
||||
// pathname: '/',
|
||||
} }
|
||||
>
|
||||
|
||||
<section class="container py-24">
|
||||
<div class="flex flex-col items-center gap-8">
|
||||
|
||||
<h1 class="title text-3xl md:text-6xl font-hairline leading-tight text-center">
|
||||
🤷♀️ Page is not compatible with Apple Silicon
|
||||
</h1>
|
||||
<h2 class="subtitle md:text-xl text-center">
|
||||
Page not found
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
Perhaps the archives are incomplete, the page moved, or was deleted.
|
||||
</div>
|
||||
|
||||
<LinkButton href="/">
|
||||
Search
|
||||
</LinkButton>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
|
||||
You can also use imported framework components directly in your markup!
|
||||
|
||||
Note: by default, these components are NOT interactive on the client.
|
||||
The `:visible` directive tells Astro to make it interactive.
|
||||
|
||||
See https://docs.astro.build/core-concepts/component-hydration/
|
||||
|
||||
-->
|
||||
|
||||
</Layout>
|
||||
|
||||
76
src/pages/app/[...appPath].astro
Normal file
76
src/pages/app/[...appPath].astro
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
// Full Astro Component Syntax:
|
||||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
|
||||
import { DoesItAPI } from '~/helpers/api/client.js'
|
||||
import { catchRedirectResponse } from '~/helpers/astro/request.js'
|
||||
import {
|
||||
getVideoImages,
|
||||
ListingDetails
|
||||
} from '~/helpers/listing-page.js'
|
||||
import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
|
||||
|
||||
import Layout from '~/src/layouts/default.astro'
|
||||
import Listing from '~/src/components/default-listing.astro'
|
||||
import VideoListing from '~/src/components/video-listing.astro'
|
||||
|
||||
|
||||
const redirectResponse = await catchRedirectResponse( Astro )
|
||||
|
||||
if ( redirectResponse !== null ) {
|
||||
return redirectResponse
|
||||
}
|
||||
|
||||
|
||||
// Get type and slug from the request path
|
||||
// so that we don't have extra parts for
|
||||
// urls like /:type/:slug/benchmarks
|
||||
const {
|
||||
pathSlug,
|
||||
subSlug = null
|
||||
} = getPathPartsFromAstroRequest( Astro.request )
|
||||
|
||||
const isBenchmarkPage = subSlug === 'benchmarks'
|
||||
|
||||
|
||||
// Astro Request reference
|
||||
// https://docs.astro.build/en/reference/api-reference/#astrorequests
|
||||
|
||||
// Request App data from API
|
||||
const appListing = await DoesItAPI.app( pathSlug ).get()
|
||||
|
||||
const listingDetails = new ListingDetails( appListing )
|
||||
|
||||
const headOptions = listingDetails.headOptions
|
||||
|
||||
|
||||
if ( isBenchmarkPage ) {
|
||||
|
||||
// Set the page title
|
||||
headOptions.title = `${ listingDetails.api.name } Benchmarks for Apple Silicon - Does It ARM`
|
||||
|
||||
const { preloads } = getVideoImages( listingDetails.initialVideo )
|
||||
|
||||
// Add image preloads for video thumbnail
|
||||
headOptions.link = [
|
||||
...headOptions.link,
|
||||
...preloads
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
---
|
||||
<Layout
|
||||
headOptions={ headOptions }
|
||||
>
|
||||
{ isBenchmarkPage ? (
|
||||
<VideoListing
|
||||
listing={ appListing }
|
||||
/>
|
||||
) : (
|
||||
<Listing
|
||||
listing={ appListing }
|
||||
/>
|
||||
)}
|
||||
|
||||
</Layout>
|
||||
45
src/pages/apple-silicon-app-test.astro
Normal file
45
src/pages/apple-silicon-app-test.astro
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
import Layout from '../layouts/default.astro'
|
||||
|
||||
import AppTestPage from '~/pages/apple-silicon-app-test.vue'
|
||||
|
||||
// Component Script:
|
||||
// You can write any JavaScript/TypeScript that you'd like here.
|
||||
// It will run during the build, but never in the browser.
|
||||
// All variables are available to use in the HTML template below.
|
||||
|
||||
|
||||
// Full Astro Component Syntax:
|
||||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
|
||||
---
|
||||
<Layout
|
||||
headOptions={ {
|
||||
title: `Apple Silicon Compatibility Test Online - Does It ARM`,
|
||||
description: `Check for Apple Silicon compatibility for any of your apps instantly before you buy an ${ global.$config.processorsVerbiage } Mac. `,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
||||
// domain,
|
||||
pathname: '/',
|
||||
} }
|
||||
>
|
||||
|
||||
<AppTestPage
|
||||
config={ global.$config }
|
||||
client:load
|
||||
/>
|
||||
|
||||
<!--
|
||||
|
||||
You can also use imported framework components directly in your markup!
|
||||
|
||||
Note: by default, these components are NOT interactive on the client.
|
||||
The `:visible` directive tells Astro to make it interactive.
|
||||
|
||||
See https://docs.astro.build/core-concepts/component-hydration/
|
||||
|
||||
-->
|
||||
|
||||
</Layout>
|
||||
217
src/pages/benchmarks.astro
Normal file
217
src/pages/benchmarks.astro
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
---
|
||||
// Component Script:
|
||||
// You can write any JavaScript/TypeScript that you'd like here.
|
||||
// It will run during the build, but never in the browser.
|
||||
// All variables are available to use in the HTML template below.
|
||||
|
||||
// Full Astro Component Syntax:
|
||||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
|
||||
import { DoesItAPI } from '~/helpers/api/client.js'
|
||||
|
||||
import Layout from '../layouts/default.astro'
|
||||
import BgPlayer from '~/src/components/video/bg-player.astro'
|
||||
import VideoRow from '~/src/components/video/row.astro'
|
||||
// import LinkButton from '~/components/link-button.vue'
|
||||
|
||||
|
||||
const pagesToGet = 10
|
||||
|
||||
const allVideos = []
|
||||
|
||||
// Run through out newest video pages and get all the videos
|
||||
await Promise.all(
|
||||
Array.from({ length: pagesToGet }, (_, i) => i + 1).map(async (page) => {
|
||||
|
||||
// console.log('Getting page', page)
|
||||
|
||||
const videoPage = await DoesItAPI.kind.tv( page ).get()
|
||||
|
||||
// Merge in the new videos
|
||||
allVideos.push( ...videoPage.items )
|
||||
|
||||
return videoPage
|
||||
})
|
||||
)
|
||||
|
||||
// Our initial video for the hero
|
||||
const heroVideo = allVideos[0]
|
||||
|
||||
// Setup video rows data for the page
|
||||
const videoRows = {
|
||||
'video-benchmarks': {
|
||||
heading: 'Video Editing Benchmarks',
|
||||
matchesCondition: video => {
|
||||
return video.tags.includes('benchmark') && video.tags.includes('video-and-motion-tools')
|
||||
},
|
||||
videos: []
|
||||
},
|
||||
'music-and-audio-tools': {
|
||||
heading: 'Music and DAW Performance',
|
||||
matchesCondition: video => {
|
||||
return video.tags.includes('music-and-audio-tools')
|
||||
},
|
||||
videos: []
|
||||
},
|
||||
'science-and-research-software': {
|
||||
heading: 'Science and Research',
|
||||
matchesCondition: video => {
|
||||
return video.tags.includes('science-and-research-software')
|
||||
},
|
||||
videos: []
|
||||
},
|
||||
'photo-and-graphic-tools': {
|
||||
heading: 'Photography and Design Compatibility',
|
||||
matchesCondition: video => {
|
||||
return video.tags.includes('photo-and-graphic-tools')
|
||||
},
|
||||
videos: []
|
||||
},
|
||||
'games': {
|
||||
heading: 'Gaming Benchmarks',
|
||||
matchesCondition: video => {
|
||||
return video.tags.includes('benchmark') && video.tags.includes('games')
|
||||
},
|
||||
videos: []
|
||||
},
|
||||
'benchmarks': {
|
||||
heading: 'Other Benchmark Videos',
|
||||
matchesCondition: video => video.tags.includes('benchmark'),
|
||||
videos: []
|
||||
},
|
||||
'performance': {
|
||||
heading: 'Performance Videos',
|
||||
matchesCondition: video => video.tags.includes('performance'),
|
||||
videos: []
|
||||
},
|
||||
|
||||
'other': {
|
||||
heading: 'More Videos',
|
||||
// Always true
|
||||
matchesCondition: () => true,
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Move videos to relevant categories one at a time
|
||||
// so that we we don't get duplicates in the rows
|
||||
|
||||
for (const video of allVideos) {
|
||||
// Look through row conditions to see if video matches
|
||||
for (const rowKey in videoRows) {
|
||||
if( videoRows[ rowKey ].matchesCondition(video) ) {
|
||||
|
||||
// Add the matching video
|
||||
videoRows[ rowKey ].videos.push(video)
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
---
|
||||
<Layout
|
||||
headOptions={ {
|
||||
title: `Benchmarks for ${ global.$config.processorsVerbiage } Processors and Apple Silicon - Does It ARM`,
|
||||
description: `Apple Silicon benchmark, performance, and compatibility videos for Macs using the ${ global.$config.processorsVerbiage } processors.`,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
||||
// domain,
|
||||
pathname: '/benchmarks',
|
||||
} }
|
||||
>
|
||||
|
||||
<main class="container relative md:static overflow-hidden md:overflow-visible pb-16">
|
||||
<div class="flex flex-col items-center text-center space-y-12">
|
||||
<BgPlayer
|
||||
video={ heroVideo }
|
||||
classes="absolute overflow-hidden w-2x-screen md:w-full pointer-events-none"
|
||||
/>
|
||||
|
||||
<div class="page-heading flex justify-start w-full">
|
||||
<h1 class="title text-2xl leading-tight mt-12 mb-6">
|
||||
Benchmarks
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="line-separator border-white border-t-2 mb-12" />
|
||||
|
||||
<a
|
||||
href={ heroVideo.endpoint }
|
||||
>
|
||||
<div
|
||||
class="relative flex flex-col w-full justify-center items-center space-y-8 py-16 md:pt-0 md:pb-12 md:px-10"
|
||||
>
|
||||
<div
|
||||
class="play-circle w-16 h-16 bg-white-2 bg-blur flex justify-center items-center outline-0 rounded-full ease"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 18 18"
|
||||
style="width:24px;height:24px;margin-left:3px"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M15.562 8.1L3.87.225c-.818-.562-1.87 0-1.87.9v15.75c0 .9 1.052 1.462 1.87.9L15.563 9.9c.584-.45.584-1.35 0-1.8z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="title text-lg md:text-2xl font-bold">
|
||||
{ heroVideo.name }
|
||||
</h2>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- <div
|
||||
class="featured-apps-container w-full"
|
||||
>
|
||||
<hr class="w-full" >
|
||||
<div class="featured-apps overflow-x-auto overflow-y-visible whitespace-no-wrap py-2 space-x-2">
|
||||
<LinkButton
|
||||
v-for="app in featuredApps"
|
||||
href={ "app.endpoint" }
|
||||
class="inline-block text-xs rounded-lg py-1 px-2"
|
||||
class-groups={{
|
||||
shadow: 'neumorphic-shadow-inner'
|
||||
}}
|
||||
>{ app.name }</LinkButton>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
{ Object.entries(videoRows).map(([ rowKey, row ]) => {
|
||||
// Skip rows that don't have enough videos
|
||||
if ( row.videos.length < 3 ) return
|
||||
|
||||
return (
|
||||
<div
|
||||
class={ `${ rowKey }-videos w-full max-w-4xl` }
|
||||
>
|
||||
<h2 class="subtitle text-xl md:text-2xl font-bold mb-3">
|
||||
{ row.heading }
|
||||
</h2>
|
||||
<VideoRow
|
||||
client:load
|
||||
|
||||
videos={ row.videos }
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}) }
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!--
|
||||
|
||||
You can also use imported framework components directly in your markup!
|
||||
|
||||
Note: by default, these components are NOT interactive on the client.
|
||||
The `:visible` directive tells Astro to make it interactive.
|
||||
|
||||
See https://docs.astro.build/core-concepts/component-hydration/
|
||||
|
||||
-->
|
||||
|
||||
</Layout>
|
||||
64
src/pages/categories.astro
Normal file
64
src/pages/categories.astro
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
---
|
||||
// Component Script:
|
||||
// You can write any JavaScript/TypeScript that you'd like here.
|
||||
// It will run during the build, but never in the browser.
|
||||
// All variables are available to use in the HTML template below.
|
||||
|
||||
// Full Astro Component Syntax:
|
||||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
|
||||
import { DoesItAPI } from '~/helpers/api/client.js'
|
||||
|
||||
import Layout from '../layouts/default.astro'
|
||||
import SimpleList from '../components/simple-list.astro'
|
||||
|
||||
const kindIndex = await DoesItAPI.kind.index.get()
|
||||
|
||||
const kinds = Object.values( kindIndex ).map( category => {
|
||||
return {
|
||||
href: `/kind/${ category.kindName }`,
|
||||
heading: category.label,
|
||||
description: category.description,
|
||||
}
|
||||
})
|
||||
---
|
||||
<Layout
|
||||
headOptions={ {
|
||||
title: 'Categories of App Support lists for Apple Silicon',
|
||||
description: `List of compatibility apps and games for Apple Silicon and the ${ this.$config.processorsVerbiage } Processors including performance reports and benchmarks`,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
||||
// domain,
|
||||
pathname: '/categories',
|
||||
} }
|
||||
>
|
||||
|
||||
<main class="container py-24">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="title text-2xl leading-tight mb-6">
|
||||
Categories
|
||||
</h1>
|
||||
|
||||
<div class="line-separator border-white border-t-2 mb-12" />
|
||||
|
||||
<SimpleList
|
||||
items={ kinds }
|
||||
/>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!--
|
||||
|
||||
You can also use imported framework components directly in your markup!
|
||||
|
||||
Note: by default, these components are NOT interactive on the client.
|
||||
The `:visible` directive tells Astro to make it interactive.
|
||||
|
||||
See https://docs.astro.build/core-concepts/component-hydration/
|
||||
|
||||
-->
|
||||
|
||||
</Layout>
|
||||
119
src/pages/device/[...devicePath].astro
Normal file
119
src/pages/device/[...devicePath].astro
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
---
|
||||
// Full Astro Component Syntax:
|
||||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
|
||||
|
||||
import { DoesItAPI } from '~/helpers/api/client.js'
|
||||
import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
|
||||
import { catchRedirectResponse } from '~/helpers/astro/request.js'
|
||||
import { deviceSupportsApp } from '~/helpers/devices.js'
|
||||
|
||||
|
||||
import Layout from '../../layouts/default.astro'
|
||||
import Search from '~/components/search-stork.vue'
|
||||
import LinkButton from '~/components/link-button.vue'
|
||||
|
||||
|
||||
// Get type and slug from the request path
|
||||
// so that we don't have extra parts for
|
||||
// urls like /:type/:slug/benchmarks
|
||||
const {
|
||||
pathname,
|
||||
pathSlug,
|
||||
subSlug = 1
|
||||
} = getPathPartsFromAstroRequest( Astro.request )
|
||||
|
||||
|
||||
const redirectResponse = await catchRedirectResponse( Astro )
|
||||
|
||||
if ( redirectResponse !== null ) {
|
||||
return redirectResponse
|
||||
}
|
||||
|
||||
const device = await DoesItAPI.device( pathSlug ).get()
|
||||
const rawAppPage = await DoesItAPI.kind( 'app' )( subSlug ).get()
|
||||
|
||||
|
||||
const appPage = {
|
||||
...rawAppPage,
|
||||
|
||||
// Map out paginnation links
|
||||
// so we stay in the context of this device
|
||||
previousPage: rawAppPage.previousPage.replace( '/kind/app', '/device/' + pathSlug ),
|
||||
nextPage: rawAppPage.nextPage.replace( '/kind/app', '/device/' + pathSlug ),
|
||||
|
||||
// Map device support over text/status
|
||||
items: rawAppPage.items.map( listing => {
|
||||
const listingIsSupported = deviceSupportsApp( device, listing )
|
||||
|
||||
return {
|
||||
...listing,
|
||||
text: listingIsSupported ? `✅ Supported on ${ device.name }` : `🚫 Not yet reported working on ${ device.name }`,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
---
|
||||
<Layout
|
||||
headOptions={{
|
||||
title: `Apple Silicon Support for ${ device.name }`,
|
||||
description: `Check reported Apple Silicon Support status of apps on ${ device.name }. `,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
||||
// domain,
|
||||
pathname
|
||||
}}
|
||||
>
|
||||
|
||||
<section class="container py-24">
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
|
||||
<div class="hero">
|
||||
<h1 class="title text-3xl md:text-5xl font-hairline leading-tight text-center pb-4">
|
||||
Apple Silicon Support for { device.name }
|
||||
</h1>
|
||||
|
||||
<div
|
||||
class="subtitle md:text-xl text-center"
|
||||
>
|
||||
{ device.description }
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div
|
||||
class="subtitle md:text-xl text-center"
|
||||
>
|
||||
Supported apps include { appPage.summary.sampleNamesShort }.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ !!device.amazonUrl &&
|
||||
<div class="flex justify-center py-8">
|
||||
<LinkButton
|
||||
href={ device.amazonUrl }
|
||||
target="_blank"
|
||||
>
|
||||
Check Pricing
|
||||
</LinkButton>
|
||||
</div>
|
||||
}
|
||||
|
||||
<Search
|
||||
kind-page={ appPage }
|
||||
|
||||
client:load
|
||||
/>
|
||||
|
||||
<!-- <ListEndButtons query="query" /> -->
|
||||
|
||||
<!-- <AllUpdatesSubscribe
|
||||
class="my-12"
|
||||
/> -->
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</Layout>
|
||||
65
src/pages/devices.astro
Normal file
65
src/pages/devices.astro
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
// Component Script:
|
||||
// You can write any JavaScript/TypeScript that you'd like here.
|
||||
// It will run during the build, but never in the browser.
|
||||
// All variables are available to use in the HTML template below.
|
||||
|
||||
// Full Astro Component Syntax:
|
||||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
|
||||
import { DoesItAPI } from '~/helpers/api/client.js'
|
||||
|
||||
import Layout from '../layouts/default.astro'
|
||||
import SimpleList from '../components/simple-list.astro'
|
||||
|
||||
const deviceIndex = await DoesItAPI.kind.device(1).get()
|
||||
|
||||
const kinds = deviceIndex.items.map( device => {
|
||||
return {
|
||||
href: device.endpoint,
|
||||
heading: device.name,
|
||||
description: device.description,
|
||||
}
|
||||
})
|
||||
|
||||
---
|
||||
<Layout
|
||||
headOptions={ {
|
||||
title: 'List of Apple Devices for Apple Silicon App Support',
|
||||
description: `List of devices for Apple Silicon and the ${ global.$config.processorsVerbiage } Processors`,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
||||
// domain,
|
||||
pathname: '/devices',
|
||||
} }
|
||||
>
|
||||
|
||||
<main class="container py-24">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="title text-2xl leading-tight mb-6">
|
||||
Categories
|
||||
</h1>
|
||||
|
||||
<div class="line-separator border-white border-t-2 mb-12" />
|
||||
|
||||
<SimpleList
|
||||
items={ kinds }
|
||||
/>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!--
|
||||
|
||||
You can also use imported framework components directly in your markup!
|
||||
|
||||
Note: by default, these components are NOT interactive on the client.
|
||||
The `:visible` directive tells Astro to make it interactive.
|
||||
|
||||
See https://docs.astro.build/core-concepts/component-hydration/
|
||||
|
||||
-->
|
||||
|
||||
</Layout>
|
||||
92
src/pages/embed/rich-results-player.astro
Normal file
92
src/pages/embed/rich-results-player.astro
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
---
|
||||
import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
|
||||
|
||||
|
||||
import Layout from '~/src/layouts/embed.astro'
|
||||
import VideoPlayer from '~/components/video/player.vue'
|
||||
|
||||
|
||||
import '@fontsource/inter/variable.css'
|
||||
|
||||
// Component Script:
|
||||
// You can write any JavaScript/TypeScript that you'd like here.
|
||||
// It will run during the build, but never in the browser.
|
||||
// All variables are available to use in the HTML template below.
|
||||
|
||||
|
||||
// Full Astro Component Syntax:
|
||||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
|
||||
// Get type and slug from the request path
|
||||
// so that we don't have extra parts for
|
||||
// urls like /:type/:slug/benchmarks
|
||||
const {
|
||||
pathSlug,
|
||||
subSlug = null,
|
||||
params
|
||||
} = getPathPartsFromAstroRequest( Astro.request )
|
||||
|
||||
const {
|
||||
name,
|
||||
'youtube-id' : youtubeId
|
||||
} = params
|
||||
|
||||
const video = {
|
||||
name,
|
||||
id: youtubeId,
|
||||
timestamps: [],
|
||||
thumbnail: {
|
||||
sizes: '(max-width: 640px) 100vw, 640px',
|
||||
srcset: `https://i.ytimg.com/vi/${ youtubeId }/default.jpg 120w, https://i.ytimg.com/vi/${ youtubeId }/mqdefault.jpg 320w, https://i.ytimg.com/vi/${ youtubeId }/hqdefault.jpg 480w, https://i.ytimg.com/vi/${ youtubeId }/sddefault.jpg 640w`,
|
||||
src: `https://i.ytimg.com/vi/${ youtubeId }/default.jpg`
|
||||
},
|
||||
}
|
||||
|
||||
---
|
||||
<Layout
|
||||
headOptions={ {
|
||||
title: 'Video - Does It ARM',
|
||||
// description: ``,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
||||
// domain,
|
||||
pathname: '/',
|
||||
} }
|
||||
>
|
||||
|
||||
<style>
|
||||
/* Clear out background color */
|
||||
html {
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="embed-main text-gray-300">
|
||||
<VideoPlayer
|
||||
client:load
|
||||
|
||||
video={ video }
|
||||
class="w-100 h-100 absolute inset-0 flex justify-center items-center"
|
||||
>
|
||||
<div class="page-heading h-full flex items-end md:p-4">
|
||||
<h1 class="title text-xs text-left md:text-2xl font-bold">
|
||||
{ video.name }
|
||||
</h1>
|
||||
</div>
|
||||
</VideoPlayer>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
|
||||
You can also use imported framework components directly in your markup!
|
||||
|
||||
Note: by default, these components are NOT interactive on the client.
|
||||
The `:visible` directive tells Astro to make it interactive.
|
||||
|
||||
See https://docs.astro.build/core-concepts/component-hydration/
|
||||
|
||||
-->
|
||||
|
||||
</Layout>
|
||||
49
src/pages/formula/[...formulaPath].astro
Normal file
49
src/pages/formula/[...formulaPath].astro
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
// Full Astro Component Syntax:
|
||||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
|
||||
import { DoesItAPI } from '~/helpers/api/client.js'
|
||||
import { catchRedirectResponse } from '~/helpers/astro/request.js'
|
||||
import {
|
||||
ListingDetails
|
||||
} from '~/helpers/listing-page.js'
|
||||
import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
|
||||
|
||||
import Layout from '~/src/layouts/default.astro'
|
||||
import Listing from '~/src/components/default-listing.astro'
|
||||
|
||||
|
||||
const redirectResponse = await catchRedirectResponse( Astro )
|
||||
|
||||
if ( redirectResponse !== null ) {
|
||||
return redirectResponse
|
||||
}
|
||||
|
||||
|
||||
// Get type and slug from the request path
|
||||
// so that we don't have extra parts for
|
||||
// urls like /:type/:slug/benchmarks
|
||||
const {
|
||||
pathSlug,
|
||||
subSlug = null
|
||||
} = getPathPartsFromAstroRequest( Astro.request )
|
||||
|
||||
|
||||
// Astro Request reference
|
||||
// https://docs.astro.build/en/reference/api-reference/#astrorequests
|
||||
|
||||
// Request App data from API
|
||||
const appListing = await DoesItAPI.formula( pathSlug ).get()
|
||||
|
||||
const listingDetails = new ListingDetails( appListing )
|
||||
|
||||
const headOptions = listingDetails.headOptions
|
||||
|
||||
---
|
||||
<Layout
|
||||
headOptions={ headOptions }
|
||||
>
|
||||
<Listing
|
||||
listing={ appListing }
|
||||
/>
|
||||
</Layout>
|
||||
76
src/pages/game/[...gamePath].astro
Normal file
76
src/pages/game/[...gamePath].astro
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
// Full Astro Component Syntax:
|
||||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
|
||||
import { DoesItAPI } from '~/helpers/api/client.js'
|
||||
import { catchRedirectResponse } from '~/helpers/astro/request.js'
|
||||
import {
|
||||
getVideoImages,
|
||||
ListingDetails
|
||||
} from '~/helpers/listing-page.js'
|
||||
import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
|
||||
|
||||
import Layout from '~/src/layouts/default.astro'
|
||||
import Listing from '~/src/components/default-listing.astro'
|
||||
import VideoListing from '~/src/components/video-listing.astro'
|
||||
|
||||
|
||||
const redirectResponse = await catchRedirectResponse( Astro )
|
||||
|
||||
if ( redirectResponse !== null ) {
|
||||
return redirectResponse
|
||||
}
|
||||
|
||||
|
||||
// Get type and slug from the request path
|
||||
// so that we don't have extra parts for
|
||||
// urls like /:type/:slug/benchmarks
|
||||
const {
|
||||
pathSlug,
|
||||
subSlug = null
|
||||
} = getPathPartsFromAstroRequest( Astro.request )
|
||||
|
||||
const isBenchmarkPage = subSlug === 'benchmarks'
|
||||
|
||||
|
||||
// Astro Request reference
|
||||
// https://docs.astro.build/en/reference/api-reference/#astrorequests
|
||||
|
||||
// Request App data from API
|
||||
const appListing = await DoesItAPI.game( pathSlug ).get()
|
||||
|
||||
const listingDetails = new ListingDetails( appListing )
|
||||
|
||||
const headOptions = listingDetails.headOptions
|
||||
|
||||
|
||||
if ( isBenchmarkPage ) {
|
||||
|
||||
// Set the page title
|
||||
headOptions.title = `${ listingDetails.api.name } Benchmarks for Apple Silicon - Does It ARM`
|
||||
|
||||
const { preloads } = getVideoImages( listingDetails.initialVideo )
|
||||
|
||||
// Add image preloads for video thumbnail
|
||||
headOptions.link = [
|
||||
...headOptions.link,
|
||||
...preloads
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
---
|
||||
<Layout
|
||||
headOptions={ headOptions }
|
||||
>
|
||||
{ isBenchmarkPage ? (
|
||||
<VideoListing
|
||||
listing={ appListing }
|
||||
/>
|
||||
) : (
|
||||
<Listing
|
||||
listing={ appListing }
|
||||
/>
|
||||
)}
|
||||
|
||||
</Layout>
|
||||
104
src/pages/games.astro
Normal file
104
src/pages/games.astro
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
---
|
||||
// Full Astro Component Syntax:
|
||||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
|
||||
|
||||
import { DoesItAPI } from '~/helpers/api/client.js'
|
||||
import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
|
||||
import { catchRedirectResponse } from '~/helpers/astro/request.js'
|
||||
import {
|
||||
categories,
|
||||
makeCategoryFilterFromCategorySlug
|
||||
} from '~/helpers/categories.js'
|
||||
|
||||
import Layout from '~/src/layouts/default.astro'
|
||||
import Search from '~/components/search-stork.vue'
|
||||
import ThomasCredit from '~/components/thomas-credit.vue'
|
||||
|
||||
|
||||
// Get type and slug from the request path
|
||||
// so that we don't have extra parts for
|
||||
// urls like /:type/:slug/benchmarks
|
||||
const {
|
||||
pathname,
|
||||
// pathSlug,
|
||||
// subSlug = 1
|
||||
} = getPathPartsFromAstroRequest( Astro.request )
|
||||
|
||||
|
||||
const redirectResponse = await catchRedirectResponse( Astro )
|
||||
|
||||
if ( redirectResponse !== null ) {
|
||||
return redirectResponse
|
||||
}
|
||||
|
||||
const rawKindPage = await DoesItAPI.kind( 'game' )( 1 ).get()
|
||||
|
||||
// Clean up unused kind data
|
||||
const kindPage = {
|
||||
...rawKindPage,
|
||||
items: rawKindPage.items.map( item => ({
|
||||
...item,
|
||||
bundles: undefined,
|
||||
}) )
|
||||
}
|
||||
|
||||
const baseFilters = []
|
||||
|
||||
const categorySlug = 'games'
|
||||
|
||||
const category = categories[ categorySlug ]
|
||||
|
||||
baseFilters.push( makeCategoryFilterFromCategorySlug( categorySlug ) )
|
||||
|
||||
const pageLabel = category?.pluralLabel || category.label
|
||||
|
||||
---
|
||||
<Layout
|
||||
headOptions={{
|
||||
title: `List of ${ pageLabel } that work on Apple Silicon?`,
|
||||
description: `Check the latest reported support status of ${ pageLabel } on Apple Silicon and ${ global.$config.processorsVerbiage } Processors. `,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
||||
// domain,
|
||||
pathname
|
||||
}}
|
||||
>
|
||||
|
||||
<section class="container py-24">
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
|
||||
<div class="hero">
|
||||
<h1 class="title text-3xl md:text-5xl font-hairline leading-tight text-center pb-4">
|
||||
{ pageLabel } that are reported to support Apple Silicon
|
||||
</h1>
|
||||
|
||||
<h2
|
||||
v-if="supportedAppList.length !== 0"
|
||||
class="subtitle md:text-xl text-center"
|
||||
>
|
||||
Supported apps include { kindPage.summary.sampleNamesShort }.
|
||||
</h2>
|
||||
|
||||
<ThomasCredit />
|
||||
</div>
|
||||
|
||||
<Search
|
||||
kind-page={ kindPage }
|
||||
base-filters={ baseFilters }
|
||||
|
||||
client:load
|
||||
/>
|
||||
|
||||
<!-- <ListEndButtons query="query" /> -->
|
||||
|
||||
<!-- <AllUpdatesSubscribe
|
||||
class="my-12"
|
||||
/> -->
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</Layout>
|
||||
72
src/pages/index.astro
Normal file
72
src/pages/index.astro
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
// Component Script:
|
||||
// You can write any JavaScript/TypeScript that you'd like here.
|
||||
// It will run during the build, but never in the browser.
|
||||
// All variables are available to use in the HTML template below.
|
||||
|
||||
// Full Astro Component Syntax:
|
||||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
|
||||
import { DoesItAPI } from '~/helpers/api/client.js'
|
||||
|
||||
import Layout from '../layouts/default.astro'
|
||||
import Search from '~/components/search-stork.vue'
|
||||
// import ListSummary from '~/components/list-summary.vue'
|
||||
import ListEndButtons from '~/components/list-end-buttons.vue'
|
||||
import AllUpdatesSubscribe from '~/components/all-updates-subscribe.vue'
|
||||
|
||||
const homePageKindPage = await DoesItAPI.kind.app(1).get()
|
||||
const allAppsSummary = await DoesItAPI('all-apps-summary').get()
|
||||
|
||||
// console.log( allAppsSummary )
|
||||
---
|
||||
<Layout
|
||||
headOptions={ {
|
||||
title: `Apple Silicon and ${ global.$config.processorsVerbiage } app and game compatibility list`,
|
||||
description: `List of compatibility apps and games for Apple Silicon and the ${ global.$config.processorsVerbiage } Processors including performance reports and benchmarks`,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
||||
// domain,
|
||||
pathname: '/',
|
||||
} }
|
||||
>
|
||||
|
||||
<section class="container py-24">
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<div class="hero">
|
||||
<h1 class="title text-3xl md:text-6xl font-hairline leading-tight text-center">
|
||||
Does It ARM?
|
||||
</h1>
|
||||
<h2 class="subtitle md:text-xl text-center">
|
||||
Apps that are reported to support Apple Silicon
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Search
|
||||
kind-page={ homePageKindPage }
|
||||
list-summary={ allAppsSummary }
|
||||
|
||||
client:load
|
||||
/>
|
||||
|
||||
<AllUpdatesSubscribe
|
||||
class="my-12"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
|
||||
You can also use imported framework components directly in your markup!
|
||||
|
||||
Note: by default, these components are NOT interactive on the client.
|
||||
The `:visible` directive tells Astro to make it interactive.
|
||||
|
||||
See https://docs.astro.build/core-concepts/component-hydration/
|
||||
|
||||
-->
|
||||
|
||||
</Layout>
|
||||
112
src/pages/kind/[...kindPath].astro
Normal file
112
src/pages/kind/[...kindPath].astro
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
---
|
||||
// Full Astro Component Syntax:
|
||||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
|
||||
|
||||
import { DoesItAPI } from '~/helpers/api/client.js'
|
||||
import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
|
||||
import { catchRedirectResponse } from '~/helpers/astro/request.js'
|
||||
import {
|
||||
categories,
|
||||
getKindToCategorySlug,
|
||||
getCategoryKindName,
|
||||
makeCategoryFilterFromCategorySlug
|
||||
} from '~/helpers/categories.js'
|
||||
|
||||
import Layout from '../../layouts/default.astro'
|
||||
import Search from '~/components/search-stork.vue'
|
||||
|
||||
|
||||
// Get type and slug from the request path
|
||||
// so that we don't have extra parts for
|
||||
// urls like /:type/:slug/benchmarks
|
||||
const {
|
||||
pathname,
|
||||
pathSlug,
|
||||
subSlug = 1
|
||||
} = getPathPartsFromAstroRequest( Astro.request )
|
||||
|
||||
|
||||
const redirectResponse = await catchRedirectResponse( Astro )
|
||||
|
||||
if ( redirectResponse !== null ) {
|
||||
return redirectResponse
|
||||
}
|
||||
|
||||
|
||||
// Try the pathSlug against categories
|
||||
// so we can load from category slugs
|
||||
const kindName = getCategoryKindName( pathSlug ) ? getCategoryKindName( pathSlug ) : pathSlug
|
||||
|
||||
const rawKindPage = await DoesItAPI.kind( kindName )( subSlug ).get()
|
||||
|
||||
// Clean up unused kind data
|
||||
const kindPage = {
|
||||
...rawKindPage,
|
||||
items: rawKindPage.items.map( item => ({
|
||||
...item,
|
||||
bundles: undefined,
|
||||
}) )
|
||||
}
|
||||
|
||||
let pageLabel = 'Mac Apps'
|
||||
const baseFilters = []
|
||||
|
||||
const categorySlug = getKindToCategorySlug( kindName )
|
||||
|
||||
// If we have a category slug, add a filter
|
||||
if ( !!categorySlug ) {
|
||||
const category = categories[ categorySlug ]
|
||||
|
||||
baseFilters.push( makeCategoryFilterFromCategorySlug( categorySlug ) )
|
||||
|
||||
pageLabel = category?.pluralLabel || category.label
|
||||
}
|
||||
|
||||
---
|
||||
<Layout
|
||||
headOptions={{
|
||||
title: `List of ${ pageLabel } that work on Apple Silicon?`,
|
||||
description: `Check the latest reported support status of ${ pageLabel } on Apple Silicon and ${ global.$config.processorsVerbiage } Processors. `,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
||||
// domain,
|
||||
pathname
|
||||
}}
|
||||
>
|
||||
|
||||
<section class="container py-24">
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
|
||||
<div class="hero">
|
||||
<h1 class="title text-3xl md:text-5xl font-hairline leading-tight text-center pb-4">
|
||||
{ pageLabel } that are reported to support Apple Silicon
|
||||
</h1>
|
||||
|
||||
<h2
|
||||
v-if="supportedAppList.length !== 0"
|
||||
class="subtitle md:text-xl text-center"
|
||||
>
|
||||
Supported apps include { kindPage.summary.sampleNamesShort }.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Search
|
||||
kind-page={ kindPage }
|
||||
base-filters={ baseFilters }
|
||||
|
||||
client:load
|
||||
/>
|
||||
|
||||
<!-- <ListEndButtons query="query" /> -->
|
||||
|
||||
<!-- <AllUpdatesSubscribe
|
||||
class="my-12"
|
||||
/> -->
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</Layout>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue