feat: initial release of 3-year-old child photo album website with PHP 8, SQLite, audio slideshow, and memories timeline
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,915 @@
|
||||
@import url('https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/Vazirmatn-font-face.css');
|
||||
|
||||
:root {
|
||||
--primary: #3b82f6;
|
||||
--primary-hover: #2563eb;
|
||||
--primary-light: #eff6ff;
|
||||
--accent: #f59e0b;
|
||||
--accent-pink: #ec4899;
|
||||
--accent-purple: #8b5cf6;
|
||||
--accent-green: #10b981;
|
||||
--bg-gradient: linear-gradient(135deg, #f0fdf4 0%, #eff6ff 50%, #faf5ff 100%);
|
||||
--card-bg: rgba(255, 255, 255, 0.92);
|
||||
--card-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.03);
|
||||
--radius-xl: 24px;
|
||||
--radius-lg: 16px;
|
||||
--radius-md: 12px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-gradient);
|
||||
min-height: 100vh;
|
||||
color: #1e293b;
|
||||
direction: rtl;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Floating background decoration */
|
||||
.floating-shapes {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shape {
|
||||
position: absolute;
|
||||
font-size: 2.2rem;
|
||||
opacity: 0.25;
|
||||
animation: float 14s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||||
50% { transform: translateY(-35px) rotate(15deg); }
|
||||
}
|
||||
|
||||
.shape:nth-child(1) { top: 10%; left: 8%; animation-delay: 0s; }
|
||||
.shape:nth-child(2) { top: 25%; right: 10%; animation-delay: 2s; }
|
||||
.shape:nth-child(3) { top: 60%; left: 12%; animation-delay: 4s; }
|
||||
.shape:nth-child(4) { top: 75%; right: 15%; animation-delay: 1s; }
|
||||
.shape:nth-child(5) { top: 45%; left: 45%; animation-delay: 3s; }
|
||||
|
||||
/* Container */
|
||||
.app-container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 16px 80px 16px;
|
||||
}
|
||||
|
||||
/* Hero & Header */
|
||||
.hero-card {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f0f9ff 100%);
|
||||
border: 2px solid #e0f2fe;
|
||||
border-radius: var(--radius-xl);
|
||||
padding: 32px 28px;
|
||||
box-shadow: var(--card-shadow);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
margin-bottom: 28px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
left: -50px;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background: radial-gradient(circle, rgba(59, 130, 246, 0.15) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.child-profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.avatar-badge {
|
||||
width: 95px;
|
||||
height: 95px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #60a5fa, #3b82f6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
box-shadow: 0 10px 20px rgba(59, 130, 246, 0.3);
|
||||
border: 4px solid #ffffff;
|
||||
animation: pulse-avatar 3s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes pulse-avatar {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.child-info h1 {
|
||||
font-size: 1.9rem;
|
||||
font-weight: 900;
|
||||
color: #1e3a8a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.child-info p {
|
||||
color: #64748b;
|
||||
font-size: 0.98rem;
|
||||
margin-top: 4px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.badge-age {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: #fef3c7;
|
||||
color: #b45309;
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #fde68a;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.quick-stats {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 12px 18px;
|
||||
min-width: 105px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.stat-box:hover {
|
||||
transform: translateY(-3px);
|
||||
border-color: #93c5fd;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Action Bar */
|
||||
.action-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(8px);
|
||||
padding: 6px;
|
||||
border-radius: 30px;
|
||||
border: 1px solid #e2e8f0;
|
||||
gap: 6px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 9px 18px;
|
||||
border-radius: 24px;
|
||||
font-weight: 700;
|
||||
font-size: 0.92rem;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: #3b82f6;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.35);
|
||||
}
|
||||
|
||||
.btn-group-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
padding: 10px 18px;
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 700;
|
||||
font-size: 0.92rem;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #2563eb, #1d4ed8);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-accent {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.btn-accent:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: #ffffff;
|
||||
color: #475569;
|
||||
border: 1px solid #cbd5e1;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: #f8fafc;
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
|
||||
/* Filters & Search */
|
||||
.filters-bar {
|
||||
background: var(--card-bg);
|
||||
padding: 16px 20px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e2e8f0;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.album-filter-scroll {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 4px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
padding: 6px 14px;
|
||||
border-radius: 20px;
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #e2e8f0;
|
||||
color: #475569;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-chip.active {
|
||||
background: #3b82f6;
|
||||
color: #ffffff;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
padding: 8px 36px 8px 14px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #cbd5e1;
|
||||
font-size: 0.88rem;
|
||||
outline: none;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.search-box input:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #94a3b8;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Gallery Grid */
|
||||
.gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 22px;
|
||||
}
|
||||
|
||||
.photo-card {
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e2e8f0;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.photo-card:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
border-color: #bfdbfe;
|
||||
}
|
||||
|
||||
.photo-media-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 75%; /* 4:3 Aspect Ratio */
|
||||
background: #f8fafc;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.photo-media {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
|
||||
.photo-card:hover .photo-media {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
.photo-badge-age {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
backdrop-filter: blur(4px);
|
||||
color: #ffffff;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.fav-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(4px);
|
||||
border: none;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.fav-btn:hover {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
.fav-btn.active {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.photo-content {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.photo-album-tag {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
color: #3b82f6;
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.photo-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 800;
|
||||
color: #1e293b;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.photo-caption {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.photo-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
font-size: 0.8rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.like-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: #fef2f2;
|
||||
color: #ef4444;
|
||||
font-weight: 700;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.like-action:hover {
|
||||
background: #fee2e2;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.card-actions-menu {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-icon-sm {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.btn-icon-sm:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Albums Tab Cards */
|
||||
.albums-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.album-box {
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 22px;
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.album-box:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: #93c5fd;
|
||||
box-shadow: 0 12px 20px -5px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.album-icon-badge {
|
||||
font-size: 2.8rem;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.album-box h3 {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
color: #1e293b;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.album-box p {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 16px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.album-count-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
font-weight: 700;
|
||||
font-size: 0.82rem;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
/* Quotes Tab */
|
||||
.quotes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.quote-card {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #fefce8 100%);
|
||||
border: 2px dashed #fde047;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 22px;
|
||||
position: relative;
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
.quote-bubble {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: #713f12;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 14px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quote-bubble::before {
|
||||
content: '“';
|
||||
font-size: 3.5rem;
|
||||
color: #facc15;
|
||||
opacity: 0.4;
|
||||
position: absolute;
|
||||
top: -25px;
|
||||
right: -10px;
|
||||
font-family: Georgia, serif;
|
||||
}
|
||||
|
||||
.quote-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 0.82rem;
|
||||
color: #a16207;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #fef08a;
|
||||
}
|
||||
|
||||
/* Milestones Timeline */
|
||||
.timeline-container {
|
||||
position: relative;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.timeline-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 32px;
|
||||
width: 4px;
|
||||
background: linear-gradient(to bottom, #3b82f6, #8b5cf6, #ec4899);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
margin-bottom: 28px;
|
||||
padding-right: 70px;
|
||||
}
|
||||
|
||||
.timeline-icon {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
top: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
border: 3px solid #3b82f6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.timeline-card {
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 20px;
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.timeline-header h3 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 800;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.timeline-age {
|
||||
background: #e0e7ff;
|
||||
color: #4338ca;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* Lightbox Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(15, 23, 42, 0.75);
|
||||
backdrop-filter: blur(8px);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-overlay.open {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-xl);
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
padding: 28px;
|
||||
position: relative;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
animation: modal-pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
@keyframes modal-pop {
|
||||
0% { transform: scale(0.9) translateY(20px); }
|
||||
100% { transform: scale(1) translateY(0); }
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: 18px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: #f1f5f9;
|
||||
border: none;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 800;
|
||||
color: #1e293b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid #cbd5e1;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
background: #f8fafc;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #3b82f6;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.file-drop-area {
|
||||
border: 2px dashed #93c5fd;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
background: #eff6ff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.file-drop-area:hover {
|
||||
background: #dbeafe;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Lightbox specific */
|
||||
.lightbox-modal-card {
|
||||
max-width: 900px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: #000000;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.lightbox-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.lightbox-content {
|
||||
flex-direction: row;
|
||||
height: 550px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightbox-img-area {
|
||||
flex: 1.4;
|
||||
background: #000000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.lightbox-img-area img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.lightbox-sidebar {
|
||||
flex: 1;
|
||||
background: #ffffff;
|
||||
color: #1e293b;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-xl);
|
||||
border: 2px dashed #cbd5e1;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 3.5rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 800;
|
||||
color: #334155;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 0.9rem;
|
||||
color: #64748b;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 640px) {
|
||||
.hero-card {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
.child-profile {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
.child-info h1 {
|
||||
justify-content: center;
|
||||
}
|
||||
.quick-stats {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.nav-tabs {
|
||||
overflow-x: auto;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.tab-btn {
|
||||
padding: 8px 12px;
|
||||
font-size: 0.82rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
// Hamed Site - Baby & Toddler Photo Album App
|
||||
|
||||
let currentTab = 'gallery';
|
||||
let activeAlbumId = null;
|
||||
let currentSearch = '';
|
||||
let currentFavoriteOnly = false;
|
||||
let allPhotos = [];
|
||||
let activeLightboxIndex = 0;
|
||||
let isAudioPlaying = false;
|
||||
let audioCtx = null;
|
||||
let musicInterval = null;
|
||||
|
||||
// Initialize on DOM load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadPhotos();
|
||||
loadStats();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
// Search input
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (searchInput) {
|
||||
let debounceTimeout;
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
clearTimeout(debounceTimeout);
|
||||
debounceTimeout = setTimeout(() => {
|
||||
currentSearch = e.target.value.trim();
|
||||
loadPhotos();
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
// Keyboard navigation for lightbox
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const lightbox = document.getElementById('lightboxModal');
|
||||
if (lightbox && lightbox.classList.contains('open')) {
|
||||
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
|
||||
prevLightbox();
|
||||
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
|
||||
nextLightbox();
|
||||
} else if (e.key === 'Escape') {
|
||||
closeModal('lightboxModal');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// File input drag & drop
|
||||
const dropArea = document.getElementById('fileDropArea');
|
||||
const fileInput = document.getElementById('photoFileInput');
|
||||
if (dropArea && fileInput) {
|
||||
dropArea.addEventListener('click', () => fileInput.click());
|
||||
fileInput.addEventListener('change', () => handleFileSelect(fileInput.files[0]));
|
||||
|
||||
['dragenter', 'dragover'].forEach(name => {
|
||||
dropArea.addEventListener(name, (e) => {
|
||||
e.preventDefault();
|
||||
dropArea.style.borderColor = '#2563eb';
|
||||
dropArea.style.background = '#dbeafe';
|
||||
});
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(name => {
|
||||
dropArea.addEventListener(name, (e) => {
|
||||
e.preventDefault();
|
||||
dropArea.style.borderColor = '#93c5fd';
|
||||
dropArea.style.background = '#eff6ff';
|
||||
});
|
||||
});
|
||||
|
||||
dropArea.addEventListener('drop', (e) => {
|
||||
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||
fileInput.files = e.dataTransfer.files;
|
||||
handleFileSelect(e.dataTransfer.files[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(file) {
|
||||
if (!file) return;
|
||||
const previewContainer = document.getElementById('photoPreviewContainer');
|
||||
const previewImg = document.getElementById('photoPreviewImg');
|
||||
const dropText = document.getElementById('dropText');
|
||||
|
||||
if (file.type.startsWith('image/')) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
previewImg.src = e.target.result;
|
||||
previewContainer.style.display = 'block';
|
||||
dropText.style.display = 'none';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
// Switch Tab
|
||||
function switchTab(tabName) {
|
||||
currentTab = tabName;
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.tab-pane').forEach(pane => {
|
||||
pane.style.display = pane.id === `tab-${tabName}` ? 'block' : 'none';
|
||||
});
|
||||
|
||||
if (tabName === 'gallery') {
|
||||
loadPhotos();
|
||||
} else if (tabName === 'albums') {
|
||||
loadAlbums();
|
||||
} else if (tabName === 'quotes') {
|
||||
loadQuotes();
|
||||
} else if (tabName === 'milestones') {
|
||||
loadMilestones();
|
||||
} else if (tabName === 'slideshow') {
|
||||
startSlideshow();
|
||||
}
|
||||
}
|
||||
|
||||
// API Helper
|
||||
async function api(action, data = null, isFormData = false) {
|
||||
try {
|
||||
let options = { method: data ? 'POST' : 'GET' };
|
||||
if (data) {
|
||||
if (isFormData) {
|
||||
options.body = data;
|
||||
} else {
|
||||
options.headers = { 'Content-Type': 'application/json' };
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
}
|
||||
const res = await fetch(`index.php?api=${action}`, options);
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
console.error('API Error:', err);
|
||||
showToast('خطا در برقراری ارتباط با سرور', 'error');
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Load Photos
|
||||
async function loadPhotos() {
|
||||
const grid = document.getElementById('photosGrid');
|
||||
if (!grid) return;
|
||||
grid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 40px;"><p>در حال بارگذاری عکسهای قشنگ... 👶🎈</p></div>';
|
||||
|
||||
let url = `photos&search=${encodeURIComponent(currentSearch)}`;
|
||||
if (activeAlbumId) url += `&album_id=${activeAlbumId}`;
|
||||
if (currentFavoriteOnly) url += `&favorite_only=1`;
|
||||
|
||||
const res = await api(url);
|
||||
if (res.success) {
|
||||
allPhotos = res.data;
|
||||
renderPhotos(res.data);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPhotos(photos) {
|
||||
const grid = document.getElementById('photosGrid');
|
||||
if (!photos || photos.length === 0) {
|
||||
grid.innerHTML = `
|
||||
<div class="empty-state" style="grid-column: 1/-1;">
|
||||
<div class="empty-state-icon">📸</div>
|
||||
<h3>هنوز عکسی در این بخش ثبت نشده!</h3>
|
||||
<p>با زدن دکمه «+ افزودن عکس جدید» اولین خاطره شیرین را اضافه کنید.</p>
|
||||
<button class="btn btn-primary" onclick="openModal('uploadPhotoModal')">+ افزودن اولین عکس</button>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = photos.map((p, index) => `
|
||||
<div class="photo-card" data-id="${p.id}">
|
||||
<div class="photo-media-wrapper" onclick="openLightbox(${index})">
|
||||
<img src="${p.file_path}" alt="${p.title}" class="photo-media" loading="lazy">
|
||||
<span class="photo-badge-age">${p.age_tag || '۳ سالگی'}</span>
|
||||
<button class="fav-btn ${p.is_favorite ? 'active' : ''}" onclick="event.stopPropagation(); toggleFavorite(${p.id})">
|
||||
${p.is_favorite ? '⭐' : '☆'}
|
||||
</button>
|
||||
</div>
|
||||
<div class="photo-content">
|
||||
<div class="photo-album-tag">
|
||||
<span>${p.album_icon || '🎈'}</span>
|
||||
<span>${p.album_title || 'آلبوم عمومی'}</span>
|
||||
</div>
|
||||
<h3 class="photo-title">${escapeHtml(p.title)}</h3>
|
||||
<p class="photo-caption">${escapeHtml(p.caption || '')}</p>
|
||||
<div class="photo-footer">
|
||||
<button class="like-action" onclick="likePhoto(${p.id})">
|
||||
<span>❤️</span>
|
||||
<span id="likes-count-${p.id}">${p.likes_count || 0}</span>
|
||||
</button>
|
||||
<div class="card-actions-menu">
|
||||
<span style="font-size: 0.75rem;">📅 ${p.photo_date || ''}</span>
|
||||
<button class="btn-icon-sm" title="حذف عکس" onclick="deletePhoto(${p.id})">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Filter Album
|
||||
function filterByAlbum(albumId, btnElement) {
|
||||
activeAlbumId = albumId;
|
||||
document.querySelectorAll('.filter-chip').forEach(c => c.classList.remove('active'));
|
||||
if (btnElement) btnElement.classList.add('active');
|
||||
loadPhotos();
|
||||
}
|
||||
|
||||
function toggleFavoriteFilter() {
|
||||
currentFavoriteOnly = !currentFavoriteOnly;
|
||||
const btn = document.getElementById('favFilterBtn');
|
||||
if (btn) {
|
||||
btn.classList.toggle('active', currentFavoriteOnly);
|
||||
btn.innerText = currentFavoriteOnly ? '⭐ فقط برگزیدهها' : '⭐ نمایش همه';
|
||||
}
|
||||
loadPhotos();
|
||||
}
|
||||
|
||||
// Like Photo
|
||||
async function likePhoto(id) {
|
||||
const res = await api(`like_photo&id=${id}`, {});
|
||||
if (res.success) {
|
||||
const countSpan = document.getElementById(`likes-count-${id}`);
|
||||
if (countSpan) countSpan.innerText = res.likes;
|
||||
showConfetti();
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle Favorite
|
||||
async function toggleFavorite(id) {
|
||||
const res = await api(`toggle_fav&id=${id}`, {});
|
||||
if (res.success) {
|
||||
loadPhotos();
|
||||
loadStats();
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Photo
|
||||
async function deletePhoto(id) {
|
||||
if (!confirm('آیا از حذف این عکس خاطرهانگیز مطمئن هستید؟')) return;
|
||||
const res = await api(`delete_photo&id=${id}`, {});
|
||||
if (res.success) {
|
||||
showToast('عکس با موفقیت حذف شد', 'success');
|
||||
loadPhotos();
|
||||
loadStats();
|
||||
}
|
||||
}
|
||||
|
||||
// Lightbox
|
||||
function openLightbox(index) {
|
||||
if (!allPhotos || allPhotos.length === 0) return;
|
||||
activeLightboxIndex = index;
|
||||
updateLightboxContent();
|
||||
openModal('lightboxModal');
|
||||
}
|
||||
|
||||
function updateLightboxContent() {
|
||||
const photo = allPhotos[activeLightboxIndex];
|
||||
if (!photo) return;
|
||||
|
||||
document.getElementById('lightboxImg').src = photo.file_path;
|
||||
document.getElementById('lightboxTitle').innerText = photo.title;
|
||||
document.getElementById('lightboxCaption').innerText = photo.caption || 'بدون توضیح';
|
||||
document.getElementById('lightboxAlbum').innerText = `${photo.album_icon || '🎈'} ${photo.album_title || 'عمومی'}`;
|
||||
document.getElementById('lightboxAge').innerText = photo.age_tag || '۳ سالگی';
|
||||
document.getElementById('lightboxDate').innerText = photo.photo_date || '';
|
||||
document.getElementById('lightboxLikes').innerText = photo.likes_count || 0;
|
||||
document.getElementById('lightboxDownloadBtn').href = photo.file_path;
|
||||
}
|
||||
|
||||
function nextLightbox() {
|
||||
if (activeLightboxIndex < allPhotos.length - 1) {
|
||||
activeLightboxIndex++;
|
||||
} else {
|
||||
activeLightboxIndex = 0;
|
||||
}
|
||||
updateLightboxContent();
|
||||
}
|
||||
|
||||
function prevLightbox() {
|
||||
if (activeLightboxIndex > 0) {
|
||||
activeLightboxIndex--;
|
||||
} else {
|
||||
activeLightboxIndex = allPhotos.length - 1;
|
||||
}
|
||||
updateLightboxContent();
|
||||
}
|
||||
|
||||
// Albums Tab
|
||||
async function loadAlbums() {
|
||||
const grid = document.getElementById('albumsGrid');
|
||||
if (!grid) return;
|
||||
grid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 40px;"><p>در حال بارگذاری آلبومها... 📂</p></div>';
|
||||
|
||||
const res = await api('albums');
|
||||
if (res.success) {
|
||||
if (!res.data || res.data.length === 0) {
|
||||
grid.innerHTML = '<p style="grid-column: 1/-1; text-align: center;">هنوز آلبومی ساخته نشده است.</p>';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = res.data.map(a => `
|
||||
<div class="album-box" onclick="filterByAlbum(${a.id}); switchTab('gallery');">
|
||||
<div class="album-icon-badge">${a.icon || '🎈'}</div>
|
||||
<h3>${escapeHtml(a.title)}</h3>
|
||||
<p>${escapeHtml(a.description || '')}</p>
|
||||
<div class="album-count-badge">📸 ${a.photos_count || 0} عکس</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// Quotes Tab
|
||||
async function loadQuotes() {
|
||||
const grid = document.getElementById('quotesGrid');
|
||||
if (!grid) return;
|
||||
grid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 40px;"><p>در حال بارگذاری حرفهای بامزه... 👶</p></div>';
|
||||
|
||||
const res = await api('quotes');
|
||||
if (res.success) {
|
||||
if (!res.data || res.data.length === 0) {
|
||||
grid.innerHTML = '<p style="grid-column: 1/-1; text-align: center;">هنوز جملهای ثبت نشده است.</p>';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = res.data.map(q => `
|
||||
<div class="quote-card">
|
||||
<div class="quote-bubble">${escapeHtml(q.quote)}</div>
|
||||
<div style="font-size: 0.85rem; color: #78350f; margin-bottom: 8px;">📍 ${escapeHtml(q.context || 'بدون توضیح')}</div>
|
||||
<div class="quote-meta">
|
||||
<span>🏷️ ${q.age_tag || '۳ سالگی'}</span>
|
||||
<span>📅 ${q.said_date || ''}</span>
|
||||
<button class="btn-icon-sm" onclick="deleteQuote(${q.id})">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteQuote(id) {
|
||||
if (!confirm('حذف این جمله بامزه؟')) return;
|
||||
const res = await api(`delete_quote&id=${id}`, {});
|
||||
if (res.success) {
|
||||
loadQuotes();
|
||||
loadStats();
|
||||
}
|
||||
}
|
||||
|
||||
// Milestones Tab
|
||||
async function loadMilestones() {
|
||||
const container = document.getElementById('milestonesList');
|
||||
if (!container) return;
|
||||
container.innerHTML = '<p style="text-align: center; padding: 30px;">در حال بارگذاری مراحل رشد... 🌟</p>';
|
||||
|
||||
const res = await api('milestones');
|
||||
if (res.success) {
|
||||
if (!res.data || res.data.length === 0) {
|
||||
container.innerHTML = '<p style="text-align: center;">هنوز مرحله رشدی ثبت نشده است.</p>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = res.data.map(m => `
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon">${m.icon || '🌟'}</div>
|
||||
<div class="timeline-card">
|
||||
<div class="timeline-header">
|
||||
<h3>${escapeHtml(m.title)}</h3>
|
||||
<span class="timeline-age">${m.age_tag || '۳ سالگی'}</span>
|
||||
</div>
|
||||
<p style="color: #475569; font-size: 0.9rem; line-height: 1.5; margin-bottom: 8px;">${escapeHtml(m.description || '')}</p>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; font-size: 0.78rem; color: #94a3b8;">
|
||||
<span>📅 تاریخ: ${m.milestone_date || ''}</span>
|
||||
<button class="btn-icon-sm" onclick="deleteMilestone(${m.id})">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMilestone(id) {
|
||||
if (!confirm('حذف این دستاورد؟')) return;
|
||||
const res = await api(`delete_milestone&id=${id}`, {});
|
||||
if (res.success) {
|
||||
loadMilestones();
|
||||
loadStats();
|
||||
}
|
||||
}
|
||||
|
||||
// Stats
|
||||
async function loadStats() {
|
||||
const res = await api('stats');
|
||||
if (res.success) {
|
||||
document.getElementById('statPhotosCount').innerText = res.data.photos;
|
||||
document.getElementById('statAlbumsCount').innerText = res.data.albums;
|
||||
document.getElementById('statQuotesCount').innerText = res.data.quotes;
|
||||
document.getElementById('statMilestonesCount').innerText = res.data.milestones;
|
||||
}
|
||||
}
|
||||
|
||||
// Forms Submission
|
||||
async function submitPhotoUpload(e) {
|
||||
e.preventDefault();
|
||||
const form = document.getElementById('uploadPhotoForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerText = 'در حال ذخیرهسازی... ⏳';
|
||||
|
||||
const res = await api('upload_photo', formData, true);
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerText = 'ذخیره در آلبوم 🎈';
|
||||
|
||||
if (res.success) {
|
||||
showToast('عکس با موفقیت به آلبوم اضافه شد!', 'success');
|
||||
closeModal('uploadPhotoModal');
|
||||
form.reset();
|
||||
document.getElementById('photoPreviewContainer').style.display = 'none';
|
||||
document.getElementById('dropText').style.display = 'block';
|
||||
loadPhotos();
|
||||
loadStats();
|
||||
showConfetti();
|
||||
} else {
|
||||
showToast(res.error || 'خطا در آپلود عکس', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function submitNewAlbum(e) {
|
||||
e.preventDefault();
|
||||
const form = document.getElementById('newAlbumForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
const res = await api('create_album', formData, true);
|
||||
if (res.success) {
|
||||
showToast('آلبوم جدید با موفقیت ساخته شد!', 'success');
|
||||
closeModal('newAlbumModal');
|
||||
form.reset();
|
||||
loadAlbums();
|
||||
loadStats();
|
||||
// Update album dropdown in upload modal
|
||||
updateAlbumSelectOptions();
|
||||
} else {
|
||||
showToast(res.error || 'خطا در ایجاد آلبوم', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function submitNewQuote(e) {
|
||||
e.preventDefault();
|
||||
const form = document.getElementById('newQuoteForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
const res = await api('create_quote', formData, true);
|
||||
if (res.success) {
|
||||
showToast('جمله بامزه ذخیره شد!', 'success');
|
||||
closeModal('newQuoteModal');
|
||||
form.reset();
|
||||
loadQuotes();
|
||||
loadStats();
|
||||
}
|
||||
}
|
||||
|
||||
async function submitNewMilestone(e) {
|
||||
e.preventDefault();
|
||||
const form = document.getElementById('newMilestoneForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
const res = await api('create_milestone', formData, true);
|
||||
if (res.success) {
|
||||
showToast('دستاورد رشد ثبت شد!', 'success');
|
||||
closeModal('newMilestoneModal');
|
||||
form.reset();
|
||||
loadMilestones();
|
||||
loadStats();
|
||||
}
|
||||
}
|
||||
|
||||
// Modal Helpers
|
||||
function openModal(id) {
|
||||
const modal = document.getElementById(id);
|
||||
if (modal) modal.classList.add('open');
|
||||
}
|
||||
|
||||
function closeModal(id) {
|
||||
const modal = document.getElementById(id);
|
||||
if (modal) modal.classList.remove('open');
|
||||
}
|
||||
|
||||
// Soothing Nursery Synthesizer for Slideshow
|
||||
function toggleLullaby() {
|
||||
if (isAudioPlaying) {
|
||||
stopLullaby();
|
||||
} else {
|
||||
playLullaby();
|
||||
}
|
||||
}
|
||||
|
||||
function playLullaby() {
|
||||
try {
|
||||
if (!audioCtx) {
|
||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
}
|
||||
isAudioPlaying = true;
|
||||
document.getElementById('musicToggleBtn').innerHTML = '🔊 توقف موزیک لایت';
|
||||
|
||||
const notes = [261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 523.25]; // C, D, E, F, G, A, C5
|
||||
let noteIdx = 0;
|
||||
|
||||
musicInterval = setInterval(() => {
|
||||
if (!isAudioPlaying) return;
|
||||
const osc = audioCtx.createOscillator();
|
||||
const gain = audioCtx.createGain();
|
||||
|
||||
const freq = notes[Math.floor(Math.random() * notes.length)];
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
|
||||
|
||||
gain.gain.setValueAtTime(0.08, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 1.2);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
|
||||
osc.start();
|
||||
osc.stop(audioCtx.currentTime + 1.2);
|
||||
}, 1200);
|
||||
|
||||
} catch (e) {
|
||||
console.error('Audio init error', e);
|
||||
}
|
||||
}
|
||||
|
||||
function stopLullaby() {
|
||||
isAudioPlaying = false;
|
||||
if (musicInterval) clearInterval(musicInterval);
|
||||
const btn = document.getElementById('musicToggleBtn');
|
||||
if (btn) btn.innerHTML = '🎵 پخش موزیک آرامشبخش';
|
||||
}
|
||||
|
||||
let slideshowTimer = null;
|
||||
let currentSlideshowIndex = 0;
|
||||
|
||||
function startSlideshow() {
|
||||
if (!allPhotos || allPhotos.length === 0) return;
|
||||
currentSlideshowIndex = 0;
|
||||
renderSlideshowSlide();
|
||||
|
||||
if (slideshowTimer) clearInterval(slideshowTimer);
|
||||
slideshowTimer = setInterval(() => {
|
||||
currentSlideshowIndex = (currentSlideshowIndex + 1) % allPhotos.length;
|
||||
renderSlideshowSlide();
|
||||
}, 4500);
|
||||
}
|
||||
|
||||
function renderSlideshowSlide() {
|
||||
const photo = allPhotos[currentSlideshowIndex];
|
||||
if (!photo) return;
|
||||
const slideImg = document.getElementById('slideshowCurrentImg');
|
||||
const slideTitle = document.getElementById('slideshowCurrentTitle');
|
||||
const slideDesc = document.getElementById('slideshowCurrentDesc');
|
||||
|
||||
if (slideImg) slideImg.src = photo.file_path;
|
||||
if (slideTitle) slideTitle.innerText = photo.title;
|
||||
if (slideDesc) slideDesc.innerText = photo.caption || `${photo.album_title || ''} • ${photo.age_tag || '۳ سالگی'}`;
|
||||
}
|
||||
|
||||
// Confetti Animation
|
||||
function showConfetti() {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const conf = document.createElement('div');
|
||||
conf.innerText = ['🎈', '⭐', '🎉', '💖', '👶', '✨'][Math.floor(Math.random() * 6)];
|
||||
conf.style.position = 'fixed';
|
||||
conf.style.left = Math.random() * 90 + 5 + 'vw';
|
||||
conf.style.top = '100vh';
|
||||
conf.style.fontSize = Math.random() * 20 + 20 + 'px';
|
||||
conf.style.zIndex = 9999;
|
||||
conf.style.pointerEvents = 'none';
|
||||
conf.style.transition = 'all 2.5s ease-out';
|
||||
document.body.appendChild(conf);
|
||||
|
||||
setTimeout(() => {
|
||||
conf.style.top = Math.random() * 20 + 'vh';
|
||||
conf.style.opacity = '0';
|
||||
conf.style.transform = `scale(${Math.random() + 0.5}) rotate(${Math.random() * 360}deg)`;
|
||||
}, 50);
|
||||
|
||||
setTimeout(() => conf.remove(), 2600);
|
||||
}
|
||||
}
|
||||
|
||||
// Toast
|
||||
function showToast(msg, type = 'info') {
|
||||
const toast = document.createElement('div');
|
||||
toast.style.position = 'fixed';
|
||||
toast.style.bottom = '24px';
|
||||
toast.style.right = '24px';
|
||||
toast.style.background = type === 'error' ? '#ef4444' : '#10b981';
|
||||
toast.style.color = '#ffffff';
|
||||
toast.style.padding = '12px 22px';
|
||||
toast.style.borderRadius = '12px';
|
||||
toast.style.fontWeight = '700';
|
||||
toast.style.fontSize = '0.92rem';
|
||||
toast.style.boxShadow = '0 10px 25px rgba(0,0,0,0.2)';
|
||||
toast.style.zIndex = 10000;
|
||||
toast.style.direction = 'rtl';
|
||||
toast.innerText = msg;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.transition = 'opacity 0.5s';
|
||||
toast.style.opacity = '0';
|
||||
setTimeout(() => toast.remove(), 500);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
<?php
|
||||
|
||||
spl_autoload_register(function ($class) {
|
||||
$prefix = 'App\\';
|
||||
$base_dir = __DIR__ . '/../src/';
|
||||
$len = strlen($prefix);
|
||||
if (strncmp($prefix, $class, $len) !== 0) {
|
||||
return;
|
||||
}
|
||||
$relative_class = substr($class, $len);
|
||||
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
|
||||
if (file_exists($file)) {
|
||||
require $file;
|
||||
}
|
||||
});
|
||||
|
||||
use App\Database;
|
||||
use App\Album;
|
||||
use App\Photo;
|
||||
use App\Quote;
|
||||
use App\Milestone;
|
||||
|
||||
// Handle JSON API Endpoints
|
||||
if (isset($_GET['api'])) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$api = $_GET['api'];
|
||||
|
||||
try {
|
||||
switch ($api) {
|
||||
case 'photos':
|
||||
$albumId = !empty($_GET['album_id']) ? (int)$_GET['album_id'] : null;
|
||||
$favOnly = !empty($_GET['favorite_only']);
|
||||
$search = !empty($_GET['search']) ? trim($_GET['search']) : null;
|
||||
echo json_encode(['success' => true, 'data' => Photo::all($albumId, $favOnly, $search)]);
|
||||
exit;
|
||||
|
||||
case 'albums':
|
||||
echo json_encode(['success' => true, 'data' => Album::all()]);
|
||||
exit;
|
||||
|
||||
case 'quotes':
|
||||
echo json_encode(['success' => true, 'data' => Quote::all()]);
|
||||
exit;
|
||||
|
||||
case 'milestones':
|
||||
echo json_encode(['success' => true, 'data' => Milestone::all()]);
|
||||
exit;
|
||||
|
||||
case 'stats':
|
||||
echo json_encode(['success' => true, 'data' => Photo::getStats()]);
|
||||
exit;
|
||||
|
||||
case 'like_photo':
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$likes = Photo::incrementLike($id);
|
||||
echo json_encode(['success' => true, 'likes' => $likes]);
|
||||
exit;
|
||||
|
||||
case 'toggle_fav':
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
Photo::toggleFavorite($id);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
|
||||
case 'delete_photo':
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
Photo::delete($id);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
|
||||
case 'delete_album':
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
Album::delete($id);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
|
||||
case 'delete_quote':
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
Quote::delete($id);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
|
||||
case 'delete_milestone':
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
Milestone::delete($id);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
|
||||
case 'upload_photo':
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
throw new Exception('متد نامعتبر است');
|
||||
}
|
||||
|
||||
if (empty($_FILES['photo']) || $_FILES['photo']['error'] !== UPLOAD_ERR_OK) {
|
||||
throw new Exception('لطفاً یک فایل تصویر معتبر انتخاب کنید');
|
||||
}
|
||||
|
||||
$file = $_FILES['photo'];
|
||||
$allowed = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml'];
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mime = finfo_file($finfo, $file['tmp_name']);
|
||||
finfo_close($finfo);
|
||||
|
||||
if (!in_array($mime, $allowed)) {
|
||||
throw new Exception('فرمت فایل پشتیبانی نمیشود (فقط JPG، PNG، WEBP، GIF)');
|
||||
}
|
||||
|
||||
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||
if (!$ext) $ext = 'jpg';
|
||||
$filename = 'photo_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
|
||||
$target = __DIR__ . '/uploads/' . $filename;
|
||||
|
||||
if (!move_uploaded_file($file['tmp_name'], $target)) {
|
||||
throw new Exception('خطا در انتقال فایل ذخیرهسازی');
|
||||
}
|
||||
|
||||
$albumId = (int)($_POST['album_id'] ?? 1);
|
||||
$title = trim($_POST['title'] ?? 'خاطره جدید');
|
||||
$caption = trim($_POST['caption'] ?? '');
|
||||
$ageTag = trim($_POST['age_tag'] ?? '۳ سالگی');
|
||||
$photoDate = $_POST['photo_date'] ?? date('Y-m-d');
|
||||
|
||||
$photoId = Photo::create($albumId, $title, 'uploads/' . $filename, $caption, $ageTag, $photoDate);
|
||||
echo json_encode(['success' => true, 'photo_id' => $photoId]);
|
||||
exit;
|
||||
|
||||
case 'create_album':
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
if (!$title) throw new Exception('عنوان آلبوم الزامی است');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$icon = trim($_POST['icon'] ?? '🎈');
|
||||
$color = trim($_POST['color'] ?? '#3b82f6');
|
||||
|
||||
$albumId = Album::create($title, $description, $icon, $color);
|
||||
echo json_encode(['success' => true, 'album_id' => $albumId]);
|
||||
exit;
|
||||
|
||||
case 'create_quote':
|
||||
$quote = trim($_POST['quote'] ?? '');
|
||||
if (!$quote) throw new Exception('متن جمله الزامی است');
|
||||
$ageTag = trim($_POST['age_tag'] ?? '۳ سالگی');
|
||||
$context = trim($_POST['context'] ?? '');
|
||||
$saidDate = $_POST['said_date'] ?? date('Y-m-d');
|
||||
|
||||
$id = Quote::create($quote, $ageTag, $context, $saidDate);
|
||||
echo json_encode(['success' => true, 'quote_id' => $id]);
|
||||
exit;
|
||||
|
||||
case 'create_milestone':
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
if (!$title) throw new Exception('عنوان دستاورد الزامی است');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$ageTag = trim($_POST['age_tag'] ?? '۳ سالگی');
|
||||
$icon = trim($_POST['icon'] ?? '🌟');
|
||||
$milestoneDate = $_POST['milestone_date'] ?? date('Y-m-d');
|
||||
|
||||
$id = Milestone::create($title, $description, $ageTag, $icon, $milestoneDate);
|
||||
echo json_encode(['success' => true, 'milestone_id' => $id]);
|
||||
exit;
|
||||
|
||||
default:
|
||||
echo json_encode(['success' => false, 'error' => 'مسیر یافت نشد']);
|
||||
exit;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Initial Data for HTML Render
|
||||
$albums = Album::all();
|
||||
$stats = Photo::getStats();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>آلبوم خاطرات حامد کوچولو 🎈 پسر ۳ ساله</title>
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Floating Background Shapes -->
|
||||
<div class="floating-shapes">
|
||||
<div class="shape">🎈</div>
|
||||
<div class="shape">⭐</div>
|
||||
<div class="shape">🧸</div>
|
||||
<div class="shape">🚗</div>
|
||||
<div class="shape">🎨</div>
|
||||
</div>
|
||||
|
||||
<div class="app-container">
|
||||
|
||||
<!-- Hero Header -->
|
||||
<header class="hero-card">
|
||||
<div class="child-profile">
|
||||
<div class="avatar-badge">👶</div>
|
||||
<div class="child-info">
|
||||
<h1>
|
||||
<span>آلبوم خاطرات حامد کوچولو</span>
|
||||
<span>🎈</span>
|
||||
</h1>
|
||||
<p>گنجینه عکسها، خندههای شیرین، اولین حرفها و لحظات ناب رشد پسر ۳ ساله ما 💖</p>
|
||||
<div class="badge-age">
|
||||
<span>🎂</span>
|
||||
<span>۳ سال و ۲ ماهه (متولد تابستان ۱۴۰۲)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quick-stats">
|
||||
<div class="stat-box">
|
||||
<div class="stat-number" id="statPhotosCount"><?= $stats['photos'] ?></div>
|
||||
<div class="stat-label">📸 عکس شیرین</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-number" id="statAlbumsCount"><?= $stats['albums'] ?></div>
|
||||
<div class="stat-label">📂 آلبوم موضوعی</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-number" id="statQuotesCount"><?= $stats['quotes'] ?></div>
|
||||
<div class="stat-label">💬 جمله بامزه</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-number" id="statMilestonesCount"><?= $stats['milestones'] ?></div>
|
||||
<div class="stat-label">🌟 دستاورد رشد</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Navigation & Main Actions -->
|
||||
<nav class="action-bar">
|
||||
<div class="nav-tabs">
|
||||
<button class="tab-btn active" data-tab="gallery" onclick="switchTab('gallery')">
|
||||
<span>📸</span>
|
||||
<span>گالری عکسها</span>
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="albums" onclick="switchTab('albums')">
|
||||
<span>📂</span>
|
||||
<span>آلبومها</span>
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="quotes" onclick="switchTab('quotes')">
|
||||
<span>💬</span>
|
||||
<span>حرفهای بامزه</span>
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="milestones" onclick="switchTab('milestones')">
|
||||
<span>🌟</span>
|
||||
<span>خط رشد</span>
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="slideshow" onclick="switchTab('slideshow')">
|
||||
<span>🎵</span>
|
||||
<span>اسلایدشو موزیکال</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group-actions">
|
||||
<button class="btn btn-primary" onclick="openModal('uploadPhotoModal')">
|
||||
<span>➕</span>
|
||||
<span>افزودن عکس جدید</span>
|
||||
</button>
|
||||
<button class="btn btn-accent" onclick="openModal('newAlbumModal')">
|
||||
<span>📁</span>
|
||||
<span>ایجاد آلبوم</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- TAB 1: Gallery -->
|
||||
<section id="tab-gallery" class="tab-pane">
|
||||
<div class="filters-bar">
|
||||
<div class="album-filter-scroll">
|
||||
<button class="filter-chip active" onclick="filterByAlbum(null, this)">همه عکسها</button>
|
||||
<?php foreach ($albums as $alb): ?>
|
||||
<button class="filter-chip" onclick="filterByAlbum(<?= $alb['id'] ?>, this)">
|
||||
<?= htmlspecialchars($alb['icon'] . ' ' . $alb['title']) ?>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
|
||||
<button id="favFilterBtn" class="btn btn-outline" onclick="toggleFavoriteFilter()" style="padding: 6px 14px; font-size: 0.85rem;">
|
||||
⭐ فقط برگزیدهها
|
||||
</button>
|
||||
<div class="search-box">
|
||||
<span class="search-icon">🔍</span>
|
||||
<input type="text" id="searchInput" placeholder="جستجو در خاطرات و عکسها...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Photos Grid -->
|
||||
<div class="gallery-grid" id="photosGrid">
|
||||
<!-- Dynamically loaded via JS -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TAB 2: Albums -->
|
||||
<section id="tab-albums" class="tab-pane" style="display: none;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 style="font-size: 1.3rem; font-weight: 800; color: #1e293b;">📂 دستهبندی آلبومهای خاطرات</h2>
|
||||
<button class="btn btn-primary" onclick="openModal('newAlbumModal')">+ آلبوم جدید</button>
|
||||
</div>
|
||||
<div class="albums-grid" id="albumsGrid">
|
||||
<!-- Dynamically loaded via JS -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TAB 3: Quotes -->
|
||||
<section id="tab-quotes" class="tab-pane" style="display: none;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<div>
|
||||
<h2 style="font-size: 1.3rem; font-weight: 800; color: #1e293b;">💬 دفترچه جملات و حرفهای بامزه</h2>
|
||||
<p style="font-size: 0.88rem; color: #64748b; margin-top: 4px;">ثبت تکهکلامها و حرفهای شیرین و خندهدار ۳ سالگی</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="openModal('newQuoteModal')">+ ثبت حرف بامزه</button>
|
||||
</div>
|
||||
<div class="quotes-grid" id="quotesGrid">
|
||||
<!-- Dynamically loaded via JS -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TAB 4: Milestones -->
|
||||
<section id="tab-milestones" class="tab-pane" style="display: none;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<div>
|
||||
<h2 style="font-size: 1.3rem; font-weight: 800; color: #1e293b;">🌟 خط زمانی رشد و دستاوردهای ۳ سالگی</h2>
|
||||
<p style="font-size: 0.88rem; color: #64748b; margin-top: 4px;">مراحل مهم، مهارتهای جدید و افتخارات کوچولوی دوستداشتنی</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="openModal('newMilestoneModal')">+ ثبت دستاورد جدید</button>
|
||||
</div>
|
||||
<div class="timeline-container" id="milestonesList">
|
||||
<!-- Dynamically loaded via JS -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TAB 5: Slideshow -->
|
||||
<section id="tab-slideshow" class="tab-pane" style="display: none;">
|
||||
<div style="text-align: center; margin-bottom: 20px;">
|
||||
<h2 style="font-size: 1.4rem; font-weight: 800; color: #1e293b; margin-bottom: 8px;">🎵 اسلایدشو موزیکال خاطرات</h2>
|
||||
<button id="musicToggleBtn" class="btn btn-accent" onclick="toggleLullaby()">
|
||||
🎵 پخش موزیک آرامشبخش
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="background: #0f172a; border-radius: var(--radius-xl); overflow: hidden; padding: 24px; text-align: center; box-shadow: var(--card-shadow); max-width: 900px; margin: 0 auto;">
|
||||
<div style="height: 480px; display: flex; align-items: center; justify-content: center; margin-bottom: 16px;">
|
||||
<img id="slideshowCurrentImg" src="" style="max-height: 100%; max-width: 100%; object-fit: contain; border-radius: 12px; transition: opacity 0.5s ease;" alt="Slideshow">
|
||||
</div>
|
||||
<h3 id="slideshowCurrentTitle" style="color: #ffffff; font-size: 1.3rem; font-weight: 800; margin-bottom: 6px;"></h3>
|
||||
<p id="slideshowCurrentDesc" style="color: #94a3b8; font-size: 0.95rem;"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- MODAL: Upload Photo -->
|
||||
<div class="modal-overlay" id="uploadPhotoModal">
|
||||
<div class="modal-card">
|
||||
<button class="modal-close-btn" onclick="closeModal('uploadPhotoModal')">✕</button>
|
||||
<div class="modal-header">
|
||||
<h2>📸 افزودن عکس خاطرهانگیز جدید</h2>
|
||||
</div>
|
||||
<form id="uploadPhotoForm" onsubmit="submitPhotoUpload(event)">
|
||||
<div class="form-group">
|
||||
<label>فایل تصویر</label>
|
||||
<div class="file-drop-area" id="fileDropArea">
|
||||
<div id="dropText">
|
||||
<span style="font-size: 2.5rem;">📷</span>
|
||||
<p style="font-weight: 700; color: #1e40af; margin-top: 8px;">برای انتخاب یا رها کردن عکس کلیک کنید</p>
|
||||
<p style="font-size: 0.8rem; color: #64748b;">(پشتیبانی از JPG, PNG, WEBP, GIF)</p>
|
||||
</div>
|
||||
<div id="photoPreviewContainer" style="display: none;">
|
||||
<img id="photoPreviewImg" src="" style="max-height: 180px; border-radius: 8px;" alt="پیشنمایش">
|
||||
<p style="font-size: 0.8rem; color: #059669; font-weight: 700; margin-top: 6px;">تصویر با موفقیت انتخاب شد ✓</p>
|
||||
</div>
|
||||
<input type="file" id="photoFileInput" name="photo" accept="image/*" style="display: none;" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>عنوان عکس یا مناسبت</label>
|
||||
<input type="text" name="title" class="form-control" placeholder="مثلاً: بازی با شنهای ساحل" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>آلبوم مربوطه</label>
|
||||
<select name="album_id" class="form-control" id="uploadAlbumSelect">
|
||||
<?php foreach ($albums as $alb): ?>
|
||||
<option value="<?= $alb['id'] ?>"><?= htmlspecialchars($alb['icon'] . ' ' . $alb['title']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>توضیحات و خاطره این عکس</label>
|
||||
<textarea name="caption" class="form-control" rows="3" placeholder="خاطره، حرف جالبی که زد یا حس و حال اون لحظه..."></textarea>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
||||
<div class="form-group">
|
||||
<label>برچسب سن کودک</label>
|
||||
<input type="text" name="age_tag" class="form-control" value="۳ سالگی" placeholder="مثلاً: ۳ سال و ۲ ماهگی">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>تاریخ ثبت عکس</label>
|
||||
<input type="date" name="photo_date" class="form-control" value="<?= date('Y-m-d') ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%; justify-content: center; margin-top: 10px; padding: 12px;">
|
||||
ذخیره در آلبوم 🎈
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL: New Album -->
|
||||
<div class="modal-overlay" id="newAlbumModal">
|
||||
<div class="modal-card">
|
||||
<button class="modal-close-btn" onclick="closeModal('newAlbumModal')">✕</button>
|
||||
<div class="modal-header">
|
||||
<h2>📁 ایجاد آلبوم جدید</h2>
|
||||
</div>
|
||||
<form id="newAlbumForm" onsubmit="submitNewAlbum(event)">
|
||||
<div class="form-group">
|
||||
<label>نام آلبوم</label>
|
||||
<input type="text" name="title" class="form-control" placeholder="مثلاً: بازیهای مهدکودک" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>توضیحات مختصر</label>
|
||||
<textarea name="description" class="form-control" rows="2" placeholder="توضیح کوتاه درباره عکسهای این آلبوم..."></textarea>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
||||
<div class="form-group">
|
||||
<label>آیکون ایموجی</label>
|
||||
<input type="text" name="icon" class="form-control" value="🎈" placeholder="🎂, 🎠, 🎨, 🚗...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>رنگ تم</label>
|
||||
<input type="color" name="color" class="form-control" value="#3b82f6" style="height: 44px; padding: 4px;">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%; justify-content: center; margin-top: 10px;">
|
||||
ساخت آلبوم ✨
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL: New Quote -->
|
||||
<div class="modal-overlay" id="newQuoteModal">
|
||||
<div class="modal-card">
|
||||
<button class="modal-close-btn" onclick="closeModal('newQuoteModal')">✕</button>
|
||||
<div class="modal-header">
|
||||
<h2>💬 ثبت جمله و حرف بامزه</h2>
|
||||
</div>
|
||||
<form id="newQuoteForm" onsubmit="submitNewQuote(event)">
|
||||
<div class="form-group">
|
||||
<label>جمله یا حرفی که پسر گلمون زد</label>
|
||||
<textarea name="quote" class="form-control" rows="3" placeholder="دقیقاً چی گفت؟..." required></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>موقعیت یا شرایطی که گفت</label>
|
||||
<input type="text" name="context" class="form-control" placeholder="مثلاً: موقع خوابیدن، وسط غذا خوردن...">
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
||||
<div class="form-group">
|
||||
<label>سن کودک در زمان گفتن</label>
|
||||
<input type="text" name="age_tag" class="form-control" value="۳ سالگی">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>تاریخ</label>
|
||||
<input type="date" name="said_date" class="form-control" value="<?= date('Y-m-d') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%; justify-content: center; margin-top: 10px;">
|
||||
ذخیره در دفترچه خاطرات 🧸
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL: New Milestone -->
|
||||
<div class="modal-overlay" id="newMilestoneModal">
|
||||
<div class="modal-card">
|
||||
<button class="modal-close-btn" onclick="closeModal('newMilestoneModal')">✕</button>
|
||||
<div class="modal-header">
|
||||
<h2>🌟 ثبت دستاورد رشد جدید</h2>
|
||||
</div>
|
||||
<form id="newMilestoneForm" onsubmit="submitNewMilestone(event)">
|
||||
<div class="form-group">
|
||||
<label>عنوان دستاورد یا مهارت جدید</label>
|
||||
<input type="text" name="title" class="form-control" placeholder="مثلاً: یادگیری پوشیدن کفش به تنهایی" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>توضیحات و جزئیات</label>
|
||||
<textarea name="description" class="form-control" rows="2" placeholder="چطور انجام داد و چه عکسالعملی داشت..."></textarea>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px;">
|
||||
<div class="form-group">
|
||||
<label>ایموجی</label>
|
||||
<input type="text" name="icon" class="form-control" value="🌟">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>سن</label>
|
||||
<input type="text" name="age_tag" class="form-control" value="۳ سالگی">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>تاریخ</label>
|
||||
<input type="date" name="milestone_date" class="form-control" value="<?= date('Y-m-d') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%; justify-content: center; margin-top: 10px;">
|
||||
ثبت در خط زمانی 🚀
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL: Lightbox Viewer -->
|
||||
<div class="modal-overlay" id="lightboxModal">
|
||||
<div class="modal-card lightbox-modal-card">
|
||||
<button class="modal-close-btn" style="z-index: 10; background: rgba(255,255,255,0.8);" onclick="closeModal('lightboxModal')">✕</button>
|
||||
<div class="lightbox-content">
|
||||
<div class="lightbox-img-area">
|
||||
<button onclick="prevLightbox()" style="position: absolute; right: 12px; background: rgba(0,0,0,0.5); color: #fff; border: none; font-size: 1.5rem; width: 44px; height: 44px; border-radius: 50%; cursor: pointer; z-index: 5;">❮</button>
|
||||
<img id="lightboxImg" src="" alt="بزرگنمایی">
|
||||
<button onclick="nextLightbox()" style="position: absolute; left: 12px; background: rgba(0,0,0,0.5); color: #fff; border: none; font-size: 1.5rem; width: 44px; height: 44px; border-radius: 50%; cursor: pointer; z-index: 5;">❯</button>
|
||||
</div>
|
||||
<div class="lightbox-sidebar">
|
||||
<div>
|
||||
<span id="lightboxAlbum" class="photo-album-tag" style="font-size: 0.9rem; margin-bottom: 8px;"></span>
|
||||
<h2 id="lightboxTitle" style="font-size: 1.3rem; font-weight: 800; margin-bottom: 10px;"></h2>
|
||||
<p id="lightboxCaption" style="font-size: 0.92rem; color: #475569; line-height: 1.6;"></p>
|
||||
</div>
|
||||
|
||||
<div style="border-top: 1px solid #e2e8f0; padding-top: 16px; margin-top: 20px;">
|
||||
<div style="display: flex; justify-content: space-between; font-size: 0.85rem; color: #64748b; margin-bottom: 14px;">
|
||||
<span>🏷️ سن: <strong id="lightboxAge"></strong></span>
|
||||
<span>📅 تاریخ: <strong id="lightboxDate"></strong></span>
|
||||
<span>❤️ لایک: <strong id="lightboxLikes"></strong></span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<a id="lightboxDownloadBtn" href="" download class="btn btn-outline" style="flex: 1; text-align: center; text-decoration: none; justify-content: center;">
|
||||
⬇️ دانلود عکس
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="assets/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 450" width="100%" height="100%">
|
||||
<defs>
|
||||
<linearGradient id="bg1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#fef3c7"/>
|
||||
<stop offset="100%" stop-color="#fde68a"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="600" height="450" rx="24" fill="url(#bg1)"/>
|
||||
<circle cx="300" cy="225" r="160" fill="#ffffff" opacity="0.8"/>
|
||||
<text x="300" y="140" font-size="70" text-anchor="middle">🎂</text>
|
||||
<text x="300" y="220" font-size="28" font-family="sans-serif" font-weight="bold" fill="#d97706" text-anchor="middle">جشن تولد ۳ سالگی حامد</text>
|
||||
<text x="300" y="270" font-size="18" font-family="sans-serif" fill="#b45309" text-anchor="middle">🎈 🎉 🎁 🧸 🎈</text>
|
||||
<text x="300" y="320" font-size="16" font-family="sans-serif" fill="#78350f" text-anchor="middle">شیرینترین روز زندگی ما</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 450" width="100%" height="100%">
|
||||
<defs>
|
||||
<linearGradient id="bg3" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#fce7f3"/>
|
||||
<stop offset="100%" stop-color="#fbcfe8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="600" height="450" rx="24" fill="url(#bg3)"/>
|
||||
<circle cx="300" cy="225" r="160" fill="#ffffff" opacity="0.85"/>
|
||||
<text x="300" y="140" font-size="70" text-anchor="middle">🎨</text>
|
||||
<text x="300" y="220" font-size="28" font-family="sans-serif" font-weight="bold" fill="#db2777" text-anchor="middle">شاهکار هنری حامد</text>
|
||||
<text x="300" y="270" font-size="18" font-family="sans-serif" fill="#be185d" text-anchor="middle">🌈 🖍️ ✨ 🖌️ 💖</text>
|
||||
<text x="300" y="320" font-size="16" font-family="sans-serif" fill="#9d174d" text-anchor="middle">دنیای پر از رنگ و خلاقیت کودکانه</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 450" width="100%" height="100%">
|
||||
<defs>
|
||||
<linearGradient id="bg5" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#ede9fe"/>
|
||||
<stop offset="100%" stop-color="#ddd6fe"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="600" height="450" rx="24" fill="url(#bg5)"/>
|
||||
<circle cx="300" cy="225" r="160" fill="#ffffff" opacity="0.85"/>
|
||||
<text x="300" y="140" font-size="70" text-anchor="middle">🧸</text>
|
||||
<text x="300" y="220" font-size="28" font-family="sans-serif" font-weight="bold" fill="#7c3aed" text-anchor="middle">مهندس کوچولوی خانه</text>
|
||||
<text x="300" y="270" font-size="18" font-family="sans-serif" fill="#6d28d9" text-anchor="middle">🧱 🚗 🚂 🚀 🌟</text>
|
||||
<text x="300" y="320" font-size="16" font-family="sans-serif" fill="#5b21b6" text-anchor="middle">تمرکز و شادی در دنیای اسباببازیها</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 450" width="100%" height="100%">
|
||||
<defs>
|
||||
<linearGradient id="bg2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#d1fae5"/>
|
||||
<stop offset="100%" stop-color="#a7f3d0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="600" height="450" rx="24" fill="url(#bg2)"/>
|
||||
<circle cx="300" cy="225" r="160" fill="#ffffff" opacity="0.85"/>
|
||||
<text x="300" y="140" font-size="70" text-anchor="middle">🎠</text>
|
||||
<text x="300" y="220" font-size="28" font-family="sans-serif" font-weight="bold" fill="#059669" text-anchor="middle">روز شاد در بوستان کودک</text>
|
||||
<text x="300" y="270" font-size="18" font-family="sans-serif" fill="#047857" text-anchor="middle">🛝 ⚽ 🎈 🚲 🌿</text>
|
||||
<text x="300" y="320" font-size="16" font-family="sans-serif" fill="#065f46" text-anchor="middle">خندههای از ته دل و دویدن روی چمنها</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 450" width="100%" height="100%">
|
||||
<defs>
|
||||
<linearGradient id="bg4" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#e0e7ff"/>
|
||||
<stop offset="100%" stop-color="#c7d2fe"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="600" height="450" rx="24" fill="url(#bg4)"/>
|
||||
<circle cx="300" cy="225" r="160" fill="#ffffff" opacity="0.85"/>
|
||||
<text x="300" y="140" font-size="70" text-anchor="middle">🏖️</text>
|
||||
<text x="300" y="220" font-size="28" font-family="sans-serif" font-weight="bold" fill="#4f46e5" text-anchor="middle">سفر خاطرهانگیز به شمال</text>
|
||||
<text x="300" y="270" font-size="18" font-family="sans-serif" fill="#4338ca" text-anchor="middle">🌊 🐚 🦀 🚗 ☀️</text>
|
||||
<text x="300" y="320" font-size="16" font-family="sans-serif" fill="#3730a3" text-anchor="middle">شنبازی و ساخت برجهای شنی</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use PDO;
|
||||
|
||||
class Album {
|
||||
public static function all(): array {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->query("
|
||||
SELECT a.*, COUNT(p.id) as photos_count
|
||||
FROM albums a
|
||||
LEFT JOIN photos p ON a.id = p.album_id
|
||||
GROUP BY a.id
|
||||
ORDER BY a.id DESC
|
||||
");
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public static function find(int $id): ?array {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->prepare("SELECT * FROM albums WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
return $stmt->fetch() ?: null;
|
||||
}
|
||||
|
||||
public static function create(string $title, ?string $description = '', ?string $icon = '🎈', ?string $color = '#3b82f6'): int {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->prepare("INSERT INTO albums (title, description, icon, color) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$title, $description, $icon, $color]);
|
||||
return (int)$db->lastInsertId();
|
||||
}
|
||||
|
||||
public static function delete(int $id): bool {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->prepare("DELETE FROM albums WHERE id = ?");
|
||||
return $stmt->execute([$id]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use PDO;
|
||||
|
||||
class Database {
|
||||
private static ?PDO $instance = null;
|
||||
|
||||
public static function getConnection(): PDO {
|
||||
if (self::$instance === null) {
|
||||
$dbDir = __DIR__ . '/../data';
|
||||
if (!is_dir($dbDir)) {
|
||||
mkdir($dbDir, 0777, true);
|
||||
}
|
||||
|
||||
$dbPath = $dbDir . '/database.sqlite';
|
||||
$isNew = !file_exists($dbPath);
|
||||
|
||||
self::$instance = new PDO('sqlite:' . $dbPath);
|
||||
self::$instance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
self::$instance->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
|
||||
if ($isNew || filesize($dbPath) === 0) {
|
||||
self::initSchema(self::$instance);
|
||||
self::seedInitialData(self::$instance);
|
||||
}
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private static function initSchema(PDO $db): void {
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS albums (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
cover_image TEXT,
|
||||
icon TEXT DEFAULT '🎈',
|
||||
color TEXT DEFAULT '#3b82f6',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS photos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
album_id INTEGER,
|
||||
title TEXT NOT NULL,
|
||||
caption TEXT,
|
||||
file_path TEXT NOT NULL,
|
||||
age_tag TEXT,
|
||||
photo_date DATE,
|
||||
is_favorite INTEGER DEFAULT 0,
|
||||
likes_count INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (album_id) REFERENCES albums(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS quotes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
quote TEXT NOT NULL,
|
||||
age_tag TEXT,
|
||||
said_date DATE,
|
||||
context TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS milestones (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
milestone_date DATE,
|
||||
age_tag TEXT,
|
||||
icon TEXT DEFAULT '🌟',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
);
|
||||
");
|
||||
|
||||
$db->exec("
|
||||
INSERT OR IGNORE INTO settings (key, value) VALUES
|
||||
('child_name', 'حامد کوچولو'),
|
||||
('birth_date', '2023-06-15'),
|
||||
('bio', 'آلبوم خاطرات شیرین، خندهها و لحظات رشد پسر ۳ ساله دوستداشتنی ما')
|
||||
");
|
||||
}
|
||||
|
||||
private static function seedInitialData(PDO $db): void {
|
||||
// Sample Albums
|
||||
$db->exec("
|
||||
INSERT INTO albums (title, description, icon, color) VALUES
|
||||
('تولد ۳ سالگی 🎂', 'جشن تولد با تم دایناسور و خندههای پرانرژی', '🎂', '#f59e0b'),
|
||||
('پارک و بازیهای شاد 🎠', 'دویدن، تاببازی، سرسره و حباببازی', '🎠', '#10b981'),
|
||||
('نقاشیها و کارهای هنری 🎨', 'شاهکارهای نقاشی انگشتی و دستسازههای جالب', '🎨', '#ec4899'),
|
||||
('سفرهای هیجانانگیز 🚗', 'دیدن ساحل دریا، جنگل و ماجراجوییهای خانوادگی', '🚗', '#6366f1'),
|
||||
('خاطرات و بازیهای خانه 🧸', 'قصه قبل از خواب، خانهسازی با لگو و بازیهای خودمانی', '🧸', '#8b5cf6')
|
||||
");
|
||||
|
||||
// Sample Quotes
|
||||
$db->exec("
|
||||
INSERT INTO quotes (quote, age_tag, said_date, context) VALUES
|
||||
('بابا خورشید خسته شد رفت لالا، ما هم بریم پیشش؟', '۳ سال و ۲ ماهگی', '2026-08-15', 'موقع غروب آفتاب کنار پنجره'),
|
||||
('مامان این دایناسوره باهام رفیق شده، واسش بشقاب بیار!', '۳ سال و ۱ ماهگی', '2026-07-20', 'وسط اتاق بازی با اسباببازیها'),
|
||||
('من دیگه قد درخت بزرگ شدم، سرم میخوره به آسمون!', '۲ سال و ۱۱ ماهگی', '2026-05-10', 'وقتی روی مبل ایستاد و دستهاشو بالا برد')
|
||||
");
|
||||
|
||||
// Sample Milestones
|
||||
$db->exec("
|
||||
INSERT INTO milestones (title, description, milestone_date, age_tag, icon) VALUES
|
||||
('اولین باری که سه چرخه سوار شد', 'بدون کمک پدال زد و کل حیاط رو دور زد و دست تکون داد', '2026-06-01', '۳ سالگی', '🚲'),
|
||||
('شمارش اعداد تا ۱۰ به فارسی و انگلیسی', 'اعداد ۱ تا ۱۰ رو با شعر و ریتم جذاب کامل یاد گرفت', '2026-07-12', '۳ سال و ۱ ماهگی', '🔢'),
|
||||
('اولین نقاشی چهره خانواده', 'مامان و بابا رو با دایرههای بزرگ و لبخندهای پهن کشید', '2026-08-05', '۳ سال و ۲ ماهگی', '🖍️')
|
||||
");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use PDO;
|
||||
|
||||
class Milestone {
|
||||
public static function all(): array {
|
||||
$db = Database::getConnection();
|
||||
return $db->query("SELECT * FROM milestones ORDER BY milestone_date DESC, id DESC")->fetchAll();
|
||||
}
|
||||
|
||||
public static function create(string $title, ?string $description = '', ?string $ageTag = '۳ سالگی', ?string $icon = '🌟', ?string $milestoneDate = null): int {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->prepare("INSERT INTO milestones (title, description, age_tag, icon, milestone_date) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$title, $description, $ageTag, $icon, $milestoneDate ?: date('Y-m-d')]);
|
||||
return (int)$db->lastInsertId();
|
||||
}
|
||||
|
||||
public static function delete(int $id): bool {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->prepare("DELETE FROM milestones WHERE id = ?");
|
||||
return $stmt->execute([$id]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use PDO;
|
||||
|
||||
class Photo {
|
||||
public static function all(?int $albumId = null, ?bool $favoriteOnly = false, ?string $search = null): array {
|
||||
$db = Database::getConnection();
|
||||
$sql = "SELECT p.*, a.title as album_title, a.icon as album_icon, a.color as album_color
|
||||
FROM photos p
|
||||
LEFT JOIN albums a ON p.album_id = a.id
|
||||
WHERE 1=1";
|
||||
$params = [];
|
||||
|
||||
if ($albumId) {
|
||||
$sql .= " AND p.album_id = ?";
|
||||
$params[] = $albumId;
|
||||
}
|
||||
|
||||
if ($favoriteOnly) {
|
||||
$sql .= " AND p.is_favorite = 1";
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$sql .= " AND (p.title LIKE ? OR p.caption LIKE ? OR p.age_tag LIKE ?)";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY p.id DESC";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public static function create(int $albumId, string $title, string $filePath, ?string $caption = '', ?string $ageTag = '۳ سالگی', ?string $photoDate = null): int {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO photos (album_id, title, caption, file_path, age_tag, photo_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([$albumId, $title, $caption, $filePath, $ageTag, $photoDate ?: date('Y-m-d')]);
|
||||
return (int)$db->lastInsertId();
|
||||
}
|
||||
|
||||
public static function toggleFavorite(int $id): bool {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->prepare("UPDATE photos SET is_favorite = (CASE WHEN is_favorite = 1 THEN 0 ELSE 1 END) WHERE id = ?");
|
||||
return $stmt->execute([$id]);
|
||||
}
|
||||
|
||||
public static function incrementLike(int $id): int {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->prepare("UPDATE photos SET likes_count = likes_count + 1 WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
$stmt2 = $db->prepare("SELECT likes_count FROM photos WHERE id = ?");
|
||||
$stmt2->execute([$id]);
|
||||
return (int)$stmt2->fetchColumn();
|
||||
}
|
||||
|
||||
public static function delete(int $id): bool {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->prepare("SELECT file_path FROM photos WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$file = $stmt->fetchColumn();
|
||||
if ($file && file_exists(__DIR__ . '/../public/' . $file)) {
|
||||
@unlink(__DIR__ . '/../public/' . $file);
|
||||
}
|
||||
|
||||
$stmtDel = $db->prepare("DELETE FROM photos WHERE id = ?");
|
||||
return $stmtDel->execute([$id]);
|
||||
}
|
||||
|
||||
public static function getStats(): array {
|
||||
$db = Database::getConnection();
|
||||
$photosCount = (int)$db->query("SELECT COUNT(*) FROM photos")->fetchColumn();
|
||||
$albumsCount = (int)$db->query("SELECT COUNT(*) FROM albums")->fetchColumn();
|
||||
$quotesCount = (int)$db->query("SELECT COUNT(*) FROM quotes")->fetchColumn();
|
||||
$milestonesCount = (int)$db->query("SELECT COUNT(*) FROM milestones")->fetchColumn();
|
||||
$favoritesCount = (int)$db->query("SELECT COUNT(*) FROM photos WHERE is_favorite = 1")->fetchColumn();
|
||||
|
||||
return [
|
||||
'photos' => $photosCount,
|
||||
'albums' => $albumsCount,
|
||||
'quotes' => $quotesCount,
|
||||
'milestones' => $milestonesCount,
|
||||
'favorites' => $favoritesCount,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use PDO;
|
||||
|
||||
class Quote {
|
||||
public static function all(): array {
|
||||
$db = Database::getConnection();
|
||||
return $db->query("SELECT * FROM quotes ORDER BY id DESC")->fetchAll();
|
||||
}
|
||||
|
||||
public static function create(string $quote, ?string $ageTag = '۳ سالگی', ?string $context = '', ?string $saidDate = null): int {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->prepare("INSERT INTO quotes (quote, age_tag, context, said_date) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$quote, $ageTag, $context, $saidDate ?: date('Y-m-d')]);
|
||||
return (int)$db->lastInsertId();
|
||||
}
|
||||
|
||||
public static function delete(int $id): bool {
|
||||
$db = Database::getConnection();
|
||||
$stmt = $db->prepare("DELETE FROM quotes WHERE id = ?");
|
||||
return $stmt->execute([$id]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/Database.php';
|
||||
|
||||
use App\Database;
|
||||
|
||||
$db = Database::getConnection();
|
||||
|
||||
$uploadDir = __DIR__ . '/../public/uploads';
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0777, true);
|
||||
}
|
||||
|
||||
// Generate cute SVGs
|
||||
$photosData = [
|
||||
[
|
||||
'file' => 'cake_3years.svg',
|
||||
'title' => 'فوت کردن شمع تولد ۳ سالگی 🎂',
|
||||
'caption' => 'وقتی همه با هم دست زدیم و با خوشحالی شمع ۳ سالگی رو فوت کرد!',
|
||||
'album_id' => 1,
|
||||
'age_tag' => '۳ سال تمام',
|
||||
'photo_date' => '2026-06-15',
|
||||
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 450" width="100%" height="100%">
|
||||
<defs>
|
||||
<linearGradient id="bg1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#fef3c7"/>
|
||||
<stop offset="100%" stop-color="#fde68a"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="600" height="450" rx="24" fill="url(#bg1)"/>
|
||||
<circle cx="300" cy="225" r="160" fill="#ffffff" opacity="0.8"/>
|
||||
<text x="300" y="140" font-size="70" text-anchor="middle">🎂</text>
|
||||
<text x="300" y="220" font-size="28" font-family="sans-serif" font-weight="bold" fill="#d97706" text-anchor="middle">جشن تولد ۳ سالگی حامد</text>
|
||||
<text x="300" y="270" font-size="18" font-family="sans-serif" fill="#b45309" text-anchor="middle">🎈 🎉 🎁 🧸 🎈</text>
|
||||
<text x="300" y="320" font-size="16" font-family="sans-serif" fill="#78350f" text-anchor="middle">شیرینترین روز زندگی ما</text>
|
||||
</svg>'
|
||||
],
|
||||
[
|
||||
'file' => 'park_fun.svg',
|
||||
'title' => 'تاببازی و خندههای بیپایان در پارک 🎠',
|
||||
'caption' => 'میگفت بابا منو بفرست تا پیش ابرا!',
|
||||
'album_id' => 2,
|
||||
'age_tag' => '۳ سال و ۱ ماهگی',
|
||||
'photo_date' => '2026-07-04',
|
||||
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 450" width="100%" height="100%">
|
||||
<defs>
|
||||
<linearGradient id="bg2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#d1fae5"/>
|
||||
<stop offset="100%" stop-color="#a7f3d0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="600" height="450" rx="24" fill="url(#bg2)"/>
|
||||
<circle cx="300" cy="225" r="160" fill="#ffffff" opacity="0.85"/>
|
||||
<text x="300" y="140" font-size="70" text-anchor="middle">🎠</text>
|
||||
<text x="300" y="220" font-size="28" font-family="sans-serif" font-weight="bold" fill="#059669" text-anchor="middle">روز شاد در بوستان کودک</text>
|
||||
<text x="300" y="270" font-size="18" font-family="sans-serif" fill="#047857" text-anchor="middle">🛝 ⚽ 🎈 🚲 🌿</text>
|
||||
<text x="300" y="320" font-size="16" font-family="sans-serif" fill="#065f46" text-anchor="middle">خندههای از ته دل و دویدن روی چمنها</text>
|
||||
</svg>'
|
||||
],
|
||||
[
|
||||
'file' => 'finger_painting.svg',
|
||||
'title' => 'اولین نقاشی انگشتی رنگینکمانی 🎨',
|
||||
'caption' => 'دستها و لباسها رنگی شد ولی اثر هنری شگفتانگیز بود!',
|
||||
'album_id' => 3,
|
||||
'age_tag' => '۳ سال و ۲ ماهگی',
|
||||
'photo_date' => '2026-08-10',
|
||||
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 450" width="100%" height="100%">
|
||||
<defs>
|
||||
<linearGradient id="bg3" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#fce7f3"/>
|
||||
<stop offset="100%" stop-color="#fbcfe8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="600" height="450" rx="24" fill="url(#bg3)"/>
|
||||
<circle cx="300" cy="225" r="160" fill="#ffffff" opacity="0.85"/>
|
||||
<text x="300" y="140" font-size="70" text-anchor="middle">🎨</text>
|
||||
<text x="300" y="220" font-size="28" font-family="sans-serif" font-weight="bold" fill="#db2777" text-anchor="middle">شاهکار هنری حامد</text>
|
||||
<text x="300" y="270" font-size="18" font-family="sans-serif" fill="#be185d" text-anchor="middle">🌈 🖍️ ✨ 🖌️ 💖</text>
|
||||
<text x="300" y="320" font-size="16" font-family="sans-serif" fill="#9d174d" text-anchor="middle">دنیای پر از رنگ و خلاقیت کودکانه</text>
|
||||
</svg>'
|
||||
],
|
||||
[
|
||||
'file' => 'sea_trip.svg',
|
||||
'title' => 'قلعهسازی با ماسه در ساحل دریا 🌊',
|
||||
'caption' => 'اولین باری بود که موجهای ریز آب به پاهای کوچولوش میخورد و قلقلکش میاومد!',
|
||||
'album_id' => 4,
|
||||
'age_tag' => '۳ سال و ۱ ماهگی',
|
||||
'photo_date' => '2026-07-28',
|
||||
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 450" width="100%" height="100%">
|
||||
<defs>
|
||||
<linearGradient id="bg4" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#e0e7ff"/>
|
||||
<stop offset="100%" stop-color="#c7d2fe"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="600" height="450" rx="24" fill="url(#bg4)"/>
|
||||
<circle cx="300" cy="225" r="160" fill="#ffffff" opacity="0.85"/>
|
||||
<text x="300" y="140" font-size="70" text-anchor="middle">🏖️</text>
|
||||
<text x="300" y="220" font-size="28" font-family="sans-serif" font-weight="bold" fill="#4f46e5" text-anchor="middle">سفر خاطرهانگیز به شمال</text>
|
||||
<text x="300" y="270" font-size="18" font-family="sans-serif" fill="#4338ca" text-anchor="middle">🌊 🐚 🦀 🚗 ☀️</text>
|
||||
<text x="300" y="320" font-size="16" font-family="sans-serif" fill="#3730a3" text-anchor="middle">شنبازی و ساخت برجهای شنی</text>
|
||||
</svg>'
|
||||
],
|
||||
[
|
||||
'file' => 'lego_house.svg',
|
||||
'title' => 'ساخت قلعه بزرگ با قطعات لگو 🏰',
|
||||
'caption' => 'نیم ساعت با دقت تمام قطعات رو چید و برای ماشینش گاراژ درست کرد.',
|
||||
'album_id' => 5,
|
||||
'age_tag' => '۳ سال و ۲ ماهگی',
|
||||
'photo_date' => '2026-08-22',
|
||||
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 450" width="100%" height="100%">
|
||||
<defs>
|
||||
<linearGradient id="bg5" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#ede9fe"/>
|
||||
<stop offset="100%" stop-color="#ddd6fe"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="600" height="450" rx="24" fill="url(#bg5)"/>
|
||||
<circle cx="300" cy="225" r="160" fill="#ffffff" opacity="0.85"/>
|
||||
<text x="300" y="140" font-size="70" text-anchor="middle">🧸</text>
|
||||
<text x="300" y="220" font-size="28" font-family="sans-serif" font-weight="bold" fill="#7c3aed" text-anchor="middle">مهندس کوچولوی خانه</text>
|
||||
<text x="300" y="270" font-size="18" font-family="sans-serif" fill="#6d28d9" text-anchor="middle">🧱 🚗 🚂 🚀 🌟</text>
|
||||
<text x="300" y="320" font-size="16" font-family="sans-serif" fill="#5b21b6" text-anchor="middle">تمرکز و شادی در دنیای اسباببازیها</text>
|
||||
</svg>'
|
||||
]
|
||||
];
|
||||
|
||||
$stmtCount = $db->query("SELECT COUNT(*) FROM photos")->fetchColumn();
|
||||
if ($stmtCount == 0) {
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO photos (album_id, title, caption, file_path, age_tag, photo_date, is_favorite, likes_count)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
|
||||
foreach ($photosData as $p) {
|
||||
$filePath = 'uploads/' . $p['file'];
|
||||
file_put_contents(__DIR__ . '/../public/' . $filePath, $p['svg']);
|
||||
$stmt->execute([
|
||||
$p['album_id'],
|
||||
$p['title'],
|
||||
$p['caption'],
|
||||
$filePath,
|
||||
$p['age_tag'],
|
||||
$p['photo_date'],
|
||||
1,
|
||||
rand(5, 18)
|
||||
]);
|
||||
}
|
||||
echo "Seeded " . count($photosData) . " sample photos!\n";
|
||||
} else {
|
||||
echo "Photos already present.\n";
|
||||
}
|
||||
Reference in New Issue
Block a user