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,
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;
@ -234,9 +237,6 @@ ALTER TABLE admin.user_media OWNER TO xssbook;
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;

View file

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

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

View file

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

View file

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

View file

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

View file

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

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);
}
.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 {

View file

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

View file

@ -11,42 +11,20 @@
}
#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-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 {
@ -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);

View file

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

View file

@ -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,
#post-container {
width: 40rem;
.tab {
max-width: 80rem;
width: 100%;
margin-left: 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 {
margin-bottom: 1rem;
}
td:nth-child(1) {
padding-right: 2rem;
}

View file

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

View file

@ -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) {

View file

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

View file

@ -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>
*/

View file

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

View file

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

View file

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

View file

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

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 /* 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>
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 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>
$('#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="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>

View file

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

View file

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

View file

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

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 */ ?>
<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"

View file

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

View file

@ -45,6 +45,7 @@ class Aesthetic {
],
'css' => [
'css/profile.css',
'css/people.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__);
// load all the helper files
require($webroot . '/helper/image.php');
require($webroot . '/helper/error.php');
require($webroot . '/helper/lang.php');

View file

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

View file

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