Last active
          May 7, 2025 17:51 
        
      - 
      
 - 
        
Save sibbng/a106b25ba0aa9f40faaa6c5e41ea4b0c to your computer and use it in GitHub Desktop.  
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | import { $, For, render } from "voby"; | |
| import { animate, createTimeline } from "animejs"; | |
| const ViewSlider = () => { | |
| const viewsData = [ | |
| { title: "View 1", content: "This is the content of View 1." }, | |
| { title: "View 2", content: "Welcome to View 2!" }, | |
| { title: "View 3", content: "You've reached View 3." }, | |
| { title: "View 4", content: "This is the final view, View 4." }, | |
| ]; | |
| const currentViewIndex = $<number>(0); | |
| const animationDuration = 600; | |
| const views: (HTMLDivElement | null)[] = new Array(viewsData.length).fill( | |
| null, | |
| ); | |
| function updateViewPositions() { | |
| views.forEach((view, index) => { | |
| if (!view) return; | |
| const offset = index - currentViewIndex(); | |
| animate(view, { | |
| x: offset === 0 ? "0%" : `${offset * 100}%`, | |
| opacity: offset === 0 ? 1 : 0, | |
| }).complete(); | |
| }); | |
| } | |
| const lastViewIndex = $(0); | |
| function navigateView(direction: number) { | |
| const nextIndex = currentViewIndex() + direction; | |
| if (nextIndex < 0 || nextIndex >= viewsData.length) return; | |
| lastViewIndex(currentViewIndex()); | |
| currentViewIndex(nextIndex); | |
| const prev = lastViewIndex(); | |
| const curr = currentViewIndex(); | |
| const outgoingView = views[prev]; | |
| const incomingView = views[curr]; | |
| const timeline = createTimeline({ | |
| defaults: { duration: animationDuration, ease: "out(5)" }, | |
| }); | |
| if (outgoingView && incomingView && prev !== curr) { | |
| if (direction > 0) { | |
| timeline | |
| .add(outgoingView, { | |
| x: "-30%", | |
| opacity: 0, | |
| }) | |
| .add( | |
| incomingView, | |
| { | |
| x: "0%", | |
| opacity: 1, | |
| }, | |
| `-20`, | |
| ); | |
| } else { | |
| timeline | |
| .add(outgoingView, { | |
| translateX: "100%", | |
| opacity: 0, | |
| }) | |
| .add( | |
| incomingView, | |
| { | |
| translateX: "0%", | |
| opacity: 1, | |
| }, | |
| `-20`, | |
| ); | |
| } | |
| } | |
| } | |
| return ( | |
| <div class="mobile-container" ref={(el) => setTimeout(() => updateViewPositions())}> | |
| <div class="view-container"> | |
| <For values={viewsData}> | |
| {(view, index) => ( | |
| <div | |
| class="view" | |
| ref={(el: HTMLDivElement | null) => { | |
| views[index] = el; | |
| }} | |
| style={{ | |
| position: "absolute", | |
| width: "100%", | |
| height: "100%", | |
| top: 0, | |
| left: 0, | |
| }} | |
| > | |
| <h2>{view.title}</h2> | |
| <p>{view.content}</p> | |
| </div> | |
| )} | |
| </For> | |
| </div> | |
| <div class="navigation-buttons"> | |
| <button | |
| class="nav-button" | |
| disabled={() => currentViewIndex() === 0} | |
| onClick={() => navigateView(-1)} | |
| > | |
| Previous | |
| </button> | |
| <button | |
| class="nav-button" | |
| disabled={() => currentViewIndex() === viewsData.length - 1} | |
| onClick={() => navigateView(1)} | |
| > | |
| Next | |
| </button> | |
| </div> | |
| <style> | |
| {` | |
| .mobile-container { | |
| width: 100%; | |
| max-width: 414px; | |
| height: 98vh; | |
| margin: 0 auto; | |
| background-color: #f0f0f0; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| box-sizing: border-box; | |
| } | |
| .view-container { | |
| flex-grow: 1; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .view { | |
| background-color: white; | |
| border-radius: 10px; | |
| padding: 20px; | |
| box-sizing: border-box; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| text-align: center; | |
| transition: box-shadow 0.2s; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.04); | |
| } | |
| h2 { | |
| font-size: 24px; | |
| margin-bottom: 10px; | |
| color: #333; | |
| } | |
| p { | |
| font-size: 16px; | |
| color: #666; | |
| } | |
| .navigation-buttons { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 10px; | |
| } | |
| .nav-button { | |
| padding: 10px 20px; | |
| background-color: #4CAF50; | |
| color: white; | |
| border: none; | |
| border-radius: 5px; | |
| font-size: 16px; | |
| cursor: pointer; | |
| transition: background-color 0.3s; | |
| } | |
| .nav-button:hover:not(:disabled) { | |
| background-color: #45a049; | |
| } | |
| .nav-button:focus { | |
| outline: none; | |
| box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.5); | |
| } | |
| .nav-button:disabled { | |
| background-color: #cccccc; | |
| cursor: not-allowed; | |
| } | |
| @media (max-width: 414px) { | |
| .mobile-container { | |
| height: 100vh; | |
| border-radius: 0; | |
| } | |
| } | |
| `} | |
| </style> | |
| </div> | |
| ); | |
| }; | |
| render(ViewSlider, document.querySelector("#app")); | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | { | |
| "imports": { | |
| "voby": "https://esm.sh/voby", | |
| "oby": "https://esm.sh/oby", | |
| "test": "https://esm.sh/oby" | |
| } | |
| } | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | @theme { | |
| --font-sans: 'Inter', sans-serif; | |
| } | |
| @custom-variant dark (&:where(.dark, .dark *)); | |
| @layer { | |
| body { | |
| @apply bg-neutral-400; | |
| } | |
| } | 
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | import { render, If, $, Observable, useEffect } from 'voby'; | |
| import { createQueryClient, QueryClientProvider, useQuery } from 'voby-query'; | |
| interface Todo { | |
| userId: number; | |
| id: number; | |
| title: string; | |
| completed: boolean; | |
| } | |
| const fetchTodo = async (id: number): Promise<Todo> => { | |
| const url = `https://jsonplaceholder.typicode.com/todos/${id}`; | |
| console.log(`Fetching todo ${id}...`); | |
| const response = await fetch(url); | |
| if (!response.ok) { | |
| if (response.status === 404) { | |
| throw new Error(`Todo with ID ${id} not found.`); | |
| } | |
| throw new Error(`HTTP error! Status: ${response.status}`); | |
| } | |
| const data: Todo = await response.json(); | |
| return data; | |
| }; | |
| const SimpleTodo = (): JSX.Element => { | |
| const todoId: Observable<number> = $(1); | |
| const query = useQuery({ | |
| queryKey: ['todo', todoId], | |
| queryFn: () => fetchTodo(todoId()), | |
| }); | |
| useEffect(() => { | |
| console.log("Query data:", query().data()) | |
| }) | |
| const handleRefetch = () => { | |
| query().refetch(); | |
| }; | |
| const handleNextTodo = () => { | |
| todoId(id => id + 1); | |
| }; | |
| return ( | |
| <div class="bg-white max-w-lg m-auto mt-20 p-4 rounded-xl"> | |
| <h1>Simple Todo Fetch</h1> | |
| <p>Current Todo ID: {todoId}</p> | |
| <div style={{ marginBottom: '1rem', display: 'flex', gap: '0.5rem' }}> | |
| <button class="bg-blue-600 rounded px-3 text-white" onClick={handleRefetch} disabled={() => query().isPending()}> | |
| {() => query().isPending() ? 'Refreshing...' : 'Refetch Current'} | |
| </button> | |
| <button class="bg-blue-600 rounded px-3 text-white" onClick={handleNextTodo} disabled={() => query().isPending()}> | |
| {() => query().isPending() ? 'Loading Next...' : 'Next Todo'} | |
| </button> | |
| <button class="bg-blue-600 rounded px-3 text-white" onClick={() => todoId(id => id - 1)} disabled={() => query().isPending()}> | |
| {() => query().isPending() ? 'Loading Prev...' : 'Previous Todo'} | |
| </button> | |
| </div> | |
| <div style={{ minHeight: '150px', position: 'relative' }}> | |
| <If when={() => query().isPending()}> | |
| <p style={{ color: '#888' }}>Loading...</p> | |
| </If> | |
| <If when={() => query().isError()}> | |
| <p style={{ color: 'red' }}>Error fetching todo: {() => query().error()?.message ?? 'Unknown error'}</p> | |
| </If> | |
| <If when={() => query().isSuccess() && query().data()}> | |
| {(data) => ( | |
| <div style={{ opacity: query().isFetching() ? 0.6 : 1, transition: 'opacity 0.2s ease-in-out' }}> | |
| <h2>Todo Item #{() => data().id}</h2> | |
| <p><strong>Todo title:</strong> {() => data().title}</p> | |
| <p><strong>Todo status:</strong> {() => data().completed ? 'Completed' : 'Pending'}</p> | |
| </div> | |
| )} | |
| </If> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| const queryClient = createQueryClient(); | |
| const App = () => ( | |
| <QueryClientProvider value={queryClient}> | |
| <SimpleTodo /> | |
| </QueryClientProvider> | |
| ); | |
| render(<App />, document.getElementById('app')); | 
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment