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.js build output
|
||||||
.nuxt
|
.nuxt
|
||||||
|
|
||||||
# Nuxt generate
|
|
||||||
dist
|
|
||||||
|
|
||||||
# vuepress build output
|
# vuepress build output
|
||||||
.vuepress/dist
|
.vuepress/dist
|
||||||
|
|
||||||
|
|
@ -80,12 +77,23 @@ dist
|
||||||
# IDE
|
# IDE
|
||||||
.idea
|
.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
|
# Other
|
||||||
/static/app-list.json
|
|
||||||
/README-temp.md
|
/README-temp.md
|
||||||
/static/**/*.json
|
|
||||||
/commits-data.json
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/static/tailwind.css
|
|
||||||
/.vscode/snipsnap.code-snippets
|
/.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";
|
* @import "utilities/skew-transforms";
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.container {
|
/* .container {
|
||||||
max-width: 1040px;
|
max-width: 1040px;
|
||||||
}
|
} */
|
||||||
|
|
||||||
.ease {
|
.ease {
|
||||||
transition-property: all;
|
transition-property: all;
|
||||||
|
|
@ -153,19 +153,79 @@ html {
|
||||||
.shimmer {
|
.shimmer {
|
||||||
animation: placeHolderShimmer 1s infinite;
|
animation: placeHolderShimmer 1s infinite;
|
||||||
animation-timing-function: linear;
|
animation-timing-function: linear;
|
||||||
background: #f6f7f8;
|
background-color: rgba( 0, 0, 0, 0.1 );
|
||||||
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-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-size: 200% 100px;
|
||||||
|
background-position: 0 0, 100% 0;
|
||||||
background-attachment: fixed;
|
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 {
|
@keyframes placeHolderShimmer {
|
||||||
0% {
|
0% {
|
||||||
background-position: 100% 0;
|
background-position: 100% 0
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
background-position: -100% 0;
|
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 fs from 'fs-extra'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
|
import semver from 'semver'
|
||||||
import { PromisePool } from '@supercharge/promise-pool'
|
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 { saveYouTubeVideos } from '~/helpers/api/youtube/build.js'
|
||||||
import buildGamesList from './helpers/build-game-list.js'
|
import buildAppList from '~/helpers/build-app-list.js'
|
||||||
import buildHomebrewList from './helpers/build-homebrew-list.js'
|
import buildGamesList from '~/helpers/build-game-list.js'
|
||||||
import buildVideoList from './helpers/build-video-list.js'
|
import buildHomebrewList from '~/helpers/build-homebrew-list.js'
|
||||||
import buildDeviceList from './helpers/build-device-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 {
|
||||||
import { buildVideoPayload, buildAppBenchmarkPayload } from './helpers/build-payload.js'
|
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 {
|
import {
|
||||||
getAppType,
|
getAppType,
|
||||||
getAppEndpoint,
|
getAppEndpoint,
|
||||||
getVideoEndpoint,
|
getVideoEndpoint,
|
||||||
isVideo
|
isVideo
|
||||||
} from './helpers/app-derived.js'
|
} from '~/helpers/app-derived.js'
|
||||||
import { makeSearchableList } from './helpers/searchable-list.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
|
// Setup dotenv
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
const commandArguments = process.argv
|
|
||||||
const cliOptions = {
|
let timeRunGetListArray = 0
|
||||||
withApi: commandArguments.includes('--with-api'),
|
let timeRunGetListByCategories = 0
|
||||||
noLists: commandArguments.includes('--no-lists'),
|
|
||||||
|
|
||||||
|
|
||||||
|
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 {
|
class BuildLists {
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
|
|
@ -59,57 +111,39 @@ class BuildLists {
|
||||||
// Main build methods
|
// Main build methods
|
||||||
{
|
{
|
||||||
name: 'app',
|
name: 'app',
|
||||||
|
endpointPrefix: 'app',
|
||||||
path: '/static/app-list.json',
|
path: '/static/app-list.json',
|
||||||
buildMethod: buildAppList,
|
buildMethod: buildAppList,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'game',
|
name: 'game',
|
||||||
|
endpointPrefix: 'game',
|
||||||
path: '/static/game-list.json',
|
path: '/static/game-list.json',
|
||||||
buildMethod: buildGamesList,
|
buildMethod: buildGamesList,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'homebrew',
|
name: 'homebrew',
|
||||||
|
endpointPrefix: 'formula',
|
||||||
path: '/static/homebrew-list.json',
|
path: '/static/homebrew-list.json',
|
||||||
buildMethod: buildHomebrewList,
|
buildMethod: buildHomebrewList,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'device',
|
name: 'device',
|
||||||
|
endpointPrefix: 'device',
|
||||||
path: '/static/device-list.json',
|
path: '/static/device-list.json',
|
||||||
buildMethod: buildDeviceList,
|
buildMethod: buildDeviceList,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Secondary Derivative built lists
|
// Secondary Derivative built lists
|
||||||
// Always goes after initial lists
|
// Always goes after initial lists
|
||||||
// since it depend on them
|
// since it depends on them
|
||||||
{
|
{
|
||||||
name: 'video',
|
name: 'video',
|
||||||
|
endpointPrefix: 'tv',
|
||||||
path: '/static/video-list.json',
|
path: '/static/video-list.json',
|
||||||
buildMethod: async () => {
|
buildMethod: async () => {
|
||||||
|
|
||||||
// console.log('this.getAllVideoAppsList()', this.getAllVideoAppsList())
|
|
||||||
|
|
||||||
return await buildVideoList( 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 => {
|
beforeSave: videoListSet => {
|
||||||
this.allVideoAppsList = this.getAllVideoAppsList()
|
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 = () => {
|
getAllVideoAppsList = () => {
|
||||||
return new Set([
|
return new Set([
|
||||||
...this.lists.app,
|
...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 ) {
|
saveToJson = async function ( content, path ) {
|
||||||
|
|
||||||
// Write the list to JSON
|
// Write the list to JSON
|
||||||
|
|
@ -149,24 +268,21 @@ class BuildLists {
|
||||||
const hasSaveMethod = listOptions.hasOwnProperty('beforeSave')
|
const hasSaveMethod = listOptions.hasOwnProperty('beforeSave')
|
||||||
const saveMethod = hasSaveMethod ? listOptions.beforeSave : listSet => Array.from( listSet )
|
const saveMethod = hasSaveMethod ? listOptions.beforeSave : listSet => Array.from( listSet )
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// console.log('listFullPath', listFullPath)
|
// console.log('listFullPath', listFullPath)
|
||||||
|
|
||||||
const saveableList = saveMethod( this.lists[listOptions.name] )
|
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
|
// 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
|
return
|
||||||
const savedListJSON = await fs.readFile(listFullPath, 'utf-8')
|
|
||||||
|
|
||||||
// console.log('savedListJSON', savedListJSON)
|
|
||||||
|
|
||||||
const savedList = JSON.parse(savedListJSON)
|
|
||||||
|
|
||||||
// Import the created JSON File
|
|
||||||
return savedList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run all listsOprions methods
|
// Run all listsOprions methods
|
||||||
|
|
@ -194,11 +310,152 @@ class BuildLists {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
saveApiEndpoints = async function ( listOptions ) {
|
getListArray ( listName ) {
|
||||||
|
console.log(`getListArray run ${ timeRunGetListArray += 1 } times`)
|
||||||
|
|
||||||
await PromisePool
|
return Array.from( this.lists[ listName ] )
|
||||||
.withConcurrency(1000)
|
}
|
||||||
.for( Array.from( this.lists[listOptions.name] ) )
|
|
||||||
|
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 ) => {
|
.process(async ( listEntry, index, pool ) => {
|
||||||
// console.log('listEntry', listEntry)
|
// console.log('listEntry', listEntry)
|
||||||
|
|
||||||
|
|
@ -211,31 +468,71 @@ class BuildLists {
|
||||||
|
|
||||||
} = listEntry
|
} = listEntry
|
||||||
|
|
||||||
const endpointPath = `./static/api${endpoint}.json`
|
const endpointPath = `${ apiDirectory }${ endpoint }.json`
|
||||||
const endpointDirectory = dirname(endpointPath)
|
const endpointDirectory = dirname(endpointPath)
|
||||||
|
|
||||||
// Stop if the endpoint is already exists
|
// Add related videos
|
||||||
if (fs.existsSync(endpointPath)) {
|
if ( this.shouldHaveRelatedVideos( listEntry ) ) {
|
||||||
console.log(`Path "${endpointPath}" already exists`)
|
listEntry.relatedVideos = getRelatedVideos({
|
||||||
|
listing: listEntry,
|
||||||
return
|
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
|
// Ensure the directory exists
|
||||||
await fs.ensureDir( endpointDirectory )
|
await fs.ensureDir( endpointDirectory )
|
||||||
|
|
||||||
// Write the endpoint to JSON
|
// 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
|
// Count saved files
|
||||||
// for ( const listEntry of this.lists[listOptions.name] ) {
|
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
|
// Save app lists to JSON
|
||||||
|
|
@ -266,33 +563,53 @@ class BuildLists {
|
||||||
console.timeEnd(methodName)
|
console.timeEnd(methodName)
|
||||||
|
|
||||||
if ( cliOptions.withApi ) {
|
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)
|
console.time(endpointMethodName)
|
||||||
|
|
||||||
await this.saveApiEndpoints( listOptions )
|
await this.saveApiEndpoints( listOptions )
|
||||||
|
|
||||||
console.timeEnd(endpointMethodName)
|
console.timeEnd(endpointMethodName)
|
||||||
|
console.log( '\n\n' )
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
console.log('Save lists finished')
|
console.log('Save lists finished')
|
||||||
|
|
||||||
return
|
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 () {
|
async build () {
|
||||||
|
|
||||||
|
await saveYouTubeVideos()
|
||||||
|
|
||||||
|
// Pull in and layer data from all sources
|
||||||
await this.buildLists()
|
await this.buildLists()
|
||||||
|
|
||||||
|
// Save the data to respective files as lists
|
||||||
await this.saveAppLists()
|
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
|
// Add list based routes
|
||||||
for ( const listKey in this.lists ) {
|
for ( const listKey in this.lists ) {
|
||||||
|
|
@ -331,14 +648,12 @@ class BuildLists {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add standard app endpoint
|
// Add standard app endpoint
|
||||||
if ( appType === 'app' || appType === 'formula' ) {
|
if ( this.shouldHaveRelatedVideos( app ) ) {
|
||||||
|
|
||||||
const relatedVideos = videosRelatedToApp( app, this.lists.video ).map(video => {
|
const relatedVideos = getRelatedVideos({
|
||||||
// console.log('video', video)
|
listing: app,
|
||||||
return {
|
videoListSet: this.lists.video,
|
||||||
...video,
|
appListSet: this.allVideoAppsList
|
||||||
endpoint: `${getAppEndpoint(app)}/benchmarks#${video.id}`
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add app or formula endpoint
|
// Add app or formula endpoint
|
||||||
|
|
@ -373,33 +688,45 @@ class BuildLists {
|
||||||
this.endpointMaps.nuxt.set( '/kind/' + slug, {} )
|
this.endpointMaps.nuxt.set( '/kind/' + slug, {} )
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for ( const [ endpointSetName, endpointSet ] of Object.entries(this.endpointMaps) ) {
|
||||||
// Save Nuxt Endpoints
|
// Save Endpoints
|
||||||
// await this.saveToJson(Array.from(this.endpointMaps.nuxt), './static/nuxt-endpoints.json')
|
await this.saveToJson(Array.from( endpointSet , ([route, payload]) => ({ route, payload })), `./static/${endpointSetName}-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')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 Nuxt Endpoints', this.endpointMaps.nuxt.size )
|
||||||
console.log('Total Eleventy Endpoints', this.endpointMaps.eleventy.size )
|
console.log('Total Eleventy Endpoints', this.endpointMaps.eleventy.size )
|
||||||
console.log('Total Endpoints', this.endpointMaps.nuxt.size + 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()
|
const listBuilder = new BuildLists()
|
||||||
|
|
||||||
listBuilder.build()
|
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>
|
<script>
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
|
import { isNuxt } from '~/helpers/environment.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -78,6 +81,10 @@ export default {
|
||||||
inputClassGroups: {
|
inputClassGroups: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
|
},
|
||||||
|
uuid: {
|
||||||
|
type: String,
|
||||||
|
default: uuid()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
|
|
@ -90,7 +97,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
inputId () {
|
inputId () {
|
||||||
return `all-updates-subscribe-${this._uid}`
|
return `all-updates-subscribe-${ this.uuid }`
|
||||||
},
|
},
|
||||||
inputClasslist () {
|
inputClasslist () {
|
||||||
const defaultClassGroups = {
|
const defaultClassGroups = {
|
||||||
|
|
@ -115,6 +122,7 @@ export default {
|
||||||
return Object.values(mergedClassGroups)
|
return Object.values(mergedClassGroups)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async trySubmit () {
|
async trySubmit () {
|
||||||
console.log('Trying submit')
|
console.log('Trying submit')
|
||||||
|
|
@ -124,10 +132,15 @@ export default {
|
||||||
|
|
||||||
// const pagePath = $nuxt.$route.path
|
// 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
|
// 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({
|
axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="doesitarm-carbon-wrapper">
|
<div class="doesitarm-carbon-wrapper">
|
||||||
<component
|
<component
|
||||||
|
:is="'script'"
|
||||||
v-once
|
v-once
|
||||||
id="_carbonads_js"
|
id="_carbonads_js"
|
||||||
|
|
||||||
:is="'script'"
|
|
||||||
src="https://cdn.carbonads.com/carbon.js?serve=CK7DVK3M&placement=doesitarmcom"
|
src="https://cdn.carbonads.com/carbon.js?serve=CK7DVK3M&placement=doesitarmcom"
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
async
|
async
|
||||||
class="include-on-static"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export default {
|
||||||
links () {
|
links () {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Scan Your Own App',
|
label: 'Scan Apps',
|
||||||
href: '/apple-silicon-app-test/'
|
href: '/apple-silicon-app-test/'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
:href="item.url"
|
:href="item.url"
|
||||||
:class="[
|
: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',
|
'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 }}
|
{{ item.label }}
|
||||||
|
|
@ -109,7 +109,7 @@
|
||||||
:href="item.url"
|
:href="item.url"
|
||||||
:class="[
|
: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',
|
'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 }}
|
{{ item.label }}
|
||||||
|
|
@ -141,7 +141,7 @@
|
||||||
<a
|
<a
|
||||||
:class="[
|
: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',
|
'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"
|
href="https://prf.hn/l/7JG0bEj"
|
||||||
>
|
>
|
||||||
|
|
@ -162,6 +162,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// https://anguscroll.com/just/just-has
|
||||||
|
import has from 'just-has'
|
||||||
|
|
||||||
import LinkButton from '~/components/link-button.vue'
|
import LinkButton from '~/components/link-button.vue'
|
||||||
|
|
||||||
export default {
|
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 () {
|
// data: function () {
|
||||||
// return {
|
// return {
|
||||||
// // isOpen: false
|
// // 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>
|
<template>
|
||||||
|
<div
|
||||||
<small class="data-credit text-sm opacity-75 text-center mb-4">
|
class="data-credit flex gap-1 justify-center text-xs opacity-75 text-center mb-4"
|
||||||
<span>Data generously provided by </span>
|
>
|
||||||
|
<span>Includes data generously provided by </span>
|
||||||
<span>
|
<span>
|
||||||
<a
|
<a
|
||||||
href="https://twitter.com/__tosh"
|
href="https://twitter.com/__tosh"
|
||||||
|
|
@ -15,6 +16,5 @@
|
||||||
class="font-bold"
|
class="font-bold"
|
||||||
>Apple Silicon Games</a>
|
>Apple Silicon Games</a>
|
||||||
</span>
|
</span>
|
||||||
</small>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,10 @@
|
||||||
|
|
||||||
<div class="cover-bottom h-full">
|
<div class="cover-bottom h-full">
|
||||||
|
|
||||||
|
<slot>
|
||||||
|
<!-- Default slot -->
|
||||||
|
</slot>
|
||||||
|
|
||||||
<slot name="cover-bottom">
|
<slot name="cover-bottom">
|
||||||
<!-- Bottom -->
|
<!-- Bottom -->
|
||||||
</slot>
|
</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
|
// App Data that is derived from other app data
|
||||||
|
|
||||||
|
import {
|
||||||
|
categories,
|
||||||
|
categoryTemplate
|
||||||
|
} from '~/helpers/categories.js'
|
||||||
|
|
||||||
export function isDevice ( listing ) {
|
export function isDevice ( listing ) {
|
||||||
if ( !listing.hasOwnProperty('endpoint') ) return false
|
if ( !listing.hasOwnProperty('endpoint') ) return false
|
||||||
|
|
||||||
|
|
@ -57,9 +62,32 @@ export function getVideoEndpoint ( video ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRouteType ( routeString ) {
|
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
|
// Remove first slash and split by remaining
|
||||||
// slashes to get first part of route
|
// 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
|
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 { isString } from './check-types.js'
|
||||||
import parseMacho from './macho/index.js'
|
import parseMacho from './macho/index.js'
|
||||||
|
import prettyBytes from 'pretty-bytes'
|
||||||
const prettyBytes = require('pretty-bytes')
|
|
||||||
|
|
||||||
|
|
||||||
const knownArchiveExtensions = new Set([
|
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 {
|
export default class AppFilesScanner {
|
||||||
|
|
||||||
constructor( {
|
constructor( {
|
||||||
observableFilesArray,
|
observableFilesArray,
|
||||||
testResultStore
|
testResultStore,
|
||||||
|
zipModule = null
|
||||||
} ) {
|
} ) {
|
||||||
// Files to process
|
// Files to process
|
||||||
this.files = observableFilesArray
|
this.files = observableFilesArray
|
||||||
|
|
||||||
this.testResultStore = testResultStore
|
this.testResultStore = testResultStore
|
||||||
|
|
||||||
// https://gildas-lormeau.github.io/zip.js/
|
this.zipModule = zipModule
|
||||||
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"]
|
|
||||||
// }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get zip () {
|
||||||
|
|
||||||
|
if ( this.zipModule ) return this.zipModule
|
||||||
|
|
||||||
|
return zip
|
||||||
|
}
|
||||||
|
|
||||||
isApp ( file ) {
|
isApp ( file ) {
|
||||||
|
|
||||||
|
|
@ -116,7 +139,9 @@ export default class AppFilesScanner {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
async unzipFile ( file ) {
|
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() {
|
fileReader.onload = function() {
|
||||||
|
|
||||||
|
|
@ -146,7 +171,7 @@ export default class AppFilesScanner {
|
||||||
// console.log('fileReader', fileReader)
|
// console.log('fileReader', fileReader)
|
||||||
|
|
||||||
// https://gildas-lormeau.github.io/zip.js/core-api.html#zip-reading
|
// 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
|
// zipReader.onprogress = console.log
|
||||||
|
|
||||||
|
|
@ -404,7 +429,7 @@ export default class AppFilesScanner {
|
||||||
const infoXml = await rootInfoEntry.getData(
|
const infoXml = await rootInfoEntry.getData(
|
||||||
// writer
|
// writer
|
||||||
// https://gildas-lormeau.github.io/zip.js/core-api.html#zip-writing
|
// https://gildas-lormeau.github.io/zip.js/core-api.html#zip-writing
|
||||||
new zip.TextWriter(),
|
new this.zip.TextWriter(),
|
||||||
// options
|
// options
|
||||||
{
|
{
|
||||||
useWebWorkers: true,
|
useWebWorkers: true,
|
||||||
|
|
@ -478,7 +503,7 @@ export default class AppFilesScanner {
|
||||||
const bundleExecutableBlob = await bundleExecutable.getData(
|
const bundleExecutableBlob = await bundleExecutable.getData(
|
||||||
// writer
|
// writer
|
||||||
// https://gildas-lormeau.github.io/zip.js/core-api.html#zip-writing
|
// https://gildas-lormeau.github.io/zip.js/core-api.html#zip-writing
|
||||||
new zip.BlobWriter(),
|
new this.zip.BlobWriter(),
|
||||||
// options
|
// options
|
||||||
{
|
{
|
||||||
useWebWorkers: true
|
useWebWorkers: true
|
||||||
|
|
@ -595,4 +620,32 @@ export default class AppFilesScanner {
|
||||||
return
|
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 fs from 'fs-extra'
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
@ -12,7 +9,11 @@ import parseDate from './parse-date'
|
||||||
import { eitherMatches } from './matching.js'
|
import { eitherMatches } from './matching.js'
|
||||||
import { getAppEndpoint } from './app-derived'
|
import { getAppEndpoint } from './app-derived'
|
||||||
import { makeSlug } from './slug.js'
|
import { makeSlug } from './slug.js'
|
||||||
|
import { byTimeThenNull } from './sort-list.js'
|
||||||
|
|
||||||
|
import {
|
||||||
|
cliOptions
|
||||||
|
} from '~/helpers/cli-options.js'
|
||||||
|
|
||||||
const md = new MarkdownIt()
|
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 )
|
scanListMap.delete( key )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -435,16 +438,8 @@ export default async function () {
|
||||||
// console.log('appList', appList)
|
// console.log('appList', appList)
|
||||||
|
|
||||||
|
|
||||||
return [
|
return ([
|
||||||
...appList,
|
...appList,
|
||||||
...Array.from( scanListMap, ([name, value]) => value )
|
...Array.from( scanListMap, ([name, value]) => value )
|
||||||
]
|
]).sort( byTimeThenNull )
|
||||||
|
|
||||||
// fs.readFile('../README.md', 'utf8')
|
|
||||||
// .then((err, data) => {
|
|
||||||
// const result = md.parse(data)
|
|
||||||
// console.log('result', result)
|
|
||||||
|
|
||||||
// return result
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
|
import fs from 'fs-extra'
|
||||||
import axios from 'axios'
|
|
||||||
import { PromisePool } from '@supercharge/promise-pool'
|
import { PromisePool } from '@supercharge/promise-pool'
|
||||||
|
|
||||||
import { fuzzyMatchesWholeWord } from './matching.js'
|
import { fuzzyMatchesWholeWord } from './matching.js'
|
||||||
|
|
@ -7,6 +6,7 @@ import { byTimeThenNull } from './sort-list.js'
|
||||||
import { getVideoEndpoint } from './app-derived.js'
|
import { getVideoEndpoint } from './app-derived.js'
|
||||||
import parseDate from './parse-date'
|
import parseDate from './parse-date'
|
||||||
import { makeSlug } from './slug.js'
|
import { makeSlug } from './slug.js'
|
||||||
|
import { youtubeVideoPath } from '~/helpers/api/youtube/build.js'
|
||||||
|
|
||||||
|
|
||||||
const inTimestamps = ( name, video ) => {
|
const inTimestamps = ( name, video ) => {
|
||||||
|
|
@ -140,13 +140,16 @@ async function handleFetchedVideo ( fetchedVideo, videoId, applist ) {
|
||||||
// Build video slug
|
// Build video slug
|
||||||
const slug = makeSlug( `${fetchedVideo.title}-i-${videoId}` )
|
const slug = makeSlug( `${fetchedVideo.title}-i-${videoId}` )
|
||||||
|
|
||||||
const apps = []
|
const appLinks = []
|
||||||
// Generate new tag set based on api data
|
// Generate new tag set based on api data
|
||||||
const tags = generateVideoTags(fetchedVideo)
|
const tags = generateVideoTags(fetchedVideo)
|
||||||
|
|
||||||
for ( const app of applist ) {
|
for ( const app of applist ) {
|
||||||
if (videoFeaturesApp(app, fetchedVideo)) {
|
if (videoFeaturesApp(app, fetchedVideo)) {
|
||||||
apps.push(app.slug)
|
appLinks.push({
|
||||||
|
name: app.name,
|
||||||
|
endpoint: app.endpoint
|
||||||
|
})
|
||||||
|
|
||||||
tags.add(app.category.slug)
|
tags.add(app.category.slug)
|
||||||
}
|
}
|
||||||
|
|
@ -165,7 +168,7 @@ async function handleFetchedVideo ( fetchedVideo, videoId, applist ) {
|
||||||
name: fetchedVideo.title,
|
name: fetchedVideo.title,
|
||||||
id: videoId,
|
id: videoId,
|
||||||
lastUpdated,
|
lastUpdated,
|
||||||
apps,
|
appLinks,
|
||||||
slug,
|
slug,
|
||||||
channel: {
|
channel: {
|
||||||
name: fetchedVideo.rawData.snippet.channelTitle,
|
name: fetchedVideo.rawData.snippet.channelTitle,
|
||||||
|
|
@ -184,12 +187,12 @@ async function handleFetchedVideo ( fetchedVideo, videoId, applist ) {
|
||||||
|
|
||||||
export default async function ( 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
|
// Fetch Commits
|
||||||
const response = await axios.get( videosJsonUrl )
|
// const response = await axios.get( videosJsonUrl )
|
||||||
// Extract commit from response data
|
// Extract commit from response data
|
||||||
const fetchedVideos = response.data
|
const fetchedVideos = await fs.readJson( youtubeVideoPath )//response.data
|
||||||
|
|
||||||
const videos = []
|
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
|
// Universal JS imports only
|
||||||
|
import shuffle from 'just-shuffle'
|
||||||
|
|
||||||
import { makeSlug } from './slug.js'
|
import { makeSlug } from './slug.js'
|
||||||
|
|
||||||
export function makeCategorySlug ( categoryName ) {
|
export function makeCategorySlug ( categoryName ) {
|
||||||
return makeSlug( categoryName )
|
return makeSlug( categoryName )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Maps iTunes Genres titles to Category IDs
|
||||||
const categoryMap = new Map([
|
const categoryMap = new Map([
|
||||||
[ 'Business', 2 ],
|
[ 'Business', 2 ],
|
||||||
[ 'Entertainment', 5 ],
|
[ 'Entertainment', 5 ],
|
||||||
|
|
@ -18,7 +22,12 @@ const categoryMap = new Map([
|
||||||
// [ 'Name', 1 ],
|
// [ 'Name', 1 ],
|
||||||
|
|
||||||
// Needs work before apps can be assigned games category
|
// 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', 100 ],
|
||||||
[ 'Games', 5 ],
|
[ 'Games', 5 ],
|
||||||
])
|
])
|
||||||
|
|
@ -42,7 +51,8 @@ export const categoryTemplate = {
|
||||||
pluralLabel: null,
|
pluralLabel: null,
|
||||||
itemSuffixLabel: null,
|
itemSuffixLabel: null,
|
||||||
icon: null,
|
icon: null,
|
||||||
requestLinks: null
|
requestLinks: null,
|
||||||
|
icon: '💻',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const categories = {
|
export const categories = {
|
||||||
|
|
@ -59,6 +69,7 @@ export const categories = {
|
||||||
label: 'Developer Tools',
|
label: 'Developer Tools',
|
||||||
pluralLabel: 'Developer Tools',
|
pluralLabel: 'Developer Tools',
|
||||||
slug: 'developer-tools',
|
slug: 'developer-tools',
|
||||||
|
snakeSlug: 'developer_tools',
|
||||||
},
|
},
|
||||||
|
|
||||||
'productivity-tools': {
|
'productivity-tools': {
|
||||||
|
|
@ -67,6 +78,7 @@ export const categories = {
|
||||||
label: 'Productivity Tools',
|
label: 'Productivity Tools',
|
||||||
pluralLabel: 'Productivity Tools',
|
pluralLabel: 'Productivity Tools',
|
||||||
slug: 'productivity-tools',
|
slug: 'productivity-tools',
|
||||||
|
snakeSlug: 'productivity_tools',
|
||||||
},
|
},
|
||||||
|
|
||||||
'video-and-motion-tools': {
|
'video-and-motion-tools': {
|
||||||
|
|
@ -75,6 +87,7 @@ export const categories = {
|
||||||
label: 'Video and Motion Tools',
|
label: 'Video and Motion Tools',
|
||||||
pluralLabel: 'Video and Motion Tools',
|
pluralLabel: 'Video and Motion Tools',
|
||||||
slug: 'video-and-motion-tools',
|
slug: 'video-and-motion-tools',
|
||||||
|
snakeSlug: 'video_and_motion_tools',
|
||||||
},
|
},
|
||||||
|
|
||||||
'social-and-communication': {
|
'social-and-communication': {
|
||||||
|
|
@ -83,6 +96,7 @@ export const categories = {
|
||||||
label: 'Social and Communication',
|
label: 'Social and Communication',
|
||||||
pluralLabel: 'Social and Communication Apps',
|
pluralLabel: 'Social and Communication Apps',
|
||||||
slug: 'social-and-communication',
|
slug: 'social-and-communication',
|
||||||
|
snakeSlug: 'social_and_communication',
|
||||||
},
|
},
|
||||||
|
|
||||||
'entertainment-and-media-apps': {
|
'entertainment-and-media-apps': {
|
||||||
|
|
@ -91,6 +105,7 @@ export const categories = {
|
||||||
label: 'Entertainment and Media Apps',
|
label: 'Entertainment and Media Apps',
|
||||||
pluralLabel: 'Entertainment and Media Apps',
|
pluralLabel: 'Entertainment and Media Apps',
|
||||||
slug: 'entertainment-and-media-apps',
|
slug: 'entertainment-and-media-apps',
|
||||||
|
snakeSlug: 'entertainment_and_media_apps',
|
||||||
},
|
},
|
||||||
|
|
||||||
'music-and-audio-tools': {
|
'music-and-audio-tools': {
|
||||||
|
|
@ -99,6 +114,7 @@ export const categories = {
|
||||||
label: 'Music and Audio Tools',
|
label: 'Music and Audio Tools',
|
||||||
pluralLabel: 'Music and Audio Tools',
|
pluralLabel: 'Music and Audio Tools',
|
||||||
slug: 'music-and-audio-tools',
|
slug: 'music-and-audio-tools',
|
||||||
|
snakeSlug: 'music_and_audio_tools',
|
||||||
},
|
},
|
||||||
|
|
||||||
'photo-and-graphic-tools': {
|
'photo-and-graphic-tools': {
|
||||||
|
|
@ -107,6 +123,7 @@ export const categories = {
|
||||||
label: 'Photo and Graphic Tools',
|
label: 'Photo and Graphic Tools',
|
||||||
pluralLabel: 'Photo and Graphic Tools',
|
pluralLabel: 'Photo and Graphic Tools',
|
||||||
slug: 'photo-and-graphic-tools',
|
slug: 'photo-and-graphic-tools',
|
||||||
|
snakeSlug: 'photo_and_graphic_tools',
|
||||||
},
|
},
|
||||||
|
|
||||||
'science-and-research-software': {
|
'science-and-research-software': {
|
||||||
|
|
@ -115,6 +132,7 @@ export const categories = {
|
||||||
label: 'Science and Research Software',
|
label: 'Science and Research Software',
|
||||||
pluralLabel: 'Science and Research Software',
|
pluralLabel: 'Science and Research Software',
|
||||||
slug: 'science-and-research-software',
|
slug: 'science-and-research-software',
|
||||||
|
snakeSlug: 'science_and_research_software',
|
||||||
},
|
},
|
||||||
|
|
||||||
'3d-and-architecture': {
|
'3d-and-architecture': {
|
||||||
|
|
@ -123,6 +141,7 @@ export const categories = {
|
||||||
label: '3D and Architecture',
|
label: '3D and Architecture',
|
||||||
pluralLabel: '3D and Architecture Applications',
|
pluralLabel: '3D and Architecture Applications',
|
||||||
slug: '3d-and-architecture',
|
slug: '3d-and-architecture',
|
||||||
|
snakeSlug: '3d_and_architecture',
|
||||||
},
|
},
|
||||||
|
|
||||||
'vpns-security-and-privacy': {
|
'vpns-security-and-privacy': {
|
||||||
|
|
@ -131,6 +150,7 @@ export const categories = {
|
||||||
label: 'VPNs, Security, and Privacy',
|
label: 'VPNs, Security, and Privacy',
|
||||||
pluralLabel: 'VPN, Security, and Privacy Applications',
|
pluralLabel: 'VPN, Security, and Privacy Applications',
|
||||||
slug: 'vpns-security-and-privacy',
|
slug: 'vpns-security-and-privacy',
|
||||||
|
snakeSlug: 'vpns_security_and_privacy',
|
||||||
},
|
},
|
||||||
|
|
||||||
'live-production-and-performance': {
|
'live-production-and-performance': {
|
||||||
|
|
@ -139,6 +159,7 @@ export const categories = {
|
||||||
label: 'Live Production and Performance',
|
label: 'Live Production and Performance',
|
||||||
pluralLabel: 'Live Production and Performance Software',
|
pluralLabel: 'Live Production and Performance Software',
|
||||||
slug: 'live-production-and-performance',
|
slug: 'live-production-and-performance',
|
||||||
|
snakeSlug: 'live_production_and_performance',
|
||||||
},
|
},
|
||||||
|
|
||||||
'system-tools': {
|
'system-tools': {
|
||||||
|
|
@ -147,6 +168,7 @@ export const categories = {
|
||||||
label: 'System Tools',
|
label: 'System Tools',
|
||||||
pluralLabel: 'System Tools',
|
pluralLabel: 'System Tools',
|
||||||
slug: 'system-tools',
|
slug: 'system-tools',
|
||||||
|
snakeSlug: 'system_tools',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Special Lists
|
// Special Lists
|
||||||
|
|
@ -156,6 +178,7 @@ export const categories = {
|
||||||
label: 'Games',
|
label: 'Games',
|
||||||
pluralLabel: 'Games',
|
pluralLabel: 'Games',
|
||||||
slug: 'games',
|
slug: 'games',
|
||||||
|
snakeSlug: 'games',
|
||||||
icon: '🎮',
|
icon: '🎮',
|
||||||
requestLinks: [
|
requestLinks: [
|
||||||
{
|
{
|
||||||
|
|
@ -171,6 +194,7 @@ export const categories = {
|
||||||
pluralLabel: 'Homebrew Formulae',
|
pluralLabel: 'Homebrew Formulae',
|
||||||
itemSuffixLabel: 'via Homebrew',
|
itemSuffixLabel: 'via Homebrew',
|
||||||
slug: 'homebrew',
|
slug: 'homebrew',
|
||||||
|
snakeSlug: 'homebrew',
|
||||||
icon: '🍺'
|
icon: '🍺'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -182,9 +206,25 @@ export const categories = {
|
||||||
label: 'Uncategorized',
|
label: 'Uncategorized',
|
||||||
pluralLabel: 'Uncategorized Software',
|
pluralLabel: 'Uncategorized Software',
|
||||||
slug: 'uncategorized',
|
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 } ] ) )
|
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]
|
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 ) {
|
export function findCategoryForTagsSet ( tags ) {
|
||||||
|
|
||||||
|
|
@ -230,3 +280,20 @@ export function findCategoryForTagsSet ( tags ) {
|
||||||
|
|
||||||
return null
|
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)
|
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 ) {
|
export function isValidHttpUrl( maybeUrl, allowUnsecure = false ) {
|
||||||
if ( !isString( maybeUrl ) ) return false
|
if ( !isString( maybeUrl ) ) return false
|
||||||
|
|
||||||
|
|
@ -22,6 +41,17 @@ export function isValidHttpUrl( maybeUrl, allowUnsecure = false ) {
|
||||||
return url.protocol === "https:"
|
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 ) {
|
export function isObject( maybeObject ) {
|
||||||
return maybeObject === Object( 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'
|
import statuses from '~/helpers/statuses'
|
||||||
|
|
||||||
export default function ( appList ) {
|
export default function ( appList ) {
|
||||||
|
if ( !Array.isArray( appList ) ) {
|
||||||
|
throw new Error(`List must be an array but is ${typeof appList}`)
|
||||||
|
}
|
||||||
|
|
||||||
const totalApps = appList.length
|
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
|
// 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.playerContainer = this.querySelector('.player-container')
|
||||||
this.playerPoster = this.querySelector('.player-poster')
|
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('video', this.video)
|
||||||
console.log('this.playerContainer', this.playerContainer)
|
// console.log('this.playerContainer', this.playerContainer)
|
||||||
|
|
||||||
|
|
||||||
// Start watchers here
|
// Start watchers here
|
||||||
|
|
@ -83,7 +83,7 @@ class LiteYTEmbed extends HTMLElement {
|
||||||
|
|
||||||
this.detectAutoplay()
|
this.detectAutoplay()
|
||||||
.then( ({ willAutoplay }) => {
|
.then( ({ willAutoplay }) => {
|
||||||
console.log('willAutoplay', willAutoplay)
|
// console.log('willAutoplay', willAutoplay)
|
||||||
|
|
||||||
// If we're allowed to autoplay
|
// If we're allowed to autoplay
|
||||||
// then start loading the player
|
// then start loading the player
|
||||||
|
|
@ -286,7 +286,7 @@ class LiteYTEmbed extends HTMLElement {
|
||||||
|
|
||||||
// const { default: canAutoPlay } = await import('can-autoplay')
|
// 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 })
|
// const willAutoplayMuted = await canAutoPlay.video({ muted: true, inline: true })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ export function MachoParser(file, callback) {
|
||||||
var cmd = null;
|
var cmd = null;
|
||||||
if(type == LOAD_COMMAND_TYPE.LC_SEGMENT) {
|
if(type == LOAD_COMMAND_TYPE.LC_SEGMENT) {
|
||||||
if(data.length < 48) {
|
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);
|
return new LoadCommand(type, data, size, off);
|
||||||
}
|
}
|
||||||
let name = new Cstr(data.slice(0, (4*uint32_t)));
|
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 data = new Uint8Array(e.target.result);
|
||||||
let magics = FindMagic(data, false); //Try to find all Mach-O magics in the byte array
|
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 where found, parse the binary.
|
||||||
if(magics.length > 0) {
|
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 { allVideoAppsListSet } from '~/helpers/get-list.js'
|
||||||
// import videoList from '~/static/video-list.json'
|
// 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 ) {
|
export function appsRelatedToVideo ( video, allVideoAppsListSet ) {
|
||||||
// console.log('allVideoAppsListSet', allVideoAppsListSet.length)
|
|
||||||
|
|
||||||
const relatedApps = []
|
const relatedApps = []
|
||||||
|
|
||||||
|
|
@ -10,7 +21,7 @@ export function appsRelatedToVideo ( video, allVideoAppsListSet ) {
|
||||||
for (const app of allVideoAppsListSet) {
|
for (const app of allVideoAppsListSet) {
|
||||||
// console.log('video', video)
|
// console.log('video', video)
|
||||||
// Skip this app if it's not listed in the videos apps
|
// 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
|
// Add this app to our featured app list
|
||||||
relatedApps.push(app)
|
relatedApps.push(app)
|
||||||
|
|
@ -22,9 +33,6 @@ export function appsRelatedToVideo ( video, allVideoAppsListSet ) {
|
||||||
export function videosRelatedToVideo ( video, allVideoAppsListSet, videoListSet ) {
|
export function videosRelatedToVideo ( video, allVideoAppsListSet, videoListSet ) {
|
||||||
const relatedVideos = {}
|
const relatedVideos = {}
|
||||||
|
|
||||||
// console.log('videoList', videoList[0])
|
|
||||||
// console.log('allVideoAppsListSet', allVideoAppsListSet[0])
|
|
||||||
|
|
||||||
const featuredApps = appsRelatedToVideo( video, allVideoAppsListSet )
|
const featuredApps = appsRelatedToVideo( video, allVideoAppsListSet )
|
||||||
|
|
||||||
// Find other videos that also feature this video's app
|
// 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) {
|
for (const app of featuredApps) {
|
||||||
// console.log('otherVideo', otherVideo)
|
// console.log('otherVideo', otherVideo)
|
||||||
// Skip if this app is not in the other video's apps
|
// 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
|
// Skip if the other video is, in fact, this video
|
||||||
if (otherVideo.slug === video.slug) continue
|
if (otherVideo.slug === video.slug) continue
|
||||||
|
|
@ -50,14 +58,38 @@ export function videosRelatedToApp ( app, videoListSet ) {
|
||||||
|
|
||||||
// console.log('videoListSet', videoListSet)
|
// console.log('videoListSet', videoListSet)
|
||||||
|
|
||||||
const relatedVideos = {}
|
const relatedVideos = []
|
||||||
|
|
||||||
// Find other videos that also feature this video's app
|
// Find other videos that also feature this video's app
|
||||||
for (const video of videoListSet) {
|
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()
|
event.stopPropagation()
|
||||||
|
|
||||||
// console.log('event.target', event.currentTarget)
|
// console.log('event.target', event.currentTarget)
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,65 @@
|
||||||
|
import { filterSeparator } from '~/helpers/constants.js'
|
||||||
|
|
||||||
|
|
||||||
const statuses = {
|
const statuses = {
|
||||||
'✅': 'native',
|
'native': {
|
||||||
'✳️': 'rosetta',
|
icon: '✅',
|
||||||
'⏹': 'no-in-progress',
|
filterLabel: 'Native Support',
|
||||||
'🚫': 'no',
|
snakeSlug: 'native',
|
||||||
'🔶': 'unreported',
|
},
|
||||||
|
'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 ) {
|
export function getStatusName ( status ) {
|
||||||
for (const key in statuses) {
|
for (const key in statusesByIcon) {
|
||||||
if (status.trim().startsWith( key )) return statuses[key]
|
if (status.trim().startsWith( key )) return statusesByIcon[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Non status matched')
|
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 ) {
|
function makeFeaturedAppsString ( featuredApps ) {
|
||||||
return featuredApps.slice(0, 5).map(app => app.name).join(', ')
|
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)
|
// 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 {
|
const {
|
||||||
siteUrl
|
siteUrl = getSiteUrl(),
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
const thumbnailUrls = video.thumbnail.srcset.split(',').map( srcSetImage => {
|
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>
|
<template>
|
||||||
<div class="app-wrapper text-gray-300 bg-gradient-to-bl from-dark to-darker bg-fixed">
|
<div>
|
||||||
<Navbar />
|
<Base>
|
||||||
<div class="app-main min-h-screen flex items-center">
|
<nuxt />
|
||||||
<nuxt />
|
</Base>
|
||||||
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Base from '~/layouts/base.vue'
|
||||||
// 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 {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Navbar,
|
Base
|
||||||
AllUpdatesSubscribe
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
currentYear () {
|
|
||||||
return new Date().getFullYear()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
30
netlify.toml
30
netlify.toml
|
|
@ -1,7 +1,26 @@
|
||||||
[build]
|
[build]
|
||||||
publish = "dist/"
|
publish = "dist/"
|
||||||
command = "npm run generate --quiet"
|
command = "npm run netlify-build"
|
||||||
# functions = "functions/"
|
|
||||||
|
|
||||||
|
# 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"
|
NPM_FLAGS = "--no-optional"
|
||||||
CI = "1"
|
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
|
# 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
|
# old node redirect
|
||||||
[[redirects]]
|
[[redirects]]
|
||||||
from = "/app/node"
|
from = "/app/node"
|
||||||
|
|
|
||||||
117
nuxt.config.js
117
nuxt.config.js
|
|
@ -1,17 +1,18 @@
|
||||||
import { promises as fs } from 'fs'
|
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 {
|
export default {
|
||||||
target: 'static',
|
target: 'static',
|
||||||
|
|
||||||
publicRuntimeConfig: {
|
publicRuntimeConfig,
|
||||||
allUpdateSubscribe: process.env.ALL_UPDATE_SUBSCRIBE,
|
|
||||||
testResultStore: process.env.TEST_RESULT_STORE,
|
|
||||||
siteUrl: process.env.URL,
|
|
||||||
...pkg.config
|
|
||||||
},
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
** Hooks
|
** Hooks
|
||||||
|
|
@ -42,107 +43,7 @@ export default {
|
||||||
/*
|
/*
|
||||||
** Headers of the page
|
** Headers of the page
|
||||||
*/
|
*/
|
||||||
head: {
|
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
|
|
||||||
// }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
** Customize the progress-bar color
|
** 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",
|
"description": "Find out the latest app support for Apple Silicon and the Apple M2 and M1 Ultra Processors",
|
||||||
"author": "Sam Carlton",
|
"author": "Sam Carlton",
|
||||||
"private": true,
|
"private": true,
|
||||||
"ava": {
|
|
||||||
"require": [
|
|
||||||
"esm"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"config": {
|
"config": {
|
||||||
"verbiage": {
|
"verbiage": {
|
||||||
"processors": "Apple M2 and M1 Ultra",
|
"processors": "Apple M2 and M1 Ultra",
|
||||||
"macs": "Apple M2 or M1 Ultra Mac",
|
"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"
|
"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": {
|
"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",
|
"test": "ava --timeout=1m --verbose",
|
||||||
"dev": "nuxt",
|
"dev": "nuxt",
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
|
|
@ -26,7 +31,18 @@
|
||||||
"start": "nuxt start",
|
"start": "nuxt start",
|
||||||
"generate-dev": "npm run generate && npm test",
|
"generate-dev": "npm run generate && npm test",
|
||||||
"generate": "npm run clone-readme && npm run build-lists && npm run generate-nuxt && npm run generate-eleventy",
|
"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-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-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",
|
"generate-postcss": "ENV=production postcss assets/css/tailwind.css --o static/tailwind.css",
|
||||||
|
|
@ -39,36 +55,60 @@
|
||||||
"precommit": "npm run lint",
|
"precommit": "npm run lint",
|
||||||
"clone-readme": "cp ./README.md README-temp.md",
|
"clone-readme": "cp ./README.md README-temp.md",
|
||||||
"cloudflare-deploy": "npm run build-api",
|
"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": {
|
"dependencies": {
|
||||||
"@11ty/eleventy-assets": "^1.0.5",
|
"@11ty/eleventy-assets": "^1.0.5",
|
||||||
|
"@astrojs/partytown": "^0.1.4",
|
||||||
|
"@astrojs/vue": "^0.1.3",
|
||||||
"@fontsource/inter": "^4.0.1",
|
"@fontsource/inter": "^4.0.1",
|
||||||
|
"@iarna/toml": "^2.2.5",
|
||||||
"@nuxtjs/sitemap": "^2.4.0",
|
"@nuxtjs/sitemap": "^2.4.0",
|
||||||
"@open-wc/webpack-import-meta-loader": "^0.4.7",
|
"@open-wc/webpack-import-meta-loader": "^0.4.7",
|
||||||
|
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
||||||
"@supercharge/promise-pool": "^2.1.0",
|
"@supercharge/promise-pool": "^2.1.0",
|
||||||
"@zip.js/zip.js": "^2.2.6",
|
"@zip.js/zip.js": "^2.2.6",
|
||||||
|
"astro": "^1.0.0-beta.27",
|
||||||
"axios": "^0.21.0",
|
"axios": "^0.21.0",
|
||||||
"can-autoplay": "^3.0.0",
|
"can-autoplay": "^3.0.0",
|
||||||
"chance": "^1.1.7",
|
"chance": "^1.1.7",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
"esbuild": "^0.11.20",
|
"esbuild": "^0.11.20",
|
||||||
|
"execa": "^5.1.1",
|
||||||
|
"fast-glob": "^3.2.11",
|
||||||
|
"fast-memoize": "^2.5.2",
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^8.1.0",
|
||||||
|
"googleapis": "^100.0.0",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
"jsdom": "^16.4.0",
|
"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",
|
"lazysizes": "^5.3.0-beta1",
|
||||||
"markdown-it": "^11.0.1",
|
"markdown-it": "^11.0.1",
|
||||||
"marked": "^1.2.7",
|
"marked": "^1.2.7",
|
||||||
|
"memoize-getters": "^1.1.0",
|
||||||
"node-html-parser": "^2.0.0",
|
"node-html-parser": "^2.0.0",
|
||||||
"observe-element-in-viewport": "0.0.15",
|
"observe-element-in-viewport": "0.0.15",
|
||||||
"plist": "^3.0.1",
|
"plist": "^3.0.1",
|
||||||
"pretty-bytes": "^5.5.0",
|
"pretty-bytes": "^5.5.0",
|
||||||
"scroll-into-view-if-needed": "^2.2.26",
|
"scroll-into-view-if-needed": "^2.2.26",
|
||||||
|
"semver": "^7.3.7",
|
||||||
|
"sitemap": "^7.1.1",
|
||||||
"slugify": "^1.4.6",
|
"slugify": "^1.4.6",
|
||||||
|
"stork-search": "^1.0.4",
|
||||||
"terser": "^4.8.0",
|
"terser": "^4.8.0",
|
||||||
"vue-gtag": "^1.16.1"
|
"uuid": "^8.3.2",
|
||||||
|
"vue": "^3.2.30",
|
||||||
|
"vue-gtag": "^2.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@11ty/eleventy": "^0.11.1",
|
"@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",
|
"@nuxt/postcss8": "^1.1.3",
|
||||||
"@nuxtjs/tailwindcss": "^3.3.4",
|
"@nuxtjs/tailwindcss": "^3.3.4",
|
||||||
"autoprefixer": "^8.6.4",
|
"autoprefixer": "^8.6.4",
|
||||||
|
|
@ -76,21 +116,19 @@
|
||||||
"babel-eslint": "^8.2.1",
|
"babel-eslint": "^8.2.1",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^4.1.10",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^8.14.0",
|
||||||
"eslint-config-prettier": "^3.1.0",
|
"eslint-plugin-vue": "^8.7.1",
|
||||||
"eslint-loader": "^2.0.0",
|
|
||||||
"eslint-plugin-prettier": "2.6.2",
|
|
||||||
"eslint-plugin-vue": "^4.0.0",
|
|
||||||
"esm": "^3.2.25",
|
"esm": "^3.2.25",
|
||||||
"fast-xml-parser": "^3.19.0",
|
"fast-xml-parser": "^3.19.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"nodemon": "^1.11.0",
|
"nodemon": "^1.11.0",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
"nuxt": "^2.14.11",
|
"nuxt": "^2.14.11",
|
||||||
"postcss": "^8.2.4",
|
"postcss": "^8.2.4",
|
||||||
"postcss-cli": "^8.3.1",
|
"postcss-cli": "^8.3.1",
|
||||||
"prettier": "1.14.3",
|
|
||||||
"replace-css-url": "^1.2.6",
|
"replace-css-url": "^1.2.6",
|
||||||
"structured-data-testing-tool": "^4.5.0",
|
"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 AppFilesScanner from '~/helpers/app-files-scanner.js'
|
||||||
|
|
||||||
|
import { isNuxt } from '~/helpers/environment.js'
|
||||||
|
|
||||||
import LinkButton from '~/components/link-button.vue'
|
import LinkButton from '~/components/link-button.vue'
|
||||||
import AllUpdatesSubscribe from '~/components/all-updates-subscribe.vue'
|
import AllUpdatesSubscribe from '~/components/all-updates-subscribe.vue'
|
||||||
|
|
@ -191,6 +192,12 @@ export default {
|
||||||
LinkButton,
|
LinkButton,
|
||||||
AllUpdatesSubscribe
|
AllUpdatesSubscribe
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
config: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
query: '',
|
query: '',
|
||||||
|
|
@ -200,7 +207,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
npm_package_config_verbiage_macs () {
|
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 () {
|
foundFiles () {
|
||||||
return this.appsBeingScanned.filter( appScan => {
|
return this.appsBeingScanned.filter( appScan => {
|
||||||
|
|
@ -264,7 +271,7 @@ export default {
|
||||||
return `Apple Silicon Compatibility Test Online`
|
return `Apple Silicon Compatibility Test Online`
|
||||||
},
|
},
|
||||||
description () {
|
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 () {
|
mounted () {
|
||||||
|
|
@ -295,13 +302,18 @@ export default {
|
||||||
console.log('Initializing scanner instance')
|
console.log('Initializing scanner instance')
|
||||||
|
|
||||||
// Bring in code
|
// 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
|
// Initialize instance
|
||||||
this.scanner = new AppFilesScanner({
|
this.scanner = new AppFilesScanner({
|
||||||
observableFilesArray: this.appsBeingScanned,
|
observableFilesArray: this.appsBeingScanned,
|
||||||
testResultStore: this.$config.testResultStore
|
testResultStore
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Setup scanner
|
||||||
|
await this.scanner.setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log('fileInputChanged files', fileList)
|
// 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