Sosyal Duvar
This commit is contained in:
parent
a299ae099d
commit
c78845f411
14 changed files with 2412 additions and 8 deletions
1
ui/.env
1
ui/.env
|
|
@ -2,3 +2,4 @@ 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
50
ui/package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
468
ui/src/components/intranet/SocialWall/CreatePost.tsx
Normal file
468
ui/src/components/intranet/SocialWall/CreatePost.tsx
Normal 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
|
||||||
97
ui/src/components/intranet/SocialWall/LocationMap.tsx
Normal file
97
ui/src/components/intranet/SocialWall/LocationMap.tsx
Normal 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 aç</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 Aç</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
|
||||||
386
ui/src/components/intranet/SocialWall/LocationPicker.tsx
Normal file
386
ui/src/components/intranet/SocialWall/LocationPicker.tsx
Normal 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
|
||||||
72
ui/src/components/intranet/SocialWall/MediaLightbox.tsx
Normal file
72
ui/src/components/intranet/SocialWall/MediaLightbox.tsx
Normal 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
|
||||||
243
ui/src/components/intranet/SocialWall/MediaManager.tsx
Normal file
243
ui/src/components/intranet/SocialWall/MediaManager.tsx
Normal 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
|
||||||
433
ui/src/components/intranet/SocialWall/PostItem.tsx
Normal file
433
ui/src/components/intranet/SocialWall/PostItem.tsx
Normal 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
|
||||||
101
ui/src/components/intranet/SocialWall/UserProfileCard.tsx
Normal file
101
ui/src/components/intranet/SocialWall/UserProfileCard.tsx
Normal 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
|
||||||
236
ui/src/components/intranet/SocialWall/index.tsx
Normal file
236
ui/src/components/intranet/SocialWall/index.tsx
Normal 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
|
||||||
312
ui/src/mocks/mockSocialPosts.ts
Normal file
312
ui/src/mocks/mockSocialPosts.ts
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -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
|
<Helmet
|
||||||
titleTemplate="%s | Sözsoft Kurs Platform"
|
titleTemplate="%s | Sözsoft Kurs Platform"
|
||||||
title={translate('::' + 'Dashboard')}
|
title={translate('::' + 'Dashboard')}
|
||||||
defaultTitle="Sözsoft Kurs Platform"
|
defaultTitle="Sözsoft Kurs Platform"
|
||||||
></Helmet>
|
/>
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
|
<SocialWall />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
1
ui/src/vite-env.d.ts
vendored
1
ui/src/vite-env.d.ts
vendored
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue