Skip to content

Commit d00235f

Browse files
committed
Improve page transitions with smoother fade effects and better error handling
1 parent c6c6f58 commit d00235f

File tree

2 files changed

+177
-74
lines changed

2 files changed

+177
-74
lines changed

public/css/theme.css

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -140,20 +140,26 @@ body {
140140
#app {
141141
min-height: 100vh;
142142
position: relative;
143+
overflow-x: hidden;
143144
}
144145

145-
.route-transition-container {
146+
.new-route-container,
147+
.current-route-container {
148+
width: 100%;
149+
min-height: 100vh;
150+
background-color: var(--background-color);
151+
}
152+
153+
.new-route-container {
146154
position: absolute;
147155
top: 0;
148156
left: 0;
149-
width: 100%;
150-
height: 100%;
151-
z-index: 10;
152-
display: flex;
153-
align-items: center;
154-
justify-content: center;
155-
background-color: var(--background-color);
156-
color: var(--text-primary);
157+
z-index: 5;
158+
}
159+
160+
.current-route-container {
161+
position: relative;
162+
z-index: 1;
157163
}
158164

159165
.loading {
@@ -162,6 +168,7 @@ body {
162168
justify-content: center;
163169
font-size: var(--font-size-lg);
164170
color: var(--text-primary);
171+
padding: var(--spacing-xl);
165172
}
166173

167174
/* Ensure all text elements maintain proper coloring during transitions */
@@ -170,6 +177,13 @@ body, h1, h2, h3, h4, h5, h6, p, span, a, button, input, textarea, select, label
170177
transition: color var(--transition-normal);
171178
}
172179

180+
/* Prevent FOUC (Flash of Unstyled Content) during transitions */
181+
.new-route-container *,
182+
.current-route-container * {
183+
color: var(--text-primary) !important;
184+
background-color: var(--background-color);
185+
}
186+
173187
/* Theme toggle styles */
174188
.theme-toggle {
175189
position: fixed;

public/js/router.js

Lines changed: 154 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -114,29 +114,6 @@ class Router {
114114
return;
115115
}
116116

117-
// Create a transition container
118-
const transitionContainer = document.createElement('div');
119-
transitionContainer.className = 'route-transition-container';
120-
transitionContainer.style.position = 'absolute';
121-
transitionContainer.style.top = '0';
122-
transitionContainer.style.left = '0';
123-
transitionContainer.style.width = '100%';
124-
transitionContainer.style.height = '100%';
125-
transitionContainer.style.backgroundColor = 'var(--background-color)';
126-
transitionContainer.style.color = 'var(--text-primary)';
127-
transitionContainer.style.opacity = '0';
128-
transitionContainer.style.transition = 'opacity 150ms ease-in-out';
129-
transitionContainer.innerHTML = '<div class="loading">Loading...</div>';
130-
131-
// Add the transition container to the root element
132-
rootElement.style.position = 'relative';
133-
rootElement.appendChild(transitionContainer);
134-
135-
// Fade in the transition container
136-
setTimeout(() => {
137-
transitionContainer.style.opacity = '1';
138-
}, 0);
139-
140117
try {
141118
// Handle route
142119
if (route) {
@@ -145,6 +122,18 @@ class Router {
145122
// Set current route
146123
this.currentRoute = route;
147124

125+
// Create a container for the new view that will be invisible at first
126+
const newViewContainer = document.createElement('div');
127+
newViewContainer.className = 'new-route-container';
128+
newViewContainer.style.position = 'absolute';
129+
newViewContainer.style.top = '0';
130+
newViewContainer.style.left = '0';
131+
newViewContainer.style.width = '100%';
132+
newViewContainer.style.height = '100%';
133+
newViewContainer.style.opacity = '0';
134+
newViewContainer.style.zIndex = '5';
135+
newViewContainer.style.transition = 'opacity 200ms ease-in-out';
136+
148137
// Get view content
149138
let content;
150139

@@ -167,54 +156,146 @@ class Router {
167156
content = '';
168157
}
169158

170-
// Prepare the new content in the background
171-
const tempContainer = document.createElement('div');
172-
tempContainer.innerHTML = content;
159+
// Set the content of the new view container
160+
newViewContainer.innerHTML = content;
161+
162+
// Make the root element a positioned container if it's not already
163+
const rootPosition = window.getComputedStyle(rootElement).position;
164+
if (rootPosition === 'static') {
165+
rootElement.style.position = 'relative';
166+
}
167+
168+
// Add the new view container to the DOM but keep it invisible
169+
rootElement.appendChild(newViewContainer);
173170

174-
// Apply theme-related styles to ensure proper coloring
175-
const themeStyles = document.createElement('style');
176-
themeStyles.textContent = `
177-
* {
178-
color: var(--text-primary);
179-
background-color: var(--background-color);
180-
transition: none !important;
171+
// Create a wrapper for the current content
172+
const currentContent = document.createElement('div');
173+
currentContent.className = 'current-route-container';
174+
currentContent.style.position = 'relative';
175+
currentContent.style.zIndex = '1';
176+
currentContent.style.opacity = '1';
177+
currentContent.style.transition = 'opacity 200ms ease-in-out';
178+
179+
// Move all current children (except the new view container) into the wrapper
180+
Array.from(rootElement.children).forEach(child => {
181+
if (child !== newViewContainer) {
182+
currentContent.appendChild(child);
181183
}
182-
`;
183-
tempContainer.appendChild(themeStyles);
184+
});
184185

185-
// Fade out the transition container
186-
transitionContainer.style.opacity = '0';
186+
// Add the current content wrapper back to the root
187+
rootElement.appendChild(currentContent);
187188

188-
// Wait for the fade out transition to complete
189-
setTimeout(() => {
190-
// Update the DOM with the prepared content
191-
console.log('Updating DOM with content');
192-
rootElement.innerHTML = content;
189+
// Wait a frame to ensure DOM is updated
190+
requestAnimationFrame(() => {
191+
// Start the transition - fade out current content and fade in new content
192+
currentContent.style.opacity = '0';
193+
newViewContainer.style.opacity = '1';
193194

194-
// Call afterRender if provided
195-
if (route.afterRender) {
196-
console.log('Calling afterRender function');
197-
setTimeout(() => {
198-
try {
199-
route.afterRender();
200-
} catch (error) {
201-
console.error('Error in afterRender:', error);
202-
}
203-
}, 0);
195+
// After transition completes, swap the content
196+
setTimeout(() => {
197+
// Remove the old content
198+
rootElement.removeChild(currentContent);
199+
200+
// Move the new content from the container to the root
201+
const newContent = Array.from(newViewContainer.children);
202+
newContent.forEach(child => rootElement.appendChild(child));
203+
204+
// Remove the now-empty container
205+
rootElement.removeChild(newViewContainer);
206+
207+
// Call afterRender if provided
208+
if (route.afterRender) {
209+
console.log('Calling afterRender function');
210+
setTimeout(() => {
211+
try {
212+
route.afterRender();
213+
} catch (error) {
214+
console.error('Error in afterRender:', error);
215+
}
216+
}, 0);
217+
}
218+
219+
// Dispatch route changed event
220+
window.dispatchEvent(new CustomEvent('route-changed', {
221+
detail: { path, route }
222+
}));
223+
}, 200); // Match the transition duration
224+
});
225+
} else {
226+
// Handle 404 with the same transition approach
227+
const newViewContainer = document.createElement('div');
228+
newViewContainer.className = 'new-route-container';
229+
newViewContainer.style.position = 'absolute';
230+
newViewContainer.style.top = '0';
231+
newViewContainer.style.left = '0';
232+
newViewContainer.style.width = '100%';
233+
newViewContainer.style.height = '100%';
234+
newViewContainer.style.opacity = '0';
235+
newViewContainer.style.zIndex = '5';
236+
newViewContainer.style.transition = 'opacity 200ms ease-in-out';
237+
238+
// Call the error handler to get the 404 content
239+
this.errorHandler(path, newViewContainer);
240+
241+
// Make the root element a positioned container if it's not already
242+
const rootPosition = window.getComputedStyle(rootElement).position;
243+
if (rootPosition === 'static') {
244+
rootElement.style.position = 'relative';
245+
}
246+
247+
// Add the new view container to the DOM but keep it invisible
248+
rootElement.appendChild(newViewContainer);
249+
250+
// Create a wrapper for the current content
251+
const currentContent = document.createElement('div');
252+
currentContent.className = 'current-route-container';
253+
currentContent.style.position = 'relative';
254+
currentContent.style.zIndex = '1';
255+
currentContent.style.opacity = '1';
256+
currentContent.style.transition = 'opacity 200ms ease-in-out';
257+
258+
// Move all current children (except the new view container) into the wrapper
259+
Array.from(rootElement.children).forEach(child => {
260+
if (child !== newViewContainer) {
261+
currentContent.appendChild(child);
204262
}
263+
});
264+
265+
// Add the current content wrapper back to the root
266+
rootElement.appendChild(currentContent);
267+
268+
// Wait a frame to ensure DOM is updated
269+
requestAnimationFrame(() => {
270+
// Start the transition - fade out current content and fade in new content
271+
currentContent.style.opacity = '0';
272+
newViewContainer.style.opacity = '1';
205273

206-
// Dispatch route changed event
207-
window.dispatchEvent(new CustomEvent('route-changed', {
208-
detail: { path, route }
209-
}));
210-
}, 150); // Match the transition duration
211-
} else {
212-
// Handle 404
213-
this.errorHandler(path, rootElement);
274+
// After transition completes, swap the content
275+
setTimeout(() => {
276+
// Remove the old content
277+
rootElement.removeChild(currentContent);
278+
279+
// Move the new content from the container to the root
280+
const newContent = Array.from(newViewContainer.children);
281+
newContent.forEach(child => rootElement.appendChild(child));
282+
283+
// Remove the now-empty container
284+
rootElement.removeChild(newViewContainer);
285+
}, 200); // Match the transition duration
286+
});
214287
}
215288
} catch (error) {
216289
console.error('Error rendering route:', error);
217-
rootElement.innerHTML = `<div class="error">Error loading page: ${error.message}</div>`;
290+
291+
// Handle errors with the same transition approach
292+
const errorContainer = document.createElement('div');
293+
errorContainer.className = 'error';
294+
errorContainer.innerHTML = `<div class="error">Error loading page: ${error.message}</div>`;
295+
296+
// Replace the content with the error message
297+
rootElement.innerHTML = '';
298+
rootElement.appendChild(errorContainer);
218299
} finally {
219300
// Clear loading state
220301
this.loading = false;
@@ -269,16 +350,24 @@ class Router {
269350
/**
270351
* Default 404 error handler
271352
* @param {string} path - Route path
272-
* @param {HTMLElement} rootElement - Root element
353+
* @param {HTMLElement} container - Container element for the error content
273354
*/
274-
defaultErrorHandler(path, rootElement) {
275-
rootElement.innerHTML = `
276-
<div class="error-page">
355+
defaultErrorHandler(path, container) {
356+
// Create error content
357+
const errorContent = document.createElement('div');
358+
errorContent.className = 'error-page';
359+
errorContent.innerHTML = `
360+
<pf-header></pf-header>
361+
<div class="error-content">
277362
<h1>404 - Page Not Found</h1>
278363
<p>The page "${path}" could not be found.</p>
279364
<a href="/" class="back-link">Go back to home</a>
280365
</div>
366+
<pf-footer></pf-footer>
281367
`;
368+
369+
// Add to container
370+
container.appendChild(errorContent);
282371
}
283372
}
284373

0 commit comments

Comments
 (0)