i did thing oh god large commit

This commit is contained in:
Freya Murphy 2024-04-05 10:46:09 -04:00
parent ddfe92fee4
commit 530bbf0587
Signed by: freya
GPG key ID: 744AB800E383AE52
38 changed files with 701 additions and 279 deletions

View file

@ -198,7 +198,7 @@ CREATE TABLE admin.media (
id INTEGER DEFAULT nextval('sys.media_id_seq'::regclass) NOT NULL, id INTEGER DEFAULT nextval('sys.media_id_seq'::regclass) NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
content BYTEA NOT NULL, content BYTEA NOT NULL,
type TEXT NOT NULL, mime TEXT NOT NULL,
created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL, created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL
); );
@ -224,9 +224,12 @@ CREATE TYPE admin.user_media_type AS ENUM (
CREATE TABLE admin.user_media ( CREATE TABLE admin.user_media (
id INTEGER DEFAULT nextval('sys.user_media_id_seq'::regclass) NOT NULL, id INTEGER DEFAULT nextval('sys.user_media_id_seq'::regclass) NOT NULL,
media_id INTEGER NOT NULL,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
type admin.user_media_type NOT NULL content BYTEA NOT NULL,
mime TEXT NOT NULL,
type admin.user_media_type NOT NULL,
created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL
); );
ALTER TABLE admin.user_media OWNER TO xssbook; ALTER TABLE admin.user_media OWNER TO xssbook;
@ -234,9 +237,6 @@ ALTER TABLE admin.user_media OWNER TO xssbook;
ALTER TABLE ONLY admin.user_media ALTER TABLE ONLY admin.user_media
ADD CONSTRAINT user_media_pkey PRIMARY KEY (id); ADD CONSTRAINT user_media_pkey PRIMARY KEY (id);
ALTER TABLE ONLY admin.user_media
ADD CONSTRAINT user_media_media_id_fkey FOREIGN KEY (media_id) REFERENCES admin.media (id) ON DELETE CASCADE;
ALTER TABLE ONLY admin.user_media ALTER TABLE ONLY admin.user_media
ADD CONSTRAINT user_media_user_id_fkey FOREIGN KEY (user_id) REFERENCES admin.user (id) ON DELETE CASCADE; ADD CONSTRAINT user_media_user_id_fkey FOREIGN KEY (user_id) REFERENCES admin.user (id) ON DELETE CASCADE;

View file

@ -1,4 +1,4 @@
CREATE FUNCTION _api.serve_media( CREATE FUNCTION _api.serve_system_media(
_media_id INTEGER _media_id INTEGER
) )
RETURNS sys."*/*" RETURNS sys."*/*"
@ -8,34 +8,30 @@ DECLARE
_headers TEXT; _headers TEXT;
_data BYTEA; _data BYTEA;
BEGIN BEGIN
SELECT FORMAT( SELECT FORMAT(
'[{"Content-Type": "%s"},' '[{"Content-Type": "%s"},'
'{"Content-Disposition": "inline; filename=\"%s\""},' '{"Content-Disposition": "inline; filename=\"%s\""},'
'{"Cache-Control": "max-age=259200"}]' '{"Cache-Control": "max-age=259200"}]'
, m.type, m.name) , m.mime, m.name)
FROM admin.media m FROM admin.media m
WHERE m.id = _media_id INTO _headers; WHERE m.id = _media_id
INTO _headers;
PERFORM SET_CONFIG('response.headers', _headers, true);
SELECT m.content SELECT m.content
FROM admin.media m FROM admin.media m
WHERE m.id = _media_id WHERE m.id = _media_id
INTO _data; INTO _data;
IF FOUND THEN IF _data IS NOT NULL THEN
PERFORM SET_CONFIG('response.headers', _headers, true);
RETURN(_data); RETURN(_data);
ELSE ELSE
PERFORM _api.raise( PERFORM _api.raise_not_found();
_msg => 'api_not_found',
_err => 404
);
END IF; END IF;
END END
$BODY$; $BODY$;
GRANT EXECUTE ON FUNCTION _api.serve_media(INTEGER) GRANT EXECUTE ON FUNCTION _api.serve_system_media(INTEGER)
TO rest_anon, rest_user; TO rest_anon, rest_user;
GRANT SELECT ON TABLE admin.media GRANT SELECT ON TABLE admin.media
TO rest_anon, rest_user; TO rest_anon, rest_user;

View file

@ -0,0 +1,37 @@
CREATE FUNCTION _api.serve_user_media(
_media_id INTEGER
)
RETURNS sys."*/*"
LANGUAGE plpgsql VOLATILE
AS $BODY$
DECLARE
_headers TEXT;
_data BYTEA;
BEGIN
SELECT FORMAT(
'[{"Content-Type": "%s"},'
'{"Content-Disposition": "inline"},'
'{"Cache-Control": "max-age=259200"}]'
, m.mime)
FROM admin.user_media m
WHERE m.id = _media_id
INTO _headers;
SELECT m.content
FROM admin.user_media m
WHERE m.id = _media_id
INTO _data;
IF _data IS NOT NULL THEN
PERFORM SET_CONFIG('response.headers', _headers, true);
RETURN(_data);
ELSE
PERFORM _api.raise_not_found();
END IF;
END
$BODY$;
GRANT EXECUTE ON FUNCTION _api.serve_user_media(INTEGER)
TO rest_anon, rest_user;
GRANT SELECT ON TABLE admin.user_media
TO rest_anon, rest_user;

View file

@ -0,0 +1,41 @@
CREATE FUNCTION _api.serve_user_or_default_media(
_user_id INTEGER,
_type admin.user_media_type,
_default TEXT
)
RETURNS sys."*/*"
LANGUAGE plpgsql VOLATILE
AS $BODY$
DECLARE
_media_id INTEGER;
BEGIN
SELECT id
FROM admin.user_media m
WHERE m.type = _type
AND m.user_id = _user_id
INTO _media_id;
IF FOUND THEN
RETURN _api.serve_user_media(_media_id);
END IF;
SELECT id
FROM admin.media m
WHERE m.name = _default
INTO _media_id;
IF FOUND THEN
RETURN _api.serve_system_media(_media_id);
END IF;
PERFORM _api_raise_not_found();
END
$BODY$;
GRANT EXECUTE ON FUNCTION _api.serve_user_or_default_media(INTEGER, admin.user_media_type, TEXT)
TO rest_anon, rest_user;
GRANT SELECT ON TABLE admin.user_media
TO rest_anon, rest_user;
GRANT SELECT ON TABLE admin.media
TO rest_anon, rest_user;

View file

@ -5,32 +5,16 @@ RETURNS sys."*/*"
LANGUAGE plpgsql VOLATILE LANGUAGE plpgsql VOLATILE
AS $BODY$ AS $BODY$
DECLARE DECLARE
_id INTEGER; _default TEXT;
_mod INTEGER;
_name TEXT;
BEGIN BEGIN
SELECT media_id INTO _id _default := 'default_avatar_' || MOD(user_id, 25) || '.png';
FROM admin.user_media m RETURN _api.serve_user_or_default_media(
WHERE m.user_id = profile_avatar.user_id user_id,
AND type = 'avatar'::admin.user_media_type; 'avatar'::admin.user_media_type,
_default
-- get default if not exists );
IF NOT FOUND THEN
_mod = MOD(user_id, 24);
_name = 'default_avatar_' || _mod || '.png';
SELECT id INTO _id
FROM admin.media
WHERE name = _name;
END IF;
RETURN _api.serve_media(_id);
END END
$BODY$; $BODY$;
GRANT EXECUTE ON FUNCTION api.profile_avatar(INTEGER) GRANT EXECUTE ON FUNCTION api.profile_avatar(INTEGER)
TO rest_anon, rest_user; TO rest_anon, rest_user;
GRANT SELECT ON TABLE admin.user_media
TO rest_anon, rest_user;
GRANT SELECT ON TABLE admin.media
TO rest_anon, rest_user;

View file

@ -4,10 +4,21 @@ CREATE FUNCTION api.profile_banner(
RETURNS sys."*/*" RETURNS sys."*/*"
LANGUAGE plpgsql VOLATILE LANGUAGE plpgsql VOLATILE
AS $BODY$ AS $BODY$
DECLARE
_default TEXT;
BEGIN BEGIN
PERFORM _api.raise_deny(); _default := 'default_banner_' || MOD(user_id, 25) || '.png';
RETURN _api.serve_user_or_default_media(
user_id,
'banner'::admin.user_media_type,
_default
);
END END
$BODY$; $BODY$;
GRANT EXECUTE ON FUNCTION api.profile_banner(INTEGER) GRANT EXECUTE ON FUNCTION api.profile_banner(INTEGER)
TO rest_anon, rest_user; TO rest_anon, rest_user;
GRANT SELECT ON TABLE admin.user_media
TO rest_anon, rest_user;
GRANT SELECT ON TABLE admin.media
TO rest_anon, rest_user;

View file

@ -6,7 +6,9 @@ CREATE VIEW api.post AS
p.created, p.created,
p.modified, p.modified,
COALESCE(c.cc, 0) COALESCE(c.cc, 0)
AS comment_count AS comment_count,
COALESCE(l.lc, 0)
AS like_count
FROM FROM
admin.post p admin.post p
LEFT JOIN ( LEFT JOIN (
@ -20,6 +22,17 @@ CREATE VIEW api.post AS
) c ) c
ON ON
p.id = c.post_id p.id = c.post_id
LEFT JOIN (
SELECT
COUNT(l.id) as lc,
l.post_id
FROM
admin.like l
GROUP BY
l.post_id
) l
ON
p.id = l.post_id
LEFT JOIN LEFT JOIN
admin.user u admin.user u
ON ON

View file

@ -15,7 +15,6 @@ GRANT USAGE ON SCHEMA _api TO rest_anon, rest_user;
-- util -- util
\i /db/rest/util/_api_trim.sql; \i /db/rest/util/_api_trim.sql;
\i /db/rest/util/_api_serve_media.sql;
\i /db/rest/util/_api_raise.sql; \i /db/rest/util/_api_raise.sql;
\i /db/rest/util/_api_raise_null.sql; \i /db/rest/util/_api_raise_null.sql;
\i /db/rest/util/_api_raise_unique.sql; \i /db/rest/util/_api_raise_unique.sql;
@ -47,6 +46,9 @@ GRANT USAGE ON SCHEMA _api TO rest_anon, rest_user;
\i /db/rest/like/api_like_delete.sql; \i /db/rest/like/api_like_delete.sql;
-- media -- media
\i /db/rest/media/_api_serve_user_media.sql;
\i /db/rest/media/_api_serve_system_media.sql;
\i /db/rest/media/_api_serve_user_or_default_media.sql;
\i /db/rest/media/api_profile_avatar.sql; \i /db/rest/media/api_profile_avatar.sql;
\i /db/rest/media/api_profile_banner.sql; \i /db/rest/media/api_profile_banner.sql;

View file

@ -13,9 +13,74 @@ CREATE VIEW api.user AS
u.profile_bio, u.profile_bio,
u.created, u.created,
u.modified, u.modified,
u.seen u.seen,
COALESCE(f.fc, 0)
AS follower_count,
COALESCE(fl.fc, 0)
AS followed_count,
COALESCE(c.cc, 0)
AS comment_count,
COALESCE(p.pc, 0)
AS post_count,
COALESCE(l.lc, 0)
AS like_count
FROM FROM
admin.user u admin.user u
LEFT JOIN (
SELECT
COUNT(f.id) as fc,
f.followee_id
FROM
admin.follow f
GROUP BY
f.followee_id
) f
ON
u.id = f.followee_id
LEFT JOIN (
SELECT
COUNT(fl.id) as fc,
fl.follower_id
FROM
admin.follow fl
GROUP BY
fl.follower_id
) fl
ON
u.id = fl.follower_id
LEFT JOIN (
SELECT
COUNT(c.id) as cc,
c.user_id
FROM
admin.comment c
GROUP BY
c.user_id
) c
ON
u.id = c.user_id
LEFT JOIN (
SELECT
COUNT(p.id) as pc,
p.user_id
FROM
admin.post p
GROUP BY
p.user_id
) p
ON
u.id = p.user_id
LEFT JOIN (
SELECT
COUNT(l.id) as lc,
l.user_id
FROM
admin.like l
GROUP BY
l.user_id
) l
ON
u.id = l.user_id
WHERE WHERE
u.deleted <> TRUE; u.deleted <> TRUE;

View file

@ -0,0 +1,16 @@
CREATE FUNCTION _api.raise_not_found()
RETURNS BOOLEAN
LANGUAGE plpgsql VOLATILE
AS $BODY$
BEGIN
PERFORM _api.raise(
_msg => 'api_not_found',
_err => 404
);
RETURN TRUE;
END
$BODY$;
GRANT EXECUTE ON FUNCTION _api.raise_not_found()
TO rest_anon, rest_user;

View file

@ -170,6 +170,21 @@ a, button {
color: var(--blue-alt); color: var(--blue-alt);
} }
.btn {
position: relative;
}
.btn-border::before {
position: absolute;
content: "";
display: block;
bottom: -1px;
left: 0;
right: 0;
height: 1px;
background: var(--blue-alt);
}
input.btn:focus { input.btn:focus {
border: none; border: none;
outline: none; outline: none;
@ -205,6 +220,7 @@ input.btn:focus {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
z-index: 5;
} }
.nav { .nav {
@ -224,7 +240,7 @@ input.btn:focus {
flex: 1; flex: 1;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
z-index: 2; z-index: 6;
} }
@media (min-width: 800px) { @media (min-width: 800px) {
@ -274,6 +290,11 @@ input.btn:focus {
.nav-center .btn.active { .nav-center .btn.active {
border-bottom: none; border-bottom: none;
} }
.nav .btn-border::before {
background: inherit;
}
} }
.nav-right .image-loading { .nav-right .image-loading {
@ -305,6 +326,10 @@ input.btn:focus {
animation: shimmer 1s linear infinite; animation: shimmer 1s linear infinite;
} }
.image-loaded {
background-color: var(--base);
}
.card { .card {
background-color: var(--surface0); background-color: var(--surface0);
border-radius: .5rem; border-radius: .5rem;
@ -393,6 +418,14 @@ input.btn:focus {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
animation: fadeIn .1s, slideInModal .1s linear; animation: fadeIn .1s, slideInModal .1s linear;
z-index: 10;
}
@media (max-width: 40rem) {
.modal {
min-width: 100%;
width: 100%;
}
} }
@keyframes slideInModal { @keyframes slideInModal {

View file

@ -2,11 +2,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} padding: 1rem;
padding-bottom: 0;
.card {
width: 40rem;
margin-bottom: 1rem;
} }
.new-post-modal textarea { .new-post-modal textarea {

View file

@ -11,42 +11,20 @@
} }
#people-container { #people-container {
display: grid;
width: 100%;
padding: 1rem 2rem;
margin-bottom: 1rem;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr) );
grid-auto-rows: max-content;
grid-gap: 2rem;
}
.profile {
width: 16rem;
text-decoration: none;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
padding: 1rem 2rem;
padding-bottom: 0;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
max-width: 90rem;
}
.profile {
width: 25rem;
}
@media(max-width: 1400px) {
#people-container {
max-width: 70rem;
}
.profile {
width: 25rem;
}
}
@media(max-width: 1000px) {
#people-container {
max-width: 50rem;
}
.profile {
width: 20rem;
}
}
.profile {
margin: 1rem;
text-decoration: none;
height: fit-content;
} }
.profile:hover { .profile:hover {
@ -54,16 +32,22 @@
} }
.profile strong { .profile strong {
font-size: 2rem; font-size: 1.5rem;
} }
.profile .pfp, .profile .pfp img { .profile .pfp, .profile .pfp img {
padding: none; padding: none;
margin: none; margin: none;
height: 6rem; width: 100%;
height: 100%;
aspect-ratio: 1;
border-radius: .3rem; border-radius: .3rem;
} }
.profile .pfp {
margin-bottom: 1rem;
}
td:nth-child(1) { td:nth-child(1) {
font-weight: bold; font-weight: bold;
color: var(--subtext); color: var(--subtext);

View file

@ -8,6 +8,22 @@
justify-content: center; justify-content: center;
} }
.post, #new-post {
margin-bottom: 1rem;
width: 40rem;
}
.post { .post {
padding-bottom: 0; padding-bottom: 0;
} }
@media(max-width: 40rem) {
.post, #new-post {
width: 100%;
}
}
.post .likes {
display: block;
padding-top: .25rem;
}

View file

@ -4,17 +4,34 @@
padding: 0; padding: 0;
} }
#profile-header { #profile-header-container {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
background-color: var(--surface0); background-color: var(--surface0);
margin-bottom: 2rem; margin-bottom: 1rem;
border-bottom: 1px solid var(--surface1);
}
#profile-header {
min-width: 0;
max-width: 80rem;
flex-grow: 1;
} }
#profile-header .banner { #profile-header .banner {
width: 100%; width: 100%;
min-height: 20rem; min-height: 30rem;
aspect-ratio: 5; aspect-ratio: 5;
} }
#profile-header .banner img {
height: 100%;
width: 100%;
object-fit: cover;
}
#profile-header .info .pfp-wrapper .pfp, #profile-header .info .pfp-wrapper .pfp,
#profile-header .info .pfp-wrapper .pfp img { #profile-header .info .pfp-wrapper .pfp img {
height: 12.5rem; height: 12.5rem;
@ -32,7 +49,7 @@
border-radius: 100%; border-radius: 100%;
position: absolute; position: absolute;
top: -2.5rem; top: -2.5rem;
left: 2rem; left: 1rem;
} }
#profile-header .info .content { #profile-header .info .content {
@ -65,13 +82,27 @@
text-align: center; text-align: center;
} }
#tab-posts, .tab {
#post-container { max-width: 80rem;
width: 40rem; width: 100%;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
padding: 0 1rem;
margin-bottom: 1rem;
}
#post-container {
max-width: 40rem;
width: 100%;
margin-left: auto;
margin-right: auto;
margin-bottom: -1rem;
} }
#post-container .post { #post-container .post {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
td:nth-child(1) {
padding-right: 2rem;
}

View file

@ -13,7 +13,8 @@ var $$ = (selector) => {
'click', 'click',
'submit', 'submit',
'each', 'each',
'error' 'error',
'one'
]; ];
let vtable = {}; let vtable = {};
@ -46,9 +47,9 @@ var $$ = (selector) => {
let config = { childList: true, subtree: true }; let config = { childList: true, subtree: true };
let MutationObserver = window.MutationObserver; let MutationObserver = window.MutationObserver;
let observer = new MutationObserver(onMutate); let observer = new MutationObserver(onMutate);
observer.observe(document.body, config); observer.observe(document.body, config);
}); });
}; };
} }
@ -121,3 +122,14 @@ $.ajaxSetup({
})(), })(),
error: errorToastAjax error: errorToastAjax
}) })
var onImgLoad = function(me) {
me.parentElement.classList.remove('image-loading');
me.parentElement.classList.add('image-loaded');
}
var onImgError = function(me) {
me.parentElement.classList.remove('image-loading');
me.parentElement.classList.add('image-loaded');
me.remove();
}

View file

@ -45,8 +45,15 @@ $$('#action-load-posts').on('click', function() {
let pageSize = Number(me.attr('pageSize')); let pageSize = Number(me.attr('pageSize'));
let postCount = Number(me.attr('postCount')); let postCount = Number(me.attr('postCount'));
let postMax = Number(me.attr('postMax')); let postMax = Number(me.attr('postMax'));
let filterUid = me.attr('userId');
let url = '/_util/post/posts?page=' + page + '&max=' + postMax; let url = '/_util/post/posts?page=' + page + '&max=' + postMax;
if (!isNaN(filterUid)) {
console.log(filterUid);
url += '&user_id=' + filterUid;
}
$.get(url, function (data) { $.get(url, function (data) {
if (data === '') { if (data === '') {
me.remove(); me.remove();
@ -101,13 +108,27 @@ $$('.action-like').on('click', function() {
let like_id = me.attr('likeId'); let like_id = me.attr('likeId');
let post_id = me.attr('postId'); let post_id = me.attr('postId');
const updateLiked = (liked) => {
let post = me.closest('.post');
let likes = post.find('.likes');
let count = likes.find('.count');
let c = Number(count[0].textContent);
c += liked ? 1 : -1;
count[0].textContent = c;
}
const onPatch = () => { const onPatch = () => {
let liked = me.hasClass('btn-blue');
me.toggleClass('btn-blue'); me.toggleClass('btn-blue');
updateLiked(!liked);
} }
const onPost = (data) => { const onPost = (data) => {
let liked = me.hasClass('btn-blue');
me.attr('likeId', data[0].id + ''); me.attr('likeId', data[0].id + '');
me.toggleClass('btn-blue'); me.toggleClass('btn-blue');
updateLiked(!liked);
} }
if (like_id) { if (like_id) {

View file

@ -79,9 +79,9 @@ class Post_controller extends Controller {
->where('p.id')->le($max); ->where('p.id')->le($max);
} }
if ($uid) { if ($filter_uid) {
$query = $query $query = $query
->where('p.user_id')->eq($uid); ->where('p.user_id')->eq($filter_uid);
} }
$posts = $query $posts = $query
@ -106,9 +106,9 @@ class Post_controller extends Controller {
->select('COUNT(p.id) as pc') ->select('COUNT(p.id) as pc')
->from('api.post p'); ->from('api.post p');
if ($uid) { if ($filter_uid) {
$query = $query $query = $query
->where('p.user_id')->eq($uid); ->where('p.user_id')->eq($filter_uid);
} }
$pc = $query $pc = $query

View file

@ -17,10 +17,17 @@ class People_controller extends Controller {
parent::index(); parent::index();
$data = $this->people_model->get_data(); $data = $this->people_model->get_data();
$this->view('header', $data); $this->view('header', $data);
$this->view('apps/people/header', $data);
$this->view('apps/people/main', $data); $this->view('apps/people/main', $data);
$this->view('apps/people/footer', $data);
$this->view('footer', $data); $this->view('footer', $data);
} }
public function content(): void {
$data = $this->people_model->get_data();
$this->view('apps/people/main', $data);
}
/** /**
* @return array<string,mixed> * @return array<string,mixed>
*/ */

View file

@ -7,12 +7,16 @@ class Profile_controller extends Controller {
// the format model // the format model
protected $format_model; protected $format_model;
// the post model // the post controller
protected $post_controller; protected $post_controller;
// the people controller
protected $people_controller;
function __construct($load) { function __construct($load) {
parent::__construct($load); parent::__construct($load);
$this->profile_model = $this->load->model('apps/profile'); $this->profile_model = $this->load->model('apps/profile');
$this->people_controller = $this->load->controller('apps/people');
$this->format_model = $this->load->model('format'); $this->format_model = $this->load->model('format');
$this->post_controller = $this->load->controller('_util/post'); $this->post_controller = $this->load->controller('_util/post');
} }

View file

@ -7,40 +7,32 @@ class People_model extends Model {
parent::__construct($load); parent::__construct($load);
$this->request_model = $this->load->model('request'); $this->request_model = $this->load->model('request');
} }
/**
private function get_filted_query($select) { * @param mixed $select
$filter_username = $this->request_model->get_str('filter_username', FALSE); */
$filter_fisrt_name = $this->request_model->get_str('filter_first_name', FALSE); private function get_filted_query($select): DatabaseQuery {
$filter_last_name = $this->request_model->get_str('filter_last_name', FALSE); $filter_type = $this->request_model->get_str('filter', FALSE);
$filter_email = $this->request_model->get_str('filter_email', FALSE); $filter_uid = $this->request_model->get_int('uid', FALSE);
$max = $this->request_model->get_int('max', FALSE); $max = $this->request_model->get_int('max', FALSE);
$query = $this->db $query = $this->db
->select($select) ->select($select)
->from('api.user u'); ->from('api.user u');
if ($filter_username) { if ($filter_type && $filter_uid) {
$query = $query switch ($filter_type) {
->where('u.username') case 'follower': {
->like('%' . $filter_username . '%'); $query = $query
} ->join('admin.follow f', 'f.follower_id = u.id AND f.followee_id', 'INNER')
->eq($filter_uid);
} break;
if ($filter_fisrt_name) { case 'followee': {
$query = $query $query = $query
->where('u.first_name') ->join('admin.follow f', 'f.followee_id = u.id AND f.follower_id', 'INNER')
->like('%'. $filter_fisrt_name . '%'); ->eq($filter_uid);
} } break;
}
if ($filter_last_name) {
$query = $query
->where('u.last_name')
->like('%' . $filter_last_name . '%');
}
if ($filter_email) {
$query = $query
->where('u.email')
->like('%' . $filter_email . '%');
} }
if ($max) { if ($max) {
@ -52,12 +44,15 @@ class People_model extends Model {
return $query; return $query;
} }
public function get_users(): array { /**
* @return array<string,mixed>
*/
public function get_users(): array {
$page = $this->request_model->get_int('page', 0); $page = $this->request_model->get_int('page', 0);
$page_size = 24; $page_size = 24;
$offset = $page_size * $page; $offset = $page_size * $page;
$users = $this->get_filted_query('*') $users = $this->get_filted_query('u.*')
->order_by('u.id', 'DESC') ->order_by('u.id', 'DESC')
->offset($offset) ->offset($offset)
->limit($page_size) ->limit($page_size)
@ -72,11 +67,16 @@ class People_model extends Model {
$max = max($max, $user['id']); $max = max($max, $user['id']);
} }
$filter_type = $this->request_model->get_str('filter', FALSE);
$filter_uid = $this->request_model->get_int('uid', FALSE);
return array( return array(
'users' => $users, 'users' => $users,
'count' => $count, 'count' => $count,
'page_size' => $page_size, 'page_size' => $page_size,
'max_id' => $max 'max_id' => $max,
'filter_type' => $filter_type || '',
'filter_uid' => $filter_uid || ''
); );
} }

View file

@ -4,7 +4,7 @@
<?php if ($self): ?> <?php if ($self): ?>
<div id="new-post" class="card"> <div id="new-post" class="card">
<div class="row grow"> <div class="row grow">
<?php $this->view('template/pfp', array('user' => $self))?> <?=pfp($self)?>
<a <a
id="action-new-post" id="action-new-post"
class="btn btn-alt btn-wide ml" class="btn btn-alt btn-wide ml"

View file

@ -4,32 +4,12 @@
class="card profile" class="card profile"
href="/profile?id=<?=$user['id']?>" href="/profile?id=<?=$user['id']?>"
> >
<div class="row"> <div class="col">
<?php $this->view('template/pfp', array('user' => $user, 'link' => FALSE)); ?> <?=pfp($user, FALSE)?>
<div class="col ml"> <div class="col ml">
<strong class=""><?=$this->format_model->name($user)?></strong> <strong class=""><?=$this->format_model->name($user)?></strong>
<span class="dim"><?=lang('joined') . ' ' . $this->format_model->date($user['created'])?></span> <span class="dim"><?=$user['username']?></span>
<span class="dim"><?=lang('seen') . ' ' . $this->format_model->date($user['seen'])?></span>
</div> </div>
</div> </div>
<hr>
<table>
<tr>
<td><?=lang('tbl_username')?></td>
<td><?=$user['username']?></td>
<tr>
<tr>
<td><?=lang('tbl_email')?></td>
<td><?=$user['email']?></td>
<tr>
<tr>
<td><?=lang('tbl_gender')?></td>
<td><?=$user['gender']?></td>
<tr>
<tr>
<td><?=lang('tbl_uid')?></td>
<td><?=$user['id']?></td>
<tr>
</table>
</a> </a>
<? <?

View file

@ -0,0 +1,3 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
</div>

View file

@ -0,0 +1,6 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<div id="main-content" class="col">
<h1 class="title"><?=lang('title')?></h1>
<h3 class="desc"><?=lang('desc')?></h3>
<hr>

View file

@ -1,67 +1,72 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?> <?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?> <?php /* vi: syntax=php */ ?>
<div id="main-content" class="col"> <div id="people-container" class="col">
<h1 class="title"><?=lang('title')?></h1> <?php
<h3 class="desc"><?=lang('desc')?></h3> $pdata = $this->people();
<hr> ?>
<div id="people-container" class="col">
<?php
$pdata = $this->people();
?>
</div>
<?php
$loaded = count($pdata['users']);
$page_size = $pdata['page_size'];
$total = $pdata['count'];
$max = $pdata['max_id'];
?>
<?php if ($loaded >= $page_size && $page_size < $total): ?>
<?=ilang('action_load_users',
id: 'action-load-users',
class: 'btn btn-line btn-wide mb',
attrs: array(
'loaded' => $loaded,
'pageSize' => $page_size,
'userCount' => $total,
'userMax' => $max
)
)?>
<script>
var urlParams = new URLSearchParams(window.location.search).toString();
$('#action-load-users').on('click', function() {
let me = $(this);
let page = me.attr('page');
if (!page) {
page = '1';
}
let newPage = Number(page) + 1;
me.attr('page', newPage + '');
let loaded = Number(me.attr('loaded'));
let pageSize = Number(me.attr('pageSize'));
let userCount = Number(me.attr('userCount'));
let userMax = Number(me.attr('userMax'));
let url = '/people/people?page=' + page + '&max=' + userMax + '&' + urlParams;
$.get(url, function (data) {
if (data === '') {
me.remove();
return;
}
let container = $('#people-container');
container.append(data);
loaded += pageSize;
if (loaded >= userCount) {
me.remove();
} else {
me.attr('loaded', loaded + '');
}
});
});
</script>
<?php endif ?>
</div> </div>
<?php
$loaded = count($pdata['users']);
$page_size = $pdata['page_size'];
$total = $pdata['count'];
$max = $pdata['max_id'];
$filter_uid = $pdata['filter_uid'];
$filer_type = $pdata['filter_type'];
?>
<?php if ($loaded >= $page_size && $page_size < $total): ?>
<?=ilang('action_load_users',
id: 'action-load-users',
class: 'btn btn-line btn-wide mb',
attrs: array(
'loaded' => $loaded,
'pageSize' => $page_size,
'userCount' => $total,
'userMax' => $max,
'filterUid' => $filter_uid,
'filterType' => $filer_type
)
)?>
<script>
$('#action-load-users').on('click', function() {
let me = $(this);
let page = me.attr('page');
if (!page) {
page = '1';
}
let newPage = Number(page) + 1;
me.attr('page', newPage + '');
let loaded = Number(me.attr('loaded'));
let pageSize = Number(me.attr('pageSize'));
let userCount = Number(me.attr('userCount'));
let userMax = Number(me.attr('userMax'));
let filterType = me.attr('filterType');
let filterUid = me.attr('filterUid');
let url = '/people/people?page=' + page + '&max=' + userMax;
if (filterType && filterUid) {
url += '&filter=' + filterType + '&uid=' + filterUid;
}
$.get(url, function (data) {
if (data === '') {
me.remove();
return;
}
let container = $('#people-container');
container.append(data);
loaded += pageSize;
if (loaded >= userCount) {
me.remove();
} else {
me.attr('loaded', loaded + '');
}
});
});
</script>
<?php endif ?>

View file

@ -1,37 +1,158 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<div id="main-content"> <div id="main-content">
<div id="profile-header" class="col"> <div id="profile-header-container">
<div class="banner image-loading"> <div id="profile-header" class="col">
<img src="/api/rpc/profile_banner?user_id=<?=$user['id']?>"> <?=image('/api/rpc/profile_banner?user_id=' . $user['id'], 'banner')?>
</div> <div class="info row">
<div class="info row"> <div class="pfp-wrapper">
<div class="pfp-wrapper"> <?=pfp($user)?>
<?php $this->view('template/pfp', array('user' => $user)); ?> </div>
</div> <div class="col content">
<div class="col content"> <strong class="name"><?=$this->format_model->name($user)?></strong>
<strong class="name"><?=$this->format_model->name($user)?></strong> <span class="dim"><?=$user['follower_count'] . ' ' . lang('followers')?></span>
<span class="dim"><?=lang('joined') . $this->format_model->date($user['created'])?></span> <?php if(strlen($user['profile_bio']) > 0): ?>
<br>
<strong><?=lang('bio')?></strong>
<span class="dim"><?=$user['profile_bio']?></span>
<?php endif; ?>
</div>
</div> </div>
<hr>
<div class="row options">
<?=ilang('action_posts',
sub: [$user['first_name']],
class: 'btn btn-blue btn-border',
id: 'action-posts'
)?>
<?=ilang('action_about',
sub: [$user['first_name']],
class: 'btn',
id: 'action-about'
)?>
<?=ilang('action_followers',
sub: [$user['first_name']],
class: 'btn',
id: 'action-followers'
)?>
<?=ilang('action_following',
sub: [$user['first_name']],
class: 'btn',
id: 'action-following'
)?>
</div> </div>
<hr>
<div class="row options">
<?=ilang('action_posts',
sub: [$user['first_name']],
class: 'btn'
)?>
<?=ilang('action_about',
sub: [$user['first_name']],
class: 'btn'
)?>
<?=ilang('action_friends',
sub: [$user['first_name']],
class: 'btn'
)?>
</div> </div>
</div> </div>
<div id="#tab-posts"> <div id="tab-posts" class="tab">
<?php <?php
$_GET['user_id'] = $user['id']; $_GET['user_id'] = $user['id'];
$this->post_controller->index(); $this->post_controller->index();
?> ?>
</div> </div>
<div id="tab-about" class="tab">
<h1><?=lang('about_general')?></h1>
<table>
<tr>
<td><strong><?=lang('about_general_username')?></strong></td>
<td><?=$user['username']?></td>
</tr>
<tr>
<td><strong><?=lang('about_general_full_name')?></strong></td>
<td><?=$user['first_name'] . ' ' . $user['last_name']?></td>
</tr>
<tr>
<td><strong><?=lang('about_general_email')?></strong></td>
<td><?=$user['email']?></td>
</tr>
<tr>
<td><strong><?=lang('about_general_gender')?></strong></td>
<td><?=$user['gender']?></td>
</tr>
<tr>
<td><strong><?=lang('about_general_birth_date')?></strong></td>
<td><?=$user['birth_date']?></td>
</tr>
</table>
<h1><?=lang('about_stats')?></h1>
<table>
<tr>
<td><strong><?=lang('about_stats_posts')?></strong></td>
<td><?=$user['post_count']?></td>
</tr>
<tr>
<td><strong><?=lang('about_stats_like')?></strong></td>
<td><?=$user['like_count']?></td>
</tr>
<tr>
<td><strong><?=lang('about_stats_comments')?></strong></td>
<td><?=$user['comment_count']?></td>
</tr>
<tr>
<td><strong><?=lang('about_stats_following')?></strong></td>
<td><?=$user['followed_count']?></td>
</tr>
<tr>
<td><strong><?=lang('about_stats_joined')?></strong></td>
<td><?=$user['created']?></td>
</tr>
<tr>
<td><strong><?=lang('about_stats_seen')?></strong></td>
<td><?=$user['seen']?></td>
</tr>
</table>
</div>
<div id="tab-followers" class="tab">
<?php
$_GET['filter'] = 'follower';
$_GET['uid'] = $user['id'];
$this->people_controller->content();
?>
</div>
<div id="tab-following" class="tab">
<?php
$_GET['filter'] = 'followee';
$_GET['uid'] = $user['id'];
$this->people_controller->content();
?>
</div>
</div>
<script>
let tabs = {};
const disableTab = (tab) => {
tab.btn.removeClass('btn-blue');
tab.btn.removeClass('btn-border');
tab.tab.css('display', 'none');
};
const enableTab = (tab) => {
tab.btn.addClass('btn-blue');
tab.btn.addClass('btn-border');
tab.tab.css('display', '');
};
const loadTab = (name, disable = true) => {
let btn = $('#action-' + name);
btn.on('click', function() {
for (let tab of Object.values(tabs)) {
disableTab(tab);
}
enableTab(tabs[name]);
});
tabs[name] = {
'btn': btn,
'tab': $('#tab-' + name)
};
if (disable) {
disableTab(tabs[name]);
}
};
loadTab('posts', false);
loadTab('about');
loadTab('followers');
loadTab('following');
</script>
</div> </div>

View file

@ -11,7 +11,7 @@
<div class="nav-center" :class="{hidden: !visible}"> <div class="nav-center" :class="{hidden: !visible}">
<a <a
id="action-home" id="action-home"
class="btn" class="btn<?=$this->main->info['app'] == 'home' ? ' btn-blue btn-border' : ''?>"
href="/home" href="/home"
title="<?=lang('action_home_tip')?>" title="<?=lang('action_home_tip')?>"
> >
@ -20,35 +20,33 @@
</a> </a>
<a <a
id="action-people" id="action-people"
class="btn" class="btn<?=$this->main->info['app'] == 'people' ? ' btn-blue btn-border' : ''?>"
href="/people" href="/people"
title="<?=lang('action_people_tip')?>" title="<?=lang('action_people_tip')?>"
> >
<i class="mi mi-lg">people</i> <i class="mi mi-lg">people</i>
<span><?=lang('action_people_text')?></span> <span><?=lang('action_people_text')?></span>
</a> </a>
<a <!--a
id="action-chat" id="action-chat"
class="btn" class="btn<?=$this->main->info['app'] == 'chat' ? ' btn-blue btn-border' : ''?>"
href="/chat" href="/chat"
title="<?=lang('action_chat_tip')?>" title="<?=lang('action_chat_tip')?>"
> >
<i class="mi mi-lg">chat</i> <i class="mi mi-lg">chat</i>
<span><?=lang('action_chat_text')?></span> <span><?=lang('action_chat_text')?></span>
</a> </a-->
</div> </div>
<div class="nav-right"> <div class="nav-right">
<button <button
id="action-hamburger" id="action-hamburger"
title="<?=lang('action_hamburger_tip')?>" title="<?=lang('action_hamburger_tip')?>"
class="btn mr"
> >
<i class="mi mi-lg">menu</i> <i class="mi mi-lg">menu</i>
</button> </button>
<?php if($self): ?> <?php if($self): ?>
<?php $this->view('template/pfp', array( <?=pfp($self)?>
'user' => $self,
'class' => 'pfp-sm ml',
)); ?>
<?php else: ?> <?php else: ?>
<?=ilang('action_login', class: 'btn', href: '/auth/login')?> <?=ilang('action_login', class: 'btn', href: '/auth/login')?>
<?php endif; ?> <?php endif; ?>

View file

@ -6,7 +6,7 @@
<form id="new-post-form"> <form id="new-post-form">
<div class="modal-content new-post-modal"> <div class="modal-content new-post-modal">
<div class="row"> <div class="row">
<?php $this->view('template/pfp', array('user' => $user))?> <?=pfp($user)?>
<div class="col ml"> <div class="col ml">
<strong><?=$user['first_name'] . ' ' . $user['last_name']?></strong> <strong><?=$user['first_name'] . ' ' . $user['last_name']?></strong>
<span class="dim"><?=lang('now')?></span> <span class="dim"><?=lang('now')?></span>

View file

@ -4,7 +4,7 @@
$format_model = $this->load->model('format'); $format_model = $this->load->model('format');
?> ?>
<div class="comment row mt"> <div class="comment row mt">
<?php $this->view('template/pfp', array('user' => $user))?> <?=pfp($user)?>
<div class="ml col sub-card"> <div class="ml col sub-card">
<div class="row"> <div class="row">
<strong><?=$format_model->name($user)?></strong> <strong><?=$format_model->name($user)?></strong>

View file

@ -1,17 +0,0 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<?php
$class = isset($class) ? $class : '';
$link = isset($link) ? $link : TRUE;
?>
<?php if($link): ?>
<a class="image-loading pfp <?=$class?>" href="/profile?id=<?=$user['id']?>">
<?php else: ?>
<div class="image-loading pfp <?=$class?>">
<?php endif; ?>
<img src="/api/rpc/profile_avatar?user_id=<?=$user['id']?>"/>
<?php if ($link): ?>
</a>
<?php else: ?>
</div>
<?php endif; ?>

View file

@ -2,7 +2,7 @@
<?php /* vi: syntax=php */ ?> <?php /* vi: syntax=php */ ?>
<div class="post card"> <div class="post card">
<div class="row"> <div class="row">
<?php $this->view('template/pfp', array('user' => $user))?> <?=pfp($user)?>
<div class="col ml"> <div class="col ml">
<strong><?=$user['first_name'] . ' ' . $user['last_name']?></strong> <strong><?=$user['first_name'] . ' ' . $user['last_name']?></strong>
<span class="dim"><?=$post['created']?></span> <span class="dim"><?=$post['created']?></span>
@ -21,6 +21,7 @@
$post_attrs['likeId'] = $post['like_id']; $post_attrs['likeId'] = $post['like_id'];
} }
?> ?>
<span class="likes dim"><span class="count"><?=$post['like_count']?></span><?=' ' . lang('likes')?></span>
<?php if ($self): ?> <?php if ($self): ?>
<hr> <hr>
<div class="row"> <div class="row">
@ -61,7 +62,7 @@
</div> </div>
<?php if ($self): ?> <?php if ($self): ?>
<div class="row pb"> <div class="row pb">
<?php $this->view('template/pfp', array('user' => $self))?> <?=pfp($self)?>
<form class="ml action-new-comment-form row"> <form class="ml action-new-comment-form row">
<input <input
type="hidden" type="hidden"

View file

@ -17,7 +17,7 @@
'pageSize' => $page_size, 'pageSize' => $page_size,
'postCount' => $total, 'postCount' => $total,
'postMax' => $max, 'postMax' => $max,
'userId' => $filterUid 'userId' => $filterUid ? json_encode($filterUid) : ''
) )
); );
} }

View file

@ -45,6 +45,7 @@ class Aesthetic {
], ],
'css' => [ 'css' => [
'css/profile.css', 'css/profile.css',
'css/people.css',
'css/post.css' 'css/post.css'
], ],
), ),

33
src/web/helper/image.php Normal file
View file

@ -0,0 +1,33 @@
<?php /* Copyright (c) 2024 Freya Murphy */
function image($src, $class = NULL, $link = NULL): string {
if ($class) {
$class = 'image-loading ' . $class;
} else {
$class = 'image-loading';
}
$content = '';
if ($link) {
$content .= '<a class="' . $class . '" href="' . $link . '">';
} else {
$content .= '<span class="' . $class . '">';
}
$content .= '<img src="' . $src . '" onerror="onImgError(this)" onload="onImgLoad(this)"/>';
if ($link) {
$content .= '</a>';
} else {
$content .= '</span>';
}
return $content;
}
function pfp(
$user,
$embedLink = TRUE,
): string {
$link = $embedLink ? '/profile?id=' . $user['id'] : NULL;
return image('/api/rpc/profile_avatar?user_id=' . $user['id'], 'pfp', link: $link);
}

View file

@ -6,6 +6,7 @@ session_start();
$webroot = dirname(__FILE__); $webroot = dirname(__FILE__);
// load all the helper files // load all the helper files
require($webroot . '/helper/image.php');
require($webroot . '/helper/error.php'); require($webroot . '/helper/error.php');
require($webroot . '/helper/lang.php'); require($webroot . '/helper/lang.php');

View file

@ -4,15 +4,34 @@ $lang['title'] = '%s\'s profile';
$lang['joined'] = 'Joined: '; $lang['joined'] = 'Joined: ';
$lang['seen'] = 'Seen: '; $lang['seen'] = 'Seen: ';
$lang['followers'] = 'Followers';
$lang['bio'] = 'Bio';
$lang['action_posts_text'] = 'Posts'; $lang['action_posts_text'] = 'Posts';
$lang['action_posts_tip'] = 'View %s\'s posts'; $lang['action_posts_tip'] = 'View %s\'s posts';
$lang['action_about_text'] = 'About'; $lang['action_about_text'] = 'About';
$lang['action_about_tip'] = 'View %s\'s information'; $lang['action_about_tip'] = 'View %s\'s information';
$lang['action_friends_text'] = 'Friends'; $lang['action_followers_text'] = 'Followers';
$lang['action_friends_tip'] = 'View %s\'s friends'; $lang['action_followers_tip'] = 'View %s\'s followres';
$lang['action_following_text'] = 'Following';
$lang['action_following_tip'] = 'View who %s is following';
$lang['action_load_posts_text'] = 'Load more posts'; $lang['action_load_posts_text'] = 'Load more posts';
$lang['action_load_posts_tip'] = 'Load more posts'; $lang['action_load_posts_tip'] = 'Load more posts';
$lang['about_general'] = 'General';
$lang['about_general_username'] = 'Username';
$lang['about_general_full_name'] = 'Full Name';
$lang['about_general_email'] = 'Email';
$lang['about_general_gender'] = 'Gender';
$lang['about_general_birth_date'] = 'Birthday';
$lang['about_stats'] = 'Statistics';
$lang['about_stats_posts'] = 'Posts Created';
$lang['about_stats_like'] = 'Posts Liked';
$lang['about_stats_comments'] = 'Comments Created';
$lang['about_stats_following'] = 'Accounts Followed';
$lang['about_stats_joined'] = 'Date Joined';
$lang['about_stats_seen'] = 'Last Seen';
?> ?>

View file

@ -46,5 +46,6 @@ $lang['action_new_post_tip'] = 'Author a new post.';
// Words // Words
$lang['now'] = 'Now'; $lang['now'] = 'Now';
$lang['likes'] = 'Likes';
?> ?>