Skip to content

Commit

Permalink
feat: move frontend-lib functions here
Browse files Browse the repository at this point in the history
frontend-lib will be deprecated after this move.
Why?
There's not much reason to have frontend-lib functions separate.
Both libs are ~0-dependency.
  • Loading branch information
kirillgroshkov committed Oct 27, 2024
1 parent 9bc7539 commit 9920416
Show file tree
Hide file tree
Showing 28 changed files with 1,171 additions and 22 deletions.
67 changes: 67 additions & 0 deletions cfg/frontend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// @naturalcycles/js-lib/cfg/frontend/tsconfig.json
//
// Shared tsconfig for Frontend applications
//
{
"compilerOptions": {
// Target/module
"target": "es2020", // es2020+ browsers, adjust to your requirements!
"lib": ["esnext", "dom", "dom.iterable"],
"module": "esnext",
"moduleResolution": "node",
"moduleDetection": "force",
// specifying these explicitly for better IDE compatibility (but they're on by default with module=nodenext)
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
// Faster compilation in general
// Support for external compilers (e.g esbuild)
// Speedup in Jest by using "isolatedModules" in 'ts-jest' config
"isolatedModules": true,

// Emit
"sourceMap": false,
"declaration": false,
// Otherwise since es2022 it defaults to true
// and starts to produce different/unexpected behavior
// https://angular.schule/blog/2022-11-use-define-for-class-fields
"useDefineForClassFields": false,
"importHelpers": true,

// Strictness
"strict": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"suppressImplicitAnyIndexErrors": false,
"noUncheckedIndexedAccess": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitOverride": true,

// Enabled should be faster, but will catch less errors
// "skipLibCheck": true,

// Disabled because of https://github.com/Microsoft/TypeScript/issues/29172
// Need to be specified in the project tsconfig
// "outDir": "dist",
// "rootDir": "./src",
// "baseUrl": "./",
// "paths": {
// "@src/*": ["src/*"]
// },
// "typeRoots": [
// "node_modules/@types",
// "src/@types"
// ],

// Other
"jsx": "preserve",
"pretty": true,
"newLine": "lf",
"experimentalDecorators": true,
// "emitDecoratorMetadata": true // use if needed
},
// Need to be specified in the project tsconfig
// "include": ["src"],
// "exclude": ["**/__exclude", "**/@linked"]
}
6 changes: 6 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ export default defineConfig({
{ text: 'Types', link: '/types' },
{ text: 'Lazy', link: '/lazy' },
{ text: 'Fetcher', link: '/fetcher' },
{ text: 'loadScript, loadCSS', link: '/loadScript' },
{ text: 'TranslationService', link: '/translation' },
{ text: 'Analytics', link: '/analytics' },
{ text: 'AdminService', link: '/adminService' },
{ text: 'Image', link: '/image' },
{ text: 'BotDetectionService', link: '/bot' },
],
},
],
Expand Down
28 changes: 28 additions & 0 deletions docs/adminService.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# AdminService <Badge text="experimental" type="warning"/>

## Admin Mode

Admin mode is activated by a combination of keys on a keyboard (no mobile support), visualized with
the RedDot™ on the page.

Function `startListening()` enables listening for a key combination (Ctrl+Shift+L by default).

Example:

```ts
const admin = new AdminService({
onEnter: () => console.log('Entered Admin mode'),
onExit: () => console.log('Exited Admin mode'),
onRedDotClick: () => alert('RedDot clicked'),
})

admin.startListening()
```

Try pressing `Ctrl+Shift+L` on the keyboard to see the RedDot™ in action.

<script setup>
import AdminModeDemo from './components/AdminModeDemo.vue'
</script>

<AdminModeDemo/>
28 changes: 28 additions & 0 deletions docs/analytics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Analytics

## loadGTag

Example:

```ts
await loadGTag('UA-634xxxx-xx')
// gtag is loaded now
```

## loadGTM

Example:

```ts
await loadGTM('GTM-WJ6xxx')
// GTM is loaded now
```

## loadHotjar

Example:

```ts
await loadHotjar('19xxxxx')
// Hotjar is loaded now
```
29 changes: 29 additions & 0 deletions docs/bot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# BotDetectionService <Badge text="experimental" type="warning"/>

Allows to detect simple (non-sophisticated) bots.

Example usage:

```ts
import { BotDetectionService } from '@naturalcycles/js-lib'

const botDetectionService = new BotDetectionService()

botDetectionService.isBot() // true/false
botDetectionService.isCDP() // true/false
botDetectionService.getBotReason() // BotReason enum
```

## Demo

<script setup>
import {BotDetectionService} from "../src";
const botDetectionService = new BotDetectionService()
</script>

<pre>
isBot: {{ botDetectionService.isBot() }}
isCDP: {{ botDetectionService.isCDP() }}
isBotOrCDP: {{ botDetectionService.isBotOrCDP() }}
botReason: {{ botDetectionService.getBotReason() || 'null' }}
</pre>
14 changes: 14 additions & 0 deletions docs/components/AdminModeDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { AdminService } from '../../src'
const adminService = new AdminService({
onChange: adminMode => console.log({ adminMode }),
onRedDotClick: () => alert('RedDot clicked'),
})
onMounted(() => {
adminService.startListening()
})
</script>
<template></template>
132 changes: 132 additions & 0 deletions docs/components/FitImagesDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { ImageFitter, FitImage, _deepCopy, AnyFunction, StringMap } from '../../src'
const imageIds = [
'a8ZYS21_Toc',
'rpxnS9CtEDw',
'Ck-qAr0qbAI',
'h5UOgcq1Dkw',
'Jwhzumwgq9Q',
'2aLB0aQI5v4',
'0Q_XPEm2oVQ',
'bz0H2d753_U',
'oSIl84tpYYY',
'cX0Yxw38cx8',
'Y6Oi_1aGKPg',
'AqGhEk1khbE',
'XDvvt_IEH_w',
'leSvrOiu-nE',
'lkeBDBTwjWQ',
'6tJ50mdoyY4',
'wqJW5B9Q05I',
'Q2xGYGSu0Qo',
'Ai-AnKx5bSM',
'O4TA_kXW9as',
'aV31XuctrM8',
'zwoYd0ZiBmc',
'vMGM9Y48eIY',
]
const maxHeight = ref(300)
const margin = ref(8)
const images = ref<FitImage[]>([])
const fitter = ref<ImageFitter | undefined>()
onMounted(async () => {
// Preload images
const map: StringMap<FitImage> = {}
await new Promise(resolve => {
imageIds.forEach(id => {
const img = new Image()
img.onload = () => {
const { width, height, src } = img
map[id] = {
src,
aspectRatio: width / height,
}
if (Object.keys(map).length === imageIds.length) resolve()
}
img.src = `https://source.unsplash.com/${id}`
})
})
images.value = imageIds.map(id => map[id]!)
update()
})
onUnmounted(() => {
fitter.value?.stop()
})
watch(() => margin.value + maxHeight.value, update)
function update() {
// console.log('update!')
const containerElement = document.getElementById('imagesContainer')!
fitter.value?.stop()
fitter.value = new ImageFitter({
containerElement,
images: images.value,
maxHeight: maxHeight.value,
margin: margin.value,
onChange: newImages => {
images.value = _deepCopy(newImages)
},
})
}
</script>

<template>
<div class="app-content">
<p>
<span class="label">maxHeight: {{ maxHeight }}</span>
<input type="range" min="10" max="400" step="10" v-model="maxHeight" /><br />
<span class="label">margin: {{ margin }}</span>
<input type="range" min="0" max="20" v-model="margin" /><br />
</p>

<p v-if="!images.length">Loading images...</p>

<div id="imagesContainer" :style="{ margin: `-${margin / 2}px` }">
<img
v-for="im in images"
:style="{
width: `${im.fitWidth}px`,
height: `${im.fitHeight}px`,
margin: `${margin / 2}px`,
}"
:src="im.src"
alt="img"
/>
</div>
</div>
</template>

<style>
#imagesContainer {
line-height: 0;
border: 1px solid #888;
}
#imagesContainer img {
margin: 4px;
display: inline-block;
}
.label {
display: inline-block;
width: 160px;
}
input {
width: 300px;
}
</style>
63 changes: 63 additions & 0 deletions docs/components/LoadScriptDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<script setup lang="ts">
import { ref } from 'vue'
import { _stringify, loadCSS, loadScript } from '../../src'
const loading = ref(false)
async function loadGood() {
await load(`https://unpkg.com/[email protected]/dist/jquery.js`)
}
async function loadBad() {
await load(`https://unpkg.com/jqueryNON_EXISTING`)
}
async function loadGoodCSS() {
await loadStylesheet(`https://cdn.simplecss.org/simple.min.css`)
}
async function loadBadCSS() {
await loadStylesheet(`https://cdn.simplecss.org/simpleNOTFOUND.min.css`)
}
async function load(src: string) {
loading.value = true
try {
await loadScript(src)
alert('loaded ok')
} catch (err) {
alert(_stringify(err))
} finally {
loading.value = false
}
}
async function loadStylesheet(src: string) {
loading.value = true
try {
await loadCSS(src)
alert('loaded ok')
} catch (err) {
alert(_stringify(err))
} finally {
loading.value = false
}
}
</script>

<template>
<div class="app-content">
<button @click="loadGood">Load good script</button>
<button @click="loadBad">Load bad script</button>
<br /><br />
<button @click="loadGoodCSS">Load good CSS</button>
<button @click="loadBadCSS">Load bad CSS</button>
<p v-if="loading">loading...</p>
</div>
</template>

<style>
@import '/custom.css';
</style>
Loading

0 comments on commit 9920416

Please sign in to comment.