diff options
38 files changed, 691 insertions, 269 deletions
diff --git a/src/db/migrations/0000.sql b/src/db/migrations/0000.sql index 7e14ac4..aa20d2f 100644 --- a/src/db/migrations/0000.sql +++ b/src/db/migrations/0000.sql @@ -198,7 +198,7 @@ CREATE TABLE admin.media ( id INTEGER DEFAULT nextval('sys.media_id_seq'::regclass) NOT NULL, name TEXT NOT NULL, content BYTEA NOT NULL, - type TEXT NOT NULL, + mime TEXT NOT NULL, created 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 ( id INTEGER DEFAULT nextval('sys.user_media_id_seq'::regclass) NOT NULL, - media_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; @@ -235,9 +238,6 @@ ALTER TABLE ONLY admin.user_media 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 ADD CONSTRAINT user_media_user_id_fkey FOREIGN KEY (user_id) REFERENCES admin.user (id) ON DELETE CASCADE; ALTER TABLE ONLY admin.user_media diff --git a/src/db/rest/util/_api_serve_media.sql b/src/db/rest/media/_api_serve_system_media.sql index c2e213a..5cd87c2 100644 --- a/src/db/rest/util/_api_serve_media.sql +++ b/src/db/rest/media/_api_serve_system_media.sql @@ -1,4 +1,4 @@ -CREATE FUNCTION _api.serve_media( +CREATE FUNCTION _api.serve_system_media( _media_id INTEGER ) RETURNS sys."*/*" @@ -8,34 +8,30 @@ DECLARE _headers TEXT; _data BYTEA; BEGIN - SELECT FORMAT( '[{"Content-Type": "%s"},' '{"Content-Disposition": "inline; filename=\"%s\""},' '{"Cache-Control": "max-age=259200"}]' - , m.type, m.name) + , m.mime, m.name) FROM admin.media m - WHERE m.id = _media_id INTO _headers; - - PERFORM SET_CONFIG('response.headers', _headers, true); + WHERE m.id = _media_id + INTO _headers; SELECT m.content FROM admin.media m WHERE m.id = _media_id INTO _data; - IF FOUND THEN + IF _data IS NOT NULL THEN + PERFORM SET_CONFIG('response.headers', _headers, true); RETURN(_data); ELSE - PERFORM _api.raise( - _msg => 'api_not_found', - _err => 404 - ); + PERFORM _api.raise_not_found(); END IF; END $BODY$; -GRANT EXECUTE ON FUNCTION _api.serve_media(INTEGER) +GRANT EXECUTE ON FUNCTION _api.serve_system_media(INTEGER) TO rest_anon, rest_user; GRANT SELECT ON TABLE admin.media TO rest_anon, rest_user; diff --git a/src/db/rest/media/_api_serve_user_media.sql b/src/db/rest/media/_api_serve_user_media.sql new file mode 100644 index 0000000..3487493 --- /dev/null +++ b/src/db/rest/media/_api_serve_user_media.sql @@ -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; diff --git a/src/db/rest/media/_api_serve_user_or_default_media.sql b/src/db/rest/media/_api_serve_user_or_default_media.sql new file mode 100644 index 0000000..c079ba9 --- /dev/null +++ b/src/db/rest/media/_api_serve_user_or_default_media.sql @@ -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; diff --git a/src/db/rest/media/api_profile_avatar.sql b/src/db/rest/media/api_profile_avatar.sql index 8607999..b3e456c 100644 --- a/src/db/rest/media/api_profile_avatar.sql +++ b/src/db/rest/media/api_profile_avatar.sql @@ -5,32 +5,16 @@ RETURNS sys."*/*" LANGUAGE plpgsql VOLATILE AS $BODY$ DECLARE - _id INTEGER; - _mod INTEGER; - _name TEXT; + _default TEXT; BEGIN - SELECT media_id INTO _id - FROM admin.user_media m - WHERE m.user_id = profile_avatar.user_id - AND type = 'avatar'::admin.user_media_type; - - -- 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); + _default := 'default_avatar_' || MOD(user_id, 25) || '.png'; + RETURN _api.serve_user_or_default_media( + user_id, + 'avatar'::admin.user_media_type, + _default + ); END $BODY$; GRANT EXECUTE ON FUNCTION api.profile_avatar(INTEGER) 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; diff --git a/src/db/rest/media/api_profile_banner.sql b/src/db/rest/media/api_profile_banner.sql index 272d021..d98f553 100644 --- a/src/db/rest/media/api_profile_banner.sql +++ b/src/db/rest/media/api_profile_banner.sql @@ -4,10 +4,21 @@ CREATE FUNCTION api.profile_banner( RETURNS sys."*/*" LANGUAGE plpgsql VOLATILE AS $BODY$ +DECLARE + _default TEXT; 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 $BODY$; GRANT EXECUTE ON FUNCTION api.profile_banner(INTEGER) 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; diff --git a/src/db/rest/post/api_post.sql b/src/db/rest/post/api_post.sql index 0d60473..b5c42a8 100644 --- a/src/db/rest/post/api_post.sql +++ b/src/db/rest/post/api_post.sql @@ -6,7 +6,9 @@ CREATE VIEW api.post AS p.created, p.modified, COALESCE(c.cc, 0) - AS comment_count + AS comment_count, + COALESCE(l.lc, 0) + AS like_count FROM admin.post p LEFT JOIN ( @@ -20,6 +22,17 @@ CREATE VIEW api.post AS ) c ON 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 admin.user u ON diff --git a/src/db/rest/rest.sql b/src/db/rest/rest.sql index e203f27..783866a 100644 --- a/src/db/rest/rest.sql +++ b/src/db/rest/rest.sql @@ -15,7 +15,6 @@ GRANT USAGE ON SCHEMA _api TO rest_anon, rest_user; -- util \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_null.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; -- 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_banner.sql; diff --git a/src/db/rest/user/api_user.sql b/src/db/rest/user/api_user.sql index 6735775..d71fd1b 100644 --- a/src/db/rest/user/api_user.sql +++ b/src/db/rest/user/api_user.sql @@ -13,9 +13,74 @@ CREATE VIEW api.user AS u.profile_bio, u.created, 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 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 u.deleted <> TRUE; diff --git a/src/db/rest/util/_api_raise_not_found.sql b/src/db/rest/util/_api_raise_not_found.sql new file mode 100644 index 0000000..f4997a6 --- /dev/null +++ b/src/db/rest/util/_api_raise_not_found.sql @@ -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; diff --git a/src/public/css/common.css b/src/public/css/common.css index 0750b8a..281df6b 100644 --- a/src/public/css/common.css +++ b/src/public/css/common.css @@ -170,6 +170,21 @@ a, button { 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 { border: none; outline: none; @@ -205,6 +220,7 @@ input.btn:focus { display: flex; flex-direction: row; align-items: center; + z-index: 5; } .nav { @@ -224,7 +240,7 @@ input.btn:focus { flex: 1; justify-content: center; height: 100%; - z-index: 2; + z-index: 6; } @media (min-width: 800px) { @@ -274,6 +290,11 @@ input.btn:focus { .nav-center .btn.active { border-bottom: none; } + + .nav .btn-border::before { + background: inherit; + } + } .nav-right .image-loading { @@ -305,6 +326,10 @@ input.btn:focus { animation: shimmer 1s linear infinite; } +.image-loaded { + background-color: var(--base); +} + .card { background-color: var(--surface0); border-radius: .5rem; @@ -393,6 +418,14 @@ input.btn:focus { display: flex; flex-direction: column; animation: fadeIn .1s, slideInModal .1s linear; + z-index: 10; +} + +@media (max-width: 40rem) { + .modal { + min-width: 100%; + width: 100%; + } } @keyframes slideInModal { diff --git a/src/public/css/home.css b/src/public/css/home.css index 3c2a3a1..3fdc381 100644 --- a/src/public/css/home.css +++ b/src/public/css/home.css @@ -2,11 +2,8 @@ display: flex; flex-direction: column; align-items: center; -} - -.card { - width: 40rem; - margin-bottom: 1rem; + padding: 1rem; + padding-bottom: 0; } .new-post-modal textarea { diff --git a/src/public/css/people.css b/src/public/css/people.css index 279c839..6b07eff 100644 --- a/src/public/css/people.css +++ b/src/public/css/people.css @@ -11,42 +11,20 @@ } #people-container { - margin-left: auto; - margin-right: auto; + display: grid; + width: 100%; padding: 1rem 2rem; - padding-bottom: 0; - flex-direction: row; - align-items: center; - flex-wrap: wrap; - max-width: 90rem; + margin-bottom: 1rem; + grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr) ); + grid-auto-rows: max-content; + grid-gap: 2rem; } .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; + width: 16rem; text-decoration: none; - height: fit-content; + margin-left: auto; + margin-right: auto; } .profile:hover { @@ -54,16 +32,22 @@ } .profile strong { - font-size: 2rem; + font-size: 1.5rem; } .profile .pfp, .profile .pfp img { padding: none; margin: none; - height: 6rem; + width: 100%; + height: 100%; + aspect-ratio: 1; border-radius: .3rem; } +.profile .pfp { + margin-bottom: 1rem; +} + td:nth-child(1) { font-weight: bold; color: var(--subtext); diff --git a/src/public/css/post.css b/src/public/css/post.css index 6fd7ca0..2b6a4b1 100644 --- a/src/public/css/post.css +++ b/src/public/css/post.css @@ -8,6 +8,22 @@ justify-content: center; } +.post, #new-post { + margin-bottom: 1rem; + width: 40rem; +} + .post { padding-bottom: 0; } + +@media(max-width: 40rem) { + .post, #new-post { + width: 100%; + } +} + +.post .likes { + display: block; + padding-top: .25rem; +} diff --git a/src/public/css/profile.css b/src/public/css/profile.css index a16fdfd..71cbbfa 100644 --- a/src/public/css/profile.css +++ b/src/public/css/profile.css @@ -4,17 +4,34 @@ padding: 0; } -#profile-header { +#profile-header-container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; 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 { width: 100%; - min-height: 20rem; + min-height: 30rem; 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 img { height: 12.5rem; @@ -32,7 +49,7 @@ border-radius: 100%; position: absolute; top: -2.5rem; - left: 2rem; + left: 1rem; } #profile-header .info .content { @@ -65,13 +82,27 @@ text-align: center; } -#tab-posts, +.tab { + max-width: 80rem; + width: 100%; + margin-left: auto; + margin-right: auto; + padding: 0 1rem; + margin-bottom: 1rem; +} + #post-container { - width: 40rem; + max-width: 40rem; + width: 100%; margin-left: auto; margin-right: auto; + margin-bottom: -1rem; } #post-container .post { margin-bottom: 1rem; } + +td:nth-child(1) { + padding-right: 2rem; +} diff --git a/src/public/js/lib.js b/src/public/js/lib.js index 19019ad..95f83b7 100644 --- a/src/public/js/lib.js +++ b/src/public/js/lib.js @@ -13,7 +13,8 @@ var $$ = (selector) => { 'click', 'submit', 'each', - 'error' + 'error', + 'one' ]; let vtable = {}; @@ -46,9 +47,9 @@ var $$ = (selector) => { let config = { childList: true, subtree: true }; let MutationObserver = window.MutationObserver; let observer = new MutationObserver(onMutate); + observer.observe(document.body, config); }); - }; } @@ -121,3 +122,14 @@ $.ajaxSetup({ })(), 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(); +} diff --git a/src/public/js/post.js b/src/public/js/post.js index 38bbb78..3c03bae 100644 --- a/src/public/js/post.js +++ b/src/public/js/post.js @@ -45,8 +45,15 @@ $$('#action-load-posts').on('click', function() { let pageSize = Number(me.attr('pageSize')); let postCount = Number(me.attr('postCount')); let postMax = Number(me.attr('postMax')); + let filterUid = me.attr('userId'); let url = '/_util/post/posts?page=' + page + '&max=' + postMax; + + if (!isNaN(filterUid)) { + console.log(filterUid); + url += '&user_id=' + filterUid; + } + $.get(url, function (data) { if (data === '') { me.remove(); @@ -101,13 +108,27 @@ $$('.action-like').on('click', function() { let like_id = me.attr('likeId'); 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 = () => { + let liked = me.hasClass('btn-blue'); me.toggleClass('btn-blue'); + updateLiked(!liked); } const onPost = (data) => { + let liked = me.hasClass('btn-blue'); me.attr('likeId', data[0].id + ''); me.toggleClass('btn-blue'); + updateLiked(!liked); } if (like_id) { diff --git a/src/web/_controller/_util/post.php b/src/web/_controller/_util/post.php index 4da2671..5346497 100644 --- a/src/web/_controller/_util/post.php +++ b/src/web/_controller/_util/post.php @@ -79,9 +79,9 @@ class Post_controller extends Controller { ->where('p.id')->le($max); } - if ($uid) { + if ($filter_uid) { $query = $query - ->where('p.user_id')->eq($uid); + ->where('p.user_id')->eq($filter_uid); } $posts = $query @@ -106,9 +106,9 @@ class Post_controller extends Controller { ->select('COUNT(p.id) as pc') ->from('api.post p'); - if ($uid) { + if ($filter_uid) { $query = $query - ->where('p.user_id')->eq($uid); + ->where('p.user_id')->eq($filter_uid); } $pc = $query diff --git a/src/web/_controller/apps/people.php b/src/web/_controller/apps/people.php index 19910ac..86da3b3 100644 --- a/src/web/_controller/apps/people.php +++ b/src/web/_controller/apps/people.php @@ -17,10 +17,17 @@ class People_controller extends Controller { parent::index(); $data = $this->people_model->get_data(); $this->view('header', $data); + $this->view('apps/people/header', $data); $this->view('apps/people/main', $data); + $this->view('apps/people/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> */ diff --git a/src/web/_controller/apps/profile.php b/src/web/_controller/apps/profile.php index aaed348..3bc9a91 100644 --- a/src/web/_controller/apps/profile.php +++ b/src/web/_controller/apps/profile.php @@ -7,12 +7,16 @@ class Profile_controller extends Controller { // the format model protected $format_model; - // the post model + // the post controller protected $post_controller; + // the people controller + protected $people_controller; + function __construct($load) { parent::__construct($load); $this->profile_model = $this->load->model('apps/profile'); + $this->people_controller = $this->load->controller('apps/people'); $this->format_model = $this->load->model('format'); $this->post_controller = $this->load->controller('_util/post'); } diff --git a/src/web/_model/apps/people.php b/src/web/_model/apps/people.php index 1bb110f..ade59d3 100644 --- a/src/web/_model/apps/people.php +++ b/src/web/_model/apps/people.php @@ -7,40 +7,32 @@ class People_model extends Model { parent::__construct($load); $this->request_model = $this->load->model('request'); } - - private function get_filted_query($select) { - $filter_username = $this->request_model->get_str('filter_username', FALSE); - $filter_fisrt_name = $this->request_model->get_str('filter_first_name', FALSE); - $filter_last_name = $this->request_model->get_str('filter_last_name', FALSE); - $filter_email = $this->request_model->get_str('filter_email', FALSE); - $max = $this->request_model->get_int('max', FALSE); + /** + * @param mixed $select + */ + private function get_filted_query($select): DatabaseQuery { + $filter_type = $this->request_model->get_str('filter', FALSE); + $filter_uid = $this->request_model->get_int('uid', FALSE); + $max = $this->request_model->get_int('max', FALSE); $query = $this->db ->select($select) ->from('api.user u'); - if ($filter_username) { - $query = $query - ->where('u.username') - ->like('%' . $filter_username . '%'); - } + if ($filter_type && $filter_uid) { + switch ($filter_type) { + case 'follower': { + $query = $query + ->join('admin.follow f', 'f.follower_id = u.id AND f.followee_id', 'INNER') + ->eq($filter_uid); + } break; - if ($filter_fisrt_name) { - $query = $query - ->where('u.first_name') - ->like('%'. $filter_fisrt_name . '%'); - } - - 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 . '%'); + case 'followee': { + $query = $query + ->join('admin.follow f', 'f.followee_id = u.id AND f.follower_id', 'INNER') + ->eq($filter_uid); + } break; + } } if ($max) { @@ -52,12 +44,15 @@ class People_model extends Model { 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_size = 24; $offset = $page_size * $page; - $users = $this->get_filted_query('*') + $users = $this->get_filted_query('u.*') ->order_by('u.id', 'DESC') ->offset($offset) ->limit($page_size) @@ -72,11 +67,16 @@ class People_model extends Model { $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( 'users' => $users, 'count' => $count, 'page_size' => $page_size, - 'max_id' => $max + 'max_id' => $max, + 'filter_type' => $filter_type || '', + 'filter_uid' => $filter_uid || '' ); } diff --git a/src/web/_views/apps/home/main.php b/src/web/_views/apps/home/main.php index 29bf7c3..735e3d8 100644 --- a/src/web/_views/apps/home/main.php +++ b/src/web/_views/apps/home/main.php @@ -4,7 +4,7 @@ <?php if ($self): ?> <div id="new-post" class="card"> <div class="row grow"> - <?php $this->view('template/pfp', array('user' => $self))?> + <?=pfp($self)?> <a id="action-new-post" class="btn btn-alt btn-wide ml" diff --git a/src/web/_views/apps/people/card.php b/src/web/_views/apps/people/card.php index a44b0d4..eda49b5 100644 --- a/src/web/_views/apps/people/card.php +++ b/src/web/_views/apps/people/card.php @@ -4,32 +4,12 @@ class="card profile" href="/profile?id=<?=$user['id']?>" > - <div class="row"> - <?php $this->view('template/pfp', array('user' => $user, 'link' => FALSE)); ?> + <div class="col"> + <?=pfp($user, FALSE)?> <div class="col ml"> <strong class=""><?=$this->format_model->name($user)?></strong> - <span class="dim"><?=lang('joined') . ' ' . $this->format_model->date($user['created'])?></span> - <span class="dim"><?=lang('seen') . ' ' . $this->format_model->date($user['seen'])?></span> + <span class="dim"><?=$user['username']?></span> </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> <? diff --git a/src/web/_views/apps/people/footer.php b/src/web/_views/apps/people/footer.php new file mode 100644 index 0000000..ff93026 --- /dev/null +++ b/src/web/_views/apps/people/footer.php @@ -0,0 +1,3 @@ +<?php /* Copyright (c) 2024 Freya Murphy */ ?> +<?php /* vi: syntax=php */ ?> +</div> diff --git a/src/web/_views/apps/people/header.php b/src/web/_views/apps/people/header.php new file mode 100644 index 0000000..7f3d95b --- /dev/null +++ b/src/web/_views/apps/people/header.php @@ -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> diff --git a/src/web/_views/apps/people/main.php b/src/web/_views/apps/people/main.php index 171f25c..deec4c2 100644 --- a/src/web/_views/apps/people/main.php +++ b/src/web/_views/apps/people/main.php @@ -1,67 +1,72 @@ <?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> - <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> +<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']; + $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> - 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 + ''); - $('#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 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 + '&' + urlParams; - $.get(url, function (data) { - if (data === '') { - me.remove(); - return; - } + let url = '/people/people?page=' + page + '&max=' + userMax; - let container = $('#people-container'); - container.append(data); + if (filterType && filterUid) { + url += '&filter=' + filterType + '&uid=' + filterUid; + } + + $.get(url, function (data) { + if (data === '') { + me.remove(); + return; + } - loaded += pageSize; - if (loaded >= userCount) { - me.remove(); - } else { - me.attr('loaded', loaded + ''); - } - }); + let container = $('#people-container'); + container.append(data); + + loaded += pageSize; + if (loaded >= userCount) { + me.remove(); + } else { + me.attr('loaded', loaded + ''); + } }); - </script> - <?php endif ?> -</div> + }); + </script> +<?php endif ?> diff --git a/src/web/_views/apps/profile/main.php b/src/web/_views/apps/profile/main.php index afa45bc..e3d65b5 100644 --- a/src/web/_views/apps/profile/main.php +++ b/src/web/_views/apps/profile/main.php @@ -1,37 +1,158 @@ +<?php /* Copyright (c) 2024 Freya Murphy */ ?> +<?php /* vi: syntax=php */ ?> <div id="main-content"> - <div id="profile-header" class="col"> - <div class="banner image-loading"> - <img src="/api/rpc/profile_banner?user_id=<?=$user['id']?>"> - </div> - <div class="info row"> - <div class="pfp-wrapper"> - <?php $this->view('template/pfp', array('user' => $user)); ?> - </div> - <div class="col content"> - <strong class="name"><?=$this->format_model->name($user)?></strong> - <span class="dim"><?=lang('joined') . $this->format_model->date($user['created'])?></span> + <div id="profile-header-container"> + <div id="profile-header" class="col"> + <?=image('/api/rpc/profile_banner?user_id=' . $user['id'], 'banner')?> + <div class="info row"> + <div class="pfp-wrapper"> + <?=pfp($user)?> + </div> + <div class="col content"> + <strong class="name"><?=$this->format_model->name($user)?></strong> + <span class="dim"><?=$user['follower_count'] . ' ' . lang('followers')?></span> + <?php if(strlen($user['profile_bio']) > 0): ?> + <br> + <strong><?=lang('bio')?></strong> + <span class="dim"><?=$user['profile_bio']?></span> + <?php endif; ?> + </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> - <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 id="#tab-posts"> + <div id="tab-posts" class="tab"> <?php $_GET['user_id'] = $user['id']; $this->post_controller->index(); ?> </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> diff --git a/src/web/_views/header.php b/src/web/_views/header.php index 8a0333e..f1aef01 100644 --- a/src/web/_views/header.php +++ b/src/web/_views/header.php @@ -11,7 +11,7 @@ <div class="nav-center" :class="{hidden: !visible}"> <a id="action-home" - class="btn" + class="btn<?=$this->main->info['app'] == 'home' ? ' btn-blue btn-border' : ''?>" href="/home" title="<?=lang('action_home_tip')?>" > @@ -20,35 +20,33 @@ </a> <a id="action-people" - class="btn" + class="btn<?=$this->main->info['app'] == 'people' ? ' btn-blue btn-border' : ''?>" href="/people" title="<?=lang('action_people_tip')?>" > <i class="mi mi-lg">people</i> <span><?=lang('action_people_text')?></span> </a> - <a + <!--a id="action-chat" - class="btn" + class="btn<?=$this->main->info['app'] == 'chat' ? ' btn-blue btn-border' : ''?>" href="/chat" title="<?=lang('action_chat_tip')?>" > <i class="mi mi-lg">chat</i> <span><?=lang('action_chat_text')?></span> - </a> + </a--> </div> <div class="nav-right"> <button id="action-hamburger" title="<?=lang('action_hamburger_tip')?>" + class="btn mr" > <i class="mi mi-lg">menu</i> </button> <?php if($self): ?> - <?php $this->view('template/pfp', array( - 'user' => $self, - 'class' => 'pfp-sm ml', - )); ?> + <?=pfp($self)?> <?php else: ?> <?=ilang('action_login', class: 'btn', href: '/auth/login')?> <?php endif; ?> diff --git a/src/web/_views/modal/new_post.php b/src/web/_views/modal/new_post.php index 50b9b84..15163c9 100644 --- a/src/web/_views/modal/new_post.php +++ b/src/web/_views/modal/new_post.php @@ -6,7 +6,7 @@ <form id="new-post-form"> <div class="modal-content new-post-modal"> <div class="row"> - <?php $this->view('template/pfp', array('user' => $user))?> + <?=pfp($user)?> <div class="col ml"> <strong><?=$user['first_name'] . ' ' . $user['last_name']?></strong> <span class="dim"><?=lang('now')?></span> diff --git a/src/web/_views/template/comment.php b/src/web/_views/template/comment.php index 3ff473b..cf2c0b4 100644 --- a/src/web/_views/template/comment.php +++ b/src/web/_views/template/comment.php @@ -4,7 +4,7 @@ $format_model = $this->load->model('format'); ?> <div class="comment row mt"> - <?php $this->view('template/pfp', array('user' => $user))?> + <?=pfp($user)?> <div class="ml col sub-card"> <div class="row"> <strong><?=$format_model->name($user)?></strong> diff --git a/src/web/_views/template/pfp.php b/src/web/_views/template/pfp.php deleted file mode 100644 index ebb4b5f..0000000 --- a/src/web/_views/template/pfp.php +++ /dev/null @@ -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; ?> diff --git a/src/web/_views/template/post.php b/src/web/_views/template/post.php index 0633985..fb8cef5 100644 --- a/src/web/_views/template/post.php +++ b/src/web/_views/template/post.php @@ -2,7 +2,7 @@ <?php /* vi: syntax=php */ ?> <div class="post card"> <div class="row"> - <?php $this->view('template/pfp', array('user' => $user))?> + <?=pfp($user)?> <div class="col ml"> <strong><?=$user['first_name'] . ' ' . $user['last_name']?></strong> <span class="dim"><?=$post['created']?></span> @@ -21,6 +21,7 @@ $post_attrs['likeId'] = $post['like_id']; } ?> + <span class="likes dim"><span class="count"><?=$post['like_count']?></span><?=' ' . lang('likes')?></span> <?php if ($self): ?> <hr> <div class="row"> @@ -61,7 +62,7 @@ </div> <?php if ($self): ?> <div class="row pb"> - <?php $this->view('template/pfp', array('user' => $self))?> + <?=pfp($self)?> <form class="ml action-new-comment-form row"> <input type="hidden" diff --git a/src/web/_views/template/posts.php b/src/web/_views/template/posts.php index 5fec698..137c0dd 100644 --- a/src/web/_views/template/posts.php +++ b/src/web/_views/template/posts.php @@ -17,7 +17,7 @@ 'pageSize' => $page_size, 'postCount' => $total, 'postMax' => $max, - 'userId' => $filterUid + 'userId' => $filterUid ? json_encode($filterUid) : '' ) ); } diff --git a/src/web/config/aesthetic.php b/src/web/config/aesthetic.php index 99a1959..d37f4a9 100644 --- a/src/web/config/aesthetic.php +++ b/src/web/config/aesthetic.php @@ -45,6 +45,7 @@ class Aesthetic { ], 'css' => [ 'css/profile.css', + 'css/people.css', 'css/post.css' ], ), diff --git a/src/web/helper/image.php b/src/web/helper/image.php new file mode 100644 index 0000000..ac2f808 --- /dev/null +++ b/src/web/helper/image.php @@ -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); +} diff --git a/src/web/index.php b/src/web/index.php index 688383f..dc54905 100644 --- a/src/web/index.php +++ b/src/web/index.php @@ -6,6 +6,7 @@ session_start(); $webroot = dirname(__FILE__); // load all the helper files +require($webroot . '/helper/image.php'); require($webroot . '/helper/error.php'); require($webroot . '/helper/lang.php'); diff --git a/src/web/lang/en_US/apps/profile.php b/src/web/lang/en_US/apps/profile.php index 2cc9b4e..43a3247 100644 --- a/src/web/lang/en_US/apps/profile.php +++ b/src/web/lang/en_US/apps/profile.php @@ -4,15 +4,34 @@ $lang['title'] = '%s\'s profile'; $lang['joined'] = 'Joined: '; $lang['seen'] = 'Seen: '; +$lang['followers'] = 'Followers'; +$lang['bio'] = 'Bio'; $lang['action_posts_text'] = 'Posts'; $lang['action_posts_tip'] = 'View %s\'s posts'; $lang['action_about_text'] = 'About'; $lang['action_about_tip'] = 'View %s\'s information'; -$lang['action_friends_text'] = 'Friends'; -$lang['action_friends_tip'] = 'View %s\'s friends'; +$lang['action_followers_text'] = 'Followers'; +$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_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'; + ?> diff --git a/src/web/lang/en_US/common_lang.php b/src/web/lang/en_US/common_lang.php index 7e214b5..eb60888 100644 --- a/src/web/lang/en_US/common_lang.php +++ b/src/web/lang/en_US/common_lang.php @@ -46,5 +46,6 @@ $lang['action_new_post_tip'] = 'Author a new post.'; // Words $lang['now'] = 'Now'; +$lang['likes'] = 'Likes'; ?> |