feat(post): improve media upload UX, drafts and safety actions
This commit is contained in:
commit
6030143b7a
34 files changed
+8997
No files matched your search
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
@@ -0,0 +1,110 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
type AuthFormProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
submitLabel: string;
|
||||
fields: {
|
||||
label: string;
|
||||
type: string;
|
||||
placeholder: string;
|
||||
}[];
|
||||
footerText: string;
|
||||
footerLinkText: string;
|
||||
footerLinkHref: string;
|
||||
};
|
||||
|
||||
export default function AuthForm({
|
||||
title,
|
||||
description,
|
||||
submitLabel,
|
||||
fields,
|
||||
footerText,
|
||||
footerLinkText,
|
||||
footerLinkHref,
|
||||
}: AuthFormProps) {
|
||||
// ✅ Hooks IMMER hier drin
|
||||
const [formData, setFormData] = useState<Record<string, string>>({});
|
||||
|
||||
function handleSubmit(event: React.FormEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
if (Object.values(formData).some((value) => value.trim() === '')) {
|
||||
alert('Bitte fülle alle Felder aus.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Formulardaten:', formData);
|
||||
alert('Formular ist technisch bereit ✅ (Backend kommt später)');
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
style={{
|
||||
maxWidth: '420px',
|
||||
margin: '4rem auto',
|
||||
padding: '2rem',
|
||||
border: '1px solid #e5e5e5',
|
||||
borderRadius: '16px',
|
||||
display: 'grid',
|
||||
gap: '1.5rem',
|
||||
}}
|
||||
>
|
||||
<header>
|
||||
<h1 style={{ marginBottom: '0.5rem' }}>{title}</h1>
|
||||
<p style={{ margin: 0, color: '#555' }}>{description}</p>
|
||||
</header>
|
||||
|
||||
<form onSubmit={handleSubmit} style={{ display: 'grid', gap: '1rem' }}>
|
||||
{fields.map((field) => (
|
||||
<label
|
||||
key={field.label}
|
||||
style={{ display: 'grid', gap: '0.3rem' }}
|
||||
>
|
||||
<span>{field.label}</span>
|
||||
<input
|
||||
type={field.type}
|
||||
placeholder={field.placeholder}
|
||||
value={formData[field.label] || ''}
|
||||
onChange={(event) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
[field.label]: event.target.value,
|
||||
})
|
||||
}
|
||||
style={{
|
||||
padding: '0.6rem',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
padding: '0.7rem',
|
||||
borderRadius: '10px',
|
||||
border: 'none',
|
||||
backgroundColor: '#2E7D32',
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<footer style={{ textAlign: 'center', fontSize: '0.9rem' }}>
|
||||
{footerText}{' '}
|
||||
<Link href={footerLinkHref}>{footerLinkText}</Link>
|
||||
</footer>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
type Comment = {
|
||||
id: number;
|
||||
author: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
comments: Comment[];
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
// 🔐 Simulierter eingeloggter User
|
||||
const CURRENT_USER_NAME = 'Du';
|
||||
|
||||
// 😊 Kleine, sinnvolle Emoji-Auswahl (Start)
|
||||
const EMOJIS = ['😍', '😂', '🥰', '🐶', '🐱', '❤️', '🔥', '👏', '🥹', '😎'];
|
||||
|
||||
export default function CommentModal({ comments, onClose }: Props) {
|
||||
const [localComments, setLocalComments] = useState<Comment[]>(comments);
|
||||
const [newComment, setNewComment] = useState('');
|
||||
const [showEmojis, setShowEmojis] = useState(false);
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!newComment.trim()) return;
|
||||
|
||||
const optimisticComment: Comment = {
|
||||
id: Date.now(),
|
||||
author: CURRENT_USER_NAME,
|
||||
text: newComment.trim(),
|
||||
};
|
||||
|
||||
setLocalComments((prev) => [...prev, optimisticComment]);
|
||||
setNewComment('');
|
||||
setShowEmojis(false);
|
||||
|
||||
console.log('NEW COMMENT (mock):', optimisticComment);
|
||||
}
|
||||
|
||||
function addEmoji(emoji: string) {
|
||||
setNewComment((prev) => prev + emoji);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '16px',
|
||||
width: '100%',
|
||||
maxWidth: '520px',
|
||||
maxHeight: '80vh',
|
||||
display: 'grid',
|
||||
gridTemplateRows: 'auto 1fr auto',
|
||||
}}
|
||||
>
|
||||
{/* HEADER */}
|
||||
<header
|
||||
style={{
|
||||
padding: '1rem',
|
||||
borderBottom: '1px solid #eee',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<strong>Kommentare</strong>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
cursor: 'pointer',
|
||||
fontSize: '1.2rem',
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* LISTE */}
|
||||
<section
|
||||
style={{
|
||||
padding: '1rem',
|
||||
overflowY: 'auto',
|
||||
display: 'grid',
|
||||
gap: '0.8rem',
|
||||
}}
|
||||
>
|
||||
{localComments.length === 0 && (
|
||||
<p style={{ color: '#777' }}>Noch keine Kommentare.</p>
|
||||
)}
|
||||
|
||||
{localComments.map((comment) => (
|
||||
<div key={comment.id}>
|
||||
<strong style={{ fontSize: '0.85rem' }}>
|
||||
{comment.author}
|
||||
</strong>
|
||||
<div style={{ fontSize: '0.9rem' }}>{comment.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{/* EINGABE */}
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
style={{
|
||||
padding: '0.8rem',
|
||||
borderTop: '1px solid #eee',
|
||||
display: 'grid',
|
||||
gap: '0.4rem',
|
||||
}}
|
||||
>
|
||||
{/* Emoji Picker */}
|
||||
{showEmojis && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '0.4rem',
|
||||
flexWrap: 'wrap',
|
||||
padding: '0.4rem',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: '#f5f5f5',
|
||||
}}
|
||||
>
|
||||
{EMOJIS.map((emoji) => (
|
||||
<button
|
||||
key={emoji}
|
||||
type="button"
|
||||
onClick={() => addEmoji(emoji)}
|
||||
style={{
|
||||
fontSize: '1.3rem',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input Row */}
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowEmojis((prev) => !prev)}
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
cursor: 'pointer',
|
||||
fontSize: '1.3rem',
|
||||
}}
|
||||
title="Emoji hinzufügen"
|
||||
>
|
||||
😊
|
||||
</button>
|
||||
|
||||
<input
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
placeholder="Kommentar schreiben …"
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '0.6rem 0.8rem',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
padding: '0.6rem 1rem',
|
||||
borderRadius: '12px',
|
||||
border: 'none',
|
||||
backgroundColor: '#2E7D32',
|
||||
color: 'white',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Senden
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
type FreshUser = {
|
||||
id: number;
|
||||
name: string;
|
||||
avatar: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
users: FreshUser[];
|
||||
onSelectUser: (userId: number | null) => void;
|
||||
};
|
||||
|
||||
export default function FreshPostsSlider({ users, onSelectUser }: Props) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '0.8rem',
|
||||
overflowX: 'auto',
|
||||
paddingBottom: '0.5rem',
|
||||
}}
|
||||
>
|
||||
{/* Alle */}
|
||||
<button
|
||||
onClick={() => onSelectUser(null)}
|
||||
style={{
|
||||
minWidth: '64px',
|
||||
height: '64px',
|
||||
borderRadius: '50%',
|
||||
border: '2px solid #ccc',
|
||||
background: '#f5f5f5',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Alle
|
||||
</button>
|
||||
|
||||
{users.map((user) => (
|
||||
<button
|
||||
key={user.id}
|
||||
onClick={() => onSelectUser(user.id)}
|
||||
style={{
|
||||
minWidth: '64px',
|
||||
height: '64px',
|
||||
borderRadius: '50%',
|
||||
border: '2px solid #2E7D32',
|
||||
backgroundImage: `url(${user.avatar})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
title={user.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
export default function Navigation() {
|
||||
const pathname = usePathname();
|
||||
|
||||
function isActive(path: string) {
|
||||
return pathname === path ? 'active' : '';
|
||||
}
|
||||
|
||||
return (
|
||||
<header
|
||||
style={{
|
||||
padding: '1rem 2rem',
|
||||
backgroundColor: '#2E7D32',
|
||||
}}
|
||||
>
|
||||
<nav className="navbar">
|
||||
<strong style={{ fontSize: '1.2rem', color: 'white' }}>
|
||||
OnlyPets
|
||||
</strong>
|
||||
|
||||
<Link href="/" className={isActive('/')}>Start</Link>
|
||||
<Link href="/feed" className={isActive('/feed')}>Feed</Link>
|
||||
<Link href="/explore" className={isActive('/explore')}>Entdecken</Link>
|
||||
<Link href="/post/new" className={isActive('/post/new')}>Neuer Beitrag</Link>
|
||||
<Link href="/profile" className={isActive('/profile')}>Profil</Link>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import CommentModal from '../CommentModal/CommentModal';
|
||||
|
||||
type Comment = {
|
||||
id: number;
|
||||
author: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type Media = {
|
||||
type: 'image' | 'video';
|
||||
url: string;
|
||||
aspect: '1:1' | '9:16';
|
||||
};
|
||||
|
||||
type PostCardProps = {
|
||||
petName: string;
|
||||
petType: 'Hund' | 'Katze';
|
||||
ownerName?: string;
|
||||
text: string;
|
||||
createdAt: string;
|
||||
media?: Media;
|
||||
likes: number;
|
||||
likedByCurrentUser?: boolean;
|
||||
commentsCount: number;
|
||||
commentsList: Comment[];
|
||||
};
|
||||
|
||||
// 🔐 Simulierter eingeloggter User
|
||||
const CURRENT_USER_ID = 1;
|
||||
|
||||
export default function PostCard({
|
||||
petName,
|
||||
petType,
|
||||
ownerName,
|
||||
text,
|
||||
createdAt,
|
||||
media,
|
||||
likes,
|
||||
likedByCurrentUser,
|
||||
commentsCount,
|
||||
commentsList,
|
||||
}: PostCardProps) {
|
||||
const [showComments, setShowComments] = useState(false);
|
||||
const [showMediaFullscreen, setShowMediaFullscreen] = useState(false);
|
||||
|
||||
const [localLikes, setLocalLikes] = useState(likes);
|
||||
const [liked, setLiked] = useState(!!likedByCurrentUser);
|
||||
|
||||
const isSquare = media?.aspect === '1:1';
|
||||
|
||||
// ⛔ Scroll sperren bei Vollbild
|
||||
useEffect(() => {
|
||||
if (showMediaFullscreen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}, [showMediaFullscreen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<article
|
||||
style={{
|
||||
border: '1px solid #e5e5e5',
|
||||
borderRadius: '16px',
|
||||
backgroundColor: 'white',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* HEADER */}
|
||||
<header style={{ padding: '1rem', display: 'grid', gap: '0.2rem' }}>
|
||||
<div style={{ fontWeight: 800, fontSize: '1.1rem' }}>
|
||||
{petName}{' '}
|
||||
<span style={{ fontWeight: 500, color: '#666' }}>
|
||||
({petType})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: '0.8rem', color: '#777' }}>
|
||||
{createdAt}
|
||||
{ownerName && (
|
||||
<>
|
||||
{' · '}
|
||||
<span style={{ fontStyle: 'italic' }}>
|
||||
verwaltet von {ownerName}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* MEDIA */}
|
||||
{media && (
|
||||
<div
|
||||
onClick={() => setShowMediaFullscreen(true)}
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: isSquare ? '1 / 1' : '9 / 16',
|
||||
backgroundColor: '#000',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{media.type === 'image' && (
|
||||
<img
|
||||
src={media.url}
|
||||
alt={petName}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{media.type === 'video' && (
|
||||
<video
|
||||
src={media.url}
|
||||
muted
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TEXT */}
|
||||
<div style={{ padding: '1rem', fontSize: '0.95rem' }}>
|
||||
{text}
|
||||
</div>
|
||||
|
||||
{/* FOOTER */}
|
||||
<footer
|
||||
style={{
|
||||
padding: '0 1rem 1rem',
|
||||
display: 'flex',
|
||||
gap: '1.2rem',
|
||||
fontSize: '0.85rem',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setLiked(!liked);
|
||||
setLocalLikes((prev) => (liked ? prev - 1 : prev + 1));
|
||||
|
||||
console.log('LIKE', {
|
||||
byUser: CURRENT_USER_ID,
|
||||
pet: petName,
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 600,
|
||||
color: liked ? '#d32f2f' : '#555',
|
||||
}}
|
||||
>
|
||||
❤️ {localLikes}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowComments(true)}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 600,
|
||||
color: '#2E7D32',
|
||||
}}
|
||||
>
|
||||
💬 {commentsCount} Kommentare
|
||||
</button>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
{/* 💬 Kommentare */}
|
||||
{showComments && (
|
||||
<CommentModal
|
||||
comments={commentsList}
|
||||
onClose={() => setShowComments(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 🖼️ MEDIA FULLSCREEN */}
|
||||
{showMediaFullscreen && media && (
|
||||
<div
|
||||
onClick={() => setShowMediaFullscreen(false)}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.9)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 2000,
|
||||
cursor: 'zoom-out',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
maxWidth: '95vw',
|
||||
maxHeight: '95vh',
|
||||
}}
|
||||
>
|
||||
{media.type === 'image' && (
|
||||
<img
|
||||
src={media.url}
|
||||
alt={petName}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{media.type === 'video' && (
|
||||
<video
|
||||
src={media.url}
|
||||
controls
|
||||
autoPlay
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
export const dogBreeds = [
|
||||
'Mischling',
|
||||
'Affenpinscher',
|
||||
'Afghanischer Windhund',
|
||||
'Akita',
|
||||
'Alaskan Malamute',
|
||||
'American Bulldog',
|
||||
'American Pit Bull Terrier',
|
||||
'Australian Shepherd',
|
||||
'Australian Terrier',
|
||||
'Basenji',
|
||||
'Beagle',
|
||||
'Belgischer Schäferhund',
|
||||
'Bernhardiner',
|
||||
'Bichon Frisé',
|
||||
'Border Collie',
|
||||
'Boston Terrier',
|
||||
'Boxer',
|
||||
'Bull Terrier',
|
||||
'Bulldogge',
|
||||
'Cane Corso',
|
||||
'Cavalier King Charles Spaniel',
|
||||
'Chihuahua',
|
||||
'Chow-Chow',
|
||||
'Cocker Spaniel',
|
||||
'Dalmatiner',
|
||||
'Dobermann',
|
||||
'Dogo Argentino',
|
||||
'English Setter',
|
||||
'Französische Bulldogge',
|
||||
'Golden Retriever',
|
||||
'Großer Schweizer Sennenhund',
|
||||
'Havaneser',
|
||||
'Husky',
|
||||
'Irish Setter',
|
||||
'Jack Russell Terrier',
|
||||
'Labrador Retriever',
|
||||
'Leonberger',
|
||||
'Malteser',
|
||||
'Mops',
|
||||
'Neufundländer',
|
||||
'Pekinese',
|
||||
'Pinscher',
|
||||
'Pomeranian',
|
||||
'Pudel',
|
||||
'Rottweiler',
|
||||
'Samojede',
|
||||
'Schnauzer',
|
||||
'Shar Pei',
|
||||
'Shiba Inu',
|
||||
'Shih Tzu',
|
||||
'Siberian Husky',
|
||||
'Staffordshire Bull Terrier',
|
||||
'Weimaraner',
|
||||
'West Highland White Terrier',
|
||||
'Yorkshire Terrier',
|
||||
].flatMap((breed) => [`${breed}`, `${breed} x Mix`]);
|
||||
|
||||
export const catBreeds = [
|
||||
'Mischling',
|
||||
'Abessinier',
|
||||
'American Shorthair',
|
||||
'Bengal',
|
||||
'Birma',
|
||||
'Bombay',
|
||||
'Britisch Kurzhaar',
|
||||
'Cornish Rex',
|
||||
'Devon Rex',
|
||||
'Europäisch Kurzhaar',
|
||||
'Exotic Shorthair',
|
||||
'Maine Coon',
|
||||
'Manx',
|
||||
'Norwegische Waldkatze',
|
||||
'Orientalisch Kurzhaar',
|
||||
'Perser',
|
||||
'Ragdoll',
|
||||
'Russisch Blau',
|
||||
'Savannah',
|
||||
'Scottish Fold',
|
||||
'Selkirk Rex',
|
||||
'Siam',
|
||||
'Sibirische Katze',
|
||||
'Singapura',
|
||||
'Sphynx',
|
||||
'Tonkanese',
|
||||
'Türkisch Angora',
|
||||
].flatMap((breed) => [`${breed}`, `${breed} x Mix`]);
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function ExplorePage() {
|
||||
return <h1>Entdecken</h1>;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,179 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import PostCard from '../components/PostCard/PostCard';
|
||||
import FreshPostsSlider from '../components/FreshPostsSlider/FreshPostsSlider';
|
||||
|
||||
/* ---------------- TYPES ---------------- */
|
||||
|
||||
type Comment = {
|
||||
id: number;
|
||||
author: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type Media = {
|
||||
type: 'image' | 'video';
|
||||
url: string;
|
||||
aspect: '1:1' | '9:16';
|
||||
};
|
||||
|
||||
type FeedPost = {
|
||||
id: number;
|
||||
|
||||
// 🐾 Tier (Absender)
|
||||
petId: number;
|
||||
petName: string;
|
||||
petType: 'Hund' | 'Katze';
|
||||
|
||||
// 👤 Admin (sekundär)
|
||||
ownerId: number;
|
||||
ownerName: string;
|
||||
|
||||
// 📝 Inhalt
|
||||
text: string;
|
||||
createdAt: string;
|
||||
|
||||
// 📸 / 🎥 Media
|
||||
media?: Media;
|
||||
|
||||
// ❤️ Interaktion
|
||||
likes: number;
|
||||
//likedByCurrentUser: number; // ✅ FEHLTE
|
||||
commentsCount: number;
|
||||
commentsList: Comment[];
|
||||
};
|
||||
|
||||
type FeedAdmin = {
|
||||
id: number;
|
||||
name: string;
|
||||
avatar: string;
|
||||
};
|
||||
|
||||
/* ---------------- MOCK ADMINS ---------------- */
|
||||
|
||||
const mockAdmins: FeedAdmin[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Anna',
|
||||
avatar: 'https://i.pravatar.cc/100?img=12',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Markus',
|
||||
avatar: 'https://i.pravatar.cc/100?img=32',
|
||||
},
|
||||
];
|
||||
|
||||
/* ---------------- MOCK POSTS ---------------- */
|
||||
|
||||
const mockPosts: FeedPost[] = [
|
||||
{
|
||||
id: 1,
|
||||
petId: 1,
|
||||
petName: 'Luna',
|
||||
petType: 'Katze',
|
||||
|
||||
ownerId: 1,
|
||||
ownerName: 'Anna',
|
||||
|
||||
text: 'Luna genießt die Sonne auf dem Balkon ☀️',
|
||||
createdAt: 'vor 2 Stunden',
|
||||
|
||||
media: {
|
||||
type: 'image',
|
||||
url: 'https://placekitten.com/600/600',
|
||||
aspect: '1:1',
|
||||
},
|
||||
|
||||
likes: 12,
|
||||
commentsCount: 2,
|
||||
commentsList: [
|
||||
{ id: 1, author: 'Lisa', text: 'Wie süß 😍' },
|
||||
{ id: 2, author: 'Tom', text: 'Typisch Katze!' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
petId: 2,
|
||||
petName: 'Bello',
|
||||
petType: 'Hund',
|
||||
|
||||
ownerId: 2,
|
||||
ownerName: 'Markus',
|
||||
|
||||
text: 'Spaziergang im Park – jemand hatte sehr viel Energie 🐕',
|
||||
createdAt: 'heute Morgen',
|
||||
|
||||
media: {
|
||||
type: 'video',
|
||||
url: 'https://www.w3schools.com/html/mov_bbb.mp4',
|
||||
aspect: '9:16',
|
||||
},
|
||||
|
||||
likes: 20,
|
||||
|
||||
|
||||
commentsCount: 0,
|
||||
commentsList: [],
|
||||
},
|
||||
];
|
||||
|
||||
/* ---------------- PAGE ---------------- */
|
||||
|
||||
export default function FeedPage() {
|
||||
const [selectedOwnerId, setSelectedOwnerId] = useState<number | null>(null);
|
||||
|
||||
const visiblePosts = selectedOwnerId
|
||||
? mockPosts.filter((post) => post.ownerId === selectedOwnerId)
|
||||
: mockPosts;
|
||||
|
||||
return (
|
||||
<section
|
||||
style={{
|
||||
maxWidth: '600px',
|
||||
margin: '0 auto',
|
||||
display: 'grid',
|
||||
gap: '1.5rem',
|
||||
}}
|
||||
>
|
||||
{/* HEADER */}
|
||||
<header>
|
||||
<h1 style={{ marginBottom: '0.3rem' }}>Feed</h1>
|
||||
<p style={{ margin: 0, color: '#555' }}>
|
||||
Neueste Beiträge aus der OnlyPets-Community
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* 🔄 Frische Tier-Admins Slider */}
|
||||
<FreshPostsSlider
|
||||
users={mockAdmins}
|
||||
onSelectUser={setSelectedOwnerId}
|
||||
/>
|
||||
|
||||
{/* 📰 FEED */}
|
||||
<section style={{ display: 'grid', gap: '1.5rem' }}>
|
||||
{visiblePosts.length === 0 && (
|
||||
<p style={{ color: '#777' }}>
|
||||
Keine Beiträge von diesen Tier-Admins.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{visiblePosts.map((post) => (
|
||||
<PostCard
|
||||
key={post.id}
|
||||
petName={post.petName}
|
||||
petType={post.petType}
|
||||
ownerName={post.ownerName}
|
||||
text={post.text}
|
||||
createdAt={post.createdAt}
|
||||
media={post.media}
|
||||
likes={post.likes}
|
||||
commentsCount={post.commentsCount}
|
||||
commentsList={post.commentsList}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
/* Navigation */
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.navbar a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.navbar a:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.navbar a.active {
|
||||
background-color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import './globals.css';
|
||||
import Navigation from './components/Navigation';
|
||||
|
||||
export const metadata = {
|
||||
title: 'OnlyPets',
|
||||
description: 'Die Community für Hunde und Katzen',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="de">
|
||||
<body>
|
||||
<Navigation />
|
||||
<main style={{ padding: '2rem' }}>
|
||||
{children}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import AuthForm from '../components/AuthForm/AuthForm';
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<AuthForm
|
||||
title="Anmelden"
|
||||
description="Melde dich bei OnlyPets an, um Beiträge zu sehen und zu erstellen."
|
||||
submitLabel="Anmelden"
|
||||
fields={[
|
||||
{
|
||||
label: 'E-Mail',
|
||||
type: 'email',
|
||||
placeholder: 'deine@email.de',
|
||||
},
|
||||
{
|
||||
label: 'Passwort',
|
||||
type: 'password',
|
||||
placeholder: '••••••••',
|
||||
},
|
||||
]}
|
||||
footerText="Noch keinen Account?"
|
||||
footerLinkText="Jetzt registrieren"
|
||||
footerLinkHref="/register"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<section style={{ display: 'grid', gap: '2rem' }}>
|
||||
<header style={{ display: 'grid', gap: '1rem' }}>
|
||||
<h1 style={{ fontSize: '2.2rem', margin: 0 }}>
|
||||
OnlyPets 🐾
|
||||
</h1>
|
||||
|
||||
<p style={{ fontSize: '1.05rem', margin: 0, maxWidth: '60ch' }}>
|
||||
Die Community für Hunde und Katzen. Teile Momente deiner Haustiere,
|
||||
entdecke neue Fellnasen und vernetze dich mit Tierfreunden.
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
|
||||
<Link
|
||||
href="/register"
|
||||
style={{
|
||||
backgroundColor: '#2E7D32',
|
||||
color: 'white',
|
||||
textDecoration: 'none',
|
||||
padding: '0.7rem 1rem',
|
||||
borderRadius: '10px',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Registrieren
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/login"
|
||||
style={{
|
||||
backgroundColor: '#546E7A',
|
||||
color: 'white',
|
||||
textDecoration: 'none',
|
||||
padding: '0.7rem 1rem',
|
||||
borderRadius: '10px',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Anmelden
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
|
||||
}}
|
||||
>
|
||||
<article
|
||||
style={{
|
||||
border: '1px solid #e5e5e5',
|
||||
borderRadius: '14px',
|
||||
padding: '1rem',
|
||||
}}
|
||||
>
|
||||
<h2 style={{ marginTop: 0 }}>📸 Posts</h2>
|
||||
<p style={{ marginBottom: 0 }}>
|
||||
Teile Fotos und Momente deiner Haustiere. Später kommen Likes und Kommentare dazu.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article
|
||||
style={{
|
||||
border: '1px solid #e5e5e5',
|
||||
borderRadius: '14px',
|
||||
padding: '1rem',
|
||||
}}
|
||||
>
|
||||
<h2 style={{ marginTop: 0 }}>🔍 Entdecken</h2>
|
||||
<p style={{ marginBottom: 0 }}>
|
||||
Finde Hunde und Katzen nach Namen, Art und später auch nach Rasse und Filtern.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article
|
||||
style={{
|
||||
border: '1px solid #e5e5e5',
|
||||
borderRadius: '14px',
|
||||
padding: '1rem',
|
||||
}}
|
||||
>
|
||||
<h2 style={{ marginTop: 0 }}>🐾 Haustiere</h2>
|
||||
<p style={{ marginBottom: 0 }}>
|
||||
Lege Profile für deine Tiere an. Ein Nutzer kann mehrere Haustiere haben.
|
||||
</p>
|
||||
</article>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { dogBreeds, catBreeds } from '@/app/data/breeds';
|
||||
|
||||
type PetType = 'Hund' | 'Katze';
|
||||
|
||||
type Pet = {
|
||||
id: number;
|
||||
name: string;
|
||||
type: PetType;
|
||||
breed: string;
|
||||
bio?: string;
|
||||
};
|
||||
|
||||
// 🔧 Mock-Daten (später Backend)
|
||||
const mockPets: Pet[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Luna',
|
||||
type: 'Katze',
|
||||
breed: 'Bengal x Mix',
|
||||
bio: 'Neugierig, verspielt und liebt Sonne.',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Bello',
|
||||
type: 'Hund',
|
||||
breed: 'Labrador Retriever x Mix',
|
||||
bio: 'Immer gute Laune.',
|
||||
},
|
||||
];
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
export default async function EditPetPage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
const petId = Number(id);
|
||||
|
||||
const pet = mockPets.find((p) => p.id === petId);
|
||||
|
||||
if (!pet) {
|
||||
return <p>Haustier nicht gefunden.</p>;
|
||||
}
|
||||
|
||||
return <EditPetForm pet={pet} />;
|
||||
}
|
||||
|
||||
/* ---------- FORMULAR (Client Component) ---------- */
|
||||
|
||||
function EditPetForm({ pet }: { pet: Pet }) {
|
||||
const [name, setName] = useState(pet.name);
|
||||
const [type, setType] = useState<PetType>(pet.type);
|
||||
const [breed, setBreed] = useState(pet.breed);
|
||||
const [breedSearch, setBreedSearch] = useState('');
|
||||
const [bio, setBio] = useState(pet.bio ?? '');
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
|
||||
|
||||
const breeds = type === 'Hund' ? dogBreeds : catBreeds;
|
||||
const filteredBreeds = breeds.filter((b) =>
|
||||
b.toLowerCase().includes(breedSearch.toLowerCase())
|
||||
);
|
||||
|
||||
function handleSubmit(event: React.FormEvent) {
|
||||
event.preventDefault();
|
||||
setError('');
|
||||
setSuccess('');
|
||||
|
||||
if (!name.trim()) {
|
||||
setError('Name darf nicht leer sein.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!breed) {
|
||||
setError('Bitte wähle eine Rasse aus.');
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedPetPayload = {
|
||||
id: pet.id,
|
||||
name,
|
||||
type,
|
||||
breed,
|
||||
bio: bio || null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
console.log('Haustier aktualisiert (Mock):', updatedPetPayload);
|
||||
|
||||
setSuccess('Änderungen gespeichert ✅');
|
||||
}
|
||||
function handleDelete() {
|
||||
console.log('Haustier gelöscht (Mock):', pet.id);
|
||||
|
||||
// Später:
|
||||
// await fetch(`/api/pets/${pet.id}`, { method: 'DELETE' })
|
||||
// router.push('/pets');
|
||||
|
||||
setShowDeleteConfirm(false);
|
||||
alert('Haustier wurde technisch gelöscht (Mock)');
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
style={{
|
||||
maxWidth: '600px',
|
||||
margin: '0 auto',
|
||||
display: 'grid',
|
||||
gap: '1.5rem',
|
||||
}}
|
||||
>
|
||||
<header>
|
||||
<h1>Haustier bearbeiten</h1>
|
||||
<p style={{ color: '#555' }}>
|
||||
Passe die Informationen deines Haustiers an.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
style={{
|
||||
border: '1px solid #e5e5e5',
|
||||
borderRadius: '16px',
|
||||
padding: '1rem',
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
backgroundColor: 'white',
|
||||
}}
|
||||
>
|
||||
{/* Name */}
|
||||
<div style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<strong>Name</strong>
|
||||
<input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
style={{
|
||||
padding: '0.7rem',
|
||||
borderRadius: '10px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Art */}
|
||||
<div style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<strong>Tier-Art</strong>
|
||||
<select
|
||||
value={type}
|
||||
onChange={(e) => {
|
||||
setType(e.target.value as PetType);
|
||||
setBreed('');
|
||||
setBreedSearch('');
|
||||
}}
|
||||
style={{
|
||||
padding: '0.6rem',
|
||||
borderRadius: '10px',
|
||||
border: '1px solid #ccc',
|
||||
maxWidth: '220px',
|
||||
}}
|
||||
>
|
||||
<option value="Katze">Katze</option>
|
||||
<option value="Hund">Hund</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Rasse */}
|
||||
<div style={{ display: 'grid', gap: '0.5rem' }}>
|
||||
<strong>Rasse</strong>
|
||||
|
||||
<input
|
||||
placeholder="Rasse suchen …"
|
||||
value={breedSearch}
|
||||
onChange={(e) => setBreedSearch(e.target.value)}
|
||||
style={{
|
||||
padding: '0.6rem',
|
||||
borderRadius: '10px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
|
||||
<select
|
||||
value={breed}
|
||||
onChange={(e) => setBreed(e.target.value)}
|
||||
size={Math.min(filteredBreeds.length, 6)}
|
||||
style={{
|
||||
padding: '0.6rem',
|
||||
borderRadius: '10px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
>
|
||||
{filteredBreeds.map((b) => (
|
||||
<option key={b} value={b}>
|
||||
{b}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Bio */}
|
||||
<div style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<strong>Kurzbeschreibung</strong>
|
||||
<textarea
|
||||
value={bio}
|
||||
onChange={(e) => setBio(e.target.value)}
|
||||
rows={3}
|
||||
style={{
|
||||
padding: '0.8rem',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Meldungen */}
|
||||
{error && <div style={{ background: '#f8d7da', padding: '0.7rem' }}>{error}</div>}
|
||||
{success && <div style={{ background: '#d1e7dd', padding: '0.7rem' }}>{success}</div>}
|
||||
|
||||
{/* Aktionen */}
|
||||
<div style={{ display: 'flex', gap: '0.8rem' }}>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
padding: '0.8rem 1.2rem',
|
||||
borderRadius: '12px',
|
||||
border: 'none',
|
||||
backgroundColor: '#2E7D32',
|
||||
color: 'white',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
Änderungen speichern
|
||||
</button>
|
||||
|
||||
<Link href={`/pets/${pet.id}`}>Abbrechen</Link>
|
||||
<hr style={{ margin: '1rem 0', borderColor: '#eee' }} />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
style={{
|
||||
padding: '0.7rem 1.2rem',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid #d32f2f',
|
||||
backgroundColor: 'white',
|
||||
color: '#d32f2f',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Haustier löschen
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
{showDeleteConfirm && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '16px',
|
||||
padding: '1.2rem',
|
||||
maxWidth: '420px',
|
||||
width: '100%',
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
}}
|
||||
>
|
||||
<h3>Haustier wirklich löschen?</h3>
|
||||
|
||||
<p style={{ color: '#555' }}>
|
||||
Alle Beiträge dieses Haustiers gehen dabei verloren.
|
||||
Diese Aktion kann nicht rückgängig gemacht werden. Bist du dir sicher?
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.8rem', justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(false)}
|
||||
style={{
|
||||
padding: '0.6rem 1rem',
|
||||
borderRadius: '10px',
|
||||
border: '1px solid #ccc',
|
||||
backgroundColor: 'white',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
style={{
|
||||
padding: '0.6rem 1rem',
|
||||
borderRadius: '10px',
|
||||
border: 'none',
|
||||
backgroundColor: '#d32f2f',
|
||||
color: 'white',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import PostCard from '@/app/components/PostCard/PostCard';
|
||||
|
||||
type Comment = {
|
||||
id: number;
|
||||
author: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type PetPost = {
|
||||
id: number;
|
||||
text: string;
|
||||
createdAt: string;
|
||||
likes: number;
|
||||
commentsCount: number;
|
||||
commentsList: Comment[];
|
||||
};
|
||||
|
||||
type Pet = {
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'Hund' | 'Katze';
|
||||
breed: string;
|
||||
bio?: string;
|
||||
posts: PetPost[];
|
||||
};
|
||||
|
||||
const mockPets: Pet[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Luna',
|
||||
type: 'Katze',
|
||||
breed: 'Bengal x Mix',
|
||||
bio: 'Neugierig, verspielt und liebt Sonne.',
|
||||
posts: [
|
||||
{
|
||||
id: 101,
|
||||
text: 'Luna genießt die Sonne ☀️',
|
||||
createdAt: 'vor 2 Stunden',
|
||||
likes: 12,
|
||||
commentsCount: 2,
|
||||
commentsList: [
|
||||
{ id: 1, author: 'Lisa', text: 'Wie süß 😍' },
|
||||
{ id: 2, author: 'Tom', text: 'Typisch Bengal!' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Bello',
|
||||
type: 'Hund',
|
||||
breed: 'Labrador Retriever x Mix',
|
||||
bio: 'Immer gute Laune.',
|
||||
posts: [],
|
||||
},
|
||||
];
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
export default async function PetDetailPage({ params }: PageProps) {
|
||||
const { id } = await params; // ✅ DAS ist der entscheidende Fix
|
||||
const petId = Number(id);
|
||||
|
||||
console.log('PET ID:', petId);
|
||||
|
||||
const pet = mockPets.find((p) => p.id === petId);
|
||||
|
||||
if (!pet) {
|
||||
return <p>Haustier nicht gefunden.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<section style={{ maxWidth: '600px', margin: '0 auto', display: 'grid', gap: '1.5rem' }}>
|
||||
<header style={{ border: '1px solid #e5e5e5', borderRadius: '16px', padding: '1rem' }}>
|
||||
<h1>{pet.name}</h1>
|
||||
<div>{pet.type} · {pet.breed}</div>
|
||||
{pet.bio && <p>{pet.bio}</p>}
|
||||
</header>
|
||||
|
||||
<section style={{ display: 'grid', gap: '1.2rem' }}>
|
||||
<h2>Beiträge</h2>
|
||||
|
||||
{pet.posts.length === 0 && (
|
||||
<p style={{ color: '#777' }}>Dieses Haustier hat noch keine Beiträge.</p>
|
||||
)}
|
||||
|
||||
{pet.posts.map((post) => (
|
||||
<PostCard
|
||||
key={post.id}
|
||||
petName={pet.name}
|
||||
petType={pet.type}
|
||||
author="Du"
|
||||
text={post.text}
|
||||
createdAt={post.createdAt}
|
||||
likes={post.likes}
|
||||
commentsCount={post.commentsCount}
|
||||
commentsList={post.commentsList}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { dogBreeds, catBreeds } from '@/app/data/breeds';
|
||||
|
||||
type PetType = 'Hund' | 'Katze' | '';
|
||||
|
||||
export default function NewPetPage() {
|
||||
const [name, setName] = useState('');
|
||||
const [type, setType] = useState<PetType>('');
|
||||
const [breed, setBreed] = useState('');
|
||||
const [breedSearch, setBreedSearch] = useState('');
|
||||
const [bio, setBio] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
|
||||
const breeds =
|
||||
type === 'Hund'
|
||||
? dogBreeds
|
||||
: type === 'Katze'
|
||||
? catBreeds
|
||||
: [];
|
||||
|
||||
const filteredBreeds = breeds.filter((b) =>
|
||||
b.toLowerCase().includes(breedSearch.toLowerCase())
|
||||
);
|
||||
|
||||
function handleSubmit(event: React.FormEvent) {
|
||||
event.preventDefault();
|
||||
setError('');
|
||||
setSuccess('');
|
||||
|
||||
if (!name.trim()) {
|
||||
setError('Bitte gib einen Namen für dein Haustier ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
setError('Bitte wähle eine Tier-Art aus.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!breed) {
|
||||
setError('Bitte wähle eine Rasse aus.');
|
||||
return;
|
||||
}
|
||||
|
||||
const newPetPayload = {
|
||||
name,
|
||||
type,
|
||||
breed,
|
||||
bio: bio || null,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
console.log('Neues Haustier (Mock):', newPetPayload);
|
||||
|
||||
setSuccess('Haustier wurde technisch angelegt ✅');
|
||||
setName('');
|
||||
setType('');
|
||||
setBreed('');
|
||||
setBreedSearch('');
|
||||
setBio('');
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
style={{
|
||||
maxWidth: '600px',
|
||||
margin: '0 auto',
|
||||
display: 'grid',
|
||||
gap: '1.5rem',
|
||||
}}
|
||||
>
|
||||
<header>
|
||||
<h1 style={{ marginBottom: '0.5rem' }}>Haustier hinzufügen</h1>
|
||||
<p style={{ margin: 0, color: '#555' }}>
|
||||
Lege ein neues Haustier für dein Profil an.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
style={{
|
||||
border: '1px solid #e5e5e5',
|
||||
borderRadius: '16px',
|
||||
padding: '1rem',
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
backgroundColor: 'white',
|
||||
}}
|
||||
>
|
||||
{/* Name */}
|
||||
<div style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<strong>Name</strong>
|
||||
<input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="z. B. Luna"
|
||||
style={{
|
||||
padding: '0.7rem',
|
||||
borderRadius: '10px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Art */}
|
||||
<div style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<strong>Tier-Art</strong>
|
||||
<select
|
||||
value={type}
|
||||
onChange={(e) => {
|
||||
setType(e.target.value as PetType);
|
||||
setBreed('');
|
||||
setBreedSearch('');
|
||||
}}
|
||||
style={{
|
||||
padding: '0.6rem',
|
||||
borderRadius: '10px',
|
||||
border: '1px solid #ccc',
|
||||
maxWidth: '220px',
|
||||
}}
|
||||
>
|
||||
<option value="">Bitte auswählen</option>
|
||||
<option value="Katze">Katze</option>
|
||||
<option value="Hund">Hund</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Rasse */}
|
||||
{type && (
|
||||
<div style={{ display: 'grid', gap: '0.5rem' }}>
|
||||
<strong>Rasse</strong>
|
||||
|
||||
<input
|
||||
placeholder="Rasse suchen …"
|
||||
value={breedSearch}
|
||||
onChange={(e) => setBreedSearch(e.target.value)}
|
||||
style={{
|
||||
padding: '0.6rem',
|
||||
borderRadius: '10px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
|
||||
<select
|
||||
value={breed}
|
||||
onChange={(e) => setBreed(e.target.value)}
|
||||
size={Math.min(filteredBreeds.length, 6)}
|
||||
style={{
|
||||
padding: '0.6rem',
|
||||
borderRadius: '10px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
>
|
||||
{filteredBreeds.map((b) => (
|
||||
<option key={b} value={b}>
|
||||
{b}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bio */}
|
||||
<div style={{ display: 'grid', gap: '0.4rem' }}>
|
||||
<strong>Kurzbeschreibung (optional)</strong>
|
||||
<textarea
|
||||
value={bio}
|
||||
onChange={(e) => setBio(e.target.value)}
|
||||
placeholder="Alter, Charakter, Besonderheiten …"
|
||||
rows={3}
|
||||
style={{
|
||||
padding: '0.8rem',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Meldungen */}
|
||||
{error && (
|
||||
<div style={{ background: '#f8d7da', padding: '0.7rem' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div style={{ background: '#d1e7dd', padding: '0.7rem' }}>
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Aktionen */}
|
||||
<div style={{ display: 'flex', gap: '0.8rem' }}>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
padding: '0.8rem 1.2rem',
|
||||
borderRadius: '12px',
|
||||
border: 'none',
|
||||
backgroundColor: '#2E7D32',
|
||||
color: 'white',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
Haustier speichern
|
||||
</button>
|
||||
|
||||
<Link href="/pets">Abbrechen</Link>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
type Pet = {
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'Hund' | 'Katze';
|
||||
};
|
||||
|
||||
const mockPets: Pet[] = [
|
||||
{ id: 1, name: 'Luna', type: 'Katze' },
|
||||
{ id: 2, name: 'Bello', type: 'Hund' },
|
||||
];
|
||||
|
||||
export default function PetsPage() {
|
||||
return (
|
||||
<section
|
||||
style={{
|
||||
maxWidth: '600px',
|
||||
margin: '0 auto',
|
||||
display: 'grid',
|
||||
gap: '1.5rem',
|
||||
}}
|
||||
>
|
||||
<header>
|
||||
<h1 style={{ marginBottom: '0.5rem' }}>Meine Haustiere</h1>
|
||||
<p style={{ margin: 0, color: '#555' }}>
|
||||
Verwalte deine Hunde und Katzen
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section style={{ display: 'grid', gap: '1rem' }}>
|
||||
{mockPets.map((pet) => (
|
||||
<Link
|
||||
key={pet.id}
|
||||
href={`/pets/${pet.id}`}
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
border: '1px solid #e5e5e5',
|
||||
borderRadius: '16px',
|
||||
padding: '1rem',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<strong>{pet.name}</strong>
|
||||
<div style={{ fontSize: '0.9rem', color: '#555' }}>
|
||||
{pet.type}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span style={{ fontSize: '1.2rem' }}>›</span>
|
||||
</Link>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<Link
|
||||
href="/pets/new"
|
||||
style={{
|
||||
alignSelf: 'flex-start',
|
||||
padding: '0.7rem 1rem',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: '#2E7D32',
|
||||
color: 'white',
|
||||
textDecoration: 'none',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
➕ Haustier hinzufügen
|
||||
</Link>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
type Pet = {
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'Hund' | 'Katze';
|
||||
};
|
||||
|
||||
type Media = {
|
||||
type: 'image' | 'video';
|
||||
url: string;
|
||||
aspect: '1:1' | '9:16';
|
||||
};
|
||||
|
||||
/* 🔧 Mock-Pets */
|
||||
const mockPets: Pet[] = [
|
||||
{ id: 1, name: 'Luna', type: 'Katze' },
|
||||
{ id: 2, name: 'Bello', type: 'Hund' },
|
||||
];
|
||||
|
||||
export default function NewPostPage() {
|
||||
const [petId, setPetId] = useState<number | ''>('');
|
||||
const [text, setText] = useState('');
|
||||
const [media, setMedia] = useState<Media | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
const [uploadStatus, setUploadStatus] = useState<
|
||||
'idle' | 'uploading' | 'processing' | 'done' | 'error'
|
||||
>('idle');
|
||||
const DRAFT_KEY = 'onlypets:new-post-draft';
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const raw = localStorage.getItem(DRAFT_KEY);
|
||||
if (!raw) return;
|
||||
|
||||
try {
|
||||
const draft = JSON.parse(raw);
|
||||
|
||||
if (draft.petId) setPetId(draft.petId);
|
||||
if (draft.text) setText(draft.text);
|
||||
if (draft.media) setMedia(draft.media);
|
||||
|
||||
} catch (e) {
|
||||
console.warn('Draft konnte nicht geladen werden');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const draft = {
|
||||
petId,
|
||||
text,
|
||||
media,
|
||||
};
|
||||
|
||||
localStorage.setItem(DRAFT_KEY, JSON.stringify(draft));
|
||||
}, [petId, text, media]);
|
||||
|
||||
|
||||
function handleFile(file: File) {
|
||||
setError('');
|
||||
setUploadStatus('uploading');
|
||||
|
||||
const sizeMB = file.size / (1024 * 1024);
|
||||
|
||||
if (file.type.startsWith('image/')) {
|
||||
setMedia({
|
||||
type: 'image',
|
||||
url: URL.createObjectURL(file),
|
||||
aspect: '1:1',
|
||||
});
|
||||
|
||||
// Mock: Upload sofort fertig
|
||||
setUploadStatus('done');
|
||||
}
|
||||
|
||||
else if (file.type.startsWith('video/')) {
|
||||
setUploadStatus('processing');
|
||||
|
||||
const video = document.createElement('video');
|
||||
video.preload = 'metadata';
|
||||
video.src = URL.createObjectURL(file);
|
||||
|
||||
video.onloadedmetadata = () => {
|
||||
setMedia({
|
||||
type: 'video',
|
||||
url: video.src,
|
||||
aspect: '9:16',
|
||||
});
|
||||
|
||||
setUploadStatus('done');
|
||||
};
|
||||
}
|
||||
|
||||
else {
|
||||
setError('Nur Bilder oder Videos sind erlaubt.');
|
||||
setUploadStatus('error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
if (file.type.startsWith('image/')) {
|
||||
setMedia({
|
||||
type: 'image',
|
||||
url,
|
||||
aspect: '1:1',
|
||||
});
|
||||
} else if (file.type.startsWith('video/')) {
|
||||
setMedia({
|
||||
type: 'video',
|
||||
url,
|
||||
aspect: '9:16', // Standard, später wählbar
|
||||
});
|
||||
} else {
|
||||
setError('Nur Bilder oder Videos sind erlaubt.');
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setSuccess('');
|
||||
|
||||
if (!petId) {
|
||||
setError('Bitte wähle ein Haustier aus.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!text.trim() && !media) {
|
||||
setError('Text oder Media ist erforderlich.');
|
||||
localStorage.removeItem(DRAFT_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
const pet = mockPets.find((p) => p.id === petId);
|
||||
|
||||
const postPayload = {
|
||||
petId,
|
||||
petName: pet?.name,
|
||||
text,
|
||||
media,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
console.log('NEUER POST (mock):', postPayload);
|
||||
|
||||
setSuccess('Beitrag erstellt (Mock) ✅');
|
||||
setPetId('');
|
||||
setText('');
|
||||
setMedia(null);
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
style={{
|
||||
maxWidth: '600px',
|
||||
margin: '0 auto',
|
||||
display: 'grid',
|
||||
gap: '1.2rem',
|
||||
}}
|
||||
>
|
||||
<header>
|
||||
<h1>Neuer Beitrag für ein Haustier</h1>
|
||||
<p style={{ color: '#555' }}>
|
||||
Teile einen Moment aus dem Leben deines Tieres.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
style={{
|
||||
border: '1px solid #e5e5e5',
|
||||
borderRadius: '16px',
|
||||
padding: '1rem',
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
backgroundColor: 'white',
|
||||
}}
|
||||
>
|
||||
{/* Haustier */}
|
||||
<div>
|
||||
<strong>Haustier</strong>
|
||||
<select
|
||||
value={petId}
|
||||
onChange={(e) => setPetId(Number(e.target.value))}
|
||||
style={{ width: '100%', padding: '0.6rem' }}
|
||||
>
|
||||
<option value="">Bitte auswählen</option>
|
||||
{mockPets.map((pet) => (
|
||||
<option key={pet.id} value={pet.id}>
|
||||
{pet.name} ({pet.type})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Media Upload */}
|
||||
<div>
|
||||
|
||||
{/* Media Upload */}
|
||||
<div style={{ display: 'grid', gap: '0.6rem' }}>
|
||||
<strong>Foto oder Video</strong>
|
||||
|
||||
{!media && (
|
||||
<div
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
}}
|
||||
onDragLeave={() => setIsDragging(false)}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
if (file) handleFile(file);
|
||||
}}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
style={{
|
||||
padding: '1.4rem',
|
||||
borderRadius: '16px',
|
||||
border: `2px dashed ${isDragging ? '#2E7D32' : '#bbb'}`,
|
||||
backgroundColor: isDragging ? '#eef7f0' : '#fafafa',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 600,
|
||||
color: '#555',
|
||||
textAlign: 'center',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
>
|
||||
{isDragging ? 'Hier loslassen' : '➕ Moment teilen'}
|
||||
{uploadStatus !== 'idle' && (
|
||||
<div style={{ fontSize: '0.8rem', color: '#555' }}>
|
||||
{uploadStatus === 'uploading' && 'Moment wird hochgeladen…'}
|
||||
{uploadStatus === 'processing' && 'Moment wird vorbereitet…'}
|
||||
{uploadStatus === 'done' && 'Dein Moment ist Online ✨'}
|
||||
{uploadStatus === 'error' && 'Das hat leider nicht geklappt.'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*,video/*"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) handleFile(file);
|
||||
}}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Media Preview */}
|
||||
{media && (
|
||||
<div style={{ display: 'grid', gap: '0.5rem' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: media.aspect === '1:1' ? '1 / 1' : '9 / 16',
|
||||
backgroundColor: '#000',
|
||||
overflow: 'hidden',
|
||||
borderRadius: '12px',
|
||||
}}
|
||||
>
|
||||
{media.type === 'image' && (
|
||||
<img
|
||||
src={media.url}
|
||||
alt="Preview"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{media.type === 'video' && (
|
||||
<video
|
||||
src={media.url}
|
||||
controls
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMedia(null);
|
||||
setUploadStatus('idle');
|
||||
}}
|
||||
style={{
|
||||
alignSelf: 'flex-start',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#d32f2f',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
✕ Anderes Foto / Video wählen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Text */}
|
||||
<textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder="Was macht dein Haustier gerade?"
|
||||
rows={3}
|
||||
style={{ padding: '0.8rem', borderRadius: '12px' }}
|
||||
/>
|
||||
|
||||
{/* Meldungen */}
|
||||
{error && <div style={{ color: '#d32f2f' }}>{error}</div>}
|
||||
{success && <div style={{ color: '#2E7D32' }}>{success}</div>}
|
||||
|
||||
{/* Aktionen */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: '0.5rem',
|
||||
}}
|
||||
>
|
||||
{/* Abbrechen – NICHT löschen */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
window.history.back();
|
||||
}}
|
||||
style={{
|
||||
padding: '0.8rem 1.2rem',
|
||||
borderRadius: '12px',
|
||||
border: '2px solid #d32f2f',
|
||||
backgroundColor: 'transparent',
|
||||
color: '#d32f2f',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
|
||||
|
||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||
{/* Entwurf verwerfen – LÖSCHT */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setPetId('');
|
||||
setText('');
|
||||
setMedia(null);
|
||||
setUploadStatus('idle');
|
||||
localStorage.removeItem(DRAFT_KEY);
|
||||
}}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#777',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.8rem',
|
||||
}}
|
||||
>
|
||||
Entwurf verwerfen
|
||||
</button>
|
||||
|
||||
{/* Posten – PRIMARY */}
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
padding: '0.8rem 1.2rem',
|
||||
borderRadius: '12px',
|
||||
border: 'none',
|
||||
backgroundColor: '#2E7D32',
|
||||
color: 'white',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Posten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function ProfilePage() {
|
||||
return <h1>Profil</h1>;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import AuthForm from '../components/AuthForm/AuthForm';
|
||||
|
||||
export default function RegisterPage() {
|
||||
return (
|
||||
<AuthForm
|
||||
title="Registrieren"
|
||||
description="Erstelle deinen OnlyPets-Account und werde Teil der Community."
|
||||
submitLabel="Account erstellen"
|
||||
fields={[
|
||||
{
|
||||
label: 'Benutzername',
|
||||
type: 'text',
|
||||
placeholder: 'z. B. tierfreund123',
|
||||
},
|
||||
{
|
||||
label: 'E-Mail',
|
||||
type: 'email',
|
||||
placeholder: 'deine@email.de',
|
||||
},
|
||||
{
|
||||
label: 'Passwort',
|
||||
type: 'password',
|
||||
placeholder: '••••••••',
|
||||
},
|
||||
]}
|
||||
footerText="Bereits einen Account?"
|
||||
footerLinkText="Jetzt anmelden"
|
||||
footerLinkHref="/login"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function SettingsPage() {
|
||||
return <h1>Einstellungen</h1>;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
Generated
+6538
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "16.1.1",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.1",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in new issue
Block a user