Almost complete site

Add dark / light theme

Add social sharing on meme pages

Add approving / pending admin section

Improve design

Add pagination on profiles

Make front end date time user friendly

Finish rough draft of site

And Much more...

Still need to fix a few minor things before it goes live. Almost
complete.
This commit is contained in:
dev 2021-07-26 19:39:11 -07:00
parent 52a9007882
commit dbfda5cf9e
39 changed files with 54195 additions and 663 deletions

View File

@ -1,16 +1,17 @@
## Work In Progress
Not Ready Yet...
Almost Done...
## About XmrMemes
Dependencies
Meme site for Monero. You can submit memes and get paid.
## Dependencies
```
sudo apt install php-bcmath
sudo apt install php-bcmath supervisor
```
## Running Monero Daemon
```
@ -21,16 +22,26 @@ sudo apt install php-bcmath
./monero-wallet-rpc --testnet --rpc-bind-port 28083 --disable-rpc-login --wallet-dir .
```
## Seeding
## Seeding the Database
You must seed addresses first and be connected to the daemon.
```
php artisan migrate
```
```
php artisan db:seed --class=AddressSeeder
```
## Cron Job to process payments
You must set up supervisor https://laravel.com/docs/8.x/queues#supervisor-configuration to process queues (jobs)
Then set up 1 cron job to call
```
php artisan db:seed
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
```
## This Project Uses Laravel

View File

@ -5,6 +5,8 @@ namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Jobs\ProcessPayments;
class Kernel extends ConsoleKernel
{
/**
@ -24,7 +26,7 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')->hourly();
$schedule->job(new ProcessPayments)->withoutOverlapping()->everyMinute();
}
/**

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Meme;
class ApiController extends Controller
{
public function documentation()
{
$data = [
'memes_example' => json_encode(Meme::get(), JSON_PRETTY_PRINT),
'memes_endpoint' => url('api/memes'),
];
return view('api', ['data' => $data]);
}
public function memes()
{
$memes = Meme::get();
return $memes;
}
}

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Meme;
class DashboardController extends Controller
{
@ -23,6 +24,12 @@ class DashboardController extends Controller
*/
public function index()
{
return view('dashboard');
if (\Auth::user()->is_admin === 1) {
$memes_pending = Meme::withoutGlobalScope('approved')->where('is_approved', 0)->get();
}
$data = [
'memes_pending' => $memes_pending ?? null,
];
return view('dashboard', ['data' => $data]);
}
}

View File

@ -71,6 +71,11 @@ class MemeController extends Controller
*/
public function store(Request $request)
{
$validatedData = $request->validate([
'title' => ['required', 'string', 'max:255'],
'caption' => ['string', 'max:255', 'nullable'],
'image' => ['required', 'image'],
]);
$user = \Auth::user();
$used_ids = Meme::pluck('address_id')->toArray();
$address = Address::whereNotIn('id', $used_ids)->first('id');
@ -78,7 +83,7 @@ class MemeController extends Controller
'user_id' => $user->id,
'address_id' => $address->id,
'title' => $request->input('title'),
// 'caption' => $request->input('caption'),
'caption' => $request->input('caption'),
'image' => $request->file('image'),
]);
}
@ -133,14 +138,27 @@ class MemeController extends Controller
//
}
public function approve($id)
{
if (\Auth::user()->is_admin === 1) {
$meme = Meme::withoutGlobalScope('approved')->find($id);
$meme->is_approved = 1;
$meme->save();
return redirect()->away(url()->previous());
}
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Meme $meme
* @return \Illuminate\Http\Response
*/
public function destroy(Meme $meme)
public function destroy($id)
{
//
if (\Auth::user()->is_admin === 1) {
Meme::withoutGlobalScope('approved')->where('id', $id)->delete();
return redirect()->away(url()->previous());
}
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ThemeSwitcherController extends Controller
{
public function theme(Request $request, $style)
{
$request->session()->put('theme', $style);
return redirect()->away(url()->previous());
}
}

View File

@ -2,64 +2,10 @@
namespace App\Http\Controllers;
use App\Models\Tip;
use App\Models\Address;
use App\Models\Meme;
use Illuminate\Http\Request;
use MoneroIntegrations\MoneroPhp\walletRPC;
class TipController extends Controller
{
public function check()
{
try {
$walletRPC = new walletRPC('127.0.0.1', config('app.xmr_network_port')); // Change to match your wallet (monero-wallet-rpc) IP address and port; 18083 is the customary port for mainnet, 28083 for testnet, 38083 for stagenet
$open_wallet = $walletRPC->open_wallet(config('app.xmr_wallet_name'), '');
$get_transfers = $walletRPC->get_transfers('all', true);
// dd($get_transfers);
// dd($walletRPC->get_balance());
foreach ($get_transfers['in'] as $transfer) {
$address = Address::where('address', $transfer['address'])->first();
$tip_exists = Tip::where('txid', $transfer['txid'])->first();
if ($address && !$tip_exists) {
$tip = new Tip();
$tip->address_id = $address->id;
$tip->amount = $transfer['amount'];
$tip->txid = $transfer['txid'];
$tip->height = $transfer['height'];
$tip->save();
}
}
$walletRPC->close_wallet();
} catch (\Exception $e) {
echo 'Caught exception: ', $e->getMessage(), "\n";
}
}
public function payout()
{
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
try {
$walletRPC = new walletRPC('127.0.0.1', config('app.xmr_network_port')); // Change to match your wallet (monero-wallet-rpc) IP address and port; 18083 is the customary port for mainnet, 28083 for testnet, 38083 for stagenet
$open_wallet = $walletRPC->open_wallet(config('app.xmr_wallet_name'), '');
$tip = Tip::where('is_sent', 0)->firstOrFail();
$meme = Meme::where('address_id', $tip->address_id)->first();
if ($meme->user->address) {
$send_funds = $walletRPC->sweep_all($meme->user->address, $tip->address->address_index);
$tip->is_sent = 1;
$tip->save();
}
$walletRPC->close_wallet();
} catch (\Exception $e) {
echo 'Caught exception: ', $e->getMessage(), "\n";
}
}
/**
* Display a listing of the resource.
*

View File

@ -4,13 +4,16 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Meme;
class UserController extends Controller
{
public function show($id)
{
$user = User::where('id', $id)->firstOrFail();
$data = [
'user' => User::with('memes')->where('id', $id)->firstOrFail(),
'user' => $user,
'memes' => Meme::where('user_id', $user->id)->orderByDesc('created_at')->paginate(20),
];
return view('profile', ['data' => $data]);
}

View File

@ -0,0 +1,100 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\Tip;
use App\Models\Address;
use App\Models\Meme;
use MoneroIntegrations\MoneroPhp\walletRPC;
// Can put code below inside functions to debug the Monero library
// ini_set('display_errors', 1);
// ini_set('display_startup_errors', 1);
// error_reporting(E_ALL);
class ProcessPayments implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$this->get_transactions();
$this->payout();
}
public function get_transactions()
{
try {
$walletRPC = new walletRPC('127.0.0.1', config('app.xmr_network_port')); // Change to match your wallet (monero-wallet-rpc) IP address and port; 18083 is the customary port for mainnet, 28083 for testnet, 38083 for stagenet
$open_wallet = $walletRPC->open_wallet(config('app.xmr_wallet_name'), '');
$get_transfers = $walletRPC->get_transfers('all', true);
if (isset($get_transfers['in'])) {
foreach ($get_transfers['in'] as $transfer) {
$address = Address::where('address', $transfer['address'])->first();
$tip_exists = Tip::where('txid', $transfer['txid'])->first();
if ($address && !$tip_exists) {
$tip = new Tip();
$tip->address_id = $address->id;
$tip->amount = $transfer['amount'];
$tip->txid = $transfer['txid'];
$tip->is_deposit = 1;
$tip->save();
$meme = Meme::where('address_id', $address->id)->firstOrFail();
$meme->payment_pending = 1;
$meme->save();
}
}
}
$walletRPC->close_wallet();
} catch (\Exception $e) {
echo 'Caught exception: ', $e->getMessage(), "\n";
}
}
public function payout()
{
try {
$walletRPC = new walletRPC('127.0.0.1', config('app.xmr_network_port')); // Change to match your wallet (monero-wallet-rpc) IP address and port; 18083 is the customary port for mainnet, 28083 for testnet, 38083 for stagenet
$open_wallet = $walletRPC->open_wallet(config('app.xmr_wallet_name'), '');
$meme = Meme::where('payment_pending', 1)->firstOrFail();
if ($meme->user->address) {
$send_funds = $walletRPC->sweep_all($meme->user->address, $meme->address->address_index);
if ($send_funds['amount_list']) {
$tip = new Tip;
$tip->address_id = $meme->address_id;
$tip->amount = $send_funds['amount_list'][0];
$tip->txid = $send_funds['tx_hash_list'][0];
$tip->is_deposit = 0;
$tip->save();
$meme->payment_pending = 0;
$meme->save();
}
}
$walletRPC->close_wallet();
} catch (\Exception $e) {
echo 'Caught exception: ', $e->getMessage(), "\n";
}
}
}

View File

@ -4,16 +4,25 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Storage;
use Illuminate\Database\Eloquent\SoftDeletes;
class Meme extends Model
{
use HasFactory;
use SoftDeletes;
protected $guarded = ['id'];
protected $appends = ['meme_tips_total'];
protected static function booted()
{
static::addGlobalScope('approved', function (Builder $builder) {
$builder->where('is_approved', 1);
});
}
public function user()
{
return $this->belongsTo(User::class);
@ -26,12 +35,12 @@ class Meme extends Model
public function tips()
{
return $this->hasManyThrough(Tip::class, Address::class, 'id', 'address_id', 'address_id', 'id');
return $this->hasManyThrough(Tip::class, Address::class, 'id', 'address_id', 'address_id', 'id')->orderBy('created_at', 'DESC');
}
public function getMemeTipsTotalAttribute()
{
return $this->tips->sum('amount_formatted');
return $this->tips->where('is_deposit', 1)->sum('amount_formatted');
}
public function setImageAttribute($value)

View File

@ -51,6 +51,7 @@ class User extends Authenticatable
public function tips()
{
return $this->hasManyThrough(Tip::class, Meme::class, 'address_id', 'id', 'id', 'address_id');
return $this->hasManyThrough(Tip::class, Meme::class, 'id', 'address_id');
}
@ -61,7 +62,7 @@ class User extends Authenticatable
public function getTipsTotalAttribute()
{
return $this->tips->sum('amount_formatted');
return $this->tips->where('is_deposit', 1)->sum('amount_formatted');
}
}

View File

@ -57,7 +57,7 @@ class RouteServiceProvider extends ServiceProvider
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip());
return Limit::perHour(60)->by(optional($request->user())->id ?: $request->ip());
});
}
}

View File

@ -20,6 +20,7 @@ class CreateUsersTable extends Migration
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->string('address', 95);
$table->boolean('is_admin')->default(0);
$table->rememberToken();
$table->timestamps();
});

View File

@ -20,6 +20,9 @@ class CreateMemesTable extends Migration
$table->string('title');
$table->string('caption')->nullable();
$table->string('image')->nullable();
$table->boolean('is_approved')->default(0);
$table->boolean('payment_pending')->default(0);
$table->softDeletes();
$table->timestamps();
});
}

View File

@ -18,8 +18,7 @@ class CreateTipsTable extends Migration
$table->foreignId('address_id')->constrained();
$table->bigInteger('amount');
$table->string('txid')->unique();
$table->bigInteger('height');
$table->boolean('is_sent')->default(0);
$table->boolean('is_deposit');
$table->timestamps();
});
}

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateJobsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('jobs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('jobs');
}
}

16166
public/css/app_dark.css vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,12 +7,12 @@
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root {
--blue: #ff6600;
--blue: #0d6efd;
--indigo: #6574cd;
--purple: #9561e2;
--pink: #f66d9b;
--red: #e3342f;
--orange: #f6993f;
--orange: #ff6600;
--yellow: #ffed4a;
--green: #38c172;
--teal: #4dc0b5;
@ -16155,3 +16155,7 @@ readers do not read off random characters that represent icons */
#social-links ul {
padding: 0 !important;
}
.table {
table-layout: fixed;
}

588
public/js/app.js vendored

File diff suppressed because one or more lines are too long

173
public/js/manifest.js vendored Normal file
View File

@ -0,0 +1,173 @@
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({});
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ id: moduleId,
/******/ loaded: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = __webpack_modules__;
/******/
/************************************************************************/
/******/ /* webpack/runtime/chunk loaded */
/******/ (() => {
/******/ var deferred = [];
/******/ __webpack_require__.O = (result, chunkIds, fn, priority) => {
/******/ if(chunkIds) {
/******/ priority = priority || 0;
/******/ for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
/******/ deferred[i] = [chunkIds, fn, priority];
/******/ return;
/******/ }
/******/ var notFulfilled = Infinity;
/******/ for (var i = 0; i < deferred.length; i++) {
/******/ var [chunkIds, fn, priority] = deferred[i];
/******/ var fulfilled = true;
/******/ for (var j = 0; j < chunkIds.length; j++) {
/******/ if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {
/******/ chunkIds.splice(j--, 1);
/******/ } else {
/******/ fulfilled = false;
/******/ if(priority < notFulfilled) notFulfilled = priority;
/******/ }
/******/ }
/******/ if(fulfilled) {
/******/ deferred.splice(i--, 1)
/******/ result = fn();
/******/ }
/******/ }
/******/ return result;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/global */
/******/ (() => {
/******/ __webpack_require__.g = (function() {
/******/ if (typeof globalThis === 'object') return globalThis;
/******/ try {
/******/ return this || new Function('return this')();
/******/ } catch (e) {
/******/ if (typeof window === 'object') return window;
/******/ }
/******/ })();
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/node module decorator */
/******/ (() => {
/******/ __webpack_require__.nmd = (module) => {
/******/ module.paths = [];
/******/ if (!module.children) module.children = [];
/******/ return module;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/jsonp chunk loading */
/******/ (() => {
/******/ // no baseURI
/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "/js/manifest": 0,
/******/ "css/app_dark": 0,
/******/ "css/app_light": 0
/******/ };
/******/
/******/ // no chunk on demand loading
/******/
/******/ // no prefetching
/******/
/******/ // no preloaded
/******/
/******/ // no HMR
/******/
/******/ // no HMR manifest
/******/
/******/ __webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0);
/******/
/******/ // install a JSONP callback for chunk loading
/******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
/******/ var [chunkIds, moreModules, runtime] = data;
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0;
/******/ for(moduleId in moreModules) {
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(runtime) var result = runtime(__webpack_require__);
/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/ installedChunks[chunkId][0]();
/******/ }
/******/ installedChunks[chunkIds[i]] = 0;
/******/ }
/******/ return __webpack_require__.O(result);
/******/ }
/******/
/******/ var chunkLoadingGlobal = self["webpackChunk"] = self["webpackChunk"] || [];
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
/******/ })();
/******/
/************************************************************************/
/******/
/******/
/******/ })()
;

37206
public/js/vendor.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,7 @@
{
"/js/app.js": "/js/app.js",
"/css/app.css": "/css/app.css"
"/js/app.js": "/js/app.js?id=a4f7ea48d857a5e8d88e",
"/js/manifest.js": "/js/manifest.js?id=d7335e682eb6c876d5bc",
"/css/app_dark.css": "/css/app_dark.css?id=77995f3b0798a182ca9f",
"/css/app_light.css": "/css/app_light.css?id=8971b6e0fc76badb2856",
"/js/vendor.js": "/js/vendor.js?id=964b623e0af1bb38e06e"
}

View File

@ -36,3 +36,8 @@
#social-links ul {
padding: 0 !important;
}
// Fixes text truncate not working in tables
.table {
table-layout: fixed;
}

View File

@ -1,19 +1,4 @@
// Body
$body-bg: #f8fafc;
// Typography
$font-family-sans-serif: 'Nunito', sans-serif;
$font-size-base: 0.9rem;
$line-height-base: 1.6;
// Colors
$blue: #ff6600;
$indigo: #6574cd;
$purple: #9561e2;
$pink: #f66d9b;
$red: #e3342f;
$orange: #f6993f;
$yellow: #ffed4a;
$green: #38c172;
$teal: #4dc0b5;
$cyan: #6cb2eb;

166
resources/sass/_variables_dark.scss vendored Normal file
View File

@ -0,0 +1,166 @@
// Body
$body-bg: #000000;
// Colors
$blue: #0d6efd;
$indigo: #6574cd;
$purple: #9561e2;
$pink: #f66d9b;
$red: #e3342f;
$orange: #ff6600;
$yellow: #ffed4a;
$green: #38c172;
$teal: #4dc0b5;
$cyan: #6cb2eb;
$white: #fff !default;
$gray-100: #f8f9fa !default;
$gray-200: #ebebeb !default;
$gray-300: #dee2e6 !default;
$gray-400: #ced4da !default;
$gray-500: #adb5bd !default;
$gray-600: #999 !default;
$gray-700: #444 !default;
$gray-800: #121212 !default;
$gray-900: #222 !default;
$black: #000 !default;
$primary: $orange !default;
$secondary: $gray-700 !default;
$success: $cyan !default;
$info: $cyan !default;
$warning: $yellow !default;
$danger: $red !default;
$light: $gray-800 !default;
$dark: $gray-500 !default;
$yiq-contrasted-threshold: 175 !default;
// Body
$body-bg: #121212 !default;
$body-color: $white !default;
// Links
$link-color: $primary !default;
// Tables
$table-accent-bg: $gray-900 !default;
$table-border-color: $gray-700 !default;
$table-dark-bg: $gray-500 !default;
$table-dark-border-color: darken($gray-500, 7.5%) !default;
$table-dark-color: $body-bg !default;
// Forms
$input-border-color: transparent !default;
$input-group-addon-color: $gray-500 !default;
$input-group-addon-bg: $gray-700 !default;
$custom-file-color: $gray-500 !default;
$custom-file-border-color: $gray-700 !default;
// Dropdowns
$dropdown-bg: $gray-900 !default;
$dropdown-border-color: $gray-700 !default;
$dropdown-divider-bg: $gray-700 !default;
$dropdown-link-color: $white !default;
$dropdown-link-hover-color: $white !default;
$dropdown-link-hover-bg: $primary !default;
// Navs
$nav-link-padding-x: 2rem !default;
$nav-link-disabled-color: $gray-500 !default;
$nav-tabs-border-color: $gray-700 !default;
$nav-tabs-link-hover-border-color: $nav-tabs-border-color $nav-tabs-border-color transparent !default;
$nav-tabs-link-active-color: $white !default;
$nav-tabs-link-active-border-color: $nav-tabs-border-color $nav-tabs-border-color transparent !default;
// Navbar
$navbar-dark-color: $white !default;
$navbar-dark-hover-color: $primary !default;
$navbar-light-color: rgba($white,.5) !default;
$navbar-light-hover-color: $white !default;
$navbar-light-active-color: $white !default;
$navbar-light-disabled-color: rgba($white,.3) !default;
$navbar-light-toggler-border-color: rgba($white,.1) !default;
// Pagination
$pagination-color: $white !default;
$pagination-bg: $primary !default;
$pagination-border-width: 0 !default;
$pagination-border-color: transparent !default;
$pagination-hover-color: $white !default;
$pagination-hover-bg: lighten($primary, 10%) !default;
$pagination-hover-border-color: transparent !default;
$pagination-active-bg: $pagination-hover-bg !default;
$pagination-active-border-color: transparent !default;
$pagination-disabled-color: $white !default;
$pagination-disabled-bg: darken($primary, 15%) !default;
$pagination-disabled-border-color: transparent !default;
// Jumbotron
$jumbotron-bg: $gray-900 !default;
// Cards
$card-cap-bg: $gray-700 !default;
$card-bg: $gray-900 !default;
// Popovers
$popover-bg: $gray-900 !default;
$popover-header-bg: $gray-700 !default;
// Modals
$modal-content-bg: $gray-900 !default;
$modal-content-border-color: $gray-700 !default;
$modal-header-border-color: $gray-700 !default;
// Progress bars
$progress-height: 0.625rem !default;
$progress-font-size: 0.625rem !default;
$progress-bg: $gray-700 !default;
// List group
$list-group-bg: $gray-900 !default;
$list-group-border-color: $gray-700 !default;
$list-group-hover-bg: $gray-700 !default;
// Breadcrumbs
$breadcrumb-bg: $gray-700 !default;
// Close
$close-color: $white !default;
$close-text-shadow: none !default;
// Code
$pre-color: inherit !default;

16
resources/sass/_variables_light.scss vendored Normal file
View File

@ -0,0 +1,16 @@
// Body
$body-bg: #f8fafc;
// Colors
$blue: #0d6efd;
$indigo: #6574cd;
$purple: #9561e2;
$pink: #f66d9b;
$red: #e3342f;
$orange: #ff6600;
$yellow: #ffed4a;
$green: #38c172;
$teal: #4dc0b5;
$cyan: #6cb2eb;
$primary: $orange !default;

7
resources/sass/app_dark.scss vendored Normal file
View File

@ -0,0 +1,7 @@
@import 'variables_dark';
@import 'common';
.img-qr {
background: white;
margin: 1rem 0;
}

2
resources/sass/app_light.scss vendored Normal file
View File

@ -0,0 +1,2 @@
@import 'variables_light';
@import 'common';

View File

@ -7,7 +7,7 @@
<h1>About</h1>
<p class="lead">Post Monero memes and get tipped Monero.</p>
<p class="lead mb-0">Monero (XMR) Donations Happily Accepted.</p>
<img src="{{ $data['qr'] }}" alt="QR code">
<img class="img-qr" src="{{ $data['qr'] }}" alt="QR code">
<p class="lead">{{ $data['address'] }}</p>
</div>
</div>

View File

@ -0,0 +1,23 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12">
<h1 class="text-center">API Documentation</h1>
<p class="lead mt-3 text-center">
Memes Endpoint: <code>{{ $data['memes_endpoint'] }}</code>
</p>
<p class="text-center">
API is experimental. Breaking changes may occur in the future.
</p>
<p class="mb-5 text-center">
Rate Limit: 60 Request Per Hour
</p>
<code>
<pre class="bg-white p-1">{{ $data['memes_example'] }}</pre>
</code>
</div>
</div>
</div>
@endsection

View File

@ -6,18 +6,38 @@
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Dashboard') }}</div>
<div class="card-body">
@if (session('status'))
<div class="alert alert-success" role="alert">
{{ session('status') }}
</div>
@endif
{{ __('You are logged in!') }}
</div>
</div>
</div>
</div>
@if ($data['memes_pending'])
<div class="row row-cols-1 row-cols-md-4 grid-memes mt-3">
@foreach ($data['memes_pending'] as $meme)
<div class="col mb-4">
<div class="card">
@if ($meme->image)
<img src="{{ url($meme->image) }}" class="card-img-top mx-auto d-block" alt="{{ $meme->title }} Meme">
@else
<img src="https://picsum.photos/200" class="card-img-top" alt="{{ $meme->title }} Meme">
@endif
<div class="card-body">
<h5 class="card-title">{{ $meme->title }}</h5>
<p class="card-text">{{ $meme->caption }}</p>
<p class="card-text"><a href="{{ url('meme/approve/' . $meme->id) }}">Approve</a> | <a href="{{ url('meme/destroy/' . $meme->id) }}">Delete</a></p>
<p class="card-text">By: <a href="{{ url('/user/' . $meme->user->id) }}">{{ $meme->user->name }}</a></p>
<p class="card-text">{{ $meme->created_at->diffForHumans() }}</p>
</div>
</div>
</div>
@endforeach
</div>
@endif
</div>
@endsection

View File

@ -23,7 +23,7 @@
</a>
<p class="card-text">By: <a href="{{ url('/user/' . $meme->user->id) }}">{{ $meme->user->name }}</a></p>
<p class="card-text">{{ $meme->meme_tips_total }} XMR Recieved</p>
<p class="card-text">{{ $meme->created_at }}</p>
<p class="card-text">{{ $meme->created_at->diffForHumans() }}</p>
</div>
</div>
</div>

View File

@ -13,17 +13,18 @@
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}" defer></script>
<!-- Fonts -->
{{-- <link rel="dns-prefetch" href="//fonts.gstatic.com"> --}}
{{-- <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet"> --}}
<!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
@if (Session::get('theme'))
<link href="{{ url('') . mix('css/app_' . Session::get('theme') . '.css') }}" rel="stylesheet">
@else
<link href="{{ url('') . mix('css/app_light.css') }}" rel="stylesheet">
@endif
</head>
<body>
<div id="app">
<nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
<nav class="navbar navbar-expand-md navbar-light bg-light shadow-sm">
<div class="container">
<a class="navbar-brand" href="{{ url('/') }}">
<img class="img-logo" src="{{ url('logo.png') }}" alt="">
@ -51,6 +52,14 @@
<!-- Right Side Of Navbar -->
<ul class="navbar-nav ml-auto">
<span class="nav-item mx-2 my-auto" data-toggle="tooltip" data-placement="bottom" title="{{ Session::get('theme') === 'light' ? 'Dark' : 'Light' }} Theme">
@if (Session::get('theme') === 'dark')
<a class="btn btn-light" href="{{ url('/theme/light') }}" role="button"><i class="fas fa-sun text-primary"></i></a>
@else
<a class="btn btn-light" href="{{ url('/theme/dark') }}" role="button"><i class="far fa-moon text-dark"></i></a>
@endif
</span>
<!-- Authentication Links -->
@guest
@if (Route::has('login'))
@ -96,12 +105,11 @@
</div>
</div>
</nav>
<main class="py-4">
@yield('content')
</main>
<footer class="container-fluid py-5 bg-white shadow-lg text-center">
<footer class="container-fluid py-5 bg-light shadow-lg text-center">
<div class="row">
<div class="col-6 col-md">
<ul class="list-unstyled">
@ -115,7 +123,7 @@
</div>
<div class="col-6 col-md">
<ul class="list-unstyled">
<li><a href="#">API</a></li>
<li><a href="{{ url('api') }}">API</a></li>
</ul>
</div>
<div class="col-6 col-md">

View File

@ -6,6 +6,15 @@
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Submit Meme') }}</div>
@if ($errors->any())
<div class="alert alert-danger mt-3 mb-0">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="card-body">
<form method="POST" action="{{ route('meme-create') }}" enctype="multipart/form-data">
@csrf

View File

@ -16,6 +16,9 @@
<div class="card">
<div class="card-header">
<h5 class="card-title">{{ $data['meme']->title }}</h5>
@if ($data['meme']->caption)
<p class="card-text mb-1">{{ $data['meme']->caption }}</p>
@endif
<p class="card-text">By: <a href="{{ url('/user/' . $data['meme']->user->id) }}">{{ $data['meme']->user->name }}</a></p>
</div>
@if ($data['meme']->image)
@ -30,7 +33,7 @@
</div>
<div class="card-footer">
<h5 class="card-title">{{ $data['meme']->meme_tips_total }} XMR Recieved</h5>
<p class="card-text">Published {{ $data['meme']->created_at }}</p>
<p class="card-text">Published {{ $data['meme']->created_at->diffForHumans() }}</p>
</div>
</div>
</div>
@ -38,31 +41,62 @@
<div class="row text-center justify-content-center">
<div class="col-md-12">
<h3 class="mt-4 mb-0">Tip The Creator</h3>
<img src="{{ $data['qr'] }}" alt="QR code">
<p class="">{{ $data['meme']->address->address }}</p>
<img class="img-qr" src="{{ $data['qr'] }}" alt="QR code">
<p class="text-break">{{ $data['meme']->address->address }}</p>
</div>
<div class="col-6">
<h3 class="mt-4">Monero Tips Recieved</h3>
<div class="col-12 col-lg-6">
<h4 class="mt-4">Monero Tips Recieved</h4>
<div class="table-responsive-md text-left mt-4">
<table class="table">
<thead>
<tr>
<th scope="col">Amount</th>
<th scope="col">Date Recieved</th>
<th scope="col">Transaction ID</th>
<th scope="col">Date</th>
</tr>
</thead>
<tbody>
@foreach ($data['meme']['tips'] as $tip)
@if ($tip->is_deposit === 1)
<tr>
<td>{{ $tip->amount_formatted }}</td>
<td>{{ $tip->created_at->diffForHumans() }}</td>
<td class="text-truncate">
<a href="https://testnet.xmrchain.net/tx/{{ $tip->txid }}" target="_blank">
{{ $tip->txid }}
</a>
</td>
</tr>
@endif
@endforeach
</tbody>
</table>
</div>
</div>
<div class="col-12 col-lg-6">
<h4 class="mt-4">Monero Tips Sent</h4>
<div class="table-responsive-md text-left mt-4">
<table class="table">
<thead>
<tr>
<td>{{ $tip->amount_formatted }}</td>
<td class="d-inline-block text-truncate" style="max-width: 150px;">
<a href="https://testnet.xmrchain.net/tx/{{ $tip->txid }}" target="_blank">
{{ $tip->txid }}
</a>
</td>
<td>{{ $tip->created_at }}</td>
<th scope="col">Amount</th>
<th scope="col">Date Sent</th>
<th scope="col">Transaction ID</th>
</tr>
</thead>
<tbody>
@foreach ($data['meme']['tips'] as $tip)
@if ($tip->is_deposit === 0)
<tr>
<td>{{ $tip->amount_formatted }}</td>
<td>{{ $tip->created_at->diffForHumans() }}</td>
<td class="text-truncate">
<a href="https://testnet.xmrchain.net/tx/{{ $tip->txid }}" target="_blank">
{{ $tip->txid }}
</a>
</td>
</tr>
@endif
@endforeach
</tbody>
</table>

View File

@ -8,7 +8,7 @@
<p class="text-center">Total Earnings: {{ $data['user']->tips_total }}</h3>
<p class="text-center">Total Memes: {{ $data['user']->memes_total }}</h3>
<div class="row row-cols-1 row-cols-md-4 grid-memes">
@foreach ($data['user']->memes as $meme)
@foreach ($data['memes'] as $meme)
<div class="col mb-4">
<div class="card">
<a href="{{ url('/meme/' . $meme->id) }}">
@ -23,7 +23,7 @@
<h5 class="card-title">{{ $meme->title }}</h5>
</a>
<p class="card-text">{{ $meme->meme_tips_total }} XMR Recieved</p>
<p class="card-text">{{ $meme->created_at }}</p>
<p class="card-text">{{ $meme->created_at->diffForHumans() }}</p>
</div>
</div>
</div>
@ -31,5 +31,8 @@
</div>
</div>
</div>
<div class="d-flex justify-content-center w-100">
{{ $data['memes']->links() }}
</div>
</div>
@endsection

View File

@ -17,3 +17,5 @@ use Illuminate\Support\Facades\Route;
Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});
Route::middleware('api')->get('memes', [App\Http\Controllers\ApiController::class, 'memes']);

View File

@ -15,16 +15,15 @@ use Illuminate\Support\Facades\Route;
Auth::routes();
Route::get('/', [App\Http\Controllers\MemeController::class, 'index'])->name('homepage');
Route::get('meme/{id}', [App\Http\Controllers\MemeController::class, 'show'])->name('meme');
Route::get('user/{id}', [App\Http\Controllers\UserController::class, 'show'])->name('user');
Route::get('/leaderboard', [App\Http\Controllers\MemeController::class, 'leaderboard'])->name('leaderboard');
Route::get('/about', [App\Http\Controllers\AboutController::class, 'index'])->name('about');
Route::get('/dashboard', [App\Http\Controllers\DashboardController::class, 'index'])->name('dashboard');
Route::get('meme/{id}', [App\Http\Controllers\MemeController::class, 'show'])->name('meme');
Route::get('meme/approve/{id}', [App\Http\Controllers\MemeController::class, 'approve']);
Route::get('meme/destroy/{id}', [App\Http\Controllers\MemeController::class, 'destroy']);
Route::get('/submit/meme', [App\Http\Controllers\MemeController::class, 'create'])->name('meme')->middleware('auth');
Route::post('/meme-create', [App\Http\Controllers\MemeController::class, 'store'])->name('meme-create');
// API
Route::get('tips/check', [App\Http\Controllers\TipController::class, 'check']);
Route::get('tips/payout', [App\Http\Controllers\TipController::class, 'payout']);
Route::post('/meme-create', [App\Http\Controllers\MemeController::class, 'store'])->name('meme-create')->middleware('auth');
Route::get('user/{id}', [App\Http\Controllers\UserController::class, 'show'])->name('user');
Route::get('/dashboard', [App\Http\Controllers\DashboardController::class, 'index'])->name('dashboard');
Route::get('/leaderboard', [App\Http\Controllers\MemeController::class, 'leaderboard'])->name('leaderboard');
Route::get('/api', [App\Http\Controllers\ApiController::class, 'documentation'])->name('api');
Route::get('/theme/{style}', [App\Http\Controllers\ThemeSwitcherController::class, 'theme']);

4
webpack.mix.js vendored
View File

@ -12,5 +12,5 @@ const mix = require('laravel-mix');
*/
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css')
.sourceMaps();
.sass('resources/sass/app_light.scss', 'public/css').extract().version()
.sass('resources/sass/app_dark.scss', 'public/css').extract().version();