Compare commits

...

2 Commits

Author SHA1 Message Date
Flatlogic Bot
95d8cafec2 Auto commit: 2026-03-17T21:49:25.816Z 2026-03-17 21:49:25 +00:00
Flatlogic Bot
38eb166953 WeedChat 2026-03-17 21:21:37 +00:00
8 changed files with 279 additions and 1537 deletions

View File

@ -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 {
}; };

View File

@ -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');
}
};

View File

@ -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;
} }

View File

@ -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',

View 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;

View File

@ -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;

View 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