Added plus icon and tabbar animations

This commit is contained in:
Tim Lappe 2025-04-26 03:43:59 +02:00
parent d542a9fcbc
commit e23add8881
7 changed files with 279 additions and 21 deletions

8
.cursor/mcp.json Normal file
View File

@ -0,0 +1,8 @@
{
"mcpServers": {
"Framelink Figma MCP": {
"command": "npx",
"args": ["-y", "figma-developer-mcp", "--figma-api-key=figd_EBG-hMtUUCufQTlaytbgfziehQe7RDA3u4kyUEHL", "--stdio"]
}
}
}

View File

@ -39,3 +39,7 @@
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
button {
-webkit-tap-highlight-color: transparent !important;
}

View File

@ -11,6 +11,17 @@
box-sizing: border-box; box-sizing: border-box;
} }
@keyframes fadeInTab {
from {
opacity: 0;
transform: translateX(10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.tab-bar { .tab-bar {
display: flex; display: flex;
background-color: #ffffff; background-color: #ffffff;
@ -38,6 +49,21 @@
transition: all 0.3s ease; transition: all 0.3s ease;
border-radius: 12px; border-radius: 12px;
margin: 0 4px; margin: 0 4px;
position: relative;
overflow: hidden;
}
.tab-bar-item::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 3px;
background-color: #3182ce;
transition: all 0.3s ease;
transform: translateX(-50%);
border-radius: 3px 3px 0 0;
} }
.tab-bar-item:hover { .tab-bar-item:hover {
@ -45,6 +71,10 @@
background-color: #f1f5f9; background-color: #f1f5f9;
} }
.tab-bar-item:hover::after {
width: 20px;
}
.tab-bar-item.active { .tab-bar-item.active {
color: #3182ce; color: #3182ce;
background-color: #ebf8ff; background-color: #ebf8ff;
@ -52,15 +82,29 @@
transform: translateY(-2px); transform: translateY(-2px);
} }
.tab-bar-item.active::after {
width: 40px;
}
.tab-icon { .tab-icon {
font-size: 22px; font-size: 22px;
margin-bottom: 6px; margin-bottom: 6px;
transition: transform 0.3s ease;
}
.tab-bar-item.active .tab-icon {
transform: scale(1.1);
} }
.tab-label { .tab-label {
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
white-space: nowrap; white-space: nowrap;
transition: all 0.3s ease;
}
.tab-bar-item.active .tab-label {
transform: scale(1.05);
} }
.tab-content { .tab-content {
@ -76,4 +120,5 @@
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
box-sizing: border-box; box-sizing: border-box;
animation: fadeInTab 0.4s ease-out;
} }

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
import './TabView.css'; import './TabView.css';
@ -21,6 +21,8 @@ export const TabView: React.FC<TabBarProps> = ({
}) => { }) => {
const [active, setActive] = useState(activeTab || tabs[0]?.id || ''); const [active, setActive] = useState(activeTab || tabs[0]?.id || '');
const [ActiveComponent, setActiveComponent] = useState<React.ComponentType<any> | null>(null); const [ActiveComponent, setActiveComponent] = useState<React.ComponentType<any> | null>(null);
const [isTransitioning, setIsTransitioning] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
if (activeTab && activeTab !== active) { if (activeTab && activeTab !== active) {
@ -31,18 +33,46 @@ export const TabView: React.FC<TabBarProps> = ({
useEffect(() => { useEffect(() => {
const activeTabData = tabs.find(tab => tab.id === active); const activeTabData = tabs.find(tab => tab.id === active);
if (activeTabData) { if (activeTabData) {
// Add transitioning effect
setIsTransitioning(true);
// Fade out current content
if (contentRef.current) {
contentRef.current.style.opacity = '0';
contentRef.current.style.transform = 'translateX(-10px)';
}
// Set new component after brief transition
setTimeout(() => {
setActiveComponent(() => activeTabData.component); setActiveComponent(() => activeTabData.component);
// Fade in new content
if (contentRef.current) {
contentRef.current.style.opacity = '1';
contentRef.current.style.transform = 'translateX(0)';
}
setIsTransitioning(false);
}, 100);
} }
}, [active, tabs]); }, [active, tabs]);
const handleTabClick = (tabId: string) => { const handleTabClick = (tabId: string) => {
if (tabId !== active && !isTransitioning) {
setActive(tabId); setActive(tabId);
onTabChange?.(tabId); onTabChange?.(tabId);
}
}; };
return ( return (
<div className="tab-container"> <div className="tab-container">
<div className="tab-content"> <div
className="tab-content"
ref={contentRef}
style={{
transition: 'opacity 0.3s ease, transform 0.3s ease',
}}
>
{ActiveComponent && <ActiveComponent />} {ActiveComponent && <ActiveComponent />}
</div> </div>
<div className="tab-bar"> <div className="tab-bar">

View File

@ -1,3 +1,5 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
@ -10,7 +12,7 @@ html, body {
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif; sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;

View File

@ -10,13 +10,141 @@
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
position: relative;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.875rem;
} }
.greeting { .greeting {
font-size: 2rem; font-size: 2rem;
font-weight: 700; font-weight: 600;
margin-bottom: 1.875rem;
color: #000000; color: #000000;
line-height: 0.75em;
font-family: 'Inter', sans-serif;
margin: 0;
}
.add-button {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #F2ADAD;
color: #000000;
border: none;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, background-color 0.2s;
}
.add-button:hover {
transform: scale(1.05);
background-color: #f09e9e;
}
.add-button span {
margin-top: -2px;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideDown {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(0);
opacity: 1;
}
to {
transform: translateY(-20px);
opacity: 0;
}
}
.textbox-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: transparent;
display: flex;
align-items: flex-start;
justify-content: center;
z-index: 1000;
padding-top: 20px;
animation: fadeIn 0.3s ease-out;
}
.textbox-overlay.closing {
animation: fadeIn 0.3s ease-out reverse;
}
.textbox-container {
background: white;
border-radius: 8px;
padding: 0;
width: 100%;
max-width: 90%;
position: relative;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
animation: slideDown 0.3s ease-out;
}
.textbox-container.closing {
animation: slideUp 0.3s ease-out;
}
.large-textbox {
width: 100%;
height: 60px;
padding: 16px 50px 16px 16px;
border: none;
border-radius: 8px;
font-family: 'Inter', sans-serif;
font-size: 20px;
resize: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.close-button {
position: absolute;
top: 15px;
right: 15px;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
z-index: 1001;
}
.close-button:hover {
color: #000;
} }
.events-section { .events-section {
@ -24,10 +152,12 @@
} }
.section-title { .section-title {
font-size: 1.5rem; font-size: 1.25rem;
font-weight: 600; font-weight: 700;
margin-bottom: 1rem; margin-bottom: 1rem;
color: #000000; color: #000000;
font-family: 'Inter', sans-serif;
line-height: 1.2em;
} }
.events-list { .events-list {
@ -60,13 +190,13 @@
} }
.event-item { .event-item {
background-color: #ec6a5e; background-color: #F2ADAD;
border-radius: 1rem; border-radius: 0.5rem;
padding: 1.125rem 1.25rem; padding: 1.125rem 1.25rem;
color: #ffffff; color: #000000;
cursor: pointer; cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s; transition: transform 0.2s, box-shadow 0.2s;
box-shadow: 0 2px 8px rgba(236, 106, 94, 0.3); box-shadow: 0 2px 8px rgba(242, 173, 173, 0.3);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 80px; min-height: 80px;
@ -74,7 +204,7 @@
.event-item:hover { .event-item:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(236, 106, 94, 0.4); box-shadow: 0 4px 12px rgba(242, 173, 173, 0.4);
} }
.event-title { .event-title {
@ -105,14 +235,13 @@
} }
.error { .error {
color: #ec6a5e; color: #F2ADAD;
} }
/* Responsive adjustments */ /* Responsive adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
.greeting { .greeting {
font-size: 1.75rem; font-size: 1.75rem;
margin-bottom: 1.5rem;
} }
.section-title { .section-title {
@ -142,7 +271,6 @@
.greeting { .greeting {
font-size: 1.5rem; font-size: 1.5rem;
margin-bottom: 1.25rem;
} }
.tomorrow-events .event-item { .tomorrow-events .event-item {

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { getEvents, Event } from '../../lib/api/endpoints'; import { getEvents, Event } from '../../lib/api/endpoints';
import './Home.css'; import './Home.css';
@ -9,6 +9,9 @@ const Home: React.FC = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [userName, setUserName] = useState('John'); const [userName, setUserName] = useState('John');
const [isTextboxOpen, setIsTextboxOpen] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const textInputRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
const fetchEvents = async () => { const fetchEvents = async () => {
@ -61,6 +64,25 @@ const Home: React.FC = () => {
fetchEvents(); fetchEvents();
}, []); }, []);
useEffect(() => {
if (isTextboxOpen && textInputRef.current) {
textInputRef.current.focus();
}
}, [isTextboxOpen]);
const handleOpenTextbox = () => {
setIsClosing(false);
setIsTextboxOpen(true);
};
const handleCloseTextbox = () => {
setIsClosing(true);
setTimeout(() => {
setIsTextboxOpen(false);
setIsClosing(false);
}, 300); // Match animation duration
};
const EventItem = ({ event }: { event: Event }) => ( const EventItem = ({ event }: { event: Event }) => (
<div className="event-item"> <div className="event-item">
<div className="event-title">{event.title}</div> <div className="event-title">{event.title}</div>
@ -82,7 +104,12 @@ const Home: React.FC = () => {
return ( return (
<div className="home-container"> <div className="home-container">
<h1 className="greeting">Hallo {userName}!</h1> <div className="header-container">
<h1 className="greeting">Hallo {userName}</h1>
<button className="add-button" onClick={handleOpenTextbox}>
<span>+</span>
</button>
</div>
<section className="events-section"> <section className="events-section">
<h2 className="section-title">Heute</h2> <h2 className="section-title">Heute</h2>
@ -111,7 +138,7 @@ const Home: React.FC = () => {
</section> </section>
<section className="events-section"> <section className="events-section">
<h2 className="section-title">Diese Woche</h2> <h2 className="section-title">Restliche Woche</h2>
<div className="events-list week-events"> <div className="events-list week-events">
{weekEvents.length > 0 ? ( {weekEvents.length > 0 ? (
weekEvents.map(event => ( weekEvents.map(event => (
@ -122,6 +149,20 @@ const Home: React.FC = () => {
)} )}
</div> </div>
</section> </section>
{isTextboxOpen && (
<div className={`textbox-overlay ${isClosing ? 'closing' : ''}`}>
<div className={`textbox-container ${isClosing ? 'closing' : ''}`}>
<button className="close-button" onClick={handleCloseTextbox}>×</button>
<input
ref={textInputRef}
className="large-textbox"
type="text"
placeholder="Neuen Termin eingeben..."
/>
</div>
</div>
)}
</div> </div>
); );
}; };