basic forum done

its not pretty, but its a basic outline.
This commit is contained in:
xander 2022-03-08 10:23:29 -12:00
parent fbb392188c
commit 4da9e49ee0
24 changed files with 786 additions and 63 deletions

View File

@ -8,6 +8,9 @@ use Illuminate\Foundation\Validation\ValidatesRequests;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use App\Models\User;
use App\Models\Category;
use App\Models\Post;
use App\Models\Reply;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
@ -34,6 +37,44 @@ class Controller extends BaseController
return Response()->json(["data"=>$array]);
}
public function fetchCategories() {
$categories = Category::get();
return Response()->json(["data"=>$categories]);
}
public function fetchCategory($id) {
$category = Category::where('id', $id)->first();
if (!$category) {return Response()->json(false);}
$posts = $category->posts()->paginate(20);
foreach ($posts as &$post) {
$post['creator'] = User::where('id', $post['creator_id'])->first();
}
return Response()->json(["data"=>$category, "posts"=>$posts]);
}
public function fetchPost($id) {
$post = Post::where('id', $id)->first();
if (!$post) {return Response()->json(false);}
$postA = $post->toArray();
$postA['creator'] = User::where('id', $postA['creator_id'])->first();;
$replies = $post->replies()->paginate(10);
return Response()->json(["post"=>$postA,"replies"=>$replies]);
}
public function logout(Request $request) {
$POST;

View File

@ -2,7 +2,16 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use App\Models\User;
use App\Models\Post;
use App\Models\Category;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Request;
use Auth;
class HomeController extends Controller
{
@ -25,4 +34,38 @@ class HomeController extends Controller
{
return view('home');
}
public function createPost() {
$data = Request::all();
$valid = Validator::make($data, [
'title' => ['required', 'string', 'min:3', 'max:38'],
'body' => ['required', 'string', 'min:3', 'max:380'],
]);
if ($valid->stopOnFirstFailure()->fails()) {
$error = $valid->errors()->first();
$messages = $valid->messages()->get('*');
return Response()->json(['message'=>$error, 'badInputs'=>[array_keys($messages)]]);
}
if (!isset($_POST['creator_id'])) {return Response()->json(['message'=>'System error', 'badInputs'=>['title']]);}
$user = User::where('id', $_POST['creator_id'])->first();
if (!$user) {return Response()->json(['message'=>'User not found!', 'badInputs'=>['title']]);}
$post = new Post;
$post->title = $_POST['title'];
$post->body = $_POST['body'];
$post->creator_id = $_POST['creator_id'];
//will add category support later
$post->category_id = 1;
$post->category_type = 'App\Models\Category';
$post->save();
return Response()->json('good');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
use HasFactory;
protected $table = 'categories';
function posts()
{
return $this->morphMany('App\Models\Post', 'category');
}
}

19
web/app/Models/Post.php Normal file
View File

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
protected $table = 'posts';
function replies()
{
return $this->morphMany('App\Models\Reply', 'post');
}
}

14
web/app/Models/Reply.php Normal file
View File

@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Reply extends Model
{
use HasFactory;
protected $table = 'replies';
}

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateCategoriesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('description');
$table->boolean('staffOnly')->default(false);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('categories');
}
}

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePostsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('body');
$table->boolean('pinned')->default(false);
$table->boolean('locked')->default(false);
$table->string('creator_id');
$table->string("category_id");
$table->string("category_type");
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('posts');
}
}

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateRepliesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('replies', function (Blueprint $table) {
$table->id();
$table->string('body');
$table->string('creator_id');
$table->boolean('pinned')->default(false);
$table->string("post_id");
$table->string("post_type");
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('replies');
}
}

28
web/package-lock.json generated
View File

@ -12,6 +12,7 @@
"mini-css-extract-plugin": "^2.4.3",
"react-bootstrap": "^2.0.2",
"react-google-recaptcha": "^2.1.0",
"react-router-guards": "^1.0.2",
"styled-components": "^5.3.1",
"three": "^0.135.0"
},
@ -8932,6 +8933,19 @@
"react": ">=15"
}
},
"node_modules/react-router-guards": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/react-router-guards/-/react-router-guards-1.0.2.tgz",
"integrity": "sha512-RuNR+1sa7M96Jc3/hQxYw1E1ItYRUGFn+MKSoHSYCoWm6/JneA0z7C5spC9211FlkVXm79nonNtGy7exHSyKbA==",
"dependencies": {
"tiny-invariant": "^1.0.4"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-router": ">=5.0.0",
"react-router-dom": ">=5.0.0"
}
},
"node_modules/react-router/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
@ -10263,8 +10277,7 @@
"node_modules/tiny-invariant": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==",
"dev": true
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
},
"node_modules/tiny-warning": {
"version": "1.0.3",
@ -17998,6 +18011,14 @@
"tiny-warning": "^1.0.0"
}
},
"react-router-guards": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/react-router-guards/-/react-router-guards-1.0.2.tgz",
"integrity": "sha512-RuNR+1sa7M96Jc3/hQxYw1E1ItYRUGFn+MKSoHSYCoWm6/JneA0z7C5spC9211FlkVXm79nonNtGy7exHSyKbA==",
"requires": {
"tiny-invariant": "^1.0.4"
}
},
"react-three-fiber": {
"version": "0.0.0-deprecated",
"resolved": "https://registry.npmjs.org/react-three-fiber/-/react-three-fiber-0.0.0-deprecated.tgz",
@ -19007,8 +19028,7 @@
"tiny-invariant": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==",
"dev": true
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
},
"tiny-warning": {
"version": "1.0.3",

View File

@ -36,6 +36,7 @@
"mini-css-extract-plugin": "^2.4.3",
"react-bootstrap": "^2.0.2",
"react-google-recaptcha": "^2.1.0",
"react-router-guards": "^1.0.2",
"styled-components": "^5.3.1",
"three": "^0.135.0"
}

View File

@ -10,7 +10,7 @@ const Navbar = (props) => {
<>
<nav className="navbar graphictoria-navbar fixed-top navbar-expand-md shadow-sm">
<div className="container-sm">
<NavLink className="navbar-brand" to="/">
<NavLink className="navbar-brand" to={props.user? `/home` : `/`}>
<img src="/images/logo.png" alt="Graphictoria" width="43" height="43" draggable="false"/>
</NavLink>
<button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#graphictoria-nav" aria-controls="graphictoria-nav" aria-expanded="false" aria-label="Toggle navigation">

View File

@ -24,7 +24,6 @@ export function CreateAccount(form)
resolve({message: res.message, inputs: res.badInputs});
return;
}
window.location.replace(`/`);
resolve("good");
}).catch(error=>{console.log(error);});
@ -46,7 +45,6 @@ export function LoginToAccount(form) {
resolve({message: res.message, inputs: res.badInputs});
return;
}
window.location.replace(`/`);
resolve("good");
}).catch(error=>{console.log(error);});
@ -54,6 +52,24 @@ export function LoginToAccount(form) {
}
export function CreateForum(form) {
const body = form;
var badInputs = [];
return new Promise(async (resolve, reject)=>{
axios.post(`${protocol}apis.${url}/api/create/forum`, body, {headers: {'X-CSRF-TOKEN': document.querySelector(`meta[name="csrf-token"]`).content, "X-Requested-With":"XMLHttpRequest"}}).then(data=>{
const res = data.data;
if (res.badInputs.length >= 1) {
badInputs=res.badInputs;
resolve({message: res.message, inputs: res.badInputs});
return;
}
resolve("good");
}).catch(error=>{console.log(error);});
});
}
export function LogoutOfAccount() {
const body = form;

View File

@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import { PageTransition } from '@steveeeie/react-page-transition';
import { GuardProvider, GuardedRoute } from 'react-router-guards'
import Config from '../config.js';
@ -27,6 +28,10 @@ import { Copyright } from '../Pages/Legal/Copyright.js';
import { Privacy } from '../Pages/Legal/Privacy.js';
import { Terms } from '../Pages/Legal/Terms.js';
import { getCookie } from '../helpers/utils.js';
import Dashboard from '../pages/Dashboard.js';
import Forum from '../pages/Forum.js';
import Post from '../pages/Post.js';
import CreatePost from '../pages/CreatePost.js';
axios.defaults.withCredentials = true
@ -39,46 +44,56 @@ const App = () => {
const [user, setUser] = useState([]);
function updateBanners()
{
axios.get(`${protocol}apis.${url}/banners/data`)
.then((response) => {
var result = [];
response.data.map(function(banner){
result.push(<Banner type={banner.type} description={banner.text} dismissible={banner.dismissable} />);
});
setState({banners: result});
{
axios.get(`${protocol}apis.${url}/banners/data`)
.then((response) => {
var result = [];
response.data.map(function(banner){
result.push(<Banner type={banner.type} description={banner.text} dismissible={banner.dismissable} />);
});
}
function fetchUser() {
const body = new FormData();
body.append('token', encodeURIComponent(getCookie(`gtok`)));
axios.post(`${protocol}apis.${url}/fetch/user`, body).then((res)=>{
setUser(res.data.data);
setState({banners: result});
});
return new Promise(async (resolve, reject)=>{
resolve("good");
});
}
function updateOfflineStatus()
{
axios.get(`${protocol}apis.${url}/`)
.then((response) => {
if(state.maintenance == true)
window.location.reload();
})
.catch((error) => {
if (error.response)
{
if(error.response.status == 503)
setState({maintenance: true, theme: 1});
}
})
.finally(() => {
setState({offlineFetched: true});
});
}
function fetchUser() {
const body = new FormData();
body.append('token', encodeURIComponent(getCookie(`gtok`)));
axios.post(`${protocol}apis.${url}/fetch/user`, body).then((res)=>{
setUser(res.data.data);
});
return new Promise(async (resolve, reject)=>{
resolve("good");
});
}
function updateOfflineStatus()
{
axios.get(`${protocol}apis.${url}/`)
.then((response) => {
if(state.maintenance == true)
window.location.reload();
})
.catch((error) => {
if (error.response)
{
if(error.response.status == 503)
setState({maintenance: true, theme: 1});
}
})
.finally(() => {
setState({offlineFetched: true});
});
}
const authMiddleware = (to, from, next) => {
if (to.meta.auth) {
if (user) {next();}
next.redirect(`/login`);
}else if (to.meta.guest) {
if (!user) {next();}
next.redirect(`/home`);
}
}
useEffect(async ()=>{
await fetchUser();
@ -95,6 +110,7 @@ const App = () => {
return (
!state.loading?
<Router>
<GuardProvider guards={[authMiddleware]}>
<Navbar maintenanceEnabled={state.maintenance} user={user} />
{state.banners && state.banners.length >= 1 ? state.banners : null}
@ -113,17 +129,39 @@ const App = () => {
<Route exact path="/legal/terms-of-service" component={Terms}/>
{state.maintenance ? <Route path="*" component={Maintenance}/> : null}
<Route exact path="/" component={Home}/>
<GuardedRoute exact path="/" meta={{guest: true}}>
<Home user={user}/>
</GuardedRoute>
<GuardedRoute exact path="/home" meta={{auth: true}}>
<Dashboard user={user}/>
</GuardedRoute>
<Route exact path="/forum">
<Forum user={user}/>
</Route>
<GuardedRoute exact path="/forum/post" meta={{auth: true}}>
<CreatePost user={user}/>
</GuardedRoute>
<Route exact path="/forum/category/:id">
<Forum user={user}/>
</Route>
<Route exact path="/forum/post/:id">
<Post user={user}/>
</Route>
<Route exact path="/login">
<Auth location={user && user.id? null : location.pathname}/>
</Route>
<Route exact path="/register">
<GuardedRoute exact path="/login" meta={{guest: true}}>
<Auth location={user? null : location.pathname}/>
</Route>
<Route exact path="/passwordreset">
</GuardedRoute>
<GuardedRoute exact path="/register" meta={{guest: true}}>
<Auth location={user? null : location.pathname}/>
</Route>
</GuardedRoute>
<GuardedRoute exact path="/passwordreset" meta={{guest: true}}>
<Auth location={user? null : location.pathname}/>
</GuardedRoute>
<Route exact path="/games" component={Games}/>
@ -138,6 +176,7 @@ const App = () => {
</PageTransition>
);
}}/>
</GuardProvider>
</Router>
:
<div className="gtoria-loader-center">

View File

@ -3,11 +3,11 @@
import React from 'react';
const Card = (props) => {
export const Card = (props) => {
return (
<div className="container graphictoria-center-vh">
<div className="card graphictoria-small-card shadow-sm">
<div className="card-body text-center">
<div className={`card-body ${props.padding? `p5r` : null} text-center`}>
{ props.children }
</div>
</div>
@ -15,7 +15,7 @@ const Card = (props) => {
);
};
const CardTitle = (props) => {
export const CardTitle = (props) => {
return (
<>
<h5 className="card-title fw-bold">{ props.children }</h5>
@ -23,8 +23,3 @@ const CardTitle = (props) => {
</>
);
};
export {
Card,
CardTitle
};

View File

@ -47,6 +47,10 @@ class Auth extends React.Component {
pageLabel = (<><i className="fas fa-question-circle"></i> RESET PASSWORD</>);
pageContent = (<ForgotPasswordForm />);
break;
case `guest`:
pageLabel = (<><i className="fas fa-question-circle"></i> YOU NEED TO BE A GUEST!</>);
pageContent = (<div><div>Sorry, this page is for guests only!</div></div>);
break;
default:
pageLabel = (<><i className={`"fas fa-question-circle"`}></i> YOU'RE LOGGED IN!</>);
pageContent = (<div><div>Sorry, this page is for unauthenticated members only!</div></div>);

View File

@ -23,7 +23,7 @@ const LoginForm = (props) => {
setTimeout(()=>{setValidity({...validity, error: false, inputs: res.inputs});}, 4000);
return;
}
window.location.replace(`/home`);
window.location.reload();
}).catch(error=>console.log(error));
setWaitingForSubmission(false);
}

69
web/resources/js/pages/CreatePost.js vendored Normal file
View File

@ -0,0 +1,69 @@
// © XlXi 2021
// Graphictoria 5
import React, {useState} from 'react';
import { Link } from 'react-router-dom';
import ReCAPTCHA from 'react-google-recaptcha';
import { CreateAccount, LoginToAccount, CreateForum } from '../Helpers/Auth';
import Loader from '../Components/Loader';
import { getCookie } from '../helpers/utils';
const CreatePost = (props) => {
const [waitingForSubmission, setWaitingForSubmission] = useState(false);
const [validity, setValidity] = useState({error: false, message: ``, inputs: []});
const user = props.user;
async function SubmitForm(form)
{
form.append('creator_id', user.id);
setWaitingForSubmission(true);
await CreateForum(form).then(res=>{
console.log(res);
if (res != `good`) {
setValidity({error: true, message:res.message, inputs: res.inputs});
setTimeout(()=>{setValidity({...validity, error: false, inputs: res.inputs});}, 4000);
return;
}
window.location.href=`/forum`;
}).catch(error=>console.log(error));
setWaitingForSubmission(false);
}
return (
waitingForSubmission ? <Loader/> :
<div className={`flex column jcc alc w-100`}>
<div className={`flex card card-body column alc w-40`}>
<div className={`flex row`}>
<div className="col-md-8 mb-2">
{validity.error?
<div className={`px-5 mb-10`}>
<div className={`error-dialog`}>
<p className={`mb-0`}>{validity.message}</p>
</div>
</div>
: null}
<form onSubmit={(e)=>{e.preventDefault();SubmitForm(new FormData(e.target));}} class="fs">
<input type="username" className={`form-control mb-4 ${(validity.inputs.find(input=>input == `title`)? `is-invalid` : ``)}`} placeholder="Title" name="title"/>
<textarea type="username" className={`form-control mb-4 ${(validity.inputs.find(input=>input == `body`)? `is-invalid` : ``)}`} placeholder="Body" name="body"></textarea>
<div className="d-flex mb-3">
<ReCAPTCHA
sitekey="6LeyHsUbAAAAAJ9smf-als-hXqrg7a-lHZ950-fL"
className="mx-auto"
/>
</div>
<button className="btn btn-primary px-5" type={`submit`}>POST!</button><br/>
</form>
</div>
<div className="col">
<h5><bold>Read the rules before posting!</bold></h5>
<p>Before you make a post, be sure to read the <Link to={`/forum/rules`}>rules</Link>.</p>
</div>
</div>
</div>
</div>
);
};
export default CreatePost;

49
web/resources/js/pages/Dashboard.js vendored Normal file
View File

@ -0,0 +1,49 @@
// © XlXi 2021
// Graphictoria 5
import axios from 'axios';
import React, { useEffect, useState } from "react";
import { Link, useHistory } from "react-router-dom";
import Config from '../config.js';
import SetTitle from "../Helpers/Title.js";
import Loader from '../Components/Loader.js';
import { GenericErrorModal } from './Errors.js';
import { Card, CardTitle } from '../Layouts/Card.js';
var url = Config.BaseUrl.replace('http://', '');
var protocol = Config.Protocol;
const Dashboard = (props) => {
const [state, setState] = useState({offline: false, loading: true});
const user = props.user;
useEffect(()=>{
SetTitle(`Dashboard`);
setState({...state, loading: false});
}, []);
return (
state.loading
?
<Loader />
:
<div className={`row`}>
<div className={`col`}>
<Card className={`justify-content-center`} padding={true}>
<CardTitle>Hello, {user.username}!</CardTitle>
<p>[Avatar goes here]</p>
</Card>
</div>
<div className={`col justify-content-center`}>
<p>Lmao</p>
</div>
</div>
);
}
export default Dashboard;

103
web/resources/js/pages/Forum.js vendored Normal file
View File

@ -0,0 +1,103 @@
// © XlXi 2021
// Graphictoria 5
import axios from 'axios';
import React, { useEffect, useState } from "react";
import { Link, useHistory, useParams } from "react-router-dom";
import Config from '../config.js';
import SetTitle from "../Helpers/Title.js";
import Loader from '../Components/Loader.js';
import { GenericErrorModal } from './Errors.js';
import { Card, CardTitle } from '../Layouts/Card.js';
var url = Config.BaseUrl.replace('http://', '');
var protocol = Config.Protocol;
const Forum = (props) => {
var id = useParams().id;
const [state, setState] = useState({offline: false, loading: true});
const [categories, setCategoires] = useState([]);
const [category, setCategory] = useState([]);
const [posts, setPosts] = useState({posts: [], currentPage: 0, meta: []});
const user = props.user;
if (!id) id = 1;
const fetchCategories = async () => {
await axios.get(`${protocol}apis.${url}/fetch/categories`, {headers: {"X-Requested-With":"XMLHttpRequest"}}).then(data=>{
setCategoires(data.data.data);
}).catch(error=>{console.log(error);});
}
const fetchCategory = async () => {
await axios.get(`${protocol}apis.${url}/fetch/category/${id}`, {headers: {"X-Requested-With":"XMLHttpRequest"}}).then(data=>{
if (!data.data) {window.location.href=`/forum`;return;}
setCategory(data.data.data);
setPosts({...posts, posts: data.data.posts.data, meta: data.data.posts});
}).catch(error=>{console.log(error);});
}
useEffect(async ()=>{
SetTitle(`Forum`);
await fetchCategories();
await fetchCategory();
setState({...state, loading: false});
}, []);
return (
state.loading
?
<Loader />
:
<div className={`flex jcc alc w-100 column`}>
<div class="jumbo w-60">
<div class="container">
<h1>Welcome to {category.title}!</h1>
<p>{category.description}</p>
{user?
<div className={`flex row justify-content-center`}>
<Link className={`btn btn-success w-20`} to={`/forum/post`}>Create a post</Link>
</div>
: null}
</div>
</div>
<div className="graphictoria-nav-splitter"></div>
<div className={`row w-60`}>
<div className={`col-3 justify-content-center`}>
<h4>Categories:</h4>
{categories.map(category=>(
<>
<Link to={`/forum/category/${category.id}`}>{category.title}</Link><br/>
</>
))}
</div>
<div className={`col justify-content-center`}>
{posts.posts.length <= 0 ? <p>There are currently no posts!</p> : null}
{posts.posts.map(post=>(
<>
<Link to={`/forum/post/${post.id}`} className={`flex graphic-post`}>
<div className={`flex column mr-15 alc`}>
[Avatar.]
</div>
<div className={`flex row m-0`}>
<h5 className={`m-0`}>{post.title}</h5>
<div className={`row fs12`}>
<p>Posted by:</p>
<Link to={`/user/${post.creator.id}`}>{post.creator.username}</Link>
</div>
</div>
</Link>
</>
))}
</div>
</div>
</div>
);
}
export default Forum;

View File

@ -9,7 +9,7 @@ import SetTitle from "../Helpers/Title.js";
import SocialCard from "../Components/Landing/SocialCard.js";
import { user } from "../helpers/utils.js";
const Home = () => {
const Home = (props) => {
useEffect(()=>{
SetTitle();
}, [])

62
web/resources/js/pages/Post.js vendored Normal file
View File

@ -0,0 +1,62 @@
// © XlXi 2021
// Graphictoria 5
import axios from 'axios';
import React, { useEffect, useState } from "react";
import { Link, useHistory, useParams } from "react-router-dom";
import Config from '../config.js';
import SetTitle from "../Helpers/Title.js";
import Loader from '../Components/Loader.js';
import { GenericErrorModal } from './Errors.js';
import { Card, CardTitle } from '../Layouts/Card.js';
var url = Config.BaseUrl.replace('http://', '');
var protocol = Config.Protocol;
const Post = (props) => {
var id = useParams().id;
const [state, setState] = useState({offline: false, loading: true});
const [post, setPost] = useState({post: [], replies: {replies: [], meta: [], currentPage: 0}});
const user = props.user;
const fetchPost = async () => {
await axios.get(`${protocol}apis.${url}/fetch/post/${id}`, {headers: {"X-Requested-With":"XMLHttpRequest"}}).then(data=>{
if (!data.data) {window.location.href=`/forum`;return;}
setPost({post: data.data.post, replies: {replies: data.data.replies.data, meta: data.data.replies, currentPage: 0}});
}).catch(error=>{console.log(error);});
}
useEffect(async ()=>{
SetTitle(`Forum`);
await fetchPost();
setState({...state, loading: false});
}, []);
return (
state.loading
?
<Loader />
:
<div className={`flex jcc alc w-100 column`}>
<div className={`graphic-post w-40`}>
{/* Time&Date goes here. */}
<div className={`flex w-100`}>
<div className={`flex column mr-15`}>
<p>Posted by:</p>
<Link to={`/user/${post.post.creator.id}`}>{post.post.creator.username}</Link>
</div>
<div className={`flex row`}>
<p className={`m-0`}>{post.post.body}</p>
</div>
</div>
</div>
</div>
);
}
export default Post;

View File

@ -823,6 +823,109 @@ a.list-group-item {
color: transparent !important;
}
.p5r {
padding: .5rem .5rem !important;
}
.flex {
display: flex;
}
.jcc {
justify-content: center;
}
.alc {
align-items: center;
}
.w-100 {
width: 100%;
}
.w-90 {
width: 90%;
}
.w-80 {
width: 80%;
}
.w-70 {
width: 70%;
}
.w-60 {
width: 60%;
}
.w-50 {
width: 50%;
}
.w-40 {
width: 40%;
}
.w-30 {
width: 30%;
}
.w-20 {
width: 20%;
}
.w-10 {
width: 10%;
}
.w-5 {
width: 5%;
}
.jumbo {
background-color: #222;
padding: 1.3rem 1.3rem;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
max-width: 60%;
border-radius: 0.25rem;
}
.column {
flex-direction: column;
}
.mr-15 {
margin-right: 15px;
}
.m-0 {
margin: 0px !important;
}
p {
margin: 0px !important;
}
.fs12 {
font-size: 12px !important;
}
.graphic-post {
padding: 1rem 1rem;
text-align: start;
color: inherit !important;
text-decoration: none !important;
background-color: #222 !important;
border-radius: 0.25px;
display: flex;
flex-direction: row;
align-items: center !important;
}
.error-dialog {
padding: 5px;
margin-bottom: 10px;

View File

@ -27,6 +27,14 @@ Route::get('/banners/data', 'BannerController@getBanners');
Route::get('/games/metadata', 'GamesController@isAvailable');
Route::get('/fetch/categories', 'Controller@fetchCategories');
Route::get('/fetch/category/{id}', 'Controller@fetchCategory');
Route::get('/fetch/posts/{id}', 'Controller@fetchPosts');
Route::get('/fetch/post/{id}', 'Controller@fetchPost');
Route::post('/fetch/user', 'Controller@fetchUser');
Route::post('/maintenance/bypass', 'MaintenanceController@bypass');
@ -35,6 +43,8 @@ Route::post('/account/register', 'Auth\RegisterController@create');
Route::post('/account/login', 'Controller@login');
Route::post('/api/create/forum', 'HomeController@createPost');
Route::fallback(function(){
return response('{"errors":[{"code":404,"message":"NotFound"}]}', 404)
->header('Cache-Control', 'private')

View File

@ -21,6 +21,14 @@ Route::get('/', function(){
return view('main');
});
Route::get('/home', function(){
return view('main');
});
Route::get('/forum', function(){
return view('main');
});
Route::get('/login', function(){
return view('main');
});