Skip to content

Instantly share code, notes, and snippets.

@sibbng
Last active May 7, 2025 17:51
Show Gist options
  • Save sibbng/a106b25ba0aa9f40faaa6c5e41ea4b0c to your computer and use it in GitHub Desktop.
Save sibbng/a106b25ba0aa9f40faaa6c5e41ea4b0c to your computer and use it in GitHub Desktop.
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"));
{
"imports": {
"voby": "https://esm.sh/voby",
"oby": "https://esm.sh/oby",
"test": "https://esm.sh/oby"
}
}
@theme {
--font-sans: 'Inter', sans-serif;
}
@custom-variant dark (&:where(.dark, .dark *));
@layer {
body {
@apply bg-neutral-400;
}
}
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