Skip to content

Commit

Permalink
feat(page-title): adds base experiments (#15)
Browse files Browse the repository at this point in the history
* chore(runtime): prepare eperiment providers

* feat(testing): add vsc launch config

* feat(testing): add coverage provider to jest config

* feat(inactive-title): add maqeuee for title

* chore(runtime): refactor inner structure of its slice and actions

* feat(utils): add multi-byte string funs for split and slice

* feat(page-title): update marquee helper to be mb string compatible

* feat(page-title): add glitchy and marquee titles

* feat(page-title): add array paged title replacing marquee

* feat(page-title): disable glitch title for the time being
  • Loading branch information
onetdev authored Mar 29, 2024
1 parent 93331d2 commit 554914b
Show file tree
Hide file tree
Showing 21 changed files with 352 additions and 65 deletions.
28 changes: 28 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev"
},
{
"name": "Next.js: debug client-side",
"type": "chrome",
"request": "launch",
"url": "https://localhost:3000"
},
{
"name": "Next.js: debug full stack",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev",
"serverReadyAction": {
"pattern": "- Local:.+(https?://.+)",
"uriFormat": "%s",
"action": "debugWithChrome"
}
}
]
}
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const createJestConfig = nextJest({
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'jsdom',
coverageProvider: 'v8',
};

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"lint:fix-all": "next lint -- --fix",
"type-check": "tsc --project tsconfig.json --pretty --noEmit",
"test": "jest --watch",
"test:coverage": "jest --coverage",
"test:once": "jest",
"test:ci": "jest --ci",
"prepare": "husky",
Expand Down
6 changes: 3 additions & 3 deletions public/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"title": "About this session",
"disclaimer": "These values reset every single visit and acts as a bit of debug info.",
"started_ago": "Started:",
"elapsed_seconds": "Elapsed seconds:",
"is_window_in_focus": "Is this window in focus?",
"is_windows_interacted": "Had first interaction?"
"visibility_seconds": "Elapsed seconds:",
"is_document_visible": "Is this window in focus?",
"interaction_unlocked": "Had first interaction?"
}
}
4 changes: 2 additions & 2 deletions src/features/chat_bubble/components/ActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import History from '@/features/chat_bubble/components/History';
import { useAppSelector } from '@/redux/hooks';
import { cssVars } from '@/styles/theme';
import { selectEnableSound } from '@/redux/selectors/preference';
import { selectHasInteracted } from '@/redux/selectors/runtime';
import { selectInteractionUnlocked } from '@/redux/selectors/runtime';

const zIndexBase = 20;

Expand Down Expand Up @@ -92,7 +92,7 @@ const initialMessage = () => ({
*/
const ActionButton: FunctionComponent = () => {
const enableSound = useAppSelector(selectEnableSound);
const hasInteracted = useAppSelector(selectHasInteracted);
const hasInteracted = useAppSelector(selectInteractionUnlocked);
const [history, setHistory] = useState([initialMessage()] as HistoryItem[]);
const [isOpen, setIsOpen] = useState(false);
const [badgeCounter, setBadgeCounter] = useState(1);
Expand Down
56 changes: 56 additions & 0 deletions src/features/page_title/components/GlitchyTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Head from 'next/head';
import { useCallback, useEffect, useState } from 'react';

import { random } from '@/utils/math';

export type RandomBlinkTitleProps = {
duration?: number;
enabled: boolean;
randomRange?: [number, number];
text?: string;
};
const GlitchyTitle = ({
duration = 1000,
enabled,
randomRange = [5000, 1000],
text = glitchyText,
}: RandomBlinkTitleProps) => {
const [blink, setBlink] = useState(false);
// Incrementing this will trigger a rerender/reschedule
const [runCount, setRunCount] = useState(0);

const schedule = useCallback(
(onComplete: () => void) => {
const rnd = random(Math.min(...randomRange), Math.max(...randomRange));
const inTimer = setTimeout(() => setBlink(true), rnd);
const outTimer = setTimeout(() => {
setBlink(false);
onComplete();
}, rnd + duration);

return () => {
clearTimeout(inTimer);
clearTimeout(outTimer);
};
},
[duration, randomRange],
);

useEffect(() => {
if (!enabled) {
setBlink(false);
setRunCount(0);
return;
}

const remove = schedule(() => setRunCount((prev) => prev + 1));
return remove;
}, [enabled, schedule, runCount]);

return <Head>{enabled && blink && <title>{text}</title>}</Head>;
};

// This looks soo funky :D
const glitchyText = 'Ţ̸̳̦̰̦̝͕͐̒̉̉̈́̉͛̃͆ͅh̵̛̜̲̉̋̀̐̒͒͆̕ë̸̦̝̀́̓ ̸̡̯͕̈́̈́͋͂͗M̴͔̘͙̣̞̈́̏͗̈́̾͊̊̇ͅo̵̢̤͚̯̣͕̾̿̑̓͐͝s̶̫͙̳̼̲̔̄͑̒́̓̃̎̕t̵̛̫̅͌̌̓̒͘̕ ̸͓́́̃͠A̵̲͑̅̚ṅ̸̦̗̱͈̳̰̣̿̽͋͊̚ͅn̵͙̭͆o̵̢͎͙̊̓y̷̡̛̯͙͕̞͕̱̖̾̂́͝i̴̤̠͚̯̲̣͛ṅ̸̛̖̗̟͙̻͂̏̉͒ǵ̶̡͈̒͛̌͐̄͘ ̶̢̦͉̩̗̮̬̺̂̑̆̄͆͐Ẃ̶̡̭̪͖̼̟͕́̊́̾̕e̴͓̱͐̈͌̈̔̑̕͠b̸̡̘͍͍̹̹̦͑̒̓s̷͎̥̠̦͕͋́̉̔̏i̸̱̗̻̳̦̓̓̓̍͌̓t̷̤̦̳̯̉̿̉̂̌̚ę̷̣͔͇̇͊͑';

export default GlitchyTitle;
29 changes: 29 additions & 0 deletions src/features/page_title/components/MarqueeTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Head from 'next/head';
import { useEffect, useState } from 'react';

import string_marquee from '@/features/page_title/utils/string_marquee';

type MarqueeTitleProps = {
enabled: boolean;
speedMs?: number;
text: string;
};
const MarqueeTitle = ({ enabled, speedMs = 1000, text }: MarqueeTitleProps) => {
const [time, setTime] = useState(0);

useEffect(() => {
if (!enabled) {
setTime(0);
}

const timer = setInterval(() => setTime((prev) => prev + 1), speedMs);
return () => {
clearInterval(timer);
setTime(0);
};
}, [enabled, speedMs]);

return <Head>{enabled && <title>{string_marquee(text, time)}</title>}</Head>;
};

export default MarqueeTitle;
34 changes: 34 additions & 0 deletions src/features/page_title/components/PageTitleExperiment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useAppSelector } from '@/redux/hooks';
import {
selectInteractionUnlocked,
selectIsDocumentVisible,
} from '@/redux/selectors/runtime';

import ArrayPagedTitle from './PagedTitle';

/**
* Experiments on manipulating the page title. Unfortunatelly the refresh rate
* is quite low and the title is not updated as frequently as I would like.
*/
const PageTitleExperiment = () => {
const isVisible = useAppSelector(selectIsDocumentVisible);
const hasInteracted = useAppSelector(selectInteractionUnlocked);

return (
<>
{/* It works but with the current browser landscape it is super slow. */}
{/* <MarqueeTitle
enabled={hasInteracted && !isVisible}
text="📣 Come back please 🏃‍♀️🏃 We have candy!! 🚐"
/> */}
{/* It's just not that funny when it show for more than 100ms :( */}
{/* <GlitchyTitle enabled={hasInteracted && isVisible} /> */}
<ArrayPagedTitle
enabled={hasInteracted && !isVisible}
texts={['⭐️ HEY YOU 🫵', '😜 YES YOU 😱', '📣 COME BACK 🏃']}
/>
</>
);
};

export default PageTitleExperiment;
36 changes: 36 additions & 0 deletions src/features/page_title/components/PagedTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Head from 'next/head';
import { useEffect, useState } from 'react';

type ArrayPagedTitleProps = {
enabled: boolean;
speedMs?: number;
texts: string[];
};
const ArrayPagedTitle = ({
enabled,
speedMs = 1000,
texts,
}: ArrayPagedTitleProps) => {
const [time, setTime] = useState(0);

// Every odd iteration will one of the custom lines while every even one
// will keep the original title.
const alternate = time % 2 === 1;
const text = alternate ? undefined : texts[(time / 2) % texts.length];

useEffect(() => {
if (!enabled) {
setTime(0);
}

const timer = setInterval(() => setTime((prev) => prev + 1), speedMs);
return () => {
clearInterval(timer);
setTime(0);
};
}, [enabled, speedMs]);

return <Head>{enabled && text && <title>{text}</title>}</Head>;
};

export default ArrayPagedTitle;
36 changes: 36 additions & 0 deletions src/features/page_title/utils/__test__/string_marquee.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* @jest-environment jsdom
*/
import string_marquee from '@/features/page_title/utils/string_marquee';

describe('String Marquee', () => {
const base = {
width: 10,
loopSpace: 5,
separatorChar: ' ',
};

it('should return 10 character long output even without content', () => {
expect(string_marquee('', 0, base)).toBe(' ');
});

it('should return 10 character no matter the lengt of the input', () => {
expect(string_marquee('Hello', 0, base)).toBe('Hello ');
expect(string_marquee('Hello Bello Chello', 0, base)).toBe('Hello Bell');
});

it('should offset the text by t while keeping 10 character limit and 5 loop space', () => {
expect(string_marquee('Hello', 0, base)).toBe('Hello ');
expect(string_marquee('Hello', 1, base)).toBe('ello H');
expect(string_marquee('Hello', 2, base)).toBe('llo He');
expect(string_marquee('Hello', 3, base)).toBe('lo Hel');
expect(string_marquee('Hello', 4, base)).toBe('o Hell');
expect(string_marquee('Hello', 5, base)).toBe(' Hello');
});

it('should respect multi-byte characters', () => {
expect(string_marquee('🏃📣', 0, base)).toBe('🏃📣 🏃📣 ');
expect(string_marquee('🏃📣', 1, base)).toBe('📣 🏃📣 ');
expect(string_marquee('🏃📣', 2, base)).toBe(' 🏃📣 ');
});
});
26 changes: 26 additions & 0 deletions src/features/page_title/utils/string_marquee.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { mb_string_to_char_array } from '@/utils/string';

type StringMarqueeOpts = {
width?: number;
gapLength?: number;
gapChar?: string;
};
const string_marquee = (
text: string,
t = 0,
{ width = 30, gapLength = 5, gapChar = ' ' }: StringMarqueeOpts = {},
) => {
const source = [
...mb_string_to_char_array(text),
...new Array(gapLength).fill(gapChar),
];
const tModulo = t % source.length;
let output = '';
for (let i = 0; i < width; i++) {
output += source[(tModulo + i) % source.length];
}

return output;
};

export default string_marquee;
Original file line number Diff line number Diff line change
@@ -1,43 +1,40 @@
import { useEffect, useState } from 'react';
import { useCallback, useEffect } from 'react';

import { actions as runtimeActions } from '@/redux/slices/runtime';
import { useAppDispatch } from '@/redux/hooks';
import { useAppDispatch, useAppSelector } from '@/redux/hooks';
import { selectIsDocumentVisible } from '@/redux/selectors/runtime';

/**
* This will mesaure how long the webpage has been in focus and report it to
* the redux store. Please note that if you change application but the
* browser is still visible it will count as "in focus".
* https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event
*/
const useInFocusMeter = () => {
const [isInFocus, setIsInFocusInternal] = useState(true);
const useDocumentVisibilityListener = () => {
const isInFocus = useAppSelector(selectIsDocumentVisible);
const dispatch = useAppDispatch();

const handleVisibilityChange = () => {
setIsInFocusInternal(!document.hidden);
};
const handleVisibilityChange = useCallback(() => {
dispatch(runtimeActions.setIsDocumentVisibile(!document.hidden));
}, [dispatch]);

useEffect(() => {
document.addEventListener('visibilitychange', handleVisibilityChange);
handleVisibilityChange();

return () =>
document.removeEventListener('visibilitychange', handleVisibilityChange);
}, []);

useEffect(() => {
dispatch(runtimeActions.setIsInFocus(isInFocus));
}, [dispatch, isInFocus]);
}, [handleVisibilityChange]);

useEffect(() => {
if (!isInFocus) return;

const interval = setInterval(
() => dispatch(runtimeActions.incrementInFocusSeconds()),
() => dispatch(runtimeActions.incrementVisibilitySeconds()),
1000,
);
return () => clearInterval(interval);
}, [isInFocus, dispatch]);
};

export default useInFocusMeter;
export default useDocumentVisibilityListener;
2 changes: 1 addition & 1 deletion src/hooks/useDragTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { isPointWithin } from '@/utils/dom';
import { distance, Point } from '@/utils/math';

type TimedPoint = Point & { t: Date };
type DragTrackerState = {
export type DragTrackerState = {
isActive: boolean;
isWithin: boolean;
velocity: number | null;
Expand Down
Loading

0 comments on commit 554914b

Please sign in to comment.