This commit is contained in:
Freya Murphy 2024-03-30 12:14:42 -04:00
parent 944b6b0526
commit 1f04b83be3
Signed by: freya
GPG key ID: 744AB800E383AE52
50 changed files with 965 additions and 491 deletions

View file

@ -5,7 +5,8 @@ CREATE VIEW api.post AS
p.content, p.content,
p.date p.date
FROM FROM
admin.post p; admin.post p
ORDER BY id DESC;
GRANT SELECT ON TABLE api.post GRANT SELECT ON TABLE api.post
TO rest_anon, rest_user; TO rest_anon, rest_user;

View file

@ -32,6 +32,8 @@ GRANT INSERT ON TABLE api.post
TO rest_user; TO rest_user;
GRANT INSERT ON TABLE admin.post GRANT INSERT ON TABLE admin.post
TO rest_user; TO rest_user;
GRANT UPDATE ON TABLE sys.post_id_seq
TO rest_user;
CREATE TRIGGER api_post_insert_trgr CREATE TRIGGER api_post_insert_trgr
INSTEAD OF INSERT INSTEAD OF INSERT

View file

@ -29,7 +29,7 @@ BEGIN
IF _min IS NOT NULL AND _length < _min THEN IF _min IS NOT NULL AND _length < _min THEN
PERFORM _api.raise( PERFORM _api.raise(
_msg => 'api_text_min', _msg => 'api_min_value',
_detail => _column, _detail => _column,
_hint => _min || '' _hint => _min || ''
); );
@ -37,7 +37,7 @@ BEGIN
IF _max IS NOT NULL AND _length > _max THEN IF _max IS NOT NULL AND _length > _max THEN
PERFORM _api.raise( PERFORM _api.raise(
_msg => 'api_text_max', _msg => 'api_max_value',
_detail => _column, _detail => _column,
_hint => _max || '' _hint => _max || ''
); );

View file

@ -0,0 +1,23 @@
<?php /* Copyright (c) 2024 Freya Murphy */
class _index_controller extends Controller {
// the home model
private $home_model;
// the request model
private $request_model;
// the caceh model
private $cache_model;
public function index(): void {
if ($this->main->session) {
$this->redirect('/home');
} else {
$this->redirect('/login');
}
}
}
?>

View file

@ -0,0 +1,20 @@
<?php /* Copyright (c) 2024 Freya Murphy */
class Error_controller extends Controller {
private $error_model;
function __construct($load) {
parent::__construct($load);
$this->error_model = $this->load->model('apps/error');
}
public function index() {
parent::index();
$data = $this->error_model->get_data();
$this->view('header', $data);
$this->view('apps/error/main', $data);
}
}
?>

View file

@ -1,22 +1,31 @@
<?php /* Copyright (c) 2024 Freya Murphy */ <?php /* Copyright (c) 2024 Freya Murphy */
class HomeController extends Controller { class Home_controller extends Controller {
private $model; // the home model
private $home_model;
function __construct($model) { // the request model
parent::__construct(); private $request_model;
$this->model = $model;
// the caceh model
private $cache_model;
function __construct($load) {
parent::__construct($load);
$this->home_model = $this->load->model('apps/home');
$this->request_model = $this->load->model('request');
$this->cache_model = $this->load->model('cache');
} }
public function index() { public function index(): void {
parent::index(); parent::index();
$data = $this->model->get_data(); $data = $this->home_model->get_data();
$this->view('header', $data); $this->view('header', $data);
$this->app_view('main', $data); $this->view('apps/home/main', $data);
} }
public function posts() { public function posts(): void {
$page = $this->main->get_num('page', 0); $page = $this->request_model->get_int('page', 0);
$page_size = 20; $page_size = 20;
$offset = $page * $page_size; $offset = $page * $page_size;
@ -30,7 +39,7 @@ class HomeController extends Controller {
$query = $query->select('p.*, FALSE as liked'); $query = $query->select('p.*, FALSE as liked');
} }
$query = $query->from('admin.post p'); $query = $query->from('api.post p');
if ($user) { if ($user) {
$query = $query->join('admin.like l', 'p.id = l.post_id') $query = $query->join('admin.like l', 'p.id = l.post_id')
@ -42,7 +51,7 @@ class HomeController extends Controller {
->offset($offset) ->offset($offset)
->rows(); ->rows();
$users = $this->main->get_users($posts); $users = $this->cache_model->get_users($posts);
foreach ($posts as $post) { foreach ($posts as $post) {
$data = array(); $data = array();
@ -52,9 +61,9 @@ class HomeController extends Controller {
} }
} }
public function comments() { public function comments(): void {
$page = $this->main->get_num('page', 0); $page = $this->request_model->get_int('page', 0);
$id = $this->main->get_num('id'); $id = $this->request_model->get_int('id');
$page_size = 20; $page_size = 20;
$offset = $page * $page_size; $offset = $page * $page_size;
@ -65,7 +74,7 @@ class HomeController extends Controller {
->offset($offset) ->offset($offset)
->rows(); ->rows();
$users = $this->main->get_users($comments); $users = $this->cache_model->get_users($comments);
foreach ($comments as $comment) { foreach ($comments as $comment) {
$data = array(); $data = array();
@ -75,10 +84,6 @@ class HomeController extends Controller {
} }
} }
public function new_post_modal() {
$this->modal(lang('new_post_modal_title'), 'new-post');
}
} }
?> ?>

26
web/_controller/modal.php Normal file
View file

@ -0,0 +1,26 @@
<?php /* Copyright (c) 2024 Freya Murphy */
class Modal_controller extends Controller {
function __construct($load) {
parent::__construct($load);
}
/**
* @param string $name
* @param array $data
*/
private function modal($name, $data = array()): void {
$title = lang($name . '_modal_title');
$data['title'] = $title;
$data['content'] = $name;
$this->view('template/modal', $data);
}
public function new_post(): void {
$this->modal('new_post');
}
}
?>

View file

@ -0,0 +1,22 @@
<?php /* Copyright (c) 2024 Freya Murphy */
class Template_controller extends Controller {
// the request model
private $request_model;
function __construct($load) {
parent::__construct($load);
$this->request_model = $this->load->model('request');
}
public function toast(): void {
$data = array(
'msg' => $this->request_model->get_str('msg', FALSE),
'detail' => $this->request_model->get_str('detail', FALSE),
'hint' => $this->request_model->get_str('hint', FALSE)
);
$this->view('template/toast', $data);
}
}

View file

@ -1,5 +1,9 @@
<?php /* Copyright (c) 2024 Freya Murphy */ <?php /* Copyright (c) 2024 Freya Murphy */
class ErrorModel extends Model { class Error_model extends Model {
function __construct($load) {
parent::__construct($load);
}
private function get_msg(&$data) { private function get_msg(&$data) {
if (!array_key_exists('code', $_GET)) { if (!array_key_exists('code', $_GET)) {
@ -22,7 +26,7 @@ class ErrorModel extends Model {
} }
} }
public function get_data() { public function get_data(): array {
$data = parent::get_data(); $data = parent::get_data();
$this->get_msg($data); $this->get_msg($data);
return $data; return $data;

View file

@ -1,7 +1,11 @@
<?php /* Copyright (c) 2024 Freya Murphy */ <?php /* Copyright (c) 2024 Freya Murphy */
class HomeModel extends Model { class Home_model extends Model {
private function get_posts() { function __construct($load) {
parent::__construct($load);
}
private function get_posts(): array {
return $this->db return $this->db
->select('*') ->select('*')
->from('admin.post') ->from('admin.post')
@ -9,11 +13,10 @@ class HomeModel extends Model {
->rows(); ->rows();
} }
public function get_data() { public function get_data(): array {
$data = parent::get_data(); $data = parent::get_data();
$data['title'] = lang('title'); $data['title'] = lang('title');
$data['posts'] = $this->get_posts(); $data['posts'] = $this->get_posts();
return $data; return $data;
} }
} }
?>

37
web/_model/cache.php Normal file
View file

@ -0,0 +1,37 @@
<?php /* Copyright (c) 2024 Freya Murphy */
class Cache_model extends Model {
// the user cache
private $users;
function __construct($load) {
parent::__construct($load);
$this->users = array();
}
/**
* Gets a array of users
*/
public function get_users($objs) {
$ids = array();
foreach ($objs as $obj) {
$id = $obj['user_id'];
if (!array_key_exists($id, $this->users)) {
array_push($ids, intval($id));
}
}
if (!empty($ids)) {
$result = $this->main->db
->select('*')
->from('api.user')
->where_in('id', $ids)
->rows();
foreach ($result as $user) {
$id = $user['id'];
$this->users[$id] = $user;
}
}
return $this->users;
}
}

45
web/_model/format.php Normal file
View file

@ -0,0 +1,45 @@
<?php /* Copyright (c) 2024 Freya Murphy */
class Format_model extends Modal {
function __construct($load) {
parent::__construct($load);
}
/**
* Formats a users's name
* @param array $user - the $user
* @returns the user's formatted display name
*/
public function name($user) {
$name = '';
if ($user['first_name']) {
$name .= $user['first_name'];
}
if ($user['middle_name']) {
if ($name != '') {
$name .= ' ';
}
$name .= $user['middle_name'];
}
if ($user['last_name']) {
if ($name != '') {
$name .= ' ';
}
$name .= $user['last_name'];
}
if ($name == '') {
$name = '@' . $user['username'];
}
return $name;
}
/**
* Formats a date
* @param string $date - the data in RFC3999 format
* @returns the formatted date
*/
public function date($date) {
return $date;
}
}

84
web/_model/main.php Normal file
View file

@ -0,0 +1,84 @@
<?php /* Copyright (c) 2024 Freya Murphy */
class Main_model {
// the website database
public $db;
// the current user session (can be NULL)
public $session;
// current loaded users
private $users;
// stores the current request info
public $info;
/**
* Loads the main model
* @param Loader $load - the main loader object
*/
function __construct($load) {
/// load the database helper
$this->db = new DatabaseHelper();
/// load the current session
if (array_key_exists('jwt', $_SESSION)) {
$this->get_session($_SESSION['jwt']);
} else {
$this->session = NULL;
};
/// init other vars
$this->users = array();
}
/**
* Loads current session
* @param string $jwt - the user provided JWT
*/
private function get_session($jwt) {
$query = $this->db
->select("_api.verify_jwt('" . $jwt . "') AS user_id;");
$result = $query->row();
$user_id = $result['user_id'];
if ($user_id) {
$this->session = array(
'id' => $user_id,
'jwt' => $jwt
);
}
}
/**
* Loads a css html link
* @param string $path - the path to the css file
*/
public function link_css($path) {
return '<link rel="stylesheet" href="/public/' . $path . '">';
}
/**
* Loads a js html link
* @param string $path - the path to the js file
*/
public function link_js($path) {
return '<script src="/public/'. $path . '"></script>';
}
/**
* Gets the current user
*/
public function user() {
if ($this->session) {
return $this->db
->select('*')
->from('api.user')
->where('id')
->eq($this->session['id'])
->row();
} else {
return NULL;
}
}
}
?>

40
web/_model/request.php Normal file
View file

@ -0,0 +1,40 @@
<?php /* Copyright (c) 2024 Freya Murphy */
class Request_model extends Model {
function __construct($load) {
parent::__construct($load);
}
/**
* Loads a string from the GET request
* @param string $key - the name for the query param
* @param string $default - the default value if not exists
*/
public function get_str($key, $default = NULL): string | NULL {
if (!array_key_exists($key, $_GET)) {
return $default;
} else {
return $_GET[$key];
}
}
/**
* Loads a number from the GET request
* @param string $key - the name for the query param
* @param int $default - the default value if not exists
*/
public function get_int($key, $default = NULL): int | NULL {
if (!array_key_exists($key, $_GET)) {
return $default;
} else {
$val = $_GET[$key];
$val = intval($val);
if ($val < 0) {
return 0;
} else {
return $val;
}
}
}
}

View file

@ -9,7 +9,7 @@ class Aesthetic {
'js' => [ 'js' => [
'js/jquery-3.7.1.min.js', 'js/jquery-3.7.1.min.js',
'js/lib.js', 'js/lib.js',
'js/modal.js', 'js/shared/modal.js',
], ],
'css' => [ 'css' => [
'css/common.css' 'css/common.css'
@ -22,7 +22,8 @@ class Aesthetic {
), ),
'home' => array( 'home' => array(
'js' => [ 'js' => [
'js/post.js', 'js/shared/post.js',
'js/routes/home.js',
], ],
'css' => [ 'css' => [
'css/home.css', 'css/home.css',

7
web/config/routes.php Normal file
View file

@ -0,0 +1,7 @@
<?php /* Copyright (c) 2024 Freya Murphy */
$routes = array();
$routes['home'] = 'apps/home';
$routes['error'] = 'apps/error';
$routes[''] = '_index';

49
web/core/_controller.php Normal file
View file

@ -0,0 +1,49 @@
<?php /* Copyright (c) 2024 Freya Murphy */
abstract class Controller {
// the main model
public $main;
// the loader
public $load;
// the database
public $db;
/**
* Creates a constructor
* @param Loader $load - the website loaded object
*/
function __construct($load) {
$this->load = $load;
$this->main = $this->load->model('main');
$this->db = $this->main->db;
$info = $this->main->info;
$lang = $info['lang'];
$this->load->lang($lang);
$app = $info['app'];
if ($app) {
$this->load->app_lang($lang, $app);
}
}
public function index() {}
public function redirect($link) {
header('Location: '. $link, true, 301);
die();
}
protected function view($__name, $data = array()) {
$__root = $GLOBALS['webroot'];
$__path = $__root . '/views/' . $__name . '.php';
if (is_file($__path)) {
extract($data);
require($__path);
return;
}
}
}
?>

44
web/core/_model.php Normal file
View file

@ -0,0 +1,44 @@
<?php /* Copyright (c) 2024 Freya Murphy */
abstract class Model {
// the main model
// shared by all controllers and models
public $main;
public $load;
// the database
public $db;
private $config;
/**
* Creates a model
* @param Loader $load - the main loader object
*/
function __construct($load) {
$this->load = $load;
$this->main = $this->load->model('main');
$this->db = $this->main->db;
$this->config = new Aesthetic();
}
/**
* @returns the base model data
*/
public function get_data(): array {
$data = array();
$data['self'] = $this->main->user();
$info = $this->main->info;
$app = $info['app'];
if ($app) {
$files = $this->config->get_files($app);
$data = array_merge($data, $files);
} else {
$files = $this->config->get_files();
$data = array_merge($data, $files);
}
return $data;
}
}

View file

@ -1,55 +0,0 @@
<?php /* Copyright (c) 2024 Freya Murphy */
abstract class Controller {
// the main model
public $main;
// the loader
public $load;
// the database
public $db;
function __construct() {
$this->main = $GLOBALS['__vars']['main'];
$this->load = $GLOBALS['__vars']['load'];
$this->db = $this->main->db;
$info = $this->main->info;
$lang_code = $info['lang'];
$route_name = $info['route'];
$this->load->lang($lang_code);
$this->load->route_lang($lang_code, $route_name);
}
public function index() {}
protected function view($__name, $data = array()) {
$__root = $GLOBALS['webroot'];
$__path = $__root . '/views/' . $__name . '.php';
if (is_file($__path)) {
extract($data);
require($__path);
return;
}
}
protected function app_view($__name, $data = array()) {
$__root = $GLOBALS['webroot'];
$__route = $this->main->info['route'];
$__path = $__root . '/routes/' . $__route . '/views/' . $__name . '.php';
if (is_file($__path)) {
extract($data);
require($__path);
return;
}
}
protected function modal($title, $content, $data = array()) {
$data['title'] = $title;
$data['content'] = $content;
$this->view('template/modal', $data);
}
}
?>

View file

@ -170,4 +170,3 @@ class DatabaseHelper {
} }
} }
?>

View file

@ -1,16 +1,79 @@
<?php /* Copyright (c) 2024 Freya Murphy */ <?php /* Copyright (c) 2024 Freya Murphy */
class Loader { class Loader {
// keep track of what has been loaded
private $loaded;
function __construct() {
$this->loaded = array();
}
/**
* Loads a $type of object from a $dir with a given $name
* @param string $name - the name of the object to load
* @param string $dir - the directory theese objects are stored in
* @param string $type - the type of the object
*/
private function load_type($name, $dir, $type): object|NULL {
$path = $dir . '/' . $name . '.php';
if (array_key_exists($path, $this->loaded)) {
return $this->loaded[$path];
}
if (!file_exists($path)) {
return NULL;
}
$parts = explode('/', $name);
$part = end($parts);
$class = ucfirst($part) . '_' . $type;
require($path);
$ref = NULL;
try {
$ref = new ReflectionClass($class);
} catch (Exception $_e) {}
if ($ref === NULL) {
return NULL;
}
$obj = $ref->newInstance($this);
$this->loaded[$path] = $obj;
return $obj;
}
/**
* Loads a model
* @param string $name - the name of the model to load
*/
public function model($name): object|NULL {
$root = $GLOBALS['webroot'];
$dir = $root . '/_model';
return $this->load_type($name, $dir, 'model');
}
/**
* Loads a controller
* @param string $name - the name of the controller to load
*/
public function controller($name): Controller|NULL {
$root = $GLOBALS['webroot'];
$dir = $root . '/_controller';
return $this->load_type($name, $dir, 'controller');
}
/** /**
* Loads the given common lang * Loads the given common lang
* @param lang_code - the language code * @param string $lang_code 0 the language code
*/ */
public function lang($lang_code) { public function lang($lang_code): void {
$dir = $GLOBALS['webroot'] . '/lang/' . $lang_code . '/'; $dir = $GLOBALS['webroot'] . '/lang/' . $lang_code . '/';
$lang = $GLOBALS['lang']; $lang = $GLOBALS['lang'];
if ($handle = opendir($dir)) { if ($handle = opendir($dir)) {
while (false !== ($entry = readdir($handle))) { while (false !== ($entry = readdir($handle))) {
if ($entry === '.' || $entry === '..' || $entry === 'routes') { if ($entry === '.' || $entry === '..' || $entry === 'apps') {
continue; continue;
} }
$path = $dir . $entry; $path = $dir . $entry;
@ -21,12 +84,12 @@ class Loader {
} }
/** /**
* Loads a given route specific lang * Loads a given app specific lang
* @param lang_coed - the language code * @param string $lang_code - the language code
* #param name - the name of the route * @param string $name - the name of the app
*/ */
public function route_lang($lang_code, $name) { public function app_lang($lang_code, $name): void {
$dir = $GLOBALS['webroot'] . '/lang/' . $lang_code . '/routes/'; $dir = $GLOBALS['webroot'] . '/lang/' . $lang_code . '/apps/';
$file = $dir . $name . '.php'; $file = $dir . $name . '.php';
if (file_exists($file)) { if (file_exists($file)) {
$lang = $GLOBALS['lang']; $lang = $GLOBALS['lang'];

View file

@ -1,123 +0,0 @@
<?php /* Copyright (c) 2024 Freya Murphy */
class MainModel {
// loaded route infomation
public $info;
public $db;
public $user_id;
private $users;
function __construct() {
$this->info = NULL;
$this->db = new DatabaseHelper();
$this->users = array();
$_SESSION['jwt'] = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicmVzdF91c2VyIiwidXNlcl9pZCI6MSwiZXhwIjoxNzExODUxMDUzfQ.FUcFO44SWV--YtVOy7NftTF8OeeOYGZDaDHigygQxsY';
if (array_key_exists('jwt', $_SESSION)) {
$this->get_session($_SESSION['jwt']);
} else {
$this->user_id = NULL;
};
}
private function get_session($jwt) {
$query = $this->db
->select("_api.verify_jwt('" . $jwt . "') AS user_id;");
$result = $query->row();
$user_id = $result['user_id'];
if ($user_id) {
$this->user_id = $user_id;
}
}
public function link_css($path) {
return '<link rel="stylesheet" href="/public/' . $path . '">';
}
public function link_js($path) {
return '<script src="/public/'. $path . '"></script>';
}
public function user() {
if ($this->user_id) {
return $this->db
->select('*')
->from('api.user')
->where('id')
->eq($this->user_id)
->row();
} else {
return NULL;
}
}
public function get_num($key, $default = NULL) {
if (!array_key_exists($key, $_GET)) {
if ($default !== NULL) {
return $default;
} else {
error_page(400, lang('error_400'));
}
} else {
$val = $_GET[$key];
$val = intval($val);
if ($val < 0) {
return 0;
} else {
return $val;
}
}
}
public function get_users($objs) {
$ids = array();
foreach ($objs as $obj) {
$id = $obj['user_id'];
if (!array_key_exists($id, $this->users)) {
array_push($ids, intval($id));
}
}
if (!empty($ids)) {
$result = $this->db
->select('*')
->from('api.user')
->where_in('id', $ids)
->rows();
foreach ($result as $user) {
$id = $user['id'];
$this->users[$id] = $user;
}
}
return $this->users;
}
public function display_name($user) {
$name = '';
if ($user['first_name']) {
$name .= $user['first_name'];
}
if ($user['middle_name']) {
if ($name != '') {
$name .= ' ';
}
$name .= $user['middle_name'];
}
if ($user['last_name']) {
if ($name != '') {
$name .= ' ';
}
$name .= $user['last_name'];
}
if ($name == '') {
$name = '@' . $user['username'];
}
return $name;
}
public function display_date($date) {
return $date;
}
}
?>

View file

@ -1,29 +0,0 @@
<?php /* Copyright (c) 2024 Freya Murphy */
abstract class Model {
// the main model
// shared by all controllers and models
public $main;
public $load;
// the database
public $db;
private $config;
function __construct() {
$this->main = $GLOBALS['__vars']['main'];
$this->load = $GLOBALS['__vars']['load'];
$this->db = $this->main->db;
$this->config = new Aesthetic();
}
public function get_data() {
$data = array();
$route = $this->main->info['route'];
$files = $this->config->get_files($route);
$data = array_merge($data, $files);
$data['self'] = $this->main->user();
return $data;
}
}
?>

View file

@ -1,127 +1,147 @@
<?php /* Copyright (c) 2024 Freya Murphy */ <?php /* Copyright (c) 2024 Freya Murphy */
class Router { class Router {
private $main; // the loader
private $load; private $load;
private $routes;
function load_route($route) { // the main model
$name = $route['name']; private $main;
$controller_cls = $route['controller'];
$model_cls = $route['model'];
$root = $GLOBALS['webroot']; /**
$dir = $root . '/routes/' . $name; * Creates a router
require($dir . '/model.php'); * @param Loader $load - the main laoder object
require($dir . '/controller.php'); */
function __construct($load) {
$model_ref = new ReflectionClass($model_cls);
$model = $model_ref->newInstance();
$controller_ref = new ReflectionClass($controller_cls);
$controller = $controller_ref->newInstance($model);
return $controller;
}
function __construct($main, $load) {
$routes = array(
'home' => array(
'slugs' => ['', 'home'],
'model' => 'HomeModel',
'controller' => 'HomeController',
),
);
$this->routes = array();
foreach ($routes as $name => $route) {
foreach ($route['slugs'] as $slug) {
$this->routes[$slug] = $route;
$this->routes[$slug]['name'] = $name;
}
}
$this->main = $main;
$this->load = $load; $this->load = $load;
$this->main = $this->load->model('main');
} }
function get_info() { /**
$uri = parse_url($_SERVER['REQUEST_URI']); * @param string $path - the current request path
* Gets the current route
* @return array<string,mixed>
*/
private function get_req_route($path): array {
// trim the path
$path = trim($path);
// remove first '/'
$path = substr($path, 1);
// get path parts
$parts = explode('/', $path);
$len = count($parts);
// get route info
$route = array();
// e.g. /
if ($path === '') {
$route = array(
'route' => '',
'slug' => 'index',
);
// e.g. /home /login
} else if ($len === 1) {
$route = array(
'route' => $parts[0],
'slug' => 'index',
);
// e.g. /home/posts
} else {
$route = array (
'route' => implode('/', array_slice($parts, 0, -1)),
'slug' => end($parts)
);
};
$route['app'] = $route['route'];
$routes = $GLOBALS['routes'];
if (array_key_exists($route['route'], $routes)) {
$route['route'] = $routes[$route['route']];
}
return $route;
}
/**
* Gets the curret request info
* @return array<string,mixed>
*/
private function get_req(): array {
$method = $_SERVER['REQUEST_METHOD']; $method = $_SERVER['REQUEST_METHOD'];
$parts = explode('/', $uri['path']);
$slug = sizeof($parts) > 1 ? $parts[1] : ''; $uri = parse_url($_SERVER['REQUEST_URI']);
$path = sizeof($parts) > 2 ? $parts[2] : 'index'; $path = $uri['path'];
if (sizeof($parts) > 3) { return array_merge(
return NULL; array(
} 'uri' => $uri,
'method' => $method,
return array( 'lang' => $this->get_lang(),
'method' => $method, ),
'uri' => $uri, $this->get_req_route($path),
'slug' => $slug,
'path' => $path
); );
} }
function handle_error($code) { /**
$route = array( * Gets the current language
'name' => 'error', * @return string
'model' => 'ErrorModel', */
'controller' => 'ErrorController' private function get_lang(): string {
); return 'en_US';
$this->main->info = array( }
'slug' => 'error',
'lang' => 'en_US', /**
'route' => 'error' * Handles a router error code
); * @param int $code - the http error code
$controller = $this->load_route($route); * @param bool $recursed
*/
private function handle_error($code, $recursed): void {
if ($recursed) {
die($code . ' (recursed)');
}
$this->main->info['slug'] = 'index';
$this->main->info['app'] = 'error';
$this->main->info['route'] = 'apps/error';
$req = $this->main->info;
$_GET['code'] = $code; $_GET['code'] = $code;
http_response_code($code);
$controller->index(); $this->handle_req($req, TRUE);
} }
public function handle_request() { /**
$request = $this->get_info(); * @param array $req
* @param bool $recursed
*/
private function handle_req($req, $recursed = FALSE): void {
$controller = $this->load->controller($req['route']);
if ($request === NULL) { if ($controller === NULL) {
$this->handle_error(404); $this->handle_error(404, $recursed);
return; return;
} }
$slug = $request['slug'];
if (!array_key_exists($slug, $this->routes)) {
$this->handle_error(404);
return;
}
$route = $this->routes[$slug];
$this->main->info = array(
'lang' => 'en_US',
'slug' => $slug,
'route' => $route['name'],
);
$controller = $this->load_route($route);
$path = $request['path'];
$ref = NULL; $ref = NULL;
try { try {
$ref = new ReflectionMethod($controller, $path); $ref = new ReflectionMethod($controller, $req['slug']);
} catch (Exception $_e) {} } catch (Exception $_e) {}
if ($ref === NULL || !$ref->isPublic()) { if ($ref === NULL || !$ref->isPublic()) {
$this->handle_error(404); $this->handle_error(404, $recursed);
return; return;
} }
$ref->invoke($controller); $ref->invoke($controller);
}
/**
* Handels the incomming reuqest
*/
public function handle_request(): void {
$req = $this->get_req();
$this->main->info = $req;
$this->handle_req($req);
} }
} }

9
web/helper/error.php Normal file
View file

@ -0,0 +1,9 @@
<?php /* Copyright (c) 2024 Freya Murphy */
function error_page($code, $msg) {
$root = $GLOBALS['webroot'];
error_reporting(E_ERROR | E_PARSE);
http_response_code($code);
require($root . '/views/template/error.php');
die();
}

77
web/helper/lang.php Normal file
View file

@ -0,0 +1,77 @@
<?php /* Copyright (c) 2024 Freya Murphy */
$lang = array();
function lang($key, $default = NULL, $sub = NULL) {
$lang = $GLOBALS['lang'];
if(array_key_exists($key, $lang)) {
if ($sub) {
return sprintf($lang[$key], ...$sub);
} else {
return $lang[$key];
}
} else if ($default !== NULL) {
return $default;
} else {
return $key;
}
}
function ilang($key,
$class = NULL,
$id = NULL,
$href = NULL,
$click = NULL,
$attrs = array(),
$sub = NULL,
$button = FALSE,
) {
$text = lang($key . "_text", FALSE, sub: $sub);
$tip = lang($key . "_tip", FALSE);
$icon = lang($key . "_icon", FALSE);
$content = lang($key . "_content", FALSE);
if ($click || $button) {
echo '<button ';
} else {
echo '<a ';
}
if ($tip) {
echo 'title="' . $tip . '" ';
echo 'aria-label="' . $tip . '" ';
}
if ($class) {
echo 'class="' . $class . '" ';
}
if ($id) {
echo 'id="' . $id . '" ';
}
if ($click) {
echo 'onclick="' . $click . '" ';
}
if ($href) {
echo 'href="' . $href . '" ';
}
foreach ($attrs as $key => $attr) {
echo $key . '="' . $attr . '" ';
}
echo '> ';
if ($icon) {
echo '<i class="' . $icon . '">';
if ($content) {
echo $content;
}
echo '</i>';
}
if ($text) {
echo '<span';
if ($icon) {
echo ' class="ml-sm"';
}
echo '>' . $text . '</span>';
}
if ($click) {
echo '</button>';
} else {
echo '</a>';
}
}

View file

@ -2,114 +2,26 @@
session_start(); session_start();
$lang = array();
$__vars = array();
$webroot = dirname(__FILE__); $webroot = dirname(__FILE__);
function error_page($code, $msg) { // load all the helper files
$root = $GLOBALS['webroot']; require($webroot . '/helper/error.php');
error_reporting(E_ERROR | E_PARSE); require($webroot . '/helper/lang.php');
http_response_code($code);
require($root . '/core/error.php');
die();
}
function lang($key, $default = NULL, $sub = NULL) { // load all the config files
$lang = $GLOBALS['lang']; require($webroot . '/config/aesthetic.php');
if(array_key_exists($key, $lang)) { require($webroot . '/config/routes.php');
if ($sub) {
return sprintf($lang[$key], ...$sub);
} else {
return $lang[$key];
}
} else if ($default !== NULL) {
return $default;
} else {
return $key;
}
}
function ilang($key, // load all core files
$class = NULL, require($webroot . '/core/_controller.php');
$id = NULL, require($webroot . '/core/_model.php');
$href = NULL, require($webroot . '/core/database.php');
$click = NULL, require($webroot . '/core/loader.php');
$attrs = array(), require($webroot . '/core/router.php');
$sub = NULL,
$button = FALSE,
) {
$text = lang($key . "_text", FALSE, sub: $sub);
$tip = lang($key . "_tip", FALSE);
$icon = lang($key . "_icon", FALSE);
$content = lang($key . "_content", FALSE);
if ($click || $button) {
echo '<button ';
} else {
echo '<a ';
}
if ($tip) {
echo 'title="' . $tip . '" ';
echo 'aria-label="' . $tip . '" ';
}
if ($class) {
echo 'class="' . $class . '" ';
}
if ($id) {
echo 'id="' . $id . '" ';
}
if ($click) {
echo 'onclick="' . $click . '" ';
}
if ($href) {
echo 'href="' . $href . '" ';
}
foreach ($attrs as $key => $attr) {
echo $key . '="' . $attr . '" ';
}
echo '> ';
if ($icon) {
echo '<i class="' . $icon . '">';
if ($content) {
echo $content;
}
echo '</i>';
}
if ($text) {
echo '<span';
if ($icon) {
echo ' class="ml-sm"';
}
echo '>' . $text . '</span>';
}
if ($click) {
echo '</button>';
} else {
echo '</a>';
}
}
function __init() { function __init() {
$root = $GLOBALS['webroot'];
// load all core files
require($root . '/core/database.php');
require($root . '/core/aesthetic.php');
require($root . '/core/controller.php');
require($root . '/core/model.php');
require($root . '/core/loader.php');
require($root . '/core/main.php');
require($root . '/core/router.php');
$main = new MainModel();
$load = new Loader(); $load = new Loader();
$router = new Router($main, $load); $router = new Router($load);
$GLOBALS['__vars']['main'] = $main;
$GLOBALS['__vars']['load'] = $load;
$GLOBALS['__vars']['router'] = $router;
$router->handle_request(); $router->handle_request();
}; };
@ -118,5 +30,3 @@ if (!file_exists('/status/ready')) {
} }
__init(); __init();
?>

View file

@ -0,0 +1,26 @@
<?php /* Copyright (c) 2024 Freya Murphy */
// user column
$lang['api_column_username'] = 'username';
$lang['api_column_first_name'] = 'first name';
$lang['api_column_last_name'] = 'last name';
$lang['api_column_middle_name'] = 'middle name';
$lang['api_column_email'] = 'email';
$lang['api_column_gender'] = 'gender';
$lang['api_column_join_date'] = 'join date';
$lang['api_column_birth_date'] = 'birth date';
$lang['api_column_profile_avatar'] = 'avatar image';
$lang['api_column_profile_banner'] = 'banner image';
$lang['api_column_profile_bio'] = 'profile bio';
// post column
$lang['api_column_content'] = 'post content';
// error messages
$lang['api_denied'] = 'Action was denied';
$lang['api_null_value'] = '%s cannot be empty';
$lang['api_unique_value'] = '%s is not available (not unique)';
$lang['api_min_value'] = '%s length cannot be less than %s';
$lang['api_max_value'] = '%s length cannot exceed %s';
?>

View file

@ -0,0 +1,9 @@
<?php
$lang['title'] = 'Home';
// actions
$lang['action_load_posts_text'] = 'Load more posts';
$lang['action_load_posts_tip'] = 'Load more posts';
?>

View file

@ -1,4 +1,4 @@
<?php <?php /* Copyright (c) 2024 Freya Murphy */
// Navigation Bar Lang // Navigation Bar Lang
$lang['action_home_text'] = 'Home'; $lang['action_home_text'] = 'Home';
@ -29,6 +29,10 @@ $lang['action_load_comments_tip'] = 'Load more comments';
// General // General
$lang['action_submit_text'] = 'Submit'; $lang['action_submit_text'] = 'Submit';
$lang['action_submit_tip'] = 'Submit'; $lang['action_submit_tip'] = 'Submit';
$lang['action_close_text'] = '';
$lang['action_close_tip'] = 'Close';
$lang['action_close_icon'] = 'mi mi-sm';
$lang['action_close_content'] = 'close';
// Modals // Modals
$lang['action_modal_close_text'] = ''; $lang['action_modal_close_text'] = '';
@ -36,6 +40,10 @@ $lang['action_modal_close_tip'] = 'Close modal.';
$lang['action_modal_close_icon'] = 'mi mi-sm'; $lang['action_modal_close_icon'] = 'mi mi-sm';
$lang['action_modal_close_content'] = 'close'; $lang['action_modal_close_content'] = 'close';
$lang['new_post_modal_title'] = 'Author New Post';
$lang['action_new_post_text'] = 'What\'s on your mind, %s';
$lang['action_new_post_tip'] = 'Author a new post.';
// Words // Words
$lang['now'] = 'Now'; $lang['now'] = 'Now';

View file

@ -1,4 +1,4 @@
<?php <?php /* Copyright (c) 2024 Freya Murphy */
$lang['error_400'] = 'Bad request'; $lang['error_400'] = 'Bad request';
$lang['error_404'] = 'Resource not found'; $lang['error_404'] = 'Resource not found';

View file

@ -1,14 +0,0 @@
<?php
$lang['title'] = 'Home';
// actions
$lang['action_new_post_text'] = 'What\'s on your mind, %s';
$lang['action_new_post_tip'] = 'Author a new post.';
$lang['action_load_posts_text'] = 'Load more posts';
$lang['action_load_posts_tip'] = 'Load more posts';
// modals
$lang['new_post_modal_title'] = 'Author New Post';
?>

View file

@ -52,6 +52,8 @@ body {
} }
header { header {
top: 0;
position: sticky;
height: 3.5rem; height: 3.5rem;
background-color: var(--primary); background-color: var(--primary);
display: flex; display: flex;
@ -145,6 +147,10 @@ input:focus {
align-items: center; align-items: center;
} }
.nav {
position: sticky;
}
.nav-right { .nav-right {
flex: 1; flex: 1;
justify-content: flex-end; justify-content: flex-end;
@ -333,6 +339,30 @@ input:focus {
border-radius: .5rem; border-radius: .5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
animation: fadeIn .1s, slideInModal .1s linear;
}
@keyframes slideInModal {
0% {
animation-timing-function: ease-in;
transform: translate(-50%, -60%);
}
}
@keyframes slideIn {
0% {
animation-timing-function: ease-out;
transform: translate(0, -50%);
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
} }
.modal>form { .modal>form {
@ -402,3 +432,30 @@ button[type="submit"]:hover {
background-color: var(--logo); background-color: var(--logo);
} }
#toast-container {
position: fixed;
top: 4rem;
left: 100%;
transform: translateX(-110%);
margin-top: 1rem;
z-index: 10000;
}
.toast {
padding: .75rem;
margin: .5rem;
border-radius: .5rem;
min-width: 15rem;
font-family: sfpro;
animation: fadeIn .1s, slideIn .25s linear;
display: flex;
justify-content: space-between;
}
.toast.error {
background-color: var(--error);
}
.toast.success {
background-color: var(--success);
}

View file

@ -1,4 +1,8 @@
///
/// document ready functions
///
let ready = false; let ready = false;
$(function() { $(function() {
@ -15,9 +19,14 @@ var r$ = function(callback) {
} }
} }
function observe(containerSelector, elementSelector, callback) { ///
r$(() => { /// dom observer
/// checks for elements on the DOM now and added later
///
function observe(containerSelector, elementSelector, callback) {
r$(() => {
$(containerSelector + ' ' + elementSelector).each(function (_, e) { $(containerSelector + ' ' + elementSelector).each(function (_, e) {
let me = $(e); let me = $(e);
callback(me); callback(me);
@ -37,9 +46,64 @@ function observe(containerSelector, elementSelector, callback) {
}; };
var target = $(containerSelector)[0]; var target = $(containerSelector)[0];
if (!target) {
console.warn('[observe] didnt find container: ', containerSelector);
return;
}
var config = { childList: true, subtree: true }; var config = { childList: true, subtree: true };
var MutationObserver = window.MutationObserver; var MutationObserver = window.MutationObserver;
var observer = new MutationObserver(onMutationsObserved); var observer = new MutationObserver(onMutationsObserved);
observer.observe(target, config); observer.observe(target, config);
}); });
} }
///
/// ajax setup
///
let ajaxHeaders = {};
ajaxHeaders['Content-Type'] = 'application/json';
if (jwtStr) {
ajaxHeaders['Authorization'] = 'Bearer ' + jwtStr
}
$.ajaxSetup({
headers: ajaxHeaders
})
///
/// ajax error handle
///
var errorToast = (xhr) => {
let data = xhr.responseJSON;
let msg = data.message;
let detail = data.details;
let hint = data.hint;
let query = '?msg=' + msg;
if (detail) {
query += '&detail=' + detail;
}
if (hint) {
query += '&hint=' + hint;
}
let url = '/template/toast' + query;
$.get(url, function (data) {
$('#toast-container').prepend(data);
})
}
observe('#toast-container', '.action-close-toast', function(el) {
el.on('click', function() {
el.parent().remove();
});
setTimeout(function() {
el.parent().remove();
}, 5000);
});

View file

@ -1,4 +1,4 @@
observe('.post', '.action-load-comments', function(me) { observe('#main-content', '.action-load-comments', function(me) {
me.on('click', function() { me.on('click', function() {
let page = me.attr('page'); let page = me.attr('page');
if (!page) { if (!page) {

View file

@ -1,20 +0,0 @@
<?php /* Copyright (c) 2024 Freya Murphy */
class ErrorController extends Controller {
private $model;
function __construct($model) {
parent::__construct();
$this->model = $model;
}
public function index() {
parent::index();
$data = $this->model->get_data();
$this->view('header', $data);
$this->app_view('error', $data);
}
}
?>

View file

@ -1,4 +0,0 @@
<div id="error">
<h1><?=$title?></h1>
<span><?=$msg?></span>
</div>

View file

@ -0,0 +1,6 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<div id="error">
<h1><?=$title?></h1>
<span><?=$msg?></span>
</div>

View file

@ -1,4 +1,5 @@
<?php // vi: syntax=php ?> <?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<div id="main-content"> <div id="main-content">
<?php if ($self): ?> <?php if ($self): ?>
<div id="new-post" class="card"> <div id="new-post" class="card">
@ -15,7 +16,7 @@
</div> </div>
<script> <script>
$('#action-new-post').on('click', function() { $('#action-new-post').on('click', function() {
$.get( "/home/new_post_modal", function (data) { $.get( "/modal/new_post", function (data) {
$(document.body).append(data); $(document.body).append(data);
}); });
}) })

View file

@ -1,2 +1,4 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<body> <body>
</html> </html>

View file

@ -1,10 +1,18 @@
<?php // vi: syntax=php ?> <?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<?php <?php
$self = $this->main->user(); $self = $this->main->user();
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<script>
<?php if ($this->main->session): ?>
var jwtStr = <?=json_encode($this->main->session['jwt'])?>;
<?php else: ?>
var jwtStr = null;
<?php endif; ?>
</script>
<?php <?php
foreach ($js_files as $js) { foreach ($js_files as $js) {
echo $this->main->link_js($js); echo $this->main->link_js($js);
@ -71,4 +79,6 @@
menu.toggleClass('visible'); menu.toggleClass('visible');
}); });
</script> </script>
</header> </header>
<div id="toast-container">
</div>

View file

@ -1,7 +1,9 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<?php <?php
$user = $this->main->user(); $user = $this->main->user();
?> ?>
<form> <form id="new-post-form">
<div class="modal-content new-post-modal"> <div class="modal-content new-post-modal">
<div class="row"> <div class="row">
<?php $this->view('template/pfp', array('user' => $user))?> <?php $this->view('template/pfp', array('user' => $user))?>
@ -12,8 +14,8 @@
</div> </div>
<textarea <textarea
type="text" type="text"
name="text" name="content"
id="text" id="new-post-content"
placeholder="<?=lang('action_new_post_text', sub: [$user['first_name']])?>" placeholder="<?=lang('action_new_post_text', sub: [$user['first_name']])?>"
></textarea> ></textarea>
</div> </div>
@ -26,3 +28,19 @@
)?> )?>
</div> </div>
</form> </form>
<script>
$('#new-post-form').submit(function(e) {
e.preventDefault();
let content = $('#new-post-content').val();
$.ajax({
url: '/api/post',
method: 'POST',
data: JSON.stringify({ content }),
success: function(data) {
window.location.reload();
},
error: errorToast
});
});
</script>

View file

@ -1,3 +1,5 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<div class="comment row mt"> <div class="comment row mt">
<?php $this->view('template/pfp', array('user' => $user))?> <?php $this->view('template/pfp', array('user' => $user))?>
<div class="ml col sub-card"> <div class="ml col sub-card">

View file

@ -1,3 +1,5 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<div class="modal-container"> <div class="modal-container">
<div class="modal"> <div class="modal">
<div class="modal-header row"> <div class="modal-header row">

View file

@ -1,3 +1,5 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<?php <?php
$class = isset($class) ? $class : ''; $class = isset($class) ? $class : '';
?> ?>

View file

@ -1,3 +1,5 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<div class="post card"> <div class="post card">
<div class="row"> <div class="row">
<?php $this->view('template/pfp', array('user' => $user))?> <?php $this->view('template/pfp', array('user' => $user))?>

View file

@ -0,0 +1,19 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<?php
$params = array();
if ($detail) {
array_push($params, lang('api_column_' . $detail));
}
if ($hint) {
array_push($params, $hint);
}
$msg = lang($msg, sub: $params);
?>
<div class="toast error">
<?=ucfirst($msg)?>
<?=ilang('action_close', class: 'action-close-toast')?>
</div>