Compare commits
3 commits
ef7b0e26fa
...
7e2553646c
Author | SHA1 | Date | |
---|---|---|---|
7e2553646c | |||
9ed46c335d | |||
3a82baec9d |
109 changed files with 1712 additions and 333 deletions
|
@ -1,4 +1,4 @@
|
||||||
FROM php:fpm-alpine
|
FROM php:fpm-alpine
|
||||||
RUN apk add --no-cache postgresql-dev
|
RUN apk add --no-cache postgresql-dev runuser
|
||||||
RUN docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql
|
RUN docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql
|
||||||
RUN docker-php-ext-install pdo pdo_pgsql
|
RUN docker-php-ext-install pdo pdo_pgsql
|
||||||
|
|
|
@ -87,6 +87,7 @@ server {
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
root /opt/xssbook/web;
|
||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
fastcgi_pass php:9000;
|
fastcgi_pass php:9000;
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
|
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
CREATE FUNCTION api.avatar(
|
|
||||||
user_id INTEGER DEFAULT 0
|
|
||||||
)
|
|
||||||
RETURNS sys."*/*"
|
|
||||||
LANGUAGE plpgsql VOLATILE
|
|
||||||
AS $BODY$
|
|
||||||
DECLARE
|
|
||||||
_mod INTEGER;
|
|
||||||
_name TEXT;
|
|
||||||
BEGIN
|
|
||||||
_mod = MOD(user_id, 24);
|
|
||||||
_name = 'default_avatar_' || _mod || '.png';
|
|
||||||
RETURN _api.serve_media(_name);
|
|
||||||
END
|
|
||||||
$BODY$;
|
|
||||||
|
|
||||||
GRANT EXECUTE ON FUNCTION api.avatar(INTEGER)
|
|
||||||
TO rest_anon, rest_user;
|
|
||||||
GRANT SELECT ON TABLE admin.user
|
|
||||||
TO rest_anon, rest_user;
|
|
||||||
GRANT SELECT ON TABLE admin.media
|
|
||||||
TO rest_anon, rest_user;
|
|
|
@ -1,11 +0,0 @@
|
||||||
CREATE FUNCTION _api.get_user_id()
|
|
||||||
RETURNS INTEGER
|
|
||||||
LANGUAGE plpgsql VOLATILE
|
|
||||||
AS $BODY$
|
|
||||||
BEGIN
|
|
||||||
RETURN CURRENT_SETTING(
|
|
||||||
'request.jwt.claims',
|
|
||||||
TRUE
|
|
||||||
)::JSON->>'user_id';
|
|
||||||
END
|
|
||||||
$BODY$;
|
|
|
@ -5,7 +5,7 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- '80:80'
|
- '80:80'
|
||||||
volumes:
|
volumes:
|
||||||
- ./web:/opt/xssbook
|
- ./src:/opt/xssbook:ro
|
||||||
- ./conf/nginx:/etc/nginx/conf.d:ro
|
- ./conf/nginx:/etc/nginx/conf.d:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- rest
|
- rest
|
||||||
|
@ -18,8 +18,9 @@ services:
|
||||||
env_file:
|
env_file:
|
||||||
- ./conf/postgres/database.env
|
- ./conf/postgres/database.env
|
||||||
volumes:
|
volumes:
|
||||||
- ./web:/opt/xssbook
|
- ./src:/opt/xssbook:ro
|
||||||
- ./data/status:/status
|
- ./data/status:/status:ro
|
||||||
|
- ./data/session:/var/lib/php/session
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ services:
|
||||||
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
|
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
|
||||||
volumes:
|
volumes:
|
||||||
- './data/schemas:/var/lib/postgresql/data'
|
- './data/schemas:/var/lib/postgresql/data'
|
||||||
- ./db:/db:ro
|
- ./src/db:/db:ro
|
||||||
|
|
||||||
rest:
|
rest:
|
||||||
build: ./build/postgrest
|
build: ./build/postgrest
|
||||||
|
@ -47,7 +48,7 @@ services:
|
||||||
env_file:
|
env_file:
|
||||||
- ./conf/postgres/database.env
|
- ./conf/postgres/database.env
|
||||||
volumes:
|
volumes:
|
||||||
- ./db:/db:ro
|
- ./src/db:/db:ro
|
||||||
- ./data/status:/status
|
- ./data/status:/status
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
|
|
@ -50,11 +50,12 @@ CREATE TABLE admin.user (
|
||||||
middle_name TEXT DEFAULT ''::text NOT NULL,
|
middle_name TEXT DEFAULT ''::text NOT NULL,
|
||||||
email TEXT DEFAULT ''::text NOT NULL,
|
email TEXT DEFAULT ''::text NOT NULL,
|
||||||
gender TEXT DEFAULT ''::text NOT NULL,
|
gender TEXT DEFAULT ''::text NOT NULL,
|
||||||
join_date TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
|
|
||||||
birth_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
birth_date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
profile_avatar BYTEA,
|
profile_bio TEXT DEFAULT ''::text NOT NULL,
|
||||||
profile_banner BYTEA,
|
created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
|
||||||
profile_bio TEXT DEFAULT ''::text NOT NULL
|
modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
|
||||||
|
seen TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
|
||||||
|
deleted BOOLEAN DEFAULT FALSE NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
ALTER TABLE admin.user OWNER TO xssbook;
|
ALTER TABLE admin.user OWNER TO xssbook;
|
||||||
|
@ -75,10 +76,12 @@ CREATE SEQUENCE IF NOT EXISTS sys.post_id_seq
|
||||||
ALTER TABLE sys.post_id_seq OWNER TO xssbook;
|
ALTER TABLE sys.post_id_seq OWNER TO xssbook;
|
||||||
|
|
||||||
CREATE TABLE admin.post (
|
CREATE TABLE admin.post (
|
||||||
id INTEGER DEFAULT nextval('sys.post_id_seq'::regclass) NOT NULL,
|
id INTEGER DEFAULT nextval('sys.post_id_seq'::regclass) NOT NULL,
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
content TEXT DEFAULT ''::text NOT NULL,
|
content TEXT DEFAULT ''::text NOT NULL,
|
||||||
date 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,
|
||||||
|
deleted BOOLEAN DEFAULT FALSE NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
ALTER TABLE admin.post OWNER TO xssbook;
|
ALTER TABLE admin.post OWNER TO xssbook;
|
||||||
|
@ -97,11 +100,13 @@ CREATE SEQUENCE IF NOT EXISTS sys.comment_id_seq
|
||||||
CACHE 1;
|
CACHE 1;
|
||||||
|
|
||||||
CREATE TABLE admin.comment (
|
CREATE TABLE admin.comment (
|
||||||
id INTEGER DEFAULT nextval('sys.comment_id_seq'::regclass) NOT NULL,
|
id INTEGER DEFAULT nextval('sys.comment_id_seq'::regclass) NOT NULL,
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
post_id INTEGER NOT NULL,
|
post_id INTEGER NOT NULL,
|
||||||
content TEXT DEFAULT ''::text NOT NULL,
|
content TEXT DEFAULT ''::text NOT NULL,
|
||||||
date 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,
|
||||||
|
deleted BOOLEAN DEFAULT FALSE NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
ALTER TABLE admin.comment OWNER TO xssbook;
|
ALTER TABLE admin.comment OWNER TO xssbook;
|
||||||
|
@ -115,15 +120,28 @@ ALTER TABLE ONLY admin.comment
|
||||||
ALTER TABLE ONLY admin.comment
|
ALTER TABLE ONLY admin.comment
|
||||||
ADD CONSTRAINT comment_post_id_fkey FOREIGN KEY (post_id) REFERENCES admin.post (id) ON DELETE CASCADE;
|
ADD CONSTRAINT comment_post_id_fkey FOREIGN KEY (post_id) REFERENCES admin.post (id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS sys.like_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
CREATE TABLE admin.like (
|
CREATE TABLE admin.like (
|
||||||
|
id INTEGER DEFAULT nextval('sys.like_id_seq'::regclass) NOT NULL,
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
post_id INTEGER,
|
post_id INTEGER,
|
||||||
comment_id INTEGER,
|
comment_id INTEGER,
|
||||||
date TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL
|
value BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
|
||||||
|
modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
ALTER TABLE admin.like OWNER TO xssbook;
|
ALTER TABLE admin.like OWNER TO xssbook;
|
||||||
|
|
||||||
|
ALTER TABLE ONLY admin.like
|
||||||
|
ADD CONSTRAINT like_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
ALTER TABLE ONLY admin.like
|
ALTER TABLE ONLY admin.like
|
||||||
ADD CONSTRAINT like_user_id_fkey FOREIGN KEY (user_id) REFERENCES admin.user (id) ON DELETE CASCADE;
|
ADD CONSTRAINT like_user_id_fkey FOREIGN KEY (user_id) REFERENCES admin.user (id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
@ -133,16 +151,32 @@ ALTER TABLE ONLY admin.like
|
||||||
ALTER TABLE ONLY admin.like
|
ALTER TABLE ONLY admin.like
|
||||||
ADD CONSTRAINT like_comment_id_fkey FOREIGN KEY (comment_id) REFERENCES admin.comment (id) ON DELETE CASCADE;
|
ADD CONSTRAINT like_comment_id_fkey FOREIGN KEY (comment_id) REFERENCES admin.comment (id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE ONLY admin.like
|
||||||
|
ADD CONSTRAINT like_post_id_unique UNIQUE (user_id, post_id);
|
||||||
|
|
||||||
|
ALTER TABLE ONLY admin.like
|
||||||
|
ADD CONSTRAINT like_comment_id_unique UNIQUE (user_id, comment_id);
|
||||||
|
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS sys.follow_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
CREATE TABLE admin.follow (
|
CREATE TABLE admin.follow (
|
||||||
|
id INTEGER DEFAULT nextval('sys.follow_id_seq'::regclass) NOT NULL,
|
||||||
follower_id INTEGER NOT NULL,
|
follower_id INTEGER NOT NULL,
|
||||||
followee_id INTEGER NOT NULL,
|
followee_id INTEGER NOT NULL,
|
||||||
date TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL
|
value BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL,
|
||||||
|
modified TIMESTAMP WITH TIME ZONE DEFAULT clock_timestamp() NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
ALTER TABLE admin.follow OWNER TO xssbook;
|
ALTER TABLE admin.follow OWNER TO xssbook;
|
||||||
|
|
||||||
ALTER TABLE ONLY admin.follow
|
ALTER TABLE ONLY admin.follow
|
||||||
ADD CONSTRAINT follow_pkey PRIMARY KEY (follower_id, followee_id);
|
ADD CONSTRAINT follow_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
ALTER TABLE ONLY admin.follow
|
ALTER TABLE ONLY admin.follow
|
||||||
ADD CONSTRAINT follow_follower_id FOREIGN KEY (follower_id) REFERENCES admin.user (id) ON DELETE CASCADE;
|
ADD CONSTRAINT follow_follower_id FOREIGN KEY (follower_id) REFERENCES admin.user (id) ON DELETE CASCADE;
|
||||||
|
@ -150,16 +184,64 @@ ALTER TABLE ONLY admin.follow
|
||||||
ALTER TABLE ONLY admin.follow
|
ALTER TABLE ONLY admin.follow
|
||||||
ADD CONSTRAINT follow_followee_id FOREIGN KEY (followee_id) REFERENCES admin.user (id) ON DELETE CASCADE;
|
ADD CONSTRAINT follow_followee_id FOREIGN KEY (followee_id) REFERENCES admin.user (id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE ONLY admin.follow
|
||||||
|
ADD CONSTRAINT follow_follower_unique UNIQUE (follower_id, followee_id);
|
||||||
|
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS sys.media_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
CREATE TABLE admin.media (
|
CREATE TABLE admin.media (
|
||||||
name TEXT NOT NULL,
|
id INTEGER DEFAULT nextval('sys.media_id_seq'::regclass) NOT NULL,
|
||||||
content BYTEA NOT NULL,
|
name TEXT NOT NULL,
|
||||||
type TEXT NOT NULL
|
content BYTEA NOT NULL,
|
||||||
|
type TEXT 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.media OWNER TO xssbook;
|
ALTER TABLE admin.media OWNER TO xssbook;
|
||||||
|
|
||||||
ALTER TABLE ONLY admin.media
|
ALTER TABLE ONLY admin.media
|
||||||
ADD CONSTRAINT media_pkey PRIMARY KEY (name);
|
ADD CONSTRAINT media_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
ALTER TABLE ONLY admin.media
|
||||||
|
ADD CONSTRAINT media_name_unique UNIQUE (name);
|
||||||
|
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS sys.user_media_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
CREATE TYPE admin.user_media_type AS ENUM (
|
||||||
|
'avatar', 'banner'
|
||||||
|
);
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
ALTER TABLE ONLY admin.user_media
|
||||||
|
ADD CONSTRAINT user_media_type_unique UNIQUE (user_id, type);
|
||||||
|
|
||||||
ALTER DATABASE xssbook SET search_path = admin,public;
|
ALTER DATABASE xssbook SET search_path = admin,public;
|
||||||
ALTER DATABASE xssbook SET bytea_output = 'hex';
|
ALTER DATABASE xssbook SET bytea_output = 'hex';
|
|
@ -4,10 +4,24 @@ CREATE VIEW api.comment AS
|
||||||
c.user_id,
|
c.user_id,
|
||||||
c.post_id,
|
c.post_id,
|
||||||
c.content,
|
c.content,
|
||||||
c.date
|
c.created,
|
||||||
|
c.modified
|
||||||
FROM
|
FROM
|
||||||
admin.comment c
|
admin.comment c
|
||||||
ORDER BY id ASC;
|
LEFT JOIN
|
||||||
|
admin.post p
|
||||||
|
ON
|
||||||
|
p.id = c.post_id
|
||||||
|
LEFT JOIN
|
||||||
|
admin.user u
|
||||||
|
ON
|
||||||
|
u.id = c.user_id
|
||||||
|
WHERE
|
||||||
|
c.deleted <> TRUE AND
|
||||||
|
p.deleted <> TRUE AND
|
||||||
|
u.deleted <> TRUE
|
||||||
|
ORDER BY
|
||||||
|
id ASC;
|
||||||
|
|
||||||
GRANT SELECT ON TABLE api.comment
|
GRANT SELECT ON TABLE api.comment
|
||||||
TO rest_anon, rest_user;
|
TO rest_anon, rest_user;
|
|
@ -11,9 +11,10 @@ BEGIN
|
||||||
PERFORM _api.raise_deny();
|
PERFORM _api.raise_deny();
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
DELETE FROM admin.comment
|
UPDATE admin.comment SET
|
||||||
WHERE user_id = _user_id
|
deleted = TRUE,
|
||||||
AND id = OLD.id;
|
modified = clock_timestamp()
|
||||||
|
WHERE id = OLD.id;
|
||||||
END
|
END
|
||||||
$BODY$;
|
$BODY$;
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ GRANT EXECUTE ON FUNCTION _api.comment_delete()
|
||||||
TO rest_user;
|
TO rest_user;
|
||||||
GRANT DELETE ON TABLE api.comment
|
GRANT DELETE ON TABLE api.comment
|
||||||
TO rest_user;
|
TO rest_user;
|
||||||
GRANT DELETE ON TABLE admin.comment
|
GRANT UPDATE ON TABLE admin.comment
|
||||||
TO rest_user;
|
TO rest_user;
|
||||||
|
|
||||||
CREATE TRIGGER api_comment_delete_trgr
|
CREATE TRIGGER api_comment_delete_trgr
|
|
@ -34,7 +34,9 @@ BEGIN
|
||||||
_user_id,
|
_user_id,
|
||||||
NEW.post_id,
|
NEW.post_id,
|
||||||
NEW.content
|
NEW.content
|
||||||
);
|
)
|
||||||
|
RETURNING id
|
||||||
|
INTO NEW.id;
|
||||||
|
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END
|
END
|
|
@ -27,8 +27,9 @@ BEGIN
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
IF _changed THEN
|
IF _changed THEN
|
||||||
UPDATE admin.comment
|
UPDATE admin.comment SET
|
||||||
SET content = NEW.content
|
content = NEW.content,
|
||||||
|
modified = clock_timestamp()
|
||||||
WHERE id = OLD.id;
|
WHERE id = OLD.id;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
16
src/db/rest/like/api_like.sql
Normal file
16
src/db/rest/like/api_like.sql
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
CREATE VIEW api.like AS
|
||||||
|
SELECT
|
||||||
|
l.id,
|
||||||
|
l.user_id,
|
||||||
|
l.post_id,
|
||||||
|
l.comment_id,
|
||||||
|
l.value,
|
||||||
|
l.created,
|
||||||
|
l.modified
|
||||||
|
FROM
|
||||||
|
admin.like l;
|
||||||
|
|
||||||
|
GRANT SELECT ON TABLE api.like
|
||||||
|
TO rest_anon, rest_user;
|
||||||
|
GRANT SELECT ON TABLE admin.like
|
||||||
|
TO rest_anon, rest_user;
|
32
src/db/rest/like/api_like_delete.sql
Normal file
32
src/db/rest/like/api_like_delete.sql
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
CREATE FUNCTION _api.like_delete()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql VOLATILE
|
||||||
|
AS $BODY$
|
||||||
|
DECLARE
|
||||||
|
_user_id INTEGER;
|
||||||
|
BEGIN
|
||||||
|
_user_id = _api.get_user_id();
|
||||||
|
|
||||||
|
IF OLD.user_id <> _user_id THEN
|
||||||
|
PERFORM _api.raise_deny();
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
UPDATE admin.like SET
|
||||||
|
value = FALSE,
|
||||||
|
modified = clock_timestamp()
|
||||||
|
WHERE id = OLD.id;
|
||||||
|
END
|
||||||
|
$BODY$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION _api.like_delete()
|
||||||
|
TO rest_user;
|
||||||
|
GRANT DELETE ON TABLE api.like
|
||||||
|
TO rest_user;
|
||||||
|
GRANT UPDATE ON TABLE admin.like
|
||||||
|
TO rest_user;
|
||||||
|
|
||||||
|
CREATE TRIGGER api_like_delete_trgr
|
||||||
|
INSTEAD OF DELETE
|
||||||
|
ON api.like
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE _api.like_delete();
|
51
src/db/rest/like/api_like_insert.sql
Normal file
51
src/db/rest/like/api_like_insert.sql
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
CREATE FUNCTION _api.like_insert()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql VOLATILE
|
||||||
|
AS $BODY$
|
||||||
|
DECLARE
|
||||||
|
_user_id INTEGER;
|
||||||
|
BEGIN
|
||||||
|
_user_id = _api.get_user_id();
|
||||||
|
|
||||||
|
IF
|
||||||
|
NEW.post_id IS NULL AND
|
||||||
|
NEW.comment_id IS NULL
|
||||||
|
THEN
|
||||||
|
-- for now
|
||||||
|
PERFORM _api.raise_deny();
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
NEW.value := COALESCE(NEW.value, TRUE);
|
||||||
|
|
||||||
|
INSERT INTO admin.like (
|
||||||
|
user_id,
|
||||||
|
post_id,
|
||||||
|
comment_id,
|
||||||
|
value
|
||||||
|
) VALUES (
|
||||||
|
_user_id,
|
||||||
|
NEW.post_id,
|
||||||
|
NEW.comment_id,
|
||||||
|
NEW.value
|
||||||
|
)
|
||||||
|
RETURNING id
|
||||||
|
INTO NEW.id;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$BODY$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION _api.like_insert()
|
||||||
|
TO rest_user;
|
||||||
|
GRANT INSERT ON TABLE api.like
|
||||||
|
TO rest_user;
|
||||||
|
GRANT INSERT ON TABLE admin.like
|
||||||
|
TO rest_user;
|
||||||
|
GRANT UPDATE ON TABLE sys.like_id_seq
|
||||||
|
TO rest_user;
|
||||||
|
|
||||||
|
CREATE TRIGGER api_like_insert_trgr
|
||||||
|
INSTEAD OF INSERT
|
||||||
|
ON api.like
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE _api.like_insert();
|
44
src/db/rest/like/api_like_update.sql
Normal file
44
src/db/rest/like/api_like_update.sql
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
CREATE FUNCTION _api.like_update()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql VOLATILE
|
||||||
|
AS $BODY$
|
||||||
|
DECLARE
|
||||||
|
_user_id INTEGER;
|
||||||
|
_changed BOOLEAN;
|
||||||
|
BEGIN
|
||||||
|
_user_id = _api.get_user_id();
|
||||||
|
_changed = FALSE;
|
||||||
|
|
||||||
|
IF OLD.user_id <> _user_id THEN
|
||||||
|
PERFORM _api.raise_deny();
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
NEW.value = COALESCE(NEW.value, OLD.value);
|
||||||
|
|
||||||
|
IF NEW.value IS DISTINCT FROM OLD.value THEN
|
||||||
|
_changed = TRUE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF _changed THEN
|
||||||
|
UPDATE admin.like SET
|
||||||
|
value = NEW.value,
|
||||||
|
modified = clock_timestamp()
|
||||||
|
WHERE id = OLD.id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$BODY$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION _api.like_update()
|
||||||
|
TO rest_user;
|
||||||
|
GRANT UPDATE ON TABLE api.like
|
||||||
|
TO rest_user;
|
||||||
|
GRANT UPDATE ON TABLE admin.like
|
||||||
|
TO rest_user;
|
||||||
|
|
||||||
|
CREATE TRIGGER api_like_update_trgr
|
||||||
|
INSTEAD OF UPDATE
|
||||||
|
ON api.like
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE _api.like_update();
|
|
@ -8,6 +8,7 @@ DECLARE
|
||||||
_payload JSON;
|
_payload JSON;
|
||||||
_valid BOOLEAN;
|
_valid BOOLEAN;
|
||||||
_jwt_secret TEXT;
|
_jwt_secret TEXT;
|
||||||
|
_user_id INTEGER;
|
||||||
BEGIN
|
BEGIN
|
||||||
SELECT jwt_secret INTO _jwt_secret
|
SELECT jwt_secret INTO _jwt_secret
|
||||||
FROM sys.database_info
|
FROM sys.database_info
|
||||||
|
@ -28,7 +29,13 @@ BEGIN
|
||||||
RETURN NULL;
|
RETURN NULL;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
RETURN _payload->>'user_id';
|
_user_id = _payload->>'user_id';
|
||||||
|
|
||||||
|
UPDATE admin.user
|
||||||
|
SET seen = clock_timestamp()
|
||||||
|
WHERE id = _user_id;
|
||||||
|
|
||||||
|
RETURN _user_id;
|
||||||
END
|
END
|
||||||
$BODY$;
|
$BODY$;
|
||||||
|
|
||||||
|
@ -36,3 +43,5 @@ GRANT EXECUTE ON FUNCTION _api.verify_jwt(TEXT)
|
||||||
TO rest_anon, rest_user;
|
TO rest_anon, rest_user;
|
||||||
GRANT SELECT ON TABLE sys.database_info
|
GRANT SELECT ON TABLE sys.database_info
|
||||||
TO rest_anon, rest_user;
|
TO rest_anon, rest_user;
|
||||||
|
GRANT UPDATE ON TABLE admin.user
|
||||||
|
TO rest_anon, rest_user;
|
36
src/db/rest/media/api_profile_avatar.sql
Normal file
36
src/db/rest/media/api_profile_avatar.sql
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
CREATE FUNCTION api.profile_avatar(
|
||||||
|
user_id INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
RETURNS sys."*/*"
|
||||||
|
LANGUAGE plpgsql VOLATILE
|
||||||
|
AS $BODY$
|
||||||
|
DECLARE
|
||||||
|
_id INTEGER;
|
||||||
|
_mod INTEGER;
|
||||||
|
_name 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);
|
||||||
|
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;
|
13
src/db/rest/media/api_profile_banner.sql
Normal file
13
src/db/rest/media/api_profile_banner.sql
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
CREATE FUNCTION api.profile_banner(
|
||||||
|
user_id INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
RETURNS sys."*/*"
|
||||||
|
LANGUAGE plpgsql VOLATILE
|
||||||
|
AS $BODY$
|
||||||
|
BEGIN
|
||||||
|
PERFORM _api.raise_deny();
|
||||||
|
END
|
||||||
|
$BODY$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION api.profile_banner(INTEGER)
|
||||||
|
TO rest_anon, rest_user;
|
|
@ -3,7 +3,8 @@ CREATE VIEW api.post AS
|
||||||
p.id,
|
p.id,
|
||||||
p.user_id,
|
p.user_id,
|
||||||
p.content,
|
p.content,
|
||||||
p.date,
|
p.created,
|
||||||
|
p.modified,
|
||||||
COALESCE(c.cc, 0)
|
COALESCE(c.cc, 0)
|
||||||
AS comment_count
|
AS comment_count
|
||||||
FROM
|
FROM
|
||||||
|
@ -16,8 +17,19 @@ CREATE VIEW api.post AS
|
||||||
admin.comment c
|
admin.comment c
|
||||||
GROUP BY
|
GROUP BY
|
||||||
c.post_id
|
c.post_id
|
||||||
) c ON p.id = c.post_id
|
) c
|
||||||
ORDER BY p.id DESC;
|
ON
|
||||||
|
p.id = c.post_id
|
||||||
|
LEFT JOIN
|
||||||
|
admin.user u
|
||||||
|
ON
|
||||||
|
u.id = p.user_id
|
||||||
|
WHERE
|
||||||
|
p.deleted <> TRUE
|
||||||
|
AND
|
||||||
|
u.deleted <> TRUE
|
||||||
|
ORDER BY
|
||||||
|
p.id DESC;
|
||||||
|
|
||||||
GRANT SELECT ON TABLE api.post
|
GRANT SELECT ON TABLE api.post
|
||||||
TO rest_anon, rest_user;
|
TO rest_anon, rest_user;
|
|
@ -11,9 +11,10 @@ BEGIN
|
||||||
PERFORM _api.raise_deny();
|
PERFORM _api.raise_deny();
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
DELETE FROM admin.post
|
UPDATE admin.post SET
|
||||||
WHERE user_id = _user_id
|
deleted = TRUE,
|
||||||
AND id = OLD.id;
|
modified = clock_timestamp()
|
||||||
|
WHERE id = OLD.id;
|
||||||
END
|
END
|
||||||
$BODY$;
|
$BODY$;
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ GRANT EXECUTE ON FUNCTION _api.post_delete()
|
||||||
TO rest_user;
|
TO rest_user;
|
||||||
GRANT DELETE ON TABLE api.post
|
GRANT DELETE ON TABLE api.post
|
||||||
TO rest_user;
|
TO rest_user;
|
||||||
GRANT DELETE ON TABLE admin.post
|
GRANT UPDATE ON TABLE admin.post
|
||||||
TO rest_user;
|
TO rest_user;
|
||||||
|
|
||||||
CREATE TRIGGER api_post_delete_trgr
|
CREATE TRIGGER api_post_delete_trgr
|
|
@ -22,7 +22,9 @@ BEGIN
|
||||||
) VALUES (
|
) VALUES (
|
||||||
_user_id,
|
_user_id,
|
||||||
NEW.content
|
NEW.content
|
||||||
);
|
)
|
||||||
|
RETURNING id
|
||||||
|
INTO NEW.id;
|
||||||
|
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END
|
END
|
|
@ -27,8 +27,9 @@ BEGIN
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
IF _changed THEN
|
IF _changed THEN
|
||||||
UPDATE admin.post
|
UPDATE admin.post SET
|
||||||
SET content = NEW.content
|
content = NEW.content,
|
||||||
|
modified = clock_timestamp()
|
||||||
WHERE id = OLD.id;
|
WHERE id = OLD.id;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
|
@ -27,7 +27,6 @@ GRANT USAGE ON SCHEMA _api TO rest_anon, rest_user;
|
||||||
\i /db/rest/user/api_user_insert.sql;
|
\i /db/rest/user/api_user_insert.sql;
|
||||||
\i /db/rest/user/api_user_update.sql;
|
\i /db/rest/user/api_user_update.sql;
|
||||||
\i /db/rest/user/api_user_delete.sql;
|
\i /db/rest/user/api_user_delete.sql;
|
||||||
\i /db/rest/user/api_avatar.sql;
|
|
||||||
|
|
||||||
-- post
|
-- post
|
||||||
\i /db/rest/post/api_post.sql;
|
\i /db/rest/post/api_post.sql;
|
||||||
|
@ -41,6 +40,16 @@ GRANT USAGE ON SCHEMA _api TO rest_anon, rest_user;
|
||||||
\i /db/rest/comment/api_comment_update.sql;
|
\i /db/rest/comment/api_comment_update.sql;
|
||||||
\i /db/rest/comment/api_comment_delete.sql;
|
\i /db/rest/comment/api_comment_delete.sql;
|
||||||
|
|
||||||
|
-- like
|
||||||
|
\i /db/rest/like/api_like.sql;
|
||||||
|
\i /db/rest/like/api_like_insert.sql;
|
||||||
|
\i /db/rest/like/api_like_update.sql;
|
||||||
|
\i /db/rest/like/api_like_delete.sql;
|
||||||
|
|
||||||
|
-- media
|
||||||
|
\i /db/rest/media/api_profile_avatar.sql;
|
||||||
|
\i /db/rest/media/api_profile_banner.sql;
|
||||||
|
|
||||||
-- login
|
-- login
|
||||||
\i /db/rest/login/_api_sign_jwt.sql;
|
\i /db/rest/login/_api_sign_jwt.sql;
|
||||||
\i /db/rest/login/_api_verify_jwt.sql;
|
\i /db/rest/login/_api_verify_jwt.sql;
|
|
@ -9,13 +9,15 @@ CREATE VIEW api.user AS
|
||||||
u.middle_name,
|
u.middle_name,
|
||||||
u.email,
|
u.email,
|
||||||
u.gender,
|
u.gender,
|
||||||
u.join_date,
|
|
||||||
u.birth_date,
|
u.birth_date,
|
||||||
u.profile_avatar,
|
u.profile_bio,
|
||||||
u.profile_banner,
|
u.created,
|
||||||
u.profile_bio
|
u.modified,
|
||||||
|
u.seen
|
||||||
FROM
|
FROM
|
||||||
admin.user u;
|
admin.user u
|
||||||
|
WHERE
|
||||||
|
u.deleted <> TRUE;
|
||||||
|
|
||||||
GRANT SELECT ON TABLE api.user
|
GRANT SELECT ON TABLE api.user
|
||||||
TO rest_anon, rest_user;
|
TO rest_anon, rest_user;
|
|
@ -11,8 +11,10 @@ BEGIN
|
||||||
PERFORM _api.raise_deny();
|
PERFORM _api.raise_deny();
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
DELETE FROM admin.user
|
UPDATE admin.user SET
|
||||||
WHERE id = _user_id;
|
deleted = TRUE,
|
||||||
|
modified = clock_timestamp()
|
||||||
|
WHERE id = _user_id;
|
||||||
END
|
END
|
||||||
$BODY$;
|
$BODY$;
|
||||||
|
|
||||||
|
@ -20,7 +22,7 @@ GRANT EXECUTE ON FUNCTION _api.user_delete()
|
||||||
TO rest_user;
|
TO rest_user;
|
||||||
GRANT DELETE ON TABLE api.user
|
GRANT DELETE ON TABLE api.user
|
||||||
TO rest_user;
|
TO rest_user;
|
||||||
GRANT DELETE ON TABLE admin.user
|
GRANT UPDATE ON TABLE admin.user
|
||||||
TO rest_user;
|
TO rest_user;
|
||||||
|
|
||||||
CREATE TRIGGER api_user_delete_trgr
|
CREATE TRIGGER api_user_delete_trgr
|
|
@ -104,7 +104,9 @@ BEGIN
|
||||||
NEW.gender,
|
NEW.gender,
|
||||||
NEW.birth_date,
|
NEW.birth_date,
|
||||||
NEW.profile_bio
|
NEW.profile_bio
|
||||||
);
|
)
|
||||||
|
RETURNING id
|
||||||
|
INTO NEW.id;
|
||||||
|
|
||||||
NEW.password := NULL;
|
NEW.password := NULL;
|
||||||
|
|
|
@ -145,7 +145,8 @@ BEGIN
|
||||||
email = NEW.email,
|
email = NEW.email,
|
||||||
gender = NEW.gender,
|
gender = NEW.gender,
|
||||||
birth_date = NEW.birth_date,
|
birth_date = NEW.birth_date,
|
||||||
profile_bio = NEW.profile_bio
|
profile_bio = NEW.profile_bio,
|
||||||
|
modified = clock_timestamp()
|
||||||
WHERE id = OLD.id;
|
WHERE id = OLD.id;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
22
src/db/rest/util/_api_get_user_id.sql
Normal file
22
src/db/rest/util/_api_get_user_id.sql
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
CREATE FUNCTION _api.get_user_id()
|
||||||
|
RETURNS INTEGER
|
||||||
|
LANGUAGE plpgsql VOLATILE
|
||||||
|
AS $BODY$
|
||||||
|
DECLARE
|
||||||
|
_user_id INTEGER;
|
||||||
|
BEGIN
|
||||||
|
_user_id = CURRENT_SETTING(
|
||||||
|
'request.jwt.claims',
|
||||||
|
TRUE
|
||||||
|
)::JSON->>'user_id';
|
||||||
|
|
||||||
|
UPDATE admin.user
|
||||||
|
SET seen = clock_timestamp()
|
||||||
|
WHERE id = _user_id;
|
||||||
|
|
||||||
|
RETURN _user_id;
|
||||||
|
END
|
||||||
|
$BODY$;
|
||||||
|
|
||||||
|
GRANT UPDATE ON TABLE admin.user
|
||||||
|
TO rest_anon, rest_user;
|
|
@ -1,5 +1,5 @@
|
||||||
CREATE FUNCTION _api.serve_media(
|
CREATE FUNCTION _api.serve_media(
|
||||||
_name TEXT
|
_media_id INTEGER
|
||||||
)
|
)
|
||||||
RETURNS sys."*/*"
|
RETURNS sys."*/*"
|
||||||
LANGUAGE plpgsql VOLATILE
|
LANGUAGE plpgsql VOLATILE
|
||||||
|
@ -15,13 +15,13 @@ BEGIN
|
||||||
'{"Cache-Control": "max-age=259200"}]'
|
'{"Cache-Control": "max-age=259200"}]'
|
||||||
, m.type, m.name)
|
, m.type, m.name)
|
||||||
FROM admin.media m
|
FROM admin.media m
|
||||||
WHERE m.name = _name INTO _headers;
|
WHERE m.id = _media_id INTO _headers;
|
||||||
|
|
||||||
PERFORM SET_CONFIG('response.headers', _headers, true);
|
PERFORM SET_CONFIG('response.headers', _headers, true);
|
||||||
|
|
||||||
SELECT m.content
|
SELECT m.content
|
||||||
FROM admin.media m
|
FROM admin.media m
|
||||||
WHERE m.name = _name
|
WHERE m.id = _media_id
|
||||||
INTO _data;
|
INTO _data;
|
||||||
|
|
||||||
IF FOUND THEN
|
IF FOUND THEN
|
||||||
|
@ -35,7 +35,7 @@ BEGIN
|
||||||
END
|
END
|
||||||
$BODY$;
|
$BODY$;
|
||||||
|
|
||||||
GRANT EXECUTE ON FUNCTION _api.serve_media(TEXT)
|
GRANT EXECUTE ON FUNCTION _api.serve_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;
|
45
src/public/css/auth.css
Normal file
45
src/public/css/auth.css
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
#main-content {
|
||||||
|
padding-top: 20rem;
|
||||||
|
padding-bottom: 5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branding {
|
||||||
|
max-width: 30rem;
|
||||||
|
margin-right: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branding h1 {
|
||||||
|
color: var(--blue);
|
||||||
|
font-family: facebook;
|
||||||
|
font-size: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branding span {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 30rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: 1200px) {
|
||||||
|
#main-content {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branding {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
margin-top: 4rem;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,44 @@
|
||||||
:root {
|
:root {
|
||||||
--primary: #242424 !important;
|
--white: #E4E6EB;
|
||||||
--secondary: #181818 !important;
|
--blue: #1778f2;
|
||||||
--hover: #1b1b1b !important;
|
--red: #f02849;
|
||||||
--light: #3e4042 !important;
|
--green: #30ab5a;
|
||||||
--mild: #1b1b1b !important;
|
|
||||||
--medium: #e2ded6 !important;
|
--blue-alt: #1D85FC;
|
||||||
--extreme: #e2ded6 !important;
|
--green-alt: #39B463;
|
||||||
--logo: #1778f2 !important;
|
|
||||||
--error: #f02849 !important;
|
--font: Helvetica;
|
||||||
--success: #30ab5a !important;
|
}
|
||||||
--text: #ffffff !important;
|
|
||||||
--banner: #6b6b6b !important;
|
:root {
|
||||||
--popup: #242424cc !important;
|
--base :#18191A;
|
||||||
|
--surface0: #242526;
|
||||||
|
--surface1: #3A3B3C;
|
||||||
|
--surface2: #4E4F50;
|
||||||
|
|
||||||
|
--text: #E4E6EB;
|
||||||
|
--subtext: #B0B3B8;
|
||||||
|
--btntext: #E4E6EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
:root {
|
||||||
|
--base: #f0f2f5;
|
||||||
|
--surface0: #ffffff;
|
||||||
|
--surface1: #f0f2f5;
|
||||||
|
--surface2: #dadde1;
|
||||||
|
|
||||||
|
--text: #000000;
|
||||||
|
--subtext: #1d2129;
|
||||||
|
--btntext: #606770;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Helvetica Neue';
|
||||||
|
font-style: normal;
|
||||||
|
src: url("/public/font/helvetica-neue.otf") format("opentype");
|
||||||
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
|
@ -33,14 +60,8 @@
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: sfprobold;
|
|
||||||
src: url("/public/font/sfprobold.otf") format("opentype");
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--secondary);
|
background-color: var(--surface0);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -48,93 +69,132 @@ body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-family: sfpro;
|
font-family: var(--font);
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-content {
|
||||||
|
background-color: var(--base);
|
||||||
|
padding-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
top: 0;
|
top: 0;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
height: 3.5rem;
|
height: 3.5rem;
|
||||||
background-color: var(--primary);
|
background-color: var(--surface0);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
|
border-bottom: 1px solid var(--surface1);
|
||||||
}
|
}
|
||||||
|
|
||||||
header .logo {
|
header .logo {
|
||||||
font-family: facebook;
|
font-family: facebook;
|
||||||
color: var(--logo);
|
color: var(--blue);
|
||||||
font-size: 2.25rem;
|
font-size: 2.25rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
margin-top: .75rem;
|
margin-top: .75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
color: var(--subtext);
|
||||||
|
font-size: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
color: var(--surface2);
|
||||||
|
background-color: var(--surface2);
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, button, input, div {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
a, button, input {
|
a, button, input {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
font-family: sfprobold;
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a, button {
|
a, button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
form button {
|
.btn {
|
||||||
padding: .5rem;
|
color: var(--btntext);
|
||||||
border-radius: .5rem;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
flex-direction: row;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
padding: .4rem .6rem;
|
||||||
|
border-radius: .25rem;
|
||||||
|
background-color: transparent;
|
||||||
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:focus {
|
.btn:hover {
|
||||||
|
background-color: var(--surface1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-alt {
|
||||||
|
background-color: var(--surface1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-alt:hover {
|
||||||
|
background-color: var(--surface2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-wide {
|
||||||
|
width: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-line:hover {
|
||||||
|
background-color: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-blue {
|
||||||
|
color: var(--blue-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.btn:focus {
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-entry {
|
.btn-submit {
|
||||||
display: flex;
|
color: var(--white);
|
||||||
flex-direction: row;
|
background-color: var(--blue);
|
||||||
text-decoration: none;
|
flex-grow: 1;
|
||||||
align-items: center;
|
padding: .5rem;
|
||||||
color: var(--text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav .header-entry {
|
.btn-submit:hover {
|
||||||
height: 100%;
|
background-color: var(--blue-alt);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-center .header-entry:hover {
|
.btn-success {
|
||||||
background-color: var(--hover);
|
color: var(--white);
|
||||||
|
background-color: var(--green);
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-action {
|
.btn-success:hover {
|
||||||
justify-content: center;
|
background-color: var(--green-alt);
|
||||||
align-items: center;
|
|
||||||
padding: .35rem;
|
|
||||||
margin: .25rem;
|
|
||||||
border-radius: .25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action:hover {
|
|
||||||
background-color: var(--hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-blue:hover {
|
|
||||||
color: var(--logo);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .btn-blue {
|
|
||||||
border-bottom: 1px solid var(--logo);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-line:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav,
|
.nav,
|
||||||
|
@ -167,12 +227,13 @@ input:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
@media (min-width: 800px) {
|
||||||
.header-entry > span {
|
.nav-center .btn > span {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-center .header-entry {
|
.nav-center .btn {
|
||||||
padding: 0 3rem;
|
padding: 0 3rem;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#action-hamburger {
|
#action-hamburger {
|
||||||
|
@ -187,7 +248,7 @@ input:focus {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
background-color: var(--primary);
|
background-color: var(--surface0);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
transform: translateX(0%);
|
transform: translateX(0%);
|
||||||
|
@ -198,18 +259,18 @@ input:focus {
|
||||||
display: inherit !important;
|
display: inherit !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-center .header-entry {
|
.nav-center .btn {
|
||||||
width: calc(100% - 3rem);
|
width: calc(100% - 3rem);
|
||||||
padding: .75rem 0rem !important;
|
padding: .75rem 0rem !important;
|
||||||
padding-left: 3rem !important;
|
padding-left: 3rem !important;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-center .header-entry > span {
|
.nav-center .btn > span {
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-center .header-entry.active {
|
.nav-center .btn.active {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -218,11 +279,6 @@ input:focus {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-right .header-entry {
|
|
||||||
padding: 0;
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
to {
|
to {
|
||||||
background-position-x: 0%;
|
background-position-x: 0%;
|
||||||
|
@ -242,40 +298,32 @@ input:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-loading {
|
.image-loading {
|
||||||
background: linear-gradient(-45deg, var(--secondary) 0%, var(--primary) 25%, var(--secondary) 50%);
|
background: linear-gradient(-45deg, var(--surface0) 0%, var(--base) 25%, var(--surface0) 50%);
|
||||||
background-size: 500%;
|
background-size: 500%;
|
||||||
background-position-x: 150%;
|
background-position-x: 150%;
|
||||||
animation: shimmer 1s linear infinite;
|
animation: shimmer 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background-color: var(--primary);
|
background-color: var(--surface0);
|
||||||
border-radius: .5rem;
|
border-radius: .5rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.card form {
|
.card form {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card .sub-card {
|
.card .sub-card {
|
||||||
background-color: var(--secondary);
|
background-color: var(--surface1);
|
||||||
border-radius: .5rem;
|
border-radius: .5rem;
|
||||||
padding: .75rem;
|
padding: .75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 10px;
|
|
||||||
width: calc(100% - 20px);
|
|
||||||
background-color: var(--secondary);
|
|
||||||
font-family: sfpro;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input:hover {
|
|
||||||
background-color: var(--hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -314,8 +362,12 @@ input:focus {
|
||||||
margin-bottom: .75rem;
|
margin-bottom: .75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pb {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.dim {
|
.dim {
|
||||||
color: var(--medium);
|
color: var(--subtext);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-container {
|
.modal-container {
|
||||||
|
@ -329,7 +381,7 @@ input:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
background-color: var(--primary);
|
background-color: var(--surface0);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
@ -372,18 +424,14 @@ input:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
font-family: sfprobold;
|
font-weight: bold;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-bottom: 1px solid var(--light);
|
border-bottom: 1px solid var(--surface1);
|
||||||
text-align: center;
|
|
||||||
margin: 0 1rem;
|
|
||||||
border-radius: .5rem .5rem 0 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-left: 1rem;
|
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
padding: 1rem;
|
padding: 1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
|
@ -402,9 +450,9 @@ input:focus {
|
||||||
|
|
||||||
.float-right {
|
.float-right {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: translate(0%, -50%);
|
top: 50%;
|
||||||
top: 45%;
|
left: 100%;
|
||||||
right: 0;
|
transform: translate(-125%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mi {
|
.mi {
|
||||||
|
@ -421,17 +469,6 @@ input:focus {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[type="submit"] {
|
|
||||||
text-align: center;
|
|
||||||
background-color: var(--logo);
|
|
||||||
flex-grow: 1;
|
|
||||||
padding: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button[type="submit"]:hover {
|
|
||||||
background-color: var(--logo);
|
|
||||||
}
|
|
||||||
|
|
||||||
#toast-container {
|
#toast-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 4rem;
|
top: 4rem;
|
||||||
|
@ -442,20 +479,92 @@ button[type="submit"]:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast {
|
.toast {
|
||||||
|
color: var(--white);
|
||||||
padding: .75rem;
|
padding: .75rem;
|
||||||
margin: .5rem;
|
margin: .5rem;
|
||||||
border-radius: .5rem;
|
border-radius: .5rem;
|
||||||
min-width: 15rem;
|
min-width: 15rem;
|
||||||
font-family: sfpro;
|
|
||||||
animation: fadeIn .1s, slideIn .25s linear;
|
animation: fadeIn .1s, slideIn .25s linear;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast.error {
|
.toast.error {
|
||||||
background-color: var(--error);
|
background-color: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast.success {
|
.toast.success {
|
||||||
background-color: var(--success);
|
background-color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
form input:not(.btn) {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
outline: 2px solid var(--surface2);
|
||||||
|
border-radius: .25rem;
|
||||||
|
padding: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
form input:not(.btn):focus {
|
||||||
|
outline-color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
form .rel label:not(.static) {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(.5rem, -40%);
|
||||||
|
color: var(--subtext);
|
||||||
|
transition: all 0.2s ease-out;
|
||||||
|
pointer-events: none;
|
||||||
|
width: fit-content;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus + label:not(.static),
|
||||||
|
input:not(:placeholder-shown) + label:not(.static) {
|
||||||
|
color: var(--text);
|
||||||
|
top: 0;
|
||||||
|
padding: .5rem;
|
||||||
|
padding-top: 0;
|
||||||
|
font-size: .75rem;
|
||||||
|
transform: translate(.5rem, -25%);
|
||||||
|
background-color: var(--surface0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rel {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rel input {
|
||||||
|
width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=radio] {
|
||||||
|
padding: 3rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio label {
|
||||||
|
border: 1px solid var(--surface2);
|
||||||
|
height: fit-content;
|
||||||
|
width: 100%;
|
||||||
|
padding: .75rem;
|
||||||
|
border-radius: .25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio input {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 100%;
|
||||||
|
transform: translate(-250%, -70%);
|
||||||
|
width: fit-content;
|
||||||
|
outline: none !important;
|
||||||
}
|
}
|
|
@ -1,16 +1,16 @@
|
||||||
#error {
|
#main-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 10rem;
|
padding: 10rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#error h1 {
|
#main-content h1 {
|
||||||
color: var(--logo);
|
color: var(--blue);
|
||||||
font-family: Facebook;
|
font-family: Facebook;
|
||||||
font-size: 5rem;
|
font-size: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#error span {
|
#main-content span {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
|
@ -1,9 +1,7 @@
|
||||||
#main-content {
|
#main-content {
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
@ -15,7 +13,6 @@
|
||||||
border: none;
|
border: none;
|
||||||
resize: none;
|
resize: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
font-family: sfpro;
|
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -23,4 +20,5 @@
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
font-family: var(--font);
|
||||||
}
|
}
|
69
src/public/css/people.css
Normal file
69
src/public/css/people.css
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-left: 3rem;
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
margin-left: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#people-container {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
grid-row-gap: 2rem;
|
||||||
|
grid-auto-rows: 1fr;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 90rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile {
|
||||||
|
margin: 1rem;
|
||||||
|
text-decoration: none;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile:hover {
|
||||||
|
outline: 1px solid var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile strong {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile .pfp, .profile .pfp img {
|
||||||
|
padding: none;
|
||||||
|
margin: none;
|
||||||
|
height: 6rem;
|
||||||
|
border-radius: .3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: 1400px) {
|
||||||
|
#people-container {
|
||||||
|
max-width: 90rem;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: 1000px) {
|
||||||
|
#people-container {
|
||||||
|
max-width: 50rem;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td:nth-child(1) {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--subtext);
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:nth-child(2) {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
|
@ -1,16 +1,13 @@
|
||||||
.post hr {
|
|
||||||
color: var(--light);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post hr:nth-of-type(1) {
|
|
||||||
margin-top: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-load-comments {
|
.action-load-comments {
|
||||||
margin-left: 4rem;
|
margin-left: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#action-load-posts {
|
#action-load-posts {
|
||||||
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
BIN
src/public/font/helvetica-neue.otf
Normal file
BIN
src/public/font/helvetica-neue.otf
Normal file
Binary file not shown.
|
@ -12,7 +12,8 @@ var $$ = (selector) => {
|
||||||
'on',
|
'on',
|
||||||
'click',
|
'click',
|
||||||
'submit',
|
'submit',
|
||||||
'each'
|
'each',
|
||||||
|
'error'
|
||||||
];
|
];
|
||||||
|
|
||||||
let vtable = {};
|
let vtable = {};
|
||||||
|
@ -58,11 +59,19 @@ var $$ = (selector) => {
|
||||||
/// ajax error handle
|
/// ajax error handle
|
||||||
///
|
///
|
||||||
|
|
||||||
var errorToast = (xhr) => {
|
var errorToastAjax = (xhr) => {
|
||||||
let data = xhr.responseJSON;
|
let data = xhr.responseJSON;
|
||||||
let msg = data.message;
|
|
||||||
let detail = data.details;
|
let msg, detail, hint;
|
||||||
let hint = data.hint;
|
|
||||||
|
if (data) {
|
||||||
|
msg = data.message;
|
||||||
|
detail = data.details;
|
||||||
|
hint = data.hint;
|
||||||
|
} else {
|
||||||
|
msg = 'api_unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
let query = '?msg=' + msg;
|
let query = '?msg=' + msg;
|
||||||
if (detail) {
|
if (detail) {
|
||||||
|
@ -77,6 +86,13 @@ var errorToast = (xhr) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errorToast = (msg) => {
|
||||||
|
let url = '/template/toast?msg=' + msg;
|
||||||
|
$.get(url, function (data) {
|
||||||
|
$('#toast-container').prepend(data);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
$$('.action-close-toast').on('click', function() {
|
$$('.action-close-toast').on('click', function() {
|
||||||
$(this).parent().remove();
|
$(this).parent().remove();
|
||||||
});
|
});
|
||||||
|
@ -94,12 +110,14 @@ $$('.action-close-toast').each(function() {
|
||||||
|
|
||||||
$.ajaxSetup({
|
$.ajaxSetup({
|
||||||
headers: (() => {
|
headers: (() => {
|
||||||
let ajaxHeaders = {};
|
let ajaxHeaders = {
|
||||||
ajaxHeaders['Content-Type'] = 'application/json';
|
'Content-Type': 'application/json',
|
||||||
|
'Prefer': 'return=representation'
|
||||||
|
};
|
||||||
if (jwtStr) {
|
if (jwtStr) {
|
||||||
ajaxHeaders['Authorization'] = 'Bearer ' + jwtStr
|
ajaxHeaders['Authorization'] = 'Bearer ' + jwtStr
|
||||||
}
|
}
|
||||||
return ajaxHeaders;
|
return ajaxHeaders;
|
||||||
})(),
|
})(),
|
||||||
error: errorToast
|
error: errorToastAjax
|
||||||
})
|
})
|
|
@ -70,12 +70,59 @@ $$('.action-new-comment-form').on('submit', function(e) {
|
||||||
let input = me.find('.action-new-comment');
|
let input = me.find('.action-new-comment');
|
||||||
let content = input.val();
|
let content = input.val();
|
||||||
let post_id = input.attr('postId');
|
let post_id = input.attr('postId');
|
||||||
|
|
||||||
|
const getComment = function(data) {
|
||||||
|
if (data) {
|
||||||
|
let container = me.closest('.post').find('.comments');
|
||||||
|
container.prepend(data);
|
||||||
|
}
|
||||||
|
input.val('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const onComment = function(data) {
|
||||||
|
let id = data[0].id;
|
||||||
|
$.get({
|
||||||
|
url: '/_util/post/comment?id=' + id,
|
||||||
|
success: getComment
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/api/comment',
|
url: '/api/comment',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: JSON.stringify({ post_id, content }),
|
data: JSON.stringify({ post_id, content }),
|
||||||
success: function(_data) {
|
success: onComment
|
||||||
window.location.reload();
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$$('.action-like').on('click', function() {
|
||||||
|
let me = $(this);
|
||||||
|
let liked = me.hasClass('btn-blue');
|
||||||
|
let like_id = me.attr('likeId');
|
||||||
|
let post_id = me.attr('postId');
|
||||||
|
|
||||||
|
const onPatch = () => {
|
||||||
|
me.toggleClass('btn-blue');
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPost = (data) => {
|
||||||
|
me.attr('likeId', data[0].id + '');
|
||||||
|
me.toggleClass('btn-blue');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (like_id) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/like?id=eq.' + like_id,
|
||||||
|
method: 'PATCH',
|
||||||
|
data: JSON.stringify({ value: !liked }),
|
||||||
|
success: onPatch
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/like',
|
||||||
|
method: 'POST',
|
||||||
|
data: JSON.stringify({ post_id, value: true }),
|
||||||
|
success: onPost,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -14,7 +14,7 @@ class _index_controller extends Controller {
|
||||||
if ($this->main->session) {
|
if ($this->main->session) {
|
||||||
$this->redirect('/home');
|
$this->redirect('/home');
|
||||||
} else {
|
} else {
|
||||||
$this->redirect('/login');
|
$this->redirect('/auth/login');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,39 @@ class Post_controller extends Controller {
|
||||||
$this->view('template/posts');
|
$this->view('template/posts');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function post(): void {
|
||||||
|
$pid = $this->request_model->get_int('id', 0);
|
||||||
|
|
||||||
|
$post = $this->db
|
||||||
|
->select('p.*, l.id as like_id')
|
||||||
|
->from('api.post p')
|
||||||
|
->join('api.like l', 'p.id = l.post_id AND l.user_id')
|
||||||
|
->eq($pid)
|
||||||
|
->where('p.id')
|
||||||
|
->eq($pid)
|
||||||
|
->row();
|
||||||
|
|
||||||
|
if (!$post) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$users = $this->cache_model->get_users([$post]);
|
||||||
|
$uid = $post['user_id'];
|
||||||
|
|
||||||
|
if (!array_key_exists($uid, $users)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $users[$uid];
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'user' => $user,
|
||||||
|
'page_size' => $this->page_size,
|
||||||
|
'post' => $post
|
||||||
|
);
|
||||||
|
$this->view('template/post', $data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string,mixed>
|
* @return array<string,mixed>
|
||||||
*/
|
*/
|
||||||
|
@ -30,28 +63,23 @@ class Post_controller extends Controller {
|
||||||
$offset = $page * $this->page_size;
|
$offset = $page * $this->page_size;
|
||||||
|
|
||||||
$user = $this->main->user();
|
$user = $this->main->user();
|
||||||
|
$uid = isset($user) ? $user['id'] : NULL;
|
||||||
|
|
||||||
$query = $this->db;
|
$query = $this->db;
|
||||||
|
|
||||||
if ($user) {
|
$query = $this->db
|
||||||
$query = $query->select('p.*, l.post_id IS NOT NULL as liked');
|
->select('p.*, l.id as like_id')
|
||||||
} else {
|
->from('api.post p')
|
||||||
$query = $query->select('p.*, FALSE as liked');
|
->join('api.like l', 'p.id = l.post_id AND l.user_id')
|
||||||
}
|
->eq($uid);
|
||||||
|
|
||||||
$query = $query->from('api.post p');
|
|
||||||
|
|
||||||
if ($user) {
|
|
||||||
$query = $query->join('admin.like l', 'p.id = l.post_id AND l.user_id')
|
|
||||||
->eq($user['id']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($max) {
|
if ($max) {
|
||||||
$query = $query
|
$query = $query
|
||||||
->where('id')->le($max);
|
->where('p.id')->le($max);
|
||||||
}
|
}
|
||||||
|
|
||||||
$posts = $query
|
$posts = $query
|
||||||
|
->order_by('p.id', 'DESC')
|
||||||
->limit($this->page_size)
|
->limit($this->page_size)
|
||||||
->offset($offset)
|
->offset($offset)
|
||||||
->rows();
|
->rows();
|
||||||
|
@ -73,7 +101,6 @@ class Post_controller extends Controller {
|
||||||
->from('api.post p')
|
->from('api.post p')
|
||||||
->row()['pc'];
|
->row()['pc'];
|
||||||
|
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
'loaded' => count($posts),
|
'loaded' => count($posts),
|
||||||
'total' => $pc,
|
'total' => $pc,
|
||||||
|
@ -82,6 +109,36 @@ class Post_controller extends Controller {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function comment(): void {
|
||||||
|
$cid = $this->request_model->get_int('id', 0);
|
||||||
|
|
||||||
|
$comment = $this->db
|
||||||
|
->select('*')
|
||||||
|
->from('api.comment')
|
||||||
|
->where('id')
|
||||||
|
->eq($cid)
|
||||||
|
->row();
|
||||||
|
|
||||||
|
if (!$comment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$users = $this->cache_model->get_users([$comment]);
|
||||||
|
$uid = $comment['user_id'];
|
||||||
|
|
||||||
|
if (!array_key_exists($uid, $users)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $users[$uid];
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'user' => $user,
|
||||||
|
'comment' => $comment
|
||||||
|
);
|
||||||
|
$this->view('template/comment', $data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string,mixed>
|
* @return array<string,mixed>
|
||||||
*/
|
*/
|
||||||
|
@ -105,6 +162,7 @@ class Post_controller extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
$comments = $query
|
$comments = $query
|
||||||
|
->order_by('id', 'ASC')
|
||||||
->limit($this->page_size)
|
->limit($this->page_size)
|
||||||
->offset($offset)
|
->offset($offset)
|
||||||
->rows();
|
->rows();
|
||||||
|
@ -112,6 +170,17 @@ class Post_controller extends Controller {
|
||||||
$users = $this->cache_model->get_users($comments);
|
$users = $this->cache_model->get_users($comments);
|
||||||
$max = 0;
|
$max = 0;
|
||||||
|
|
||||||
|
// only add this hr when not logged in
|
||||||
|
// otherwise its added automatically by
|
||||||
|
// the like and comment buttons
|
||||||
|
if (
|
||||||
|
count($comments) &&
|
||||||
|
$page == 0 &&
|
||||||
|
$this->main->session === NULL
|
||||||
|
) {
|
||||||
|
echo '<hr>';
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($comments as $comment) {
|
foreach ($comments as $comment) {
|
||||||
$max = max($max, $comment['id']);
|
$max = max($max, $comment['id']);
|
||||||
$data = array();
|
$data = array();
|
56
src/web/_controller/apps/auth.php
Normal file
56
src/web/_controller/apps/auth.php
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<?php /* Copyright (c) 2024 Freya Murphy */
|
||||||
|
class Auth_controller extends Controller {
|
||||||
|
|
||||||
|
// the home model
|
||||||
|
private $auth_model;
|
||||||
|
|
||||||
|
// the post controller
|
||||||
|
protected $post_controller;
|
||||||
|
|
||||||
|
function __construct($load) {
|
||||||
|
parent::__construct($load);
|
||||||
|
$this->auth_model = $this->load->model('apps/auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(): void {
|
||||||
|
if ($this->main->session) {
|
||||||
|
$this->redirect('/home');
|
||||||
|
} else {
|
||||||
|
$this->redirect('/auth/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function login(): void {
|
||||||
|
if ($this->main->session) {
|
||||||
|
$this->redirect('/home');
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::index();
|
||||||
|
$data = $this->auth_model->get_data();
|
||||||
|
$this->view('header_empty', $data);
|
||||||
|
$this->view('apps/auth/login', $data);
|
||||||
|
$this->view('footer', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logout(): void {
|
||||||
|
if ($this->main->session) {
|
||||||
|
$_SESSION['jwt'] = NULL;
|
||||||
|
}
|
||||||
|
$this->redirect('/auth/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(): void {
|
||||||
|
if (!$this->is_ajax()) {
|
||||||
|
$this->error(400);
|
||||||
|
}
|
||||||
|
if (!isset($_POST['key']) || !isset($_POST['value'])) {
|
||||||
|
$this->error(400);
|
||||||
|
}
|
||||||
|
$key = $_POST['key'];
|
||||||
|
$value = $_POST['value'];
|
||||||
|
$_SESSION[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
|
@ -8,11 +8,12 @@ class Error_controller extends Controller {
|
||||||
$this->error_model = $this->load->model('apps/error');
|
$this->error_model = $this->load->model('apps/error');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index() {
|
public function index(): void {
|
||||||
parent::index();
|
parent::index();
|
||||||
$data = $this->error_model->get_data();
|
$data = $this->error_model->get_data();
|
||||||
$this->view('header', $data);
|
$this->view('header', $data);
|
||||||
$this->view('apps/error/main', $data);
|
$this->view('apps/error/main', $data);
|
||||||
|
$this->view('footer', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -18,6 +18,7 @@ class Home_controller extends Controller {
|
||||||
$data = $this->home_model->get_data();
|
$data = $this->home_model->get_data();
|
||||||
$this->view('header', $data);
|
$this->view('header', $data);
|
||||||
$this->view('apps/home/main', $data);
|
$this->view('apps/home/main', $data);
|
||||||
|
$this->view('footer', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
41
src/web/_controller/apps/people.php
Normal file
41
src/web/_controller/apps/people.php
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<?php /* Copyright (c) 2024 Freya Murphy */
|
||||||
|
class People_controller extends Controller {
|
||||||
|
|
||||||
|
// the people model
|
||||||
|
private $people_model;
|
||||||
|
|
||||||
|
// format model
|
||||||
|
protected $format_model;
|
||||||
|
|
||||||
|
function __construct($load) {
|
||||||
|
parent::__construct($load);
|
||||||
|
$this->people_model = $this->load->model('apps/people');
|
||||||
|
$this->format_model = $this->load->model('format');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(): void {
|
||||||
|
parent::index();
|
||||||
|
$data = $this->people_model->get_data();
|
||||||
|
$this->view('header', $data);
|
||||||
|
$this->view('apps/people/main', $data);
|
||||||
|
$this->view('footer', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
public function people(): array {
|
||||||
|
$data = $this->people_model->get_users();
|
||||||
|
|
||||||
|
$this->view('apps/people/people', $data);
|
||||||
|
|
||||||
|
$max = 0;
|
||||||
|
foreach ($data['users'] as $user) {
|
||||||
|
$max = max($max, $user['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
|
@ -20,6 +20,14 @@ class Modal_controller extends Controller {
|
||||||
public function new_post(): void {
|
public function new_post(): void {
|
||||||
$this->modal('new_post');
|
$this->modal('new_post');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function register(): void {
|
||||||
|
$this->load->app_lang(
|
||||||
|
$this->main->info['lang'],
|
||||||
|
'auth'
|
||||||
|
);
|
||||||
|
$this->modal('register');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
13
src/web/_model/apps/auth.php
Normal file
13
src/web/_model/apps/auth.php
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?php /* Copyright (c) 2024 Freya Murphy */
|
||||||
|
class Auth_model extends Model {
|
||||||
|
|
||||||
|
function __construct($load) {
|
||||||
|
parent::__construct($load);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_data(): array {
|
||||||
|
$data = parent::get_data();
|
||||||
|
$data['title'] = lang('login');
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,22 +7,18 @@ class Error_model extends Model {
|
||||||
|
|
||||||
private function get_msg(&$data) {
|
private function get_msg(&$data) {
|
||||||
if (!array_key_exists('code', $_GET)) {
|
if (!array_key_exists('code', $_GET)) {
|
||||||
|
http_response_code(500);
|
||||||
$data['msg'] = lang('error');
|
$data['msg'] = lang('error');
|
||||||
$data['title'] = '500';
|
$data['title'] = '500';
|
||||||
} else {
|
} else {
|
||||||
$code = $_GET['code'];
|
$code = $_GET['code'];
|
||||||
|
http_response_code($code);
|
||||||
$data['title'] = $code;
|
$data['title'] = $code;
|
||||||
switch ($code) {
|
$msg = lang('error_' . $code, FALSE);
|
||||||
case '404':
|
if (!$msg) {
|
||||||
$data['msg'] = lang('error_404');
|
$msg = lang('error');
|
||||||
break;
|
|
||||||
case '500':
|
|
||||||
$data['msg'] = lang('error_500');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$data['msg'] = lang('error');
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
$data['msg'] = $msg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
88
src/web/_model/apps/people.php
Normal file
88
src/web/_model/apps/people.php
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<?php /* Copyright (c) 2024 Freya Murphy */
|
||||||
|
class People_model extends Model {
|
||||||
|
|
||||||
|
private $request_model;
|
||||||
|
|
||||||
|
function __construct($load) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
$query = $this->db
|
||||||
|
->select($select)
|
||||||
|
->from('api.user u');
|
||||||
|
|
||||||
|
if ($filter_username) {
|
||||||
|
$query = $query
|
||||||
|
->where('u.username')
|
||||||
|
->like('%' . $filter_username . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
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 . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($max) {
|
||||||
|
$query = $query
|
||||||
|
->where('u.id')
|
||||||
|
->le($max);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
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('*')
|
||||||
|
->order_by('u.id', 'DESC')
|
||||||
|
->offset($offset)
|
||||||
|
->limit($page_size)
|
||||||
|
->rows();
|
||||||
|
|
||||||
|
$count = $this->get_filted_query('COUNT(u.id) AS count')
|
||||||
|
->row()['count'];
|
||||||
|
|
||||||
|
$max = 0;
|
||||||
|
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$max = max($max, $user['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'users' => $users,
|
||||||
|
'count' => $count,
|
||||||
|
'page_size' => $page_size,
|
||||||
|
'max_id' => $max
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_data(): array {
|
||||||
|
$data = parent::get_data();
|
||||||
|
$data['title'] = lang('title');
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,7 +39,8 @@ class Format_model extends Model {
|
||||||
* @returns the formatted date
|
* @returns the formatted date
|
||||||
*/
|
*/
|
||||||
public function date($date) {
|
public function date($date) {
|
||||||
return $date;
|
$date=date_create($date);
|
||||||
|
return date_format($date, "Y-m-d H:i");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -53,7 +53,7 @@ class Main_model {
|
||||||
*/
|
*/
|
||||||
private function asset_stamp($path): int {
|
private function asset_stamp($path): int {
|
||||||
$root = $GLOBALS['webroot'];
|
$root = $GLOBALS['webroot'];
|
||||||
$path = $root . '/public/' . $path;
|
$path = $root . '/../public/' . $path;
|
||||||
return filemtime($path);
|
return filemtime($path);
|
||||||
}
|
}
|
||||||
|
|
86
src/web/_views/apps/auth/login.php
Normal file
86
src/web/_views/apps/auth/login.php
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
|
||||||
|
<?php /* vi: syntax=php */ ?>
|
||||||
|
<div id="main-content">
|
||||||
|
<div class="branding col">
|
||||||
|
<h1>xssbook</h1>
|
||||||
|
<span><?=lang('login_branding')?></span>
|
||||||
|
</div>
|
||||||
|
<div class="form card col">
|
||||||
|
<form id="action-login" class="col" action="">
|
||||||
|
<div class="rel mb">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
id="login-username"
|
||||||
|
placeholder=" "
|
||||||
|
>
|
||||||
|
<label for="username">
|
||||||
|
<?=lang('ph_username')?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="rel mb">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
id="login-password"
|
||||||
|
placeholder=" "
|
||||||
|
>
|
||||||
|
<label for="password">
|
||||||
|
<?=lang('ph_password')?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<?=ilang('action_login',
|
||||||
|
class: 'btn btn-submit btn-wide',
|
||||||
|
button: TRUE,
|
||||||
|
attrs: array('type' => 'submit')
|
||||||
|
)?>
|
||||||
|
<?=ilang('action_forgot_passwd',
|
||||||
|
class: 'btn btn-line btn-blue btn-wide mt'
|
||||||
|
)?>
|
||||||
|
</form>
|
||||||
|
<hr>
|
||||||
|
<?=ilang('action_create_account',
|
||||||
|
id: 'action-register',
|
||||||
|
class: 'btn btn-success btn-wide',
|
||||||
|
button: TRUE,
|
||||||
|
attrs: array('type' => 'submit')
|
||||||
|
)?>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
var onLogin = function(data) {
|
||||||
|
let jwt = data.token;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/auth/update',
|
||||||
|
method: 'POST',
|
||||||
|
data: JSON.stringify({
|
||||||
|
key: 'jwt',
|
||||||
|
value: jwt
|
||||||
|
}),
|
||||||
|
success: function (_) {
|
||||||
|
window.location = '/home';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
$('#action-login').on('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
let username = $('#login-username').val();
|
||||||
|
let password = $('#login-password').val();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/rpc/login',
|
||||||
|
method: 'POST',
|
||||||
|
data: JSON.stringify({ username, password }),
|
||||||
|
success: onLogin
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#action-register').on('click', function() {
|
||||||
|
$.get( "/modal/register", function (data) {
|
||||||
|
$(document.body).append(data);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</div>
|
|
@ -1,6 +1,6 @@
|
||||||
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
|
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
|
||||||
<?php /* vi: syntax=php */ ?>
|
<?php /* vi: syntax=php */ ?>
|
||||||
<div id="error">
|
<div id="main-content">
|
||||||
<h1><?=$title?></h1>
|
<h1><?=$title?></h1>
|
||||||
<span><?=$msg?></span>
|
<span><?=$msg?></span>
|
||||||
</div>
|
</div>
|
|
@ -7,7 +7,7 @@
|
||||||
<?php $this->view('template/pfp', array('user' => $self))?>
|
<?php $this->view('template/pfp', array('user' => $self))?>
|
||||||
<a
|
<a
|
||||||
id="action-new-post"
|
id="action-new-post"
|
||||||
class="input btn-fake ml"
|
class="btn btn-alt btn-wide ml"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
aria-label="<?=lang('action_new_post_tip')?>"
|
aria-label="<?=lang('action_new_post_tip')?>"
|
||||||
>
|
>
|
35
src/web/_views/apps/people/card.php
Normal file
35
src/web/_views/apps/people/card.php
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
|
||||||
|
<?php /* vi: syntax=php */ ?>
|
||||||
|
<a
|
||||||
|
class="card profile"
|
||||||
|
href="/profile?id=<?=$user['id']?>"
|
||||||
|
>
|
||||||
|
<div class="row">
|
||||||
|
<?php $this->view('template/pfp', array('user' => $user, 'link' => 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>
|
||||||
|
</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>
|
||||||
|
<?
|
67
src/web/_views/apps/people/main.php
Normal file
67
src/web/_views/apps/people/main.php
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
<?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 mt 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>
|
7
src/web/_views/apps/people/people.php
Normal file
7
src/web/_views/apps/people/people.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
|
||||||
|
<?php /* vi: syntax=php */ ?>
|
||||||
|
<?php
|
||||||
|
foreach($users as $user) {
|
||||||
|
$this->view('apps/people/card', array('user' => $user));
|
||||||
|
}
|
||||||
|
?>
|
|
@ -1,4 +1,8 @@
|
||||||
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
|
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
|
||||||
<?php /* vi: syntax=php */ ?>
|
<?php /* vi: syntax=php */ ?>
|
||||||
|
<footer>
|
||||||
|
Freya Murphy © 2023 | <a href="https://freya.cat">freya.cat</a>
|
||||||
|
</footer>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -2,28 +2,8 @@
|
||||||
<?php /* vi: syntax=php */ ?>
|
<?php /* vi: syntax=php */ ?>
|
||||||
<?php
|
<?php
|
||||||
$self = $this->main->user();
|
$self = $this->main->user();
|
||||||
|
$this->view('header_empty', $data);
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<script>
|
|
||||||
<?php if ($this->main->session): ?>
|
|
||||||
var jwtStr = <?=json_encode($this->main->session['jwt'])?>;
|
|
||||||
<?php else: ?>
|
|
||||||
var jwtStr = null;
|
|
||||||
<?php endif; ?>
|
|
||||||
</script>
|
|
||||||
<?php
|
|
||||||
foreach ($js_files as $js) {
|
|
||||||
echo $this->main->link_js($js);
|
|
||||||
}
|
|
||||||
foreach ($css_files as $css) {
|
|
||||||
echo $this->main->link_css($css);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<title><?=$title?></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header class="nav">
|
<header class="nav">
|
||||||
<div class="nav-left">
|
<div class="nav-left">
|
||||||
<span class="logo">xssbook</span>
|
<span class="logo">xssbook</span>
|
||||||
|
@ -31,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="header-entry btn btn-hover btn-action btn-blue"
|
class="btn"
|
||||||
href="/home"
|
href="/home"
|
||||||
title="<?=lang('action_home_tip')?>"
|
title="<?=lang('action_home_tip')?>"
|
||||||
>
|
>
|
||||||
|
@ -40,7 +20,7 @@
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
id="action-people"
|
id="action-people"
|
||||||
class="header-entry btn btn-hover btn-action btn-blue"
|
class="btn"
|
||||||
href="/people"
|
href="/people"
|
||||||
title="<?=lang('action_people_tip')?>"
|
title="<?=lang('action_people_tip')?>"
|
||||||
>
|
>
|
||||||
|
@ -49,7 +29,7 @@
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
id="action-chat"
|
id="action-chat"
|
||||||
class="header-entry btn btn-hover btn-action btn-blue"
|
class="btn"
|
||||||
href="/chat"
|
href="/chat"
|
||||||
title="<?=lang('action_chat_tip')?>"
|
title="<?=lang('action_chat_tip')?>"
|
||||||
>
|
>
|
||||||
|
@ -70,7 +50,7 @@
|
||||||
'class' => 'pfp-sm ml',
|
'class' => 'pfp-sm ml',
|
||||||
)); ?>
|
)); ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?=ilang('action_login', class: 'btn btn-action', href: '/auth/login')?>
|
<?=ilang('action_login', class: 'btn', href: '/auth/login')?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
@ -80,5 +60,3 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</header>
|
</header>
|
||||||
<div id="toast-container">
|
|
||||||
</div>
|
|
23
src/web/_views/header_empty.php
Normal file
23
src/web/_views/header_empty.php
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script>
|
||||||
|
<?php if ($this->main->session): ?>
|
||||||
|
var jwtStr = <?=json_encode($this->main->session['jwt'])?>;
|
||||||
|
<?php else: ?>
|
||||||
|
var jwtStr = null;
|
||||||
|
<?php endif; ?>
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
foreach ($js_files as $js) {
|
||||||
|
echo $this->main->link_js($js);
|
||||||
|
}
|
||||||
|
foreach ($css_files as $css) {
|
||||||
|
echo $this->main->link_css($css);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<title><?=$title?></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="toast-container">
|
||||||
|
</div>
|
|
@ -22,7 +22,7 @@
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<?=ilang('action_submit',
|
<?=ilang('action_submit',
|
||||||
id: 'new-post-submit',
|
id: 'new-post-submit',
|
||||||
class: 'btn-action',
|
class: 'btn btn-wide btn-submit',
|
||||||
attrs: array('type' => 'submit'),
|
attrs: array('type' => 'submit'),
|
||||||
button: TRUE
|
button: TRUE
|
||||||
)?>
|
)?>
|
||||||
|
@ -32,14 +32,28 @@
|
||||||
$('#new-post-form').submit(function(e) {
|
$('#new-post-form').submit(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let content = $('#new-post-content').val();
|
let content = $('#new-post-content').val();
|
||||||
|
let me = $(this);
|
||||||
|
|
||||||
|
const getPost = function(data) {
|
||||||
|
if (data) {
|
||||||
|
$('#post-container').prepend(data);
|
||||||
|
}
|
||||||
|
me.closest('.modal-container').remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPost = function(data) {
|
||||||
|
let id = data[0].id;
|
||||||
|
$.get({
|
||||||
|
url: '/_util/post/post?id=' + id,
|
||||||
|
success: getPost
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/api/post',
|
url: '/api/post',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: JSON.stringify({ content }),
|
data: JSON.stringify({ content }),
|
||||||
success: function(data) {
|
success: onPost
|
||||||
window.location.reload();
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
173
src/web/_views/modal/register.php
Normal file
173
src/web/_views/modal/register.php
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
|
||||||
|
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
|
||||||
|
<?php /* vi: syntax=php */ ?>
|
||||||
|
<form id="register-form">
|
||||||
|
<div class="modal-content register-modal col">
|
||||||
|
<label class="static">
|
||||||
|
<?=lang('ph_basic_info')?>
|
||||||
|
</label>
|
||||||
|
<div class="row mt">
|
||||||
|
<div class="rel btn-wide">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="first_name"
|
||||||
|
id="register-first-name"
|
||||||
|
placeholder=" "
|
||||||
|
>
|
||||||
|
<label for="first_name">
|
||||||
|
<?=lang('ph_first_name')?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="rel ml btn-wide">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="last_name"
|
||||||
|
id="register-last-name"
|
||||||
|
placeholder=" "
|
||||||
|
>
|
||||||
|
<label for="last_name">
|
||||||
|
<?=lang('ph_last_name')?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rel mt">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
id="register-username"
|
||||||
|
placeholder=" "
|
||||||
|
>
|
||||||
|
<label for="username">
|
||||||
|
<?=lang('ph_username')?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="rel mt">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
id="register-password"
|
||||||
|
placeholder=" "
|
||||||
|
>
|
||||||
|
<label for="password">
|
||||||
|
<?=lang('ph_password')?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="rel mt">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="email"
|
||||||
|
id="register-email"
|
||||||
|
placeholder=" "
|
||||||
|
>
|
||||||
|
<label for="email">
|
||||||
|
<?=lang('ph_email')?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label for="birth_date" class="mt static">
|
||||||
|
<?=lang('ph_birth_date')?>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
class="mt"
|
||||||
|
type="date"
|
||||||
|
name="birth_date"
|
||||||
|
id="register-birth-date"
|
||||||
|
>
|
||||||
|
<label for="gender" class="mt static">
|
||||||
|
<?=lang('ph_gender')?>
|
||||||
|
</label>
|
||||||
|
<div class="row mt" data-type="radio" data-name="gender-wrapper">
|
||||||
|
<div class="rel radio mr">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="register-gender-male"
|
||||||
|
name="gender"
|
||||||
|
value="male"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
for="register-gender-male"
|
||||||
|
class="static"
|
||||||
|
>
|
||||||
|
<?=lang('ph_gender_male')?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="rel radio mr">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="register-gender-female"
|
||||||
|
name="gender"
|
||||||
|
value="female"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
for="register-gender-female"
|
||||||
|
class="static"
|
||||||
|
>
|
||||||
|
<?=lang('ph_gender_female')?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="rel radio">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="register-gender-lettuce"
|
||||||
|
name="gender"
|
||||||
|
value="lettuce"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
for="register-gender-lettuce"
|
||||||
|
class="static"
|
||||||
|
>
|
||||||
|
<?=lang('ph_gender_lettuce')?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<?=ilang('action_register',
|
||||||
|
id: 'register-submit',
|
||||||
|
class: 'btn btn-wide btn-success',
|
||||||
|
attrs: array('type' => 'submit'),
|
||||||
|
button: TRUE
|
||||||
|
)?>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<script>
|
||||||
|
$('#register-form').submit(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const form = event.target;
|
||||||
|
const formFields = form.elements;
|
||||||
|
|
||||||
|
let first_name = formFields.first_name.value.trim();
|
||||||
|
let last_name = formFields.last_name.value.trim();
|
||||||
|
let username = formFields.username.value.trim();
|
||||||
|
let password = formFields.password.value.trim();
|
||||||
|
let email = formFields.email.value.trim();
|
||||||
|
let birth_date = formFields.birth_date.value.trim();
|
||||||
|
let gender = formFields.gender.value.trim();
|
||||||
|
|
||||||
|
if(birth_date === '') {
|
||||||
|
errorToast('toast_date_empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSuccess = () => {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/rpc/login',
|
||||||
|
method: 'POST',
|
||||||
|
data: JSON.stringify({
|
||||||
|
username, password
|
||||||
|
}),
|
||||||
|
success: onLogin
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/user',
|
||||||
|
method: 'POST',
|
||||||
|
data: JSON.stringify({
|
||||||
|
first_name, last_name, username, password,
|
||||||
|
email, birth_date, gender
|
||||||
|
}),
|
||||||
|
success: onSuccess
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -8,7 +8,7 @@
|
||||||
<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>
|
||||||
<span class="dim ml"><?=$format_model->date($comment['date'])?></span>
|
<span class="dim ml"><?=$format_model->date($comment['created'])?></span>
|
||||||
</div>
|
</div>
|
||||||
<?=$comment['content']?>
|
<?=$comment['content']?>
|
||||||
</div>
|
</div>
|
17
src/web/_views/template/pfp.php
Normal file
17
src/web/_views/template/pfp.php
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?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; ?>
|
|
@ -5,7 +5,7 @@
|
||||||
<?php $this->view('template/pfp', array('user' => $user))?>
|
<?php $this->view('template/pfp', array('user' => $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['date']?></span>
|
<span class="dim"><?=$post['created']?></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
|
@ -13,20 +13,28 @@
|
||||||
</p>
|
</p>
|
||||||
<?php
|
<?php
|
||||||
$self = $this->main->user();
|
$self = $this->main->user();
|
||||||
|
$liked = $post['like_id'] ? 'btn-blue' : '';
|
||||||
|
$post_attrs = array(
|
||||||
|
'postId' => $post['id']
|
||||||
|
);
|
||||||
|
if ($post['like_id'] !== NULL) {
|
||||||
|
$post_attrs['likeId'] = $post['like_id'];
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<?php if ($self): ?>
|
<?php if ($self): ?>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<?=ilang('action_like', class: 'grow btn btn-hover btn-action')?>
|
<?=ilang('action_like',
|
||||||
<?=ilang('action_comment', class: 'grow btn btn-hover btn-action action-comment',
|
class: 'btn btn-wide action-like ' . $liked,
|
||||||
click: '$(\'#new-comment-' . $post['id'] . '\').focus()'
|
attrs: $post_attrs
|
||||||
|
)?>
|
||||||
|
<?=ilang('action_comment', class: 'btn btn-wide action-comment',
|
||||||
|
click: '$(\'#action-new-comment-' . $post['id'] . '\').focus()'
|
||||||
)?>
|
)?>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<?php else: ?>
|
|
||||||
<hr>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<div class="col comments">
|
<div class="col comments pb">
|
||||||
<?php
|
<?php
|
||||||
$_GET = array('id' => $post['id']);
|
$_GET = array('id' => $post['id']);
|
||||||
$cdata = $this->comments();
|
$cdata = $this->comments();
|
||||||
|
@ -52,16 +60,17 @@
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
<?php if ($self): ?>
|
<?php if ($self): ?>
|
||||||
<div class="row grow mt">
|
<div class="row pb">
|
||||||
<?php $this->view('template/pfp', array('user' => $user))?>
|
<?php $this->view('template/pfp', array('user' => $self))?>
|
||||||
<form class="ml action-new-comment-form">
|
<form class="ml action-new-comment-form row">
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
name="id"
|
name="id"
|
||||||
value="<?=$post['id']?>"
|
value="<?=$post['id']?>"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="action-new-comment input"
|
id="action-new-comment-<?=$post['id']?>"
|
||||||
|
class="action-new-comment btn btn-wide btn-alt"
|
||||||
postId="<?=$post['id']?>"
|
postId="<?=$post['id']?>"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -73,3 +82,5 @@
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
if ($loaded >= $page_size && $page_size < $total) {
|
if ($loaded >= $page_size && $page_size < $total) {
|
||||||
ilang('action_load_posts',
|
ilang('action_load_posts',
|
||||||
id: 'action-load-posts',
|
id: 'action-load-posts',
|
||||||
class: 'btn btn-line mb',
|
class: 'btn btn-line btn-wide mb',
|
||||||
attrs: array(
|
attrs: array(
|
||||||
'loaded' => $loaded,
|
'loaded' => $loaded,
|
||||||
'pageSize' => $page_size,
|
'pageSize' => $page_size,
|
|
@ -11,9 +11,16 @@
|
||||||
array_push($params, $hint);
|
array_push($params, $hint);
|
||||||
}
|
}
|
||||||
|
|
||||||
$msg = lang($msg, sub: $params);
|
$lang_msg = lang($msg, FALSE, sub: $params);
|
||||||
|
|
||||||
|
if(!$lang_msg) {
|
||||||
|
$lang_msg = $msg;
|
||||||
|
} else {
|
||||||
|
$lang_msg = ucfirst($lang_msg);
|
||||||
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<div class="toast error">
|
<div class="toast error">
|
||||||
<?=ucfirst($msg)?>
|
<?=$lang_msg?>
|
||||||
<?=ilang('action_close', class: 'action-close-toast')?>
|
<?=ilang('action_close', class: 'action-close-toast')?>
|
||||||
</div>
|
</div>
|
|
@ -30,6 +30,16 @@ class Aesthetic {
|
||||||
'css/post.css'
|
'css/post.css'
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
'auth' => array(
|
||||||
|
'css' => [
|
||||||
|
'css/auth.css'
|
||||||
|
],
|
||||||
|
),
|
||||||
|
'people' => array(
|
||||||
|
'css' => [
|
||||||
|
'css/people.css'
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
|
@ -3,5 +3,7 @@
|
||||||
$routes = array();
|
$routes = array();
|
||||||
$routes['home'] = 'apps/home';
|
$routes['home'] = 'apps/home';
|
||||||
$routes['error'] = 'apps/error';
|
$routes['error'] = 'apps/error';
|
||||||
|
$routes['auth'] = 'apps/auth';
|
||||||
|
$routes['people'] = 'apps/people';
|
||||||
|
|
||||||
$routes[''] = '_index';
|
$routes[''] = '_index';
|
|
@ -45,5 +45,20 @@ abstract class Controller {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function is_ajax(): bool {
|
||||||
|
$_POST = json_decode(
|
||||||
|
file_get_contents("php://input"), true
|
||||||
|
);
|
||||||
|
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function error($code): void {
|
||||||
|
$_GET['code'] = $code;
|
||||||
|
$this->main->info['app'] = 'error';
|
||||||
|
$error_controller = $this->load->controller('apps/error');
|
||||||
|
$error_controller->index();
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
|
@ -46,6 +46,8 @@ class DatabaseQuery {
|
||||||
if (!$this->where) {
|
if (!$this->where) {
|
||||||
$this->where = TRUE;
|
$this->where = TRUE;
|
||||||
$this->query .= "WHERE ";
|
$this->query .= "WHERE ";
|
||||||
|
} else {
|
||||||
|
$this->query .= "AND ";
|
||||||
}
|
}
|
||||||
$this->query .= "$cond ";
|
$this->query .= "$cond ";
|
||||||
return $this;
|
return $this;
|
||||||
|
@ -85,6 +87,8 @@ class DatabaseQuery {
|
||||||
if (!$this->where) {
|
if (!$this->where) {
|
||||||
$this->where = TRUE;
|
$this->where = TRUE;
|
||||||
$this->query .= "WHERE ";
|
$this->query .= "WHERE ";
|
||||||
|
} else {
|
||||||
|
$this->query .= "AND ";
|
||||||
}
|
}
|
||||||
if (empty($array)) {
|
if (empty($array)) {
|
||||||
$this->query .= "FALSE\n";
|
$this->query .= "FALSE\n";
|
||||||
|
@ -95,16 +99,6 @@ class DatabaseQuery {
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function and() {
|
|
||||||
$this->query .= "AND ";
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function or() {
|
|
||||||
$this->query .= "OR ";
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function join($table, $on, $type = 'LEFT') {
|
public function join($table, $on, $type = 'LEFT') {
|
||||||
$this->query .= "$type JOIN $table ON $on\n";
|
$this->query .= "$type JOIN $table ON $on\n";
|
||||||
return $this;
|
return $this;
|
||||||
|
@ -122,6 +116,11 @@ class DatabaseQuery {
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function order_by($column, $order = 'ASC') {
|
||||||
|
$this->query .= "ORDER BY " . $column . ' ' . $order . ' ';
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function rows() {
|
public function rows() {
|
||||||
$stmt = $this->conn->prepare($this->query);
|
$stmt = $this->conn->prepare($this->query);
|
||||||
try {
|
try {
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue