Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95d8cafec2 | ||
|
|
38eb166953 |
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
@ -13,6 +12,23 @@ const Sequelize = db.Sequelize;
|
|||||||
const Op = Sequelize.Op;
|
const Op = Sequelize.Op;
|
||||||
|
|
||||||
module.exports = class UsersDBApi {
|
module.exports = class UsersDBApi {
|
||||||
|
static async getFollowersCount(userId) {
|
||||||
|
return await db.friendships.count({
|
||||||
|
where: { addresseeId: userId, status: "accepted" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
static async getFollowingCount(userId) {
|
||||||
|
return await db.friendships.count({
|
||||||
|
where: { requesterId: userId, status: "accepted" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
static async isFollowing(requesterId, addresseeId) {
|
||||||
|
const friendship = await db.friendships.findOne({
|
||||||
|
where: { requesterId, addresseeId }
|
||||||
|
});
|
||||||
|
return !!friendship;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static async create(data, options) {
|
static async create(data, options) {
|
||||||
const currentUser = (options && options.currentUser) || { id: null };
|
const currentUser = (options && options.currentUser) || { id: null };
|
||||||
@ -85,6 +101,10 @@ module.exports = class UsersDBApi {
|
|||||||
null
|
null
|
||||||
,
|
,
|
||||||
|
|
||||||
|
bio: data.data.bio || null,
|
||||||
|
birthday: data.data.birthday || null,
|
||||||
|
education: data.data.education || null,
|
||||||
|
|
||||||
importHash: data.data.importHash || null,
|
importHash: data.data.importHash || null,
|
||||||
createdById: currentUser.id,
|
createdById: currentUser.id,
|
||||||
updatedById: currentUser.id,
|
updatedById: currentUser.id,
|
||||||
@ -143,7 +163,10 @@ module.exports = class UsersDBApi {
|
|||||||
const usersData = data.map((item, index) => ({
|
const usersData = data.map((item, index) => ({
|
||||||
id: item.id || undefined,
|
id: item.id || undefined,
|
||||||
|
|
||||||
firstName: item.firstName
|
firstName: item.firstName,
|
||||||
|
bio: item.bio,
|
||||||
|
birthday: item.birthday,
|
||||||
|
education: item.education
|
||||||
||
|
||
|
||||||
null
|
null
|
||||||
,
|
,
|
||||||
@ -205,6 +228,10 @@ module.exports = class UsersDBApi {
|
|||||||
null
|
null
|
||||||
,
|
,
|
||||||
|
|
||||||
|
bio: item.bio || null,
|
||||||
|
birthday: item.birthday || null,
|
||||||
|
education: item.education || null,
|
||||||
|
|
||||||
importHash: item.importHash || null,
|
importHash: item.importHash || null,
|
||||||
createdById: currentUser.id,
|
createdById: currentUser.id,
|
||||||
updatedById: currentUser.id,
|
updatedById: currentUser.id,
|
||||||
@ -298,6 +325,9 @@ module.exports = class UsersDBApi {
|
|||||||
|
|
||||||
if (data.provider !== undefined) updatePayload.provider = data.provider;
|
if (data.provider !== undefined) updatePayload.provider = data.provider;
|
||||||
|
|
||||||
|
if (data.bio !== undefined) updatePayload.bio = data.bio;
|
||||||
|
if (data.birthday !== undefined) updatePayload.birthday = data.birthday;
|
||||||
|
if (data.education !== undefined) updatePayload.education = data.education;
|
||||||
|
|
||||||
updatePayload.updatedById = currentUser.id;
|
updatePayload.updatedById = currentUser.id;
|
||||||
|
|
||||||
@ -986,5 +1016,4 @@ module.exports = class UsersDBApi {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** @type {import('sequelize-cli').Migration} */
|
||||||
|
module.exports = {
|
||||||
|
async up (queryInterface, Sequelize) {
|
||||||
|
await queryInterface.addColumn('users', 'bio', {
|
||||||
|
type: Sequelize.DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
await queryInterface.addColumn('users', 'birthday', {
|
||||||
|
type: Sequelize.DataTypes.DATEONLY,
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
await queryInterface.addColumn('users', 'education', {
|
||||||
|
type: Sequelize.DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async down (queryInterface, Sequelize) {
|
||||||
|
await queryInterface.removeColumn('users', 'bio');
|
||||||
|
await queryInterface.removeColumn('users', 'birthday');
|
||||||
|
await queryInterface.removeColumn('users', 'education');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -104,6 +104,16 @@ provider: {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
bio: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
},
|
||||||
|
birthday: {
|
||||||
|
type: DataTypes.DATEONLY,
|
||||||
|
},
|
||||||
|
education: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
},
|
||||||
|
|
||||||
importHash: {
|
importHash: {
|
||||||
type: DataTypes.STRING(255),
|
type: DataTypes.STRING(255),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
@ -335,7 +345,9 @@ function trimStringFields(users) {
|
|||||||
users.lastName = users.lastName
|
users.lastName = users.lastName
|
||||||
? users.lastName.trim()
|
? users.lastName.trim()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
users.bio = users.bio ? users.bio.trim() : null;
|
||||||
|
users.education = users.education ? users.education.trim() : null;
|
||||||
|
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -8,6 +8,12 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
label: 'Dashboard',
|
label: 'Dashboard',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
href: '/timeline',
|
||||||
|
icon: icon.mdiViewDashboard,
|
||||||
|
label: 'Timeline',
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
href: '/users/users-list',
|
href: '/users/users-list',
|
||||||
label: 'Users',
|
label: 'Users',
|
||||||
@ -42,7 +48,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/timeline_posts/timeline_posts-list',
|
href: '/timeline_posts/timeline_posts-list',
|
||||||
label: 'Timeline posts',
|
label: 'Timeline posts (Admin)',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiPostOutline' in icon ? icon['mdiPostOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiPostOutline' in icon ? icon['mdiPostOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
@ -120,6 +126,11 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
icon: 'mdiAlertOctagonOutline' in icon ? icon['mdiAlertOctagonOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiAlertOctagonOutline' in icon ? icon['mdiAlertOctagonOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
permissions: 'READ_USER_REPORTS'
|
permissions: 'READ_USER_REPORTS'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: '/media-gallery',
|
||||||
|
label: 'Media Gallery',
|
||||||
|
icon: icon.mdiImageMultiple,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: '/profile',
|
href: '/profile',
|
||||||
label: 'Profile',
|
label: 'Profile',
|
||||||
|
|||||||
60
frontend/src/pages/media-gallery/index.tsx
Normal file
60
frontend/src/pages/media-gallery/index.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { mdiImageMultiple } from '@mdi/js';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import React, { ReactElement, useEffect, useState } from 'react';
|
||||||
|
import CardBox from '../../components/CardBox';
|
||||||
|
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||||
|
import SectionMain from '../../components/SectionMain';
|
||||||
|
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||||
|
import { getPageTitle } from '../../config';
|
||||||
|
import axios from 'axios';
|
||||||
|
import ImageField from '../../components/ImageField';
|
||||||
|
|
||||||
|
const MediaGallery = () => {
|
||||||
|
const [images, setImages] = useState<any[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPosts = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/timeline_posts');
|
||||||
|
const posts = response.data.rows || [];
|
||||||
|
const galleryImages = posts.flatMap((post: any) => post.media_images || []);
|
||||||
|
setImages(galleryImages);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch media:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchPosts();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Media Gallery')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton icon={mdiImageMultiple} title='Media Gallery' main>
|
||||||
|
{''}
|
||||||
|
</SectionTitleLineWithButton>
|
||||||
|
<CardBox>
|
||||||
|
<div className='grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4'>
|
||||||
|
{images.map((image: any, index: number) => (
|
||||||
|
<ImageField
|
||||||
|
key={index}
|
||||||
|
name={'Media'}
|
||||||
|
image={image}
|
||||||
|
className='w-full h-32 rounded-lg overflow-hidden'
|
||||||
|
imageClassName='h-full w-full object-cover'
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
MediaGallery.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MediaGallery;
|
||||||
@ -45,7 +45,10 @@ const EditUsers = () => {
|
|||||||
app_role: '',
|
app_role: '',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
avatar: [],
|
avatar: [],
|
||||||
password: ''
|
password: '',
|
||||||
|
bio: '',
|
||||||
|
birthday: null,
|
||||||
|
education: ''
|
||||||
};
|
};
|
||||||
const [initialValues, setInitialValues] = useState(initVals);
|
const [initialValues, setInitialValues] = useState(initVals);
|
||||||
|
|
||||||
@ -92,6 +95,7 @@ const EditUsers = () => {
|
|||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={(values) => handleSubmit(values)}
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
>
|
>
|
||||||
|
{({ setFieldValue, values }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<FormField>
|
<FormField>
|
||||||
<Field
|
<Field
|
||||||
@ -124,6 +128,23 @@ const EditUsers = () => {
|
|||||||
<Field name='email' placeholder='E-Mail' disabled />
|
<Field name='email' placeholder='E-Mail' disabled />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='Bio'>
|
||||||
|
<Field name='bio' placeholder='Bio' component="textarea" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='Birthday'>
|
||||||
|
<DatePicker
|
||||||
|
selected={values.birthday ? new Date(values.birthday) : null}
|
||||||
|
onChange={(date) => setFieldValue('birthday', date)}
|
||||||
|
className="px-3 py-2 max-w-full border-gray-700 rounded w-full focus:ring focus:ring-blue-600 focus:border-blue-600 border bg-white dark:bg-slate-800"
|
||||||
|
placeholderText="Select a date"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='Education'>
|
||||||
|
<Field name='education' placeholder='Education' />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
<FormField label='App Role' labelFor='app_role'>
|
<FormField label='App Role' labelFor='app_role'>
|
||||||
<Field
|
<Field
|
||||||
name='app_role'
|
name='app_role'
|
||||||
@ -166,6 +187,7 @@ const EditUsers = () => {
|
|||||||
/>
|
/>
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
</Form>
|
</Form>
|
||||||
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
@ -177,4 +199,4 @@ EditUsers.getLayout = function getLayout(page: ReactElement) {
|
|||||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditUsers;
|
export default EditUsers;
|
||||||
72
frontend/src/pages/timeline.tsx
Normal file
72
frontend/src/pages/timeline.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { mdiViewDashboard, mdiPlus } from '@mdi/js';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import React, { ReactElement, useEffect, useState } from 'react';
|
||||||
|
import CardBox from '../components/CardBox';
|
||||||
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
|
import SectionMain from '../components/SectionMain';
|
||||||
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
||||||
|
import FormField from '../components/FormField';
|
||||||
|
import BaseButton from '../components/BaseButton';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
|
import { fetch, create } from '../stores/timeline_posts/timeline_postsSlice';
|
||||||
|
import { getPageTitle } from '../config';
|
||||||
|
|
||||||
|
const TimelinePage = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { timeline_posts, loading } = useAppSelector((state) => state.timeline_posts);
|
||||||
|
const [content, setContent] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetch({}));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handlePost = () => {
|
||||||
|
if (!content.trim()) return;
|
||||||
|
dispatch(create({ content })).then(() => {
|
||||||
|
setContent('');
|
||||||
|
dispatch(fetch({}));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Timeline')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton icon={mdiViewDashboard} title="Timeline" main />
|
||||||
|
|
||||||
|
<CardBox className="mb-6">
|
||||||
|
<FormField label="What's on your mind?">
|
||||||
|
<textarea
|
||||||
|
className="w-full p-2 border border-gray-300 rounded"
|
||||||
|
rows={3}
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<BaseButton label="Post" color="info" onClick={handlePost} icon={mdiPlus} />
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{loading ? (
|
||||||
|
<p>Loading posts...</p>
|
||||||
|
) : (
|
||||||
|
timeline_posts.map((post: any) => (
|
||||||
|
<CardBox key={post.id}>
|
||||||
|
<p className="text-gray-800">{post.content}</p>
|
||||||
|
<small className="text-gray-500">{new Date(post.createdAt).toLocaleString()}</small>
|
||||||
|
</CardBox>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TimelinePage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TimelinePage;
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user