@@ -77,131 +77,93 @@ const TERMINAL_STAGE_START = CAPTION_STAGE_END + CAPTION_POST_PAUSE;
7777const TERMINAL_STAGE_END = TERMINAL_STAGE_START + TERMINAL_STAGE_LENGTH ;
7878const TERMINAL_TYPE_TRAIL = 0.8 ;
7979const TERMINAL_CHAR_SCROLL_MULT = 20 ;
80- const TERMINAL_SCROLL_HEADROOM = 0.02 ; // start auto-scroll slightly before text would clip
80+ const TERMINAL_AUTO_CHAR_RATE = 45 ; // baseline characters per second when auto-typing kicks in
81+ const TERMINAL_SCROLL_MARGIN_RATIO = 0.12 ; // keep ~12% of viewport visible above latest line
82+ const TERMINAL_SCROLL_STICKY_DISTANCE = 80 ; // px distance from bottom treated as "pinned"
8183const TEXT_SLOWNESS = 7 ;
8284const MAX_PROGRESS = TERMINAL_STAGE_END + TERMINAL_TYPE_TRAIL ;
8385const terminalEntries = [
8486 {
8587 command : 'whoami' ,
86- output : [ 'Community member. Here because something on your phone went boom.' ] ,
88+ output : [ '=> Community member. Here because something on your phone went boom.' ] ,
8789 } ,
8890 {
8991 command : 'hostname' ,
90- output : [ 'forums.jtechforums.org' ] ,
92+ output : [ '=> forums.jtechforums.org' ] ,
9193 } ,
9294 {
9395 command : 'uname -a' ,
94- output : [ 'Linux JTech-Server #1 running stable since the last "how do I block WhatsApp Status" post.' ] ,
95- } ,
96- {
97- command : 'uptime' ,
98- output : [ 'Forum active for years. Repeat questions answered: too many to count.' ] ,
96+ output : [ '=> Linux JTech-Server #1 running stable since the last “how do I block WhatsApp Status” post.' ] ,
9997 } ,
10098 {
10199 command : 'id' ,
102- output : [ 'uid=1000(you) groups=android_help,filters,mdm ,panic-posters' ] ,
100+ output : [ '=> uid=1000(you) groups=android_linux,windows,mac ,panic-posters' ] ,
103101 } ,
104102 {
105103 command : 'df -h' ,
106- output : [ '87% space used by screenshots people uploaded instead of describing the issue.' ] ,
107- } ,
108- {
109- command : 'free -h' ,
110- output : [ 'RAM consumed by filter debates: 99%' , ' RAM available for patience: questionable' ] ,
111- } ,
112- {
113- command : 'ls ~/Forums' ,
114- output : [ 'Guides/ Android/ Filters/ KosherPhones/ "Help-I-broke-my-device"/' ] ,
115- } ,
116- {
117- command : 'cat Guides/mission.txt' ,
118- output : [
119- 'Provide precise, up-to-date tech & filtering help to the Jewish community.' ,
120- ' Practical, kosher, zero balagan.' ,
121- ] ,
122- } ,
123- {
124- command : 'faq --question="What is JTech Forums?"' ,
125- output : [
126- 'A safe community for filtered tech, kosher phones, apps, MDM help,' ,
127- ' and level-headed troubleshooting.' ,
128- ] ,
104+ output : [ '=> 87% space used by screenshots people uploaded instead of describing the situation.' ] ,
129105 } ,
130106 {
131107 command : 'faq --question="Is everything kosher here?"' ,
132108 output : [
133- 'Yes. Strict content standards. No inappropriate links.' ,
134- ' This forum is built for families -- and indexed by Google.' ,
109+ '=> Yes. Strict content standards. No inappropriate links.' ,
110+ ' This forum is built for families — and indexed by Google.' ,
135111 ' Keep it clean.' ,
136112 ] ,
137113 } ,
138114 {
139115 command : 'faq --question="Can I post any random files?"' ,
140- output : [ 'No Google Drive dumps.' , ' No mystery APKs.' , ' Only trusted, safe sources allowed.' ] ,
141- } ,
142- {
143- command : 'faq --question="Why was my post removed?"' ,
144116 output : [
145- 'Probably:' ,
146- ' - off-topic' ,
147- ' - not family-safe' ,
148- ' - linked to who-knows-where' ,
149- ' - or your cousin\'s "hack" wasn\'t actually a hack.' ,
117+ '=> No Google Drive dumps.' ,
118+ ' No mystery APKs.' ,
119+ ' Only trusted, safe sources allowed.' ,
150120 ] ,
151121 } ,
152122 {
153- command : 'grep -R "Whatsapp" ./Android' ,
154- output : [ '182 results... mostly "how block Status???"' , ' Solutions exist. Take a deep breath.' ] ,
155- } ,
156- {
157- command : 'grep -R "MDM" ./Filters' ,
158- output : [ 'Found: install guides, fix guides, and "pls help my kid found Developer Options"' ] ,
159- } ,
160- {
161- command : 'curl https://forums.jtechforums.org/api/stats' ,
162- output : [
163- 'signups_last_30_days: 305+' ,
164- ' page_visits: 100000+' ,
165- ' posts: 15000+' ,
166- ' people_helped: countless' ,
167- ] ,
123+ command : 'grep -R "MDM" ./Hacking' ,
124+ output : [ '=> Found: download and install guides, fix guides, and “pls help my kid found Developer Options”' ] ,
168125 } ,
169126 {
170127 command : 'top -b -n1 | head' ,
171128 output : [
172- 'PID TASK CPU DESCRIPTION' ,
173- '101 mods 98% deleting nonsense' ,
174- '203 helpers 75% answering same question for 12th time' ,
175- '404 panicd 60% " I factory reset and now nothing works plz help" ' ,
129+ '=> PID TASK CPU DESCRIPTION' ,
130+ '=> 101 mods 98% deleting nonsense' ,
131+ '=> 203 helpers 75% answering same question for 12th time' ,
132+ '=> 404 panicd 60% “ I factory reset and now nothing works plz help” ' ,
176133 ] ,
177134 } ,
178135 {
179136 command : 'sudo -l' ,
180- output : [ 'Sorry, you don\' t have moderator permissions. Nice try though.' ] ,
137+ output : [ '=> Sorry, you don’ t have moderator permissions. Nice try though.' ] ,
181138 } ,
182139 {
183140 command : 'dmesg | tail' ,
184- output : [ 'ALERT: new thread detected about FIG phones being "hacked"' , 'RESPONSE: highly unlikely. They\'re not magic, relax.' ] ,
141+ output : [
142+ '=> ALERT: new thread detected about FIG phones being “hacked”' ,
143+ '=> RESPONSE: highly unlikely. They’re not magic, relax.' ,
144+ ] ,
185145 } ,
186146 {
187147 command : 'stat ~/first_post.txt' ,
188- output : [ 'size: 0 bytes' , 'meaning: please include details when asking for help.' ] ,
189- } ,
190- {
191- command : 'join --mode=read' ,
192- output : [ 'Browse guides. Absorb knowledge. Enjoy the calm.' ] ,
148+ output : [ '=> size: 0 bytes' , '=> meaning: please include details when asking for help.' ] ,
193149 } ,
194150 {
195151 command : 'join --mode=ask' ,
196- output : [ 'Provide device, model, filter, and what you tried.' , ' The more info you give, the faster you\'ll get helped.' ] ,
152+ output : [
153+ '=> Provide device, model, clear issue, and what you tried.' ,
154+ ' The more info you give, the faster you’ll get helped.' ,
155+ ] ,
197156 } ,
198157 {
199- command : 'join --mode=help-others' ,
200- output : [ 'Share your fixes. Share your setups.' , ' Someone out there needs exactly what you figured out.' ] ,
158+ command : 'exit' ,
159+ output : [
160+ '=> JTech Forums — keeping your tech smooth, your devices kosher,' ,
161+ ' and your sanity intact.' ,
162+ ] ,
201163 } ,
202164 {
203- command : 'exit ' ,
204- output : [ 'JTech Forums -- keeping your tech smooth, your phones kosher,' , ' and your sanity intact. '] ,
165+ command : '' ,
166+ output : [ ' ' ] ,
205167 } ,
206168] ;
207169
@@ -228,6 +190,9 @@ export default function Home() {
228190 const typingAccumulatorRef = useRef ( 0 ) ;
229191 const terminalTypedCharsRef = useRef ( 0 ) ;
230192 const terminalScrollingRef = useRef ( null ) ;
193+ const terminalPinnedRef = useRef ( true ) ;
194+ const typedLineCountRef = useRef ( 0 ) ;
195+ const progressRef = useRef ( 0 ) ;
231196 const [ leaderboardState , setLeaderboardState ] = useState ( {
232197 entries : [ ] ,
233198 status : 'idle' ,
@@ -245,15 +210,31 @@ export default function Home() {
245210 [ terminalEntries ]
246211 ) ;
247212
248- useEffect ( ( ) => {
249- if ( typeof document === 'undefined' ) return ;
250- const previousOverflow = document . body . style . overflow ;
251- document . body . style . overflow = 'hidden' ;
252- return ( ) => {
213+ useEffect ( ( ) => {
214+ if ( typeof document === 'undefined' ) return ;
215+ const previousOverflow = document . body . style . overflow ;
216+ document . body . style . overflow = 'hidden' ;
217+ return ( ) => {
253218 document . body . style . overflow = previousOverflow ;
254219 } ;
255220 } , [ ] ) ;
256221
222+ useEffect ( ( ) => {
223+ progressRef . current = progress ;
224+ } , [ progress ] ) ;
225+
226+ useEffect ( ( ) => {
227+ const scroller = terminalScrollingRef . current ;
228+ if ( ! scroller ) return undefined ;
229+ const handleScroll = ( ) => {
230+ const distanceFromBottom = scroller . scrollHeight - scroller . clientHeight - scroller . scrollTop ;
231+ terminalPinnedRef . current = distanceFromBottom <= TERMINAL_SCROLL_STICKY_DISTANCE ;
232+ } ;
233+ handleScroll ( ) ;
234+ scroller . addEventListener ( 'scroll' , handleScroll , { passive : true } ) ;
235+ return ( ) => scroller . removeEventListener ( 'scroll' , handleScroll ) ;
236+ } , [ ] ) ;
237+
257238 useEffect ( ( ) => {
258239 let rafId ;
259240 const tick = ( ) => {
@@ -320,6 +301,64 @@ export default function Home() {
320301 }
321302 } , [ progress ] ) ;
322303
304+ useEffect ( ( ) => {
305+ if ( typeof window === 'undefined' || totalTerminalChars <= 0 ) return undefined ;
306+ let rafId = null ;
307+ let lastTime = window . performance . now ( ) ;
308+
309+ const tick = ( time ) => {
310+ const deltaSeconds = Math . max ( 0 , ( time - lastTime ) / 1000 ) ;
311+ lastTime = time ;
312+ const autoActive =
313+ progressRef . current >= TERMINAL_STAGE_START && terminalTypedCharsRef . current < totalTerminalChars ;
314+ if ( autoActive && deltaSeconds > 0 ) {
315+ const charDelta = deltaSeconds * TERMINAL_AUTO_CHAR_RATE ;
316+ if ( charDelta > 0 ) {
317+ setTerminalTypedChars ( ( prev ) => {
318+ if ( prev >= totalTerminalChars ) return prev ;
319+ return Math . min ( totalTerminalChars , prev + charDelta ) ;
320+ } ) ;
321+ }
322+ }
323+ rafId = window . requestAnimationFrame ( tick ) ;
324+ } ;
325+
326+ rafId = window . requestAnimationFrame ( ( time ) => {
327+ lastTime = time ;
328+ tick ( time ) ;
329+ } ) ;
330+
331+ return ( ) => window . cancelAnimationFrame ( rafId ) ;
332+ } , [ totalTerminalChars ] ) ;
333+
334+ useEffect ( ( ) => {
335+ const scroller = terminalScrollingRef . current ;
336+ if ( ! scroller || ! terminalPinnedRef . current ) return ;
337+ const available = scroller . scrollHeight - scroller . clientHeight ;
338+ if ( available <= 0 ) {
339+ scroller . scrollTop = 0 ;
340+ return ;
341+ }
342+ const margin = scroller . clientHeight * TERMINAL_SCROLL_MARGIN_RATIO ;
343+ scroller . scrollTo ( {
344+ top : Math . max ( 0 , available - margin ) ,
345+ behavior : 'smooth' ,
346+ } ) ;
347+ } , [ terminalTypedChars ] ) ;
348+
349+ useEffect ( ( ) => {
350+ const scroller = terminalScrollingRef . current ;
351+ if ( ! scroller ) return undefined ;
352+ const observer = new ResizeObserver ( ( ) => {
353+ if ( ! terminalPinnedRef . current ) return ;
354+ const available = scroller . scrollHeight - scroller . clientHeight ;
355+ const margin = scroller . clientHeight * TERMINAL_SCROLL_MARGIN_RATIO ;
356+ scroller . scrollTop = available > 0 ? Math . max ( 0 , available - margin ) : 0 ;
357+ } ) ;
358+ observer . observe ( scroller ) ;
359+ return ( ) => observer . disconnect ( ) ;
360+ } , [ ] ) ;
361+
323362
324363 useEffect ( ( ) => {
325364 let rafId ;
@@ -407,32 +446,59 @@ const captionStageProgress =
407446const terminalStageProgress =
408447 progress <= TERMINAL_STAGE_START ? 0 : clamp ( ( progress - TERMINAL_STAGE_START ) / ( TERMINAL_STAGE_LENGTH || 0.0001 ) , 0 , 1 ) ;
409448const terminalTypingProgress = totalTerminalChars > 0 ? terminalTypedChars / totalTerminalChars : 0 ;
449+ const terminalTypingState = useMemo (
450+ ( ) => buildTerminalTypingState ( terminalEntries , terminalTypingProgress ) ,
451+ [ terminalEntries , terminalTypingProgress ]
452+ ) ;
453+ const typedLineCount = useMemo ( ( ) => {
454+ let count = 0 ;
455+ terminalTypingState . forEach ( ( entry ) => {
456+ if ( ! entry ) return ;
457+ if ( entry . command ?. length ) count += 1 ;
458+ entry . outputs ?. forEach ( ( line ) => {
459+ if ( line ?. length ) count += 1 ;
460+ } ) ;
461+ } ) ;
462+ return count ;
463+ } , [ terminalTypingState ] ) ;
464+ const terminalTotalLines = useMemo ( ( ) => {
465+ let total = 0 ;
466+ terminalEntries . forEach ( ( entry ) => {
467+ if ( ! entry ) return ;
468+ if ( entry . command ) total += 1 ;
469+ if ( Array . isArray ( entry . output ) ) total += entry . output . length ;
470+ } ) ;
471+ return Math . max ( total , 1 ) ;
472+ } , [ ] ) ;
410473
411474useEffect ( ( ) => {
412475 const scroller = terminalScrollingRef . current ;
413476 if ( ! scroller ) return undefined ;
414477
415- const syncScrollPosition = ( ) => {
416- const totalHeight = scroller . scrollHeight ;
417- const viewportHeight = scroller . clientHeight ;
418- const available = totalHeight - viewportHeight ;
478+ const syncScroll = ( ) => {
479+ const available = scroller . scrollHeight - scroller . clientHeight ;
419480 if ( available <= 0 ) {
420481 scroller . scrollTop = 0 ;
421482 return ;
422483 }
423- const visibleRatio = clamp ( viewportHeight / ( totalHeight || 1 ) , 0 , 1 ) ;
424- const scrollStart = clamp ( visibleRatio - TERMINAL_SCROLL_HEADROOM , 0 , 0.99 ) ;
425- const effectiveRange = Math . max ( 1 - scrollStart , 0.0001 ) ;
426- const effective = clamp ( ( terminalTypingProgress - scrollStart ) / effectiveRange , 0 , 1 ) ;
427- scroller . scrollTop = available * effective ;
484+
485+ const linesBeyondStart = Math . max ( typedLineCount - 5 , 0 ) ;
486+ const totalBeyondStart = Math . max ( terminalTotalLines - 5 , 1 ) ;
487+ const ratio = clamp ( linesBeyondStart / totalBeyondStart , 0 , 1 ) ;
488+
489+ const margin = scroller . clientHeight * TERMINAL_SCROLL_MARGIN_RATIO ;
490+ const maxScrollable = Math . max ( available - margin , 0 ) ;
491+ const target = ratio * maxScrollable ;
492+
493+ scroller . scrollTo ( { top : target , behavior : 'smooth' } ) ;
428494 } ;
429495
430- syncScrollPosition ( ) ;
431- const observer = new ResizeObserver ( syncScrollPosition ) ;
496+ syncScroll ( ) ;
497+ const observer = new ResizeObserver ( syncScroll ) ;
432498 observer . observe ( scroller ) ;
433499
434500 return ( ) => observer . disconnect ( ) ;
435- } , [ terminalTypingProgress ] ) ;
501+ } , [ typedLineCount , terminalTotalLines ] ) ;
436502
437503const leaderboardEntries = leaderboardState . entries ;
438504const leaderboardStatus = leaderboardState . status ;
@@ -515,11 +581,6 @@ const [topCaptionChars = [], bottomCaptionChars = []] = useMemo(
515581const fakeTypingChars = buildLineCharacters ( [ 'â–ˆ' . repeat ( 100 ) ] , fakeStageProgress ) [ 0 ] || [ ] ;
516582const showFakeLine = fakeStageProgress > 0 && fakeStageProgress < 1 ;
517583const textLayerOpacity = textRevealProgress * clamp ( 1 - terminalStageProgress * 2 , 0 , 1 ) ;
518- const terminalTypingState = useMemo (
519- ( ) => buildTerminalTypingState ( terminalEntries , terminalTypingProgress ) ,
520- [ terminalEntries , terminalTypingProgress ]
521- ) ;
522-
523584 return (
524585 < div className = "relative h-full overflow-hidden bg-slate-950 text-white" >
525586 < div
0 commit comments