Sosyal Duvar

This commit is contained in:
Sedat Öztürk 2025-10-18 23:04:24 +03:00
parent a299ae099d
commit c78845f411
14 changed files with 2412 additions and 8 deletions

View file

@ -1,4 +1,5 @@
VITE_API_URL='https://localhost:44344' VITE_API_URL='https://localhost:44344'
VITE_CDN_URL='http://localhost:4005' VITE_CDN_URL='http://localhost:4005'
VITE_REACT_APP_VERSION=$npm_package_version VITE_REACT_APP_VERSION=$npm_package_version
VITE_AI_URL='https://ai.sozsoft.com/webhook/' VITE_AI_URL='https://ai.sozsoft.com/webhook/'
VITE_GOOGLE_MAPS_API_KEY='AIzaSyAefS2rvF-xwq7OHpZ27UYxXPbMo6OwACc'

50
ui/package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "kurs-platform-ui", "name": "kurs-platform-ui",
"version": "1.0.4", "version": "1.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "kurs-platform-ui", "name": "kurs-platform-ui",
"version": "1.0.4", "version": "1.0.1",
"dependencies": { "dependencies": {
"@babel/generator": "^7.28.3", "@babel/generator": "^7.28.3",
"@babel/parser": "^7.28.0", "@babel/parser": "^7.28.0",
@ -32,6 +32,7 @@
"devextreme": "^23.2.11", "devextreme": "^23.2.11",
"devextreme-react": "^23.2.11", "devextreme-react": "^23.2.11",
"easy-peasy": "^6.0.5", "easy-peasy": "^6.0.5",
"emoji-picker-react": "^4.14.1",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"formik": "^2.4.6", "formik": "^2.4.6",
@ -52,6 +53,7 @@
"react-router-dom": "^6.14.1", "react-router-dom": "^6.14.1",
"react-select": "^5.9.0", "react-select": "^5.9.0",
"redux-state-sync": "^3.1.4", "redux-state-sync": "^3.1.4",
"yet-another-react-lightbox": "^3.25.0",
"yup": "^1.6.1" "yup": "^1.6.1"
}, },
"devDependencies": { "devDependencies": {
@ -6107,6 +6109,21 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/emoji-picker-react": {
"version": "4.14.1",
"resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.14.1.tgz",
"integrity": "sha512-4N6dCHr8Z5P52ICdncSxrY0ZcgUtb9Y3o4eBsy6HLpOMhka9AoiWMZ4/hJK0SQNYMIb7MQ+/NT5dqotpo9OXVA==",
"license": "MIT",
"dependencies": {
"flairup": "1.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16"
}
},
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "9.2.2", "version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@ -7337,6 +7354,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/flairup": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz",
"integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==",
"license": "MIT"
},
"node_modules/flat-cache": { "node_modules/flat-cache": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
@ -13998,6 +14021,29 @@
"node": ">= 14.6" "node": ">= 14.6"
} }
}, },
"node_modules/yet-another-react-lightbox": {
"version": "3.25.0",
"resolved": "https://registry.npmjs.org/yet-another-react-lightbox/-/yet-another-react-lightbox-3.25.0.tgz",
"integrity": "sha512-NaCeEXCpdwoTvoOpxNK9gdW8+oHs79yVH+D2YeVQWRjH5i32e5CoXndAAFP2p8awzVYfSonherrE9JMTpfD3EA==",
"license": "MIT",
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@types/react": "^16 || ^17 || ^18 || ^19",
"@types/react-dom": "^16 || ^17 || ^18 || ^19",
"react": "^16.8.0 || ^17 || ^18 || ^19",
"react-dom": "^16.8.0 || ^17 || ^18 || ^19"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View file

@ -40,6 +40,7 @@
"devextreme": "^23.2.11", "devextreme": "^23.2.11",
"devextreme-react": "^23.2.11", "devextreme-react": "^23.2.11",
"easy-peasy": "^6.0.5", "easy-peasy": "^6.0.5",
"emoji-picker-react": "^4.14.1",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"formik": "^2.4.6", "formik": "^2.4.6",
@ -60,6 +61,7 @@
"react-router-dom": "^6.14.1", "react-router-dom": "^6.14.1",
"react-select": "^5.9.0", "react-select": "^5.9.0",
"redux-state-sync": "^3.1.4", "redux-state-sync": "^3.1.4",
"yet-another-react-lightbox": "^3.25.0",
"yup": "^1.6.1" "yup": "^1.6.1"
}, },
"devDependencies": { "devDependencies": {

View file

@ -0,0 +1,468 @@
import React, { useState, useRef } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import classNames from 'classnames'
import EmojiPicker, { EmojiClickData } from 'emoji-picker-react'
import {
HiOutlineChartBar,
HiOutlineEmojiHappy,
HiX,
HiOutlineCollection,
HiOutlineLocationMarker
} from 'react-icons/hi'
import MediaManager, { MediaItem } from './MediaManager'
import LocationPicker, { Location } from './LocationPicker'
interface CreatePostProps {
onCreatePost: (post: {
content: string
location?: Location
media?: {
type: 'mixed' | 'poll'
mediaItems?: MediaItem[]
poll?: {
question: string
options: Array<{ text: string }>
}
}
}) => void
}
const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
const [content, setContent] = useState('')
const [mediaType, setMediaType] = useState<'media' | 'poll' | null>(null)
const [mediaItems, setMediaItems] = useState<MediaItem[]>([])
const [location, setLocation] = useState<Location | null>(null)
const [pollQuestion, setPollQuestion] = useState('')
const [pollOptions, setPollOptions] = useState(['', ''])
const [isExpanded, setIsExpanded] = useState(false)
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [showMediaManager, setShowMediaManager] = useState(false)
const [showLocationPicker, setShowLocationPicker] = useState(false)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const emojiPickerRef = useRef<HTMLDivElement>(null)
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!content.trim() && mediaItems.length === 0 && !mediaType) return
let media = undefined
if (mediaType === 'media' && mediaItems.length > 0) {
media = {
type: 'mixed' as const,
mediaItems
}
} else if (mediaType === 'poll' && pollQuestion && pollOptions.filter(o => o.trim()).length >= 2) {
media = {
type: 'poll' as const,
poll: {
question: pollQuestion,
options: pollOptions.filter(o => o.trim()).map(text => ({ text }))
}
}
}
onCreatePost({
content,
media,
location: location || undefined
})
// Reset form
setContent('')
setMediaType(null)
setMediaItems([])
setLocation(null)
setPollQuestion('')
setPollOptions(['', ''])
setIsExpanded(false)
setShowEmojiPicker(false)
}
const handleEmojiClick = (emojiData: EmojiClickData) => {
const emoji = emojiData.emoji
const textarea = textareaRef.current
if (!textarea) return
const start = textarea.selectionStart
const end = textarea.selectionEnd
const text = content
const before = text.substring(0, start)
const after = text.substring(end)
setContent(before + emoji + after)
// Set cursor position after emoji
setTimeout(() => {
textarea.selectionStart = textarea.selectionEnd = start + emoji.length
textarea.focus()
}, 0)
}
const addPollOption = () => {
if (pollOptions.length < 6) {
setPollOptions([...pollOptions, ''])
}
}
const removePollOption = (index: number) => {
if (pollOptions.length > 2) {
setPollOptions(pollOptions.filter((_, i) => i !== index))
}
}
const updatePollOption = (index: number, value: string) => {
const newOptions = [...pollOptions]
newOptions[index] = value
setPollOptions(newOptions)
}
const clearMedia = () => {
setMediaType(null)
setMediaItems([])
setPollQuestion('')
setPollOptions(['', ''])
}
const removeMediaItem = (id: string) => {
setMediaItems(mediaItems.filter((m) => m.id !== id))
}
// Close emoji picker when clicking outside
React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (emojiPickerRef.current && !emojiPickerRef.current.contains(event.target as Node)) {
setShowEmojiPicker(false)
}
}
if (showEmojiPicker) {
document.addEventListener('mousedown', handleClickOutside)
}
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [showEmojiPicker])
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 mb-6">
<form onSubmit={handleSubmit}>
{/* Text Input */}
<div className="flex gap-3 mb-4">
<img
src="https://i.pravatar.cc/150?img=1"
alt="Your avatar"
className="w-12 h-12 rounded-full object-cover"
/>
<div className="flex-1">
<textarea
ref={textareaRef}
value={content}
onChange={(e) => setContent(e.target.value)}
onFocus={() => setIsExpanded(true)}
onBlur={() => {
// Eğer içerik, medya veya konum yoksa küçült
if (!content.trim() && mediaItems.length === 0 && !location && !mediaType) {
setIsExpanded(false)
}
}}
placeholder="Ne düşünüyorsunuz?"
className={classNames(
'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none transition-all',
isExpanded ? 'min-h-[120px]' : 'min-h-[48px]'
)}
rows={isExpanded ? 4 : 1}
/>
</div>
</div>
{/* Media Preview */}
<AnimatePresence>
{mediaType === 'media' && mediaItems.length > 0 && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mb-4"
>
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
Medyalar ({mediaItems.length})
</h4>
<button
type="button"
onClick={() => {
clearMedia()
}}
className="text-sm text-red-600 hover:text-red-700 font-medium"
title="Tüm medyaları kaldır"
>
Vazgeç
</button>
</div>
<div className="grid grid-cols-4 gap-2">
{mediaItems.map((item) => (
<div key={item.id} className="relative group">
{item.type === 'image' ? (
<img
src={item.url}
alt="Preview"
className="w-full h-24 object-cover rounded-lg"
/>
) : (
<div className="w-full h-24 bg-gray-900 rounded-lg relative">
<video src={item.url} className="w-full h-full object-cover rounded-lg" />
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-10 h-10 bg-black bg-opacity-50 rounded-full flex items-center justify-center">
<div className="w-0 h-0 border-t-8 border-t-transparent border-l-12 border-l-white border-b-8 border-b-transparent ml-1"></div>
</div>
</div>
</div>
)}
<button
type="button"
onClick={() => removeMediaItem(item.id)}
className="absolute -top-2 -right-2 p-1 bg-red-600 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
>
<HiX className="w-4 h-4" />
</button>
<div className="absolute bottom-1 left-1 px-2 py-0.5 bg-black bg-opacity-70 text-white text-xs rounded">
{item.type === 'image' ? '📷' : '🎥'}
</div>
</div>
))}
<button
type="button"
onClick={() => setShowMediaManager(true)}
className="h-24 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg flex items-center justify-center hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors"
>
<HiOutlineCollection className="w-6 h-6 text-gray-400" />
</button>
</div>
</motion.div>
)}
{location && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mb-4"
>
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">Konum</h4>
<button
type="button"
onClick={() => setLocation(null)}
className="text-sm text-red-600 hover:text-red-700 font-medium"
title="Konumu kaldır"
>
Vazgeç
</button>
</div>
<div className="p-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-700">
<div className="flex items-start gap-2">
<HiOutlineLocationMarker className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">
{location.name}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
{location.address}
</p>
</div>
</div>
</div>
</motion.div>
)}
{mediaType === 'poll' && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mb-4"
>
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">Anket</h4>
<button
type="button"
onClick={() => {
clearMedia()
}}
className="text-sm text-red-600 hover:text-red-700 font-medium"
title="Anketi kaldır"
>
Vazgeç
</button>
</div>
<input
type="text"
value={pollQuestion}
onChange={(e) => setPollQuestion(e.target.value)}
placeholder="Soru"
className="w-full px-4 py-2 mb-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<div className="space-y-2">
{pollOptions.map((option, index) => (
<div key={index} className="flex gap-2">
<input
type="text"
value={option}
onChange={(e) => updatePollOption(index, e.target.value)}
placeholder={`Seçenek ${index + 1}`}
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{pollOptions.length > 2 && (
<button
type="button"
onClick={() => removePollOption(index)}
className="p-2 text-gray-500 hover:text-red-600"
>
<HiX className="w-5 h-5" />
</button>
)}
</div>
))}
</div>
{pollOptions.length < 6 && (
<button
type="button"
onClick={addPollOption}
className="mt-2 text-sm text-blue-600 hover:text-blue-700 font-medium"
>
+ Seçenek Ekle
</button>
)}
</motion.div>
)}
</AnimatePresence>
{/* Actions */}
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-gray-700"
>
<div className="flex gap-2 relative">
<button
type="button"
onClick={() => {
if (mediaType === 'media' && mediaItems.length > 0) {
// Eğer zaten medya varsa, yöneticiyi aç
setShowMediaManager(true)
} else {
// Başka bir tip seçiliyse temizle ve medya modunu aç
clearMedia()
setMediaType('media')
setShowMediaManager(true)
}
}}
className={classNames(
'p-2 rounded-full transition-colors',
mediaType === 'media'
? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
)}
title={mediaType === 'media' ? 'Medyaları düzenle' : 'Medya ekle'}
>
<HiOutlineCollection className="w-5 h-5" />
{mediaType === 'media' && mediaItems.length > 0 && (
<span className="absolute -top-1 -right-1 w-4 h-4 bg-blue-600 text-white text-xs rounded-full flex items-center justify-center">
{mediaItems.length}
</span>
)}
</button>
<button
type="button"
onClick={() => {
// Başka bir tip seçiliyse temizle
if (mediaType !== 'poll') {
clearMedia()
}
setMediaType(mediaType === 'poll' ? null : 'poll')
}}
className={classNames(
'p-2 rounded-full transition-colors',
mediaType === 'poll'
? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
)}
title={mediaType === 'poll' ? 'Anketi kaldır' : 'Anket ekle'}
>
<HiOutlineChartBar className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
className="p-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors"
title="Emoji ekle"
>
<HiOutlineEmojiHappy className="w-5 h-5" />
</button>
<button
type="button"
onClick={() => setShowLocationPicker(true)}
className={classNames(
'p-2 rounded-full transition-colors',
location
? 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-400'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
)}
title={location ? 'Konumu değiştir' : 'Konum ekle'}
>
<HiOutlineLocationMarker className="w-5 h-5" />
</button>
{/* Emoji Picker */}
{showEmojiPicker && (
<div ref={emojiPickerRef} className="absolute bottom-12 left-0 z-50">
<EmojiPicker
onEmojiClick={handleEmojiClick}
autoFocusSearch={false}
/>
</div>
)}
</div>
<button
type="submit"
disabled={!content.trim() && mediaItems.length === 0 && !mediaType}
className="px-6 py-2 bg-blue-600 text-white font-medium rounded-full hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
Paylaş
</button>
</motion.div>
)}
</AnimatePresence>
</form>
{/* Media Manager Modal */}
<AnimatePresence>
{showMediaManager && (
<MediaManager
media={mediaItems}
onChange={setMediaItems}
onClose={() => setShowMediaManager(false)}
/>
)}
</AnimatePresence>
{/* Location Picker Modal */}
<AnimatePresence>
{showLocationPicker && (
<LocationPicker
onSelect={setLocation}
onClose={() => setShowLocationPicker(false)}
/>
)}
</AnimatePresence>
</div>
)
}
export default CreatePost

View file

@ -0,0 +1,97 @@
import React from 'react'
import { HiOutlineExternalLink, HiOutlineLocationMarker } from 'react-icons/hi'
import type { Location } from './LocationPicker'
interface LocationMapProps {
location: Location
className?: string
showDirections?: boolean
}
const LocationMap: React.FC<LocationMapProps> = ({
location,
className = '',
showDirections = true
}) => {
const handleOpenGoogleMaps = () => {
const url = `https://www.google.com/maps/search/?api=1&query=${location.lat},${location.lng}&query_place_id=${location.placeId || ''}`
window.open(url, '_blank')
}
// Google Maps Static API URL (gerçek uygulamada API key eklenecek)
const getMapImageUrl = () => {
const { lat, lng } = location
const zoom = 15
const size = '600x300'
const marker = `color:red|${lat},${lng}`
// Production'da gerçek API key kullanılacak
// const apiKey = 'YOUR_GOOGLE_MAPS_API_KEY'
// return `https://maps.googleapis.com/maps/api/staticmap?center=${lat},${lng}&zoom=${zoom}&size=${size}&markers=${marker}&key=${apiKey}`
// Demo için OpenStreetMap kullanıyoruz
return `https://www.openstreetmap.org/export/embed.html?bbox=${lng - 0.01},${lat - 0.01},${lng + 0.01},${lat + 0.01}&layer=mapnik&marker=${lat},${lng}`
}
return (
<div className={`relative rounded-lg overflow-hidden bg-gray-200 dark:bg-gray-700 ${className}`}>
{/* Map Container */}
<div className="relative w-full h-64 group">
{/* OpenStreetMap iframe for demo */}
<iframe
title={`Map of ${location.name}`}
src={getMapImageUrl()}
className="w-full h-full border-0"
allowFullScreen
loading="lazy"
/>
{/* Overlay with location info */}
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent pointer-events-none" />
{/* Location Info */}
<div className="absolute bottom-0 left-0 right-0 p-4 text-white pointer-events-none">
<div className="flex items-start gap-2">
<HiOutlineLocationMarker className="w-5 h-5 mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<h3 className="font-bold text-lg mb-1 drop-shadow-lg">{location.name}</h3>
<p className="text-sm text-white/90 drop-shadow-md line-clamp-2">
{location.address}
</p>
</div>
</div>
</div>
{/* Click to open overlay - invisible but clickable */}
<button
onClick={handleOpenGoogleMaps}
className="absolute inset-0 w-full h-full cursor-pointer group"
aria-label="Google Maps'te aç"
>
<span className="sr-only">Google Maps'te </span>
</button>
{/* Hover Effect */}
<div className="absolute inset-0 bg-blue-600/0 group-hover:bg-blue-600/10 transition-colors duration-200" />
</div>
{/* Directions Button */}
{showDirections && (
<div className="p-3 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
<button
onClick={handleOpenGoogleMaps}
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
>
<HiOutlineExternalLink className="w-5 h-5" />
<span>Google Maps'te </span>
</button>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 text-center">
Yol tarifi almak için tıklayın
</p>
</div>
)}
</div>
)
}
export default LocationMap

View file

@ -0,0 +1,386 @@
import React, { useState, useEffect, useRef } from 'react'
import { motion } from 'framer-motion'
import { HiX, HiOutlineSearch, HiOutlineLocationMarker } from 'react-icons/hi'
import classNames from 'classnames'
export interface Location {
id: string
name: string
address: string
lat: number
lng: number
placeId?: string
}
interface LocationPickerProps {
onSelect: (location: Location) => void
onClose: () => void
}
// Google Maps API key - .env dosyasından alınmalı
const GOOGLE_API_KEY = import.meta.env.VITE_GOOGLE_MAPS_API_KEY || ''
declare global {
interface Window {
google: any
initGoogleMaps?: () => void
}
}
const LocationPicker: React.FC<LocationPickerProps> = ({ onSelect, onClose }) => {
const [searchQuery, setSearchQuery] = useState('')
const [locations, setLocations] = useState<Location[]>([])
const [selectedLocation, setSelectedLocation] = useState<Location | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [isGoogleLoaded, setIsGoogleLoaded] = useState(false)
const searchInputRef = useRef<HTMLInputElement>(null)
const autocompleteServiceRef = useRef<any>(null)
const placesServiceRef = useRef<any>(null)
const debounceTimerRef = useRef<NodeJS.Timeout>()
const scriptLoadedRef = useRef(false)
// Google Maps SDK'yı yükle
useEffect(() => {
if (scriptLoadedRef.current) return
const loadGoogleMaps = () => {
if (window.google && window.google.maps && window.google.maps.places) {
setIsGoogleLoaded(true)
autocompleteServiceRef.current = new window.google.maps.places.AutocompleteService()
const mapDiv = document.createElement('div')
const map = new window.google.maps.Map(mapDiv)
placesServiceRef.current = new window.google.maps.places.PlacesService(map)
return
}
if (!GOOGLE_API_KEY) {
setError('Google Maps API anahtarı bulunamadı. Lütfen .env dosyasına VITE_GOOGLE_MAPS_API_KEY ekleyin.')
return
}
// Script zaten yüklendiyse sadece bekle
const existingScript = document.querySelector('script[src*="maps.googleapis.com"]')
if (existingScript) {
const checkInterval = setInterval(() => {
if (window.google && window.google.maps && window.google.maps.places) {
clearInterval(checkInterval)
setIsGoogleLoaded(true)
autocompleteServiceRef.current = new window.google.maps.places.AutocompleteService()
const mapDiv = document.createElement('div')
const map = new window.google.maps.Map(mapDiv)
placesServiceRef.current = new window.google.maps.places.PlacesService(map)
}
}, 100)
return
}
// Yeni script ekle
const script = document.createElement('script')
script.src = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_API_KEY}&libraries=places&language=tr`
script.async = true
script.defer = true
script.onload = () => {
if (window.google && window.google.maps && window.google.maps.places) {
setIsGoogleLoaded(true)
autocompleteServiceRef.current = new window.google.maps.places.AutocompleteService()
const mapDiv = document.createElement('div')
const map = new window.google.maps.Map(mapDiv)
placesServiceRef.current = new window.google.maps.places.PlacesService(map)
}
}
script.onerror = () => {
setError('Google Maps yüklenemedi. Lütfen internet bağlantınızı kontrol edin.')
}
document.head.appendChild(script)
scriptLoadedRef.current = true
}
loadGoogleMaps()
}, [])
useEffect(() => {
searchInputRef.current?.focus()
}, [])
// Google Places Autocomplete ile konum arama
useEffect(() => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
}
if (searchQuery.trim() === '') {
setLocations([])
setError(null)
return
}
if (!isGoogleLoaded) {
return
}
debounceTimerRef.current = setTimeout(async () => {
setIsLoading(true)
setError(null)
try {
// Google Places Autocomplete Service kullan (CORS yok)
autocompleteServiceRef.current.getPlacePredictions(
{
input: searchQuery,
componentRestrictions: { country: 'tr' },
language: 'tr'
},
async (predictions: any, status: any) => {
if (status === window.google.maps.places.PlacesServiceStatus.ZERO_RESULTS) {
setLocations([])
setIsLoading(false)
return
}
if (status !== window.google.maps.places.PlacesServiceStatus.OK) {
setError('Konum arama başarısız')
setIsLoading(false)
return
}
if (!predictions || predictions.length === 0) {
setLocations([])
setIsLoading(false)
return
}
// Her bir prediction için detaylı bilgi al
const detailedLocations: Location[] = []
let completed = 0
predictions.forEach((prediction: any) => {
placesServiceRef.current.getDetails(
{
placeId: prediction.place_id,
fields: ['name', 'formatted_address', 'geometry', 'place_id']
},
(place: any, placeStatus: any) => {
completed++
if (placeStatus === window.google.maps.places.PlacesServiceStatus.OK && place) {
detailedLocations.push({
id: place.place_id,
name: place.name,
address: place.formatted_address,
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng(),
placeId: place.place_id
})
}
// Tüm istekler tamamlandıysa state'i güncelle
if (completed === predictions.length) {
setLocations(detailedLocations)
setIsLoading(false)
}
}
)
})
}
)
} catch (err) {
console.error('Location search error:', err)
setError('Konum arama sırasında bir hata oluştu')
setIsLoading(false)
}
}, 500) // 500ms debounce
return () => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
}
}
}, [searchQuery, isGoogleLoaded])
const handleSelect = (location: Location) => {
setSelectedLocation(location)
}
const handleConfirm = () => {
if (selectedLocation) {
onSelect(selectedLocation)
onClose()
}
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col"
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-bold text-gray-900 dark:text-white">Konum Ekle</h2>
<button
onClick={onClose}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors"
>
<HiX className="w-5 h-5 text-gray-500 dark:text-gray-400" />
</button>
</div>
{/* Search */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<div className="relative">
<HiOutlineSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
ref={searchInputRef}
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Konum ara..."
disabled={!isGoogleLoaded}
className="w-full pl-10 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-600 disabled:cursor-not-allowed"
/>
</div>
{!isGoogleLoaded && (
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
Google Maps yükleniyor...
</p>
)}
</div>
{/* Location List */}
<div className="flex-1 overflow-y-auto p-4">
{!isGoogleLoaded ? (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-500 dark:text-gray-400">Google Maps yükleniyor...</p>
</div>
) : isLoading ? (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-500 dark:text-gray-400">Konumlar aranıyor...</p>
</div>
) : error ? (
<div className="text-center py-12">
<HiOutlineLocationMarker className="w-16 h-16 mx-auto mb-4 text-red-400" />
<p className="text-red-500 dark:text-red-400">{error}</p>
</div>
) : searchQuery.trim() === '' ? (
<div className="text-center py-12">
<HiOutlineSearch className="w-16 h-16 mx-auto mb-4 text-gray-400" />
<p className="text-gray-500 dark:text-gray-400">
Aramak istediğiniz konumu yazın
</p>
<p className="text-sm text-gray-400 dark:text-gray-500 mt-2">
Örn: Taksim, İstanbul
</p>
</div>
) : locations.length === 0 ? (
<div className="text-center py-12">
<HiOutlineLocationMarker className="w-16 h-16 mx-auto mb-4 text-gray-400" />
<p className="text-gray-500 dark:text-gray-400">
Konum bulunamadı. Farklı bir arama yapın.
</p>
</div>
) : (
<div className="space-y-2">
{locations.map((location) => (
<button
key={location.id}
onClick={() => handleSelect(location)}
className={classNames(
'w-full text-left p-3 rounded-lg transition-all hover:bg-gray-50 dark:hover:bg-gray-700',
selectedLocation?.id === location.id
? 'bg-blue-50 dark:bg-blue-900/30 border-2 border-blue-500'
: 'border-2 border-transparent'
)}
>
<div className="flex items-start gap-3">
<div className="mt-1">
<HiOutlineLocationMarker
className={classNames(
'w-5 h-5',
selectedLocation?.id === location.id
? 'text-blue-600'
: 'text-gray-400'
)}
/>
</div>
<div className="flex-1 min-w-0">
<h3
className={classNames(
'font-semibold mb-1',
selectedLocation?.id === location.id
? 'text-blue-600 dark:text-blue-400'
: 'text-gray-900 dark:text-gray-100'
)}
>
{location.name}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
{location.address}
</p>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
{location.lat.toFixed(4)}, {location.lng.toFixed(4)}
</p>
</div>
{selectedLocation?.id === location.id && (
<div className="mt-1">
<div className="w-5 h-5 bg-blue-600 rounded-full flex items-center justify-center">
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</div>
</div>
)}
</div>
</button>
))}
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between p-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-750">
<div className="text-sm text-gray-600 dark:text-gray-400">
{selectedLocation ? (
<span className="flex items-center gap-2">
<HiOutlineLocationMarker className="w-4 h-4 text-blue-600" />
<span className="font-medium text-gray-900 dark:text-gray-100">
{selectedLocation.name}
</span>
</span>
) : (
<span>Bir konum seçin</span>
)}
</div>
<div className="flex gap-2">
<button
onClick={onClose}
className="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
İptal
</button>
<button
onClick={handleConfirm}
disabled={!selectedLocation}
className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
Ekle
</button>
</div>
</div>
</motion.div>
</div>
)
}
export default LocationPicker

View file

@ -0,0 +1,72 @@
import React from 'react'
import Lightbox from 'yet-another-react-lightbox'
import 'yet-another-react-lightbox/styles.css'
import Video from 'yet-another-react-lightbox/plugins/video'
import Zoom from 'yet-another-react-lightbox/plugins/zoom'
import Counter from 'yet-another-react-lightbox/plugins/counter'
import 'yet-another-react-lightbox/plugins/counter.css'
export interface LightboxMedia {
type: 'image' | 'video'
url?: string
urls?: string[]
}
interface MediaLightboxProps {
isOpen: boolean
onClose: () => void
media: LightboxMedia
startIndex?: number
}
const MediaLightbox: React.FC<MediaLightboxProps> = ({
isOpen,
onClose,
media,
startIndex = 0
}) => {
const slides = React.useMemo(() => {
if (media.type === 'video' && media.url) {
return [
{
type: 'video' as const,
sources: [
{
src: media.url,
type: 'video/mp4'
}
]
}
]
}
const urls = media.urls || (media.url ? [media.url] : [])
return urls.map((url) => ({
src: url
}))
}, [media])
return (
<Lightbox
open={isOpen}
close={onClose}
slides={slides}
index={startIndex}
plugins={[Video, Zoom, Counter]}
counter={{ container: { style: { top: 'unset', bottom: 0 } } }}
zoom={{
maxZoomPixelRatio: 3,
scrollToZoom: true
}}
video={{
controls: true,
autoPlay: false
}}
styles={{
container: { backgroundColor: 'rgba(0, 0, 0, 0.95)' }
}}
/>
)
}
export default MediaLightbox

View file

@ -0,0 +1,243 @@
import React, { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { HiX, HiPlus, HiOutlineLink, HiOutlineUpload } from 'react-icons/hi'
import classNames from 'classnames'
export interface MediaItem {
id: string
type: 'image' | 'video'
url: string
file?: File
}
interface MediaManagerProps {
media: MediaItem[]
onChange: (media: MediaItem[]) => void
onClose: () => void
}
const MediaManager: React.FC<MediaManagerProps> = ({ media, onChange, onClose }) => {
const [activeTab, setActiveTab] = useState<'upload' | 'url'>('upload')
const [urlInput, setUrlInput] = useState('')
const [mediaType, setMediaType] = useState<'image' | 'video'>('image')
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (!files) return
const newMedia: MediaItem[] = Array.from(files).map((file) => ({
id: Math.random().toString(36).substr(2, 9),
type: file.type.startsWith('video/') ? 'video' : 'image',
url: URL.createObjectURL(file),
file
}))
onChange([...media, ...newMedia])
e.target.value = ''
}
const handleUrlAdd = () => {
if (!urlInput.trim()) return
const newMedia: MediaItem = {
id: Math.random().toString(36).substr(2, 9),
type: mediaType,
url: urlInput
}
onChange([...media, newMedia])
setUrlInput('')
}
const removeMedia = (id: string) => {
onChange(media.filter((m) => m.id !== id))
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-3xl max-h-[90vh] overflow-hidden"
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-bold text-gray-900 dark:text-white">Medya Ekle</h2>
<button
onClick={onClose}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors"
>
<HiX className="w-5 h-5 text-gray-500 dark:text-gray-400" />
</button>
</div>
{/* Tabs */}
<div className="flex border-b border-gray-200 dark:border-gray-700 px-4">
<button
onClick={() => setActiveTab('upload')}
className={classNames(
'px-4 py-3 font-medium border-b-2 transition-colors',
activeTab === 'upload'
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
)}
>
<div className="flex items-center gap-2">
<HiOutlineUpload className="w-5 h-5" />
<span>Bilgisayarımdan Seç</span>
</div>
</button>
<button
onClick={() => setActiveTab('url')}
className={classNames(
'px-4 py-3 font-medium border-b-2 transition-colors',
activeTab === 'url'
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
)}
>
<div className="flex items-center gap-2">
<HiOutlineLink className="w-5 h-5" />
<span>URL ile Ekle</span>
</div>
</button>
</div>
{/* Content */}
<div className="p-4 overflow-y-auto max-h-[calc(90vh-240px)]">
{activeTab === 'upload' ? (
<div>
<label className="block">
<div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-8 text-center hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors cursor-pointer">
<HiOutlineUpload className="w-12 h-12 mx-auto mb-4 text-gray-400" />
<p className="text-gray-700 dark:text-gray-300 font-medium mb-1">
Dosya seçmek için tıklayın
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
Resim veya Video (PNG, JPG, GIF, MP4, MOV)
</p>
</div>
<input
type="file"
accept="image/*,video/*"
multiple
onChange={handleFileSelect}
className="hidden"
/>
</label>
</div>
) : (
<div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Medya Tipi
</label>
<div className="flex gap-2">
<button
onClick={() => setMediaType('image')}
className={classNames(
'flex-1 py-2 px-4 rounded-lg font-medium transition-colors',
mediaType === 'image'
? 'bg-blue-600 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
)}
>
Resim
</button>
<button
onClick={() => setMediaType('video')}
className={classNames(
'flex-1 py-2 px-4 rounded-lg font-medium transition-colors',
mediaType === 'video'
? 'bg-blue-600 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
)}
>
Video
</button>
</div>
</div>
<div className="flex gap-2">
<input
type="url"
value={urlInput}
onChange={(e) => setUrlInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleUrlAdd()}
placeholder={`${mediaType === 'image' ? 'Resim' : 'Video'} URL'si girin`}
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onClick={handleUrlAdd}
disabled={!urlInput.trim()}
className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
Ekle
</button>
</div>
</div>
)}
{/* Media Preview */}
{media.length > 0 && (
<div className="mt-6">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Eklenen Medyalar ({media.length})
</h3>
<div className="grid grid-cols-4 gap-3">
{media.map((item) => (
<div key={item.id} className="relative group">
{item.type === 'image' ? (
<img
src={item.url}
alt="Media preview"
className="w-full h-24 object-cover rounded-lg"
/>
) : (
<div className="w-full h-24 bg-gray-900 rounded-lg flex items-center justify-center">
<video src={item.url} className="w-full h-full object-cover rounded-lg" />
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-10 h-10 bg-black bg-opacity-50 rounded-full flex items-center justify-center">
<div className="w-0 h-0 border-t-8 border-t-transparent border-l-12 border-l-white border-b-8 border-b-transparent ml-1"></div>
</div>
</div>
</div>
)}
<button
onClick={() => removeMedia(item.id)}
className="absolute -top-2 -right-2 p-1 bg-red-600 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
>
<HiX className="w-4 h-4" />
</button>
<div className="absolute bottom-1 left-1 px-2 py-0.5 bg-black bg-opacity-70 text-white text-xs rounded">
{item.type === 'image' ? '📷' : '🎥'}
</div>
</div>
))}
</div>
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-2 p-4 border-t border-gray-200 dark:border-gray-700">
<button
onClick={onClose}
className="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
İptal
</button>
<button
onClick={onClose}
disabled={media.length === 0}
className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
Tamam ({media.length})
</button>
</div>
</motion.div>
</div>
)
}
export default MediaManager

View file

@ -0,0 +1,433 @@
import React, { useState, useRef, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import classNames from 'classnames'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import 'dayjs/locale/tr'
import {
HiHeart,
HiOutlineHeart,
HiOutlineChatAlt2,
HiOutlineTrash,
HiOutlinePaperAirplane,
} from 'react-icons/hi'
import { SocialPost } from '../../../mocks/mockSocialPosts'
import MediaLightbox from './MediaLightbox'
import LocationMap from './LocationMap'
import UserProfileCard from './UserProfileCard'
dayjs.extend(relativeTime)
dayjs.locale('tr')
interface PostItemProps {
post: SocialPost
onLike: (postId: string) => void
onComment: (postId: string, content: string) => void
onDelete: (postId: string) => void
onVote: (postId: string, optionId: string) => void
}
const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete, onVote }) => {
const [showComments, setShowComments] = useState(false)
const [commentText, setCommentText] = useState('')
const [showAllImages, setShowAllImages] = useState(false)
const [lightboxOpen, setLightboxOpen] = useState(false)
const [lightboxIndex, setLightboxIndex] = useState(0)
const [showUserCard, setShowUserCard] = useState(false)
const [hoveredCommentAuthor, setHoveredCommentAuthor] = useState<string | null>(null)
const videoRef = useRef<HTMLVideoElement>(null)
// Intersection Observer for video autoplay/pause
useEffect(() => {
const video = videoRef.current
if (!video) return
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// Video ekranda görünür - oynat
video.play().catch(err => {
console.log('Video autoplay failed:', err)
})
} else {
// Video ekrandan çıktı - durdur
video.pause()
}
})
},
{
threshold: 0.5 // Video %50 görünür olduğunda oynat
}
)
observer.observe(video)
return () => {
observer.disconnect()
}
}, [post.media?.type])
const handleSubmitComment = (e: React.FormEvent) => {
e.preventDefault()
if (commentText.trim()) {
onComment(post.id, commentText)
setCommentText('')
}
}
const getImageLayout = (images: string[]) => {
const count = images.length
if (count === 1) return 'single'
if (count === 2) return 'double'
if (count === 3) return 'triple'
return 'multiple'
}
const renderMedia = () => {
if (!post.media) return null
switch (post.media.type) {
case 'image':
if (post.media.urls && post.media.urls.length > 0) {
const layout = getImageLayout(post.media.urls)
const displayImages = showAllImages ? post.media.urls : post.media.urls.slice(0, 4)
const hasMore = post.media.urls.length > 4
return (
<>
<div
className={classNames('mt-3 rounded-lg overflow-hidden', {
'grid gap-1': layout !== 'single',
'grid-cols-2': layout === 'double' || layout === 'multiple',
'grid-cols-3': layout === 'triple'
})}
>
{displayImages.map((url, index) => (
<div
key={index}
className={classNames('relative', {
'col-span-2': layout === 'triple' && index === 0,
'aspect-video': layout === 'single',
'aspect-square': layout !== 'single'
})}
>
<img
src={url}
alt={`Post image ${index + 1}`}
className="w-full h-full object-cover cursor-pointer hover:opacity-90 transition-opacity"
onClick={() => {
setLightboxIndex(index)
setLightboxOpen(true)
}}
/>
{hasMore && index === 3 && !showAllImages && post.media?.urls && (
<div
className="absolute inset-0 bg-black bg-opacity-60 flex items-center justify-center cursor-pointer"
onClick={(e) => {
e.stopPropagation()
setShowAllImages(true)
}}
>
<span className="text-white text-2xl font-bold">
+{post.media.urls.length - 4}
</span>
</div>
)}
</div>
))}
</div>
<MediaLightbox
isOpen={lightboxOpen}
onClose={() => setLightboxOpen(false)}
media={{ type: 'image', urls: post.media.urls }}
startIndex={lightboxIndex}
/>
</>
)
} else if (post.media.url) {
return (
<>
<div
className="mt-3 rounded-lg overflow-hidden cursor-pointer"
onClick={() => setLightboxOpen(true)}
>
<img
src={post.media.url}
alt="Post"
className="w-full object-cover max-h-96 hover:opacity-90 transition-opacity"
/>
</div>
<MediaLightbox
isOpen={lightboxOpen}
onClose={() => setLightboxOpen(false)}
media={{ type: 'image', url: post.media.url }}
/>
</>
)
}
break
case 'video':
return (
<>
<div
className="mt-3 rounded-lg overflow-hidden cursor-pointer relative group"
onClick={() => setLightboxOpen(true)}
>
<video
ref={videoRef}
className="w-full max-h-96"
src={post.media.url}
loop
muted
playsInline
>
Tarayıcınız video etiketini desteklemiyor.
</video>
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-30 transition-all flex items-center justify-center">
<div className="w-16 h-16 bg-white bg-opacity-90 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<div className="w-0 h-0 border-t-10 border-t-transparent border-l-16 border-l-blue-600 border-b-10 border-b-transparent ml-1"></div>
</div>
</div>
</div>
<MediaLightbox
isOpen={lightboxOpen}
onClose={() => setLightboxOpen(false)}
media={{ type: 'video', url: post.media.url || '' }}
/>
</>
)
case 'poll':
if (post.media.poll) {
const poll = post.media.poll
const isExpired = new Date() > poll.endsAt
const hasVoted = !!poll.userVote
return (
<div className="mt-3 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<h4 className="font-medium text-gray-900 dark:text-gray-100 mb-3">
{poll.question}
</h4>
<div className="space-y-2">
{poll.options.map((option) => {
const percentage =
poll.totalVotes > 0 ? (option.votes / poll.totalVotes) * 100 : 0
const isSelected = poll.userVote === option.id
return (
<button
key={option.id}
onClick={() => !hasVoted && !isExpired && onVote(post.id, option.id)}
disabled={hasVoted || isExpired}
className={classNames(
'w-full text-left p-3 rounded-lg relative overflow-hidden transition-all',
{
'bg-blue-100 dark:bg-blue-900 border-2 border-blue-500':
isSelected,
'bg-white dark:bg-gray-600 hover:bg-gray-50 dark:hover:bg-gray-500':
!isSelected && !hasVoted && !isExpired,
'bg-white dark:bg-gray-600 cursor-not-allowed':
hasVoted || isExpired
}
)}
>
{hasVoted && (
<div
className="absolute inset-y-0 left-0 bg-blue-200 dark:bg-blue-800 transition-all"
style={{ width: `${percentage}%` }}
/>
)}
<div className="relative z-10 flex justify-between items-center">
<span className="font-medium text-gray-900 dark:text-gray-100">
{option.text}
</span>
{hasVoted && (
<span className="text-sm font-semibold text-gray-700 dark:text-gray-200">
{percentage.toFixed(0)}%
</span>
)}
</div>
</button>
)
})}
</div>
<div className="mt-3 text-sm text-gray-600 dark:text-gray-400">
{poll.totalVotes} oy {isExpired ? 'Sona erdi' : dayjs(poll.endsAt).fromNow() + ' bitiyor'}
</div>
</div>
)
}
break
}
return null
}
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 mb-4"
>
{/* Header */}
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div
className="relative"
onMouseEnter={() => setShowUserCard(true)}
onMouseLeave={() => setShowUserCard(false)}
>
<img
src={post.author.avatar}
alt={post.author.name}
className="w-12 h-12 rounded-full object-cover cursor-pointer ring-2 ring-transparent hover:ring-blue-500 transition-all"
/>
<AnimatePresence>
{showUserCard && (
<UserProfileCard user={post.author} position="bottom" />
)}
</AnimatePresence>
</div>
<div>
<h3 className="font-semibold text-gray-900 dark:text-gray-100">
{post.author.name}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
{post.author.title} {dayjs(post.createdAt).fromNow()}
</p>
</div>
</div>
{post.isOwnPost && (
<button
onClick={() => onDelete(post.id)}
className="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-full transition-colors"
title="Gönderiyi sil"
>
<HiOutlineTrash className="w-5 h-5" />
</button>
)}
</div>
{/* Content */}
<div className="mb-3">
<p className="text-gray-800 dark:text-gray-200 whitespace-pre-wrap">{post.content}</p>
{renderMedia()}
{/* Location */}
{post.location && (
<div className="mt-3">
<LocationMap location={post.location} />
</div>
)}
</div>
{/* Actions */}
<div className="flex items-center gap-6 pt-3 border-t border-gray-100 dark:border-gray-700">
<button
onClick={() => onLike(post.id)}
className={classNames(
'flex items-center gap-2 transition-colors',
post.likes.isLiked
? 'text-red-600 hover:text-red-700'
: 'text-gray-600 dark:text-gray-400 hover:text-red-600'
)}
>
{post.likes.isLiked ? (
<HiHeart className="w-5 h-5" />
) : (
<HiOutlineHeart className="w-5 h-5" />
)}
<span className="text-sm font-medium">{post.likes.count}</span>
</button>
<button
onClick={() => setShowComments(!showComments)}
className="flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 transition-colors"
>
<HiOutlineChatAlt2 className="w-5 h-5" />
<span className="text-sm font-medium">{post.comments.length}</span>
</button>
</div>
{/* Comments Section */}
<AnimatePresence>
{showComments && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700"
>
{/* Comment Form */}
<form onSubmit={handleSubmitComment} className="mb-4">
<div className="flex gap-2">
<input
type="text"
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
placeholder="Yorum yazın..."
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-full bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
disabled={!commentText.trim()}
className="p-2 bg-blue-600 text-white rounded-full hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
<HiOutlinePaperAirplane className="w-5 h-5" />
</button>
</div>
</form>
{/* Comments List */}
<div className="space-y-3">
{post.comments.map((comment) => (
<div key={comment.id} className="flex gap-3">
<div
className="relative"
onMouseEnter={() => setHoveredCommentAuthor(comment.id)}
onMouseLeave={() => setHoveredCommentAuthor(null)}
>
<img
src={comment.author.avatar}
alt={comment.author.name}
className="w-8 h-8 rounded-full object-cover cursor-pointer ring-2 ring-transparent hover:ring-blue-500 transition-all"
/>
<AnimatePresence>
{hoveredCommentAuthor === comment.id && (
<UserProfileCard
user={{
id: comment.author.id,
name: comment.author.name,
avatar: comment.author.avatar,
title: 'Çalışan' // Default title for comments
}}
position="bottom"
/>
)}
</AnimatePresence>
</div>
<div className="flex-1">
<div className="bg-gray-100 dark:bg-gray-700 rounded-lg px-4 py-2">
<h4 className="font-semibold text-sm text-gray-900 dark:text-gray-100">
{comment.author.name}
</h4>
<p className="text-sm text-gray-800 dark:text-gray-200">{comment.content}</p>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 ml-4">
{dayjs(comment.createdAt).fromNow()}
</p>
</div>
</div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
)
}
export default PostItem

View file

@ -0,0 +1,101 @@
import React from 'react'
import { motion } from 'framer-motion'
import { HiMail, HiPhone, HiBriefcase, HiLocationMarker } from 'react-icons/hi'
interface UserProfileCardProps {
user: {
id: string
name: string
avatar: string
title: string
email?: string
phone?: string
department?: string
location?: string
}
position?: 'top' | 'bottom'
}
const UserProfileCard: React.FC<UserProfileCardProps> = ({ user, position = 'bottom' }) => {
return (
<motion.div
initial={{ opacity: 0, scale: 0.95, y: position === 'bottom' ? -10 : 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: position === 'bottom' ? -10 : 10 }}
transition={{ duration: 0.15 }}
className={`absolute left-0 ${
position === 'bottom' ? 'top-full mt-2' : 'bottom-full mb-2'
} z-50 w-72 bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 p-4`}
>
{/* Header */}
<div className="flex items-start gap-3 mb-3 pb-3 border-b border-gray-200 dark:border-gray-700">
<img
src={user.avatar}
alt={user.name}
className="w-16 h-16 rounded-full object-cover ring-2 ring-blue-500"
/>
<div className="flex-1 min-w-0">
<h3 className="font-bold text-gray-900 dark:text-gray-100 text-lg mb-1">
{user.name}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<HiBriefcase className="w-4 h-4" />
{user.title}
</p>
</div>
</div>
{/* Contact Info */}
<div className="space-y-2">
{user.email && (
<div className="flex items-center gap-2 text-sm">
<HiMail className="w-4 h-4 text-gray-400 flex-shrink-0" />
<a
href={`mailto:${user.email}`}
className="text-blue-600 dark:text-blue-400 hover:underline truncate"
>
{user.email}
</a>
</div>
)}
{user.phone && (
<div className="flex items-center gap-2 text-sm">
<HiPhone className="w-4 h-4 text-gray-400 flex-shrink-0" />
<a
href={`tel:${user.phone}`}
className="text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400"
>
{user.phone}
</a>
</div>
)}
{user.department && (
<div className="flex items-center gap-2 text-sm">
<HiBriefcase className="w-4 h-4 text-gray-400 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">{user.department}</span>
</div>
)}
{user.location && (
<div className="flex items-center gap-2 text-sm">
<HiLocationMarker className="w-4 h-4 text-gray-400 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">{user.location}</span>
</div>
)}
</div>
{/* Arrow indicator */}
<div
className={`absolute left-6 ${
position === 'bottom' ? '-top-2' : '-bottom-2'
} w-4 h-4 bg-white dark:bg-gray-800 border-l border-t border-gray-200 dark:border-gray-700 transform ${
position === 'bottom' ? 'rotate-45' : '-rotate-135'
}`}
/>
</motion.div>
)
}
export default UserProfileCard

View file

@ -0,0 +1,236 @@
import React, { useState } from 'react'
import { AnimatePresence } from 'framer-motion'
import { mockSocialPosts, SocialPost } from '../../../mocks/mockSocialPosts'
import PostItem from './PostItem'
import { MediaItem } from './MediaManager'
import CreatePost from './CreatePost'
import { Location } from './LocationPicker'
const SocialWall: React.FC = () => {
const [posts, setPosts] = useState<SocialPost[]>(mockSocialPosts)
const [filter, setFilter] = useState<'all' | 'mine'>('all')
const handleCreatePost = (postData: {
content: string
location?: Location
media?: {
type: 'mixed' | 'poll'
mediaItems?: MediaItem[]
poll?: {
question: string
options: Array<{ text: string }>
}
}
}) => {
let mediaForPost = undefined
if (postData.media) {
if (postData.media.type === 'mixed' && postData.media.mediaItems) {
// Convert MediaItems to post format
const images = postData.media.mediaItems.filter(m => m.type === 'image')
const videos = postData.media.mediaItems.filter(m => m.type === 'video')
if (images.length > 0 && videos.length === 0) {
mediaForPost = {
type: 'image' as const,
urls: images.map(i => i.url)
}
} else if (videos.length > 0 && images.length === 0) {
mediaForPost = {
type: 'video' as const,
url: videos[0].url
}
} else if (images.length > 0 || videos.length > 0) {
// Mixed media - use first image for now
mediaForPost = {
type: 'image' as const,
urls: images.map(i => i.url)
}
}
} else if (postData.media.type === 'poll' && postData.media.poll) {
mediaForPost = {
type: 'poll' as const,
poll: {
question: postData.media.poll.question,
options: postData.media.poll.options.map((opt, index) => ({
id: `opt-${index}`,
text: opt.text,
votes: 0
})),
totalVotes: 0,
endsAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
}
}
}
}
const newPost: SocialPost = {
id: Date.now().toString(),
author: {
id: 'currentUser',
name: 'Siz',
avatar: 'https://i.pravatar.cc/150?img=1',
title: 'Çalışan'
},
content: postData.content,
createdAt: new Date(),
media: mediaForPost,
location: postData.location,
likes: {
count: 0,
isLiked: false,
users: []
},
comments: [],
isOwnPost: true
}
setPosts([newPost, ...posts])
}
const handleLike = (postId: string) => {
setPosts(
posts.map((post) => {
if (post.id === postId) {
return {
...post,
likes: {
...post.likes,
count: post.likes.isLiked ? post.likes.count - 1 : post.likes.count + 1,
isLiked: !post.likes.isLiked
}
}
}
return post
})
)
}
const handleComment = (postId: string, content: string) => {
setPosts(
posts.map((post) => {
if (post.id === postId) {
const newComment = {
id: Date.now().toString(),
author: {
id: 'currentUser',
name: 'Siz',
avatar: 'https://i.pravatar.cc/150?img=1'
},
content,
createdAt: new Date()
}
return {
...post,
comments: [...post.comments, newComment]
}
}
return post
})
)
}
const handleDelete = (postId: string) => {
if (window.confirm('Bu gönderiyi silmek istediğinizden emin misiniz?')) {
setPosts(posts.filter((post) => post.id !== postId))
}
}
const handleVote = (postId: string, optionId: string) => {
setPosts(
posts.map((post) => {
if (post.id === postId && post.media?.type === 'poll' && post.media.poll) {
const poll = post.media.poll
// If user already voted, don't allow voting again
if (poll.userVote) {
return post
}
return {
...post,
media: {
...post.media,
poll: {
...poll,
options: poll.options.map((opt) =>
opt.id === optionId ? { ...opt, votes: opt.votes + 1 } : opt
),
totalVotes: poll.totalVotes + 1,
userVote: optionId
}
}
}
}
return post
})
)
}
const filteredPosts = filter === 'mine' ? posts.filter((post) => post.isOwnPost) : posts
return (
<div className="max-w-2xl mx-auto py-6 px-4">
{/* Header */}
<div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">
Sosyal Duvar
</h1>
<p className="text-gray-600 dark:text-gray-400">
Ekip arkadaşlarınızla düşüncelerinizi paylaşın
</p>
</div>
{/* Filter Tabs */}
<div className="flex gap-4 mb-6 border-b border-gray-200 dark:border-gray-700">
<button
onClick={() => setFilter('all')}
className={`pb-3 px-1 border-b-2 transition-colors font-medium ${
filter === 'all'
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
Tüm Gönderiler
</button>
<button
onClick={() => setFilter('mine')}
className={`pb-3 px-1 border-b-2 transition-colors font-medium ${
filter === 'mine'
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
Gönderilerim
</button>
</div>
{/* Create Post */}
<CreatePost onCreatePost={handleCreatePost} />
{/* Posts Feed */}
<AnimatePresence>
{filteredPosts.length > 0 ? (
filteredPosts.map((post) => (
<PostItem
key={post.id}
post={post}
onLike={handleLike}
onComment={handleComment}
onDelete={handleDelete}
onVote={handleVote}
/>
))
) : (
<div className="text-center py-12">
<p className="text-gray-500 dark:text-gray-400 text-lg">
{filter === 'mine' ? 'Henüz bir gönderi paylaşmadınız.' : 'Henüz gönderi yok.'}
</p>
</div>
)}
</AnimatePresence>
</div>
)
}
export default SocialWall

View file

@ -0,0 +1,312 @@
export interface SocialPost {
id: string
author: {
id: string
name: string
avatar: string
title: string
email?: string
phone?: string
department?: string
location?: string
}
content: string
createdAt: Date
location?: {
id: string
name: string
address: string
lat: number
lng: number
placeId?: string
}
media?: {
type: 'image' | 'video' | 'poll'
url?: string
urls?: string[]
poll?: {
question: string
options: Array<{
id: string
text: string
votes: number
}>
totalVotes: number
endsAt: Date
userVote?: string
}
}
likes: {
count: number
isLiked: boolean
users: Array<{ id: string; name: string; avatar: string }>
}
comments: Array<{
id: string
author: {
id: string
name: string
avatar: string
}
content: string
createdAt: Date
}>
isOwnPost: boolean
}
export const mockSocialPosts: SocialPost[] = [
{
id: '1',
author: {
id: 'user1',
name: 'Ahmet Yılmaz',
avatar: 'https://i.pravatar.cc/150?img=12',
title: 'Yazılım Geliştirici',
email: 'ahmet.yilmaz@sozsoft.com',
phone: '+90 532 123 45 67',
department: 'Yazılım Geliştirme',
location: 'İstanbul, Türkiye'
},
content:
'Yeni proje üzerinde çalışıyoruz! React ve TypeScript ile harika bir deneyim oluşturuyoruz. Ekip çalışması harika gidiyor! 🚀',
createdAt: new Date('2024-10-15T10:30:00'),
location: {
id: '1',
name: 'Taksim Meydanı',
address: 'Taksim, Gümüşsuyu Mahallesi, 34437 Beyoğlu/İstanbul',
lat: 41.0369,
lng: 28.9850,
placeId: 'ChIJBQRGmL25yhQRXwqRTHAwAAQ'
},
media: {
type: 'image',
url: 'https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&q=80'
},
likes: {
count: 24,
isLiked: true,
users: [
{ id: 'user2', name: 'Ayşe Demir', avatar: 'https://i.pravatar.cc/150?img=5' },
{ id: 'user3', name: 'Mehmet Kaya', avatar: 'https://i.pravatar.cc/150?img=8' }
]
},
comments: [
{
id: 'c1',
author: {
id: 'user2',
name: 'Ayşe Demir',
avatar: 'https://i.pravatar.cc/150?img=5'
},
content: 'Harika görünüyor! Başarılar 👏',
createdAt: new Date('2024-10-15T11:00:00')
},
{
id: 'c2',
author: {
id: 'user3',
name: 'Mehmet Kaya',
avatar: 'https://i.pravatar.cc/150?img=8'
},
content: 'TypeScript gerçekten fark yaratıyor!',
createdAt: new Date('2024-10-15T11:30:00')
}
],
isOwnPost: false
},
{
id: '2',
author: {
id: 'currentUser',
name: 'Siz',
avatar: 'https://i.pravatar.cc/150?img=1',
title: 'Proje Yöneticisi'
},
content:
'Bu hafta sprint planlamasını yaptık. Ekibimizle birlikte yeni özellikleri değerlendirdik. Heyecan verici bir hafta olacak!',
createdAt: new Date('2024-10-16T09:00:00'),
media: {
type: 'poll',
poll: {
question: 'Hangi özelliği öncelikli olarak geliştirmeliyiz?',
options: [
{ id: 'p1', text: 'Kullanıcı profilleri', votes: 12 },
{ id: 'p2', text: 'Bildirim sistemi', votes: 8 },
{ id: 'p3', text: 'Mesajlaşma', votes: 15 },
{ id: 'p4', text: 'Raporlama', votes: 5 }
],
totalVotes: 40,
endsAt: new Date('2024-10-20T23:59:59'),
userVote: 'p3'
}
},
likes: {
count: 18,
isLiked: false,
users: []
},
comments: [
{
id: 'c3',
author: {
id: 'user4',
name: 'Fatma Şahin',
avatar: 'https://i.pravatar.cc/150?img=9'
},
content: 'Mesajlaşma özelliğine kesinlikle ihtiyacımız var!',
createdAt: new Date('2024-10-16T10:15:00')
}
],
isOwnPost: true
},
{
id: '3',
author: {
id: 'user5',
name: 'Zeynep Arslan',
avatar: 'https://i.pravatar.cc/150?img=10',
title: 'UI/UX Tasarımcı'
},
content:
'Yeni tasarım sistemimizin ilk prototipini hazırladık! Kullanıcı deneyimini iyileştirmek için çok çalıştık. Geri bildirimlerinizi bekliyorum! 🎨',
createdAt: new Date('2024-10-17T14:20:00'),
media: {
type: 'image',
urls: [
'https://images.unsplash.com/photo-1561070791-2526d30994b5?w=800&q=80',
'https://images.unsplash.com/photo-1586717799252-bd134ad00e26?w=800&q=80',
'https://images.unsplash.com/photo-1609921212029-bb5a28e60960?w=800&q=80'
]
},
likes: {
count: 42,
isLiked: true,
users: [
{ id: 'user1', name: 'Ahmet Yılmaz', avatar: 'https://i.pravatar.cc/150?img=12' }
]
},
comments: [
{
id: 'c4',
author: {
id: 'user6',
name: 'Can Öztürk',
avatar: 'https://i.pravatar.cc/150?img=11'
},
content: 'Tasarımlar çok şık! Renk paleti özellikle güzel 😍',
createdAt: new Date('2024-10-17T15:00:00')
},
{
id: 'c5',
author: {
id: 'user7',
name: 'Elif Yıldız',
avatar: 'https://i.pravatar.cc/150?img=20'
},
content: 'Dark mode opsiyonu da olacak mı?',
createdAt: new Date('2024-10-17T15:30:00')
}
],
isOwnPost: false
},
{
id: '4',
author: {
id: 'user8',
name: 'Burak Çelik',
avatar: 'https://i.pravatar.cc/150?img=13',
title: 'DevOps Mühendisi'
},
content:
'CI/CD pipeline güncellememiz tamamlandı! Deployment süremiz %40 azaldı. Otomasyonun gücü 💪',
createdAt: new Date('2024-10-18T08:45:00'),
media: {
type: 'video',
url: 'https://www.w3schools.com/html/mov_bbb.mp4'
},
likes: {
count: 31,
isLiked: false,
users: []
},
comments: [
{
id: 'c6',
author: {
id: 'user9',
name: 'Deniz Koç',
avatar: 'https://i.pravatar.cc/150?img=14'
},
content: 'Harika iş! Detayları paylaşabilir misin?',
createdAt: new Date('2024-10-18T09:15:00')
}
],
isOwnPost: false
},
{
id: '5',
author: {
id: 'user10',
name: 'Selin Aydın',
avatar: 'https://i.pravatar.cc/150?img=15',
title: 'İK Müdürü'
},
content:
'Ekip üyelerimize yeni eğitim programımızı duyurmak istiyorum! 🎓 React, TypeScript ve Modern Web Geliştirme konularında kapsamlı bir program hazırladık.',
createdAt: new Date('2024-10-14T16:00:00'),
likes: {
count: 56,
isLiked: true,
users: []
},
comments: [
{
id: 'c7',
author: {
id: 'user1',
name: 'Ahmet Yılmaz',
avatar: 'https://i.pravatar.cc/150?img=12'
},
content: 'Ne zaman başlıyor?',
createdAt: new Date('2024-10-14T16:30:00')
},
{
id: 'c8',
author: {
id: 'user10',
name: 'Selin Aydın',
avatar: 'https://i.pravatar.cc/150?img=15'
},
content: 'Gelecek hafta başlıyoruz! Kayıt linki mail ile paylaşılacak.',
createdAt: new Date('2024-10-14T17:00:00')
}
],
isOwnPost: false
},
{
id: '6',
author: {
id: 'user11',
name: 'Deniz Öztürk',
avatar: 'https://i.pravatar.cc/150?img=20',
title: 'Proje Yöneticisi'
},
content: 'Bugün müşteri ile harika bir toplantı yaptık! Yeni projenin detaylarını konuştuk. 🎯',
createdAt: new Date('2024-10-17T14:00:00'),
location: {
id: '4',
name: 'Sultanahmet Meydanı',
address: 'Sultanahmet Mahallesi, 34122 Fatih/İstanbul',
lat: 41.0058,
lng: 28.9768,
placeId: 'ChIJ7fVVZiy5yhQRzsXXXXXXXXk'
},
likes: {
count: 18,
isLiked: false,
users: []
},
comments: [],
isOwnPost: false
}
]

View file

@ -1,15 +1,21 @@
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import SocialWall from '@/components/intranet/SocialWall'
const Dashboard = () => { const Dashboard = () => {
const { translate } = useLocalization() const { translate } = useLocalization()
return ( return (
<Helmet <>
titleTemplate="%s | Sözsoft Kurs Platform" <Helmet
title={translate('::' + 'Dashboard')} titleTemplate="%s | Sözsoft Kurs Platform"
defaultTitle="Sözsoft Kurs Platform" title={translate('::' + 'Dashboard')}
></Helmet> defaultTitle="Sözsoft Kurs Platform"
/>
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<SocialWall />
</div>
</>
) )
} }

View file

@ -6,6 +6,7 @@ interface ImportMetaEnv {
readonly VITE_CDN_URL: string readonly VITE_CDN_URL: string
readonly VITE_REACT_APP_VERSION: string readonly VITE_REACT_APP_VERSION: string
readonly VITE_AI_URL: string readonly VITE_AI_URL: string
readonly VITE_GOOGLE_MAPS_API_KEY: string
} }
interface ImportMeta { interface ImportMeta {