Compare commits
2 Commits
d03bab8e02
...
20b2581a19
Author | SHA1 | Date |
---|---|---|
Notoric | 20b2581a19 | |
Notoric | 25a8619c00 |
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\Link_interaction;
|
||||||
|
|
||||||
|
class Link_interactionController extends Controller
|
||||||
|
{
|
||||||
|
public static function getList(string $id) {
|
||||||
|
$link_interaction = new Link_interaction();
|
||||||
|
$link_interactions = $link_interaction->getRecords($id);
|
||||||
|
return $link_interactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function access(Request $request) {
|
||||||
|
$link_interaction = new Link_interaction();
|
||||||
|
$link_interaction->create($request->id, $request->ip());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getCountryArray(string $id) {
|
||||||
|
$link_interaction = new Link_interaction();
|
||||||
|
$country_array = $link_interaction->getCountryArray($id);
|
||||||
|
return $country_array;
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ class RegisterController extends Controller {
|
||||||
|
|
||||||
auth()->login($user);
|
auth()->login($user);
|
||||||
|
|
||||||
return redirect('/profile');
|
return redirect('/home');
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
return redirect()->back()->withInput($request->input())->withErrors($e->errors());
|
return redirect()->back()->withInput($request->input())->withErrors($e->errors());
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Link_interaction;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Models\Shortlink;
|
use App\Models\Shortlink;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
|
||||||
class ShortlinkController extends Controller
|
class ShortlinkController extends Controller
|
||||||
{
|
{
|
||||||
|
@ -18,6 +20,21 @@ class ShortlinkController extends Controller
|
||||||
]);
|
]);
|
||||||
|
|
||||||
//check if url returns 200 at its final redirect
|
//check if url returns 200 at its final redirect
|
||||||
|
$guzzle = new Client([
|
||||||
|
'timeout' => 5
|
||||||
|
]);
|
||||||
|
try {
|
||||||
|
$response = $guzzle->get($request->url, ['allow_redirects' => ['track_redirects' => true]]);
|
||||||
|
if ($response->getStatusCode() != 200) {
|
||||||
|
return back()->withErrors([
|
||||||
|
'error' => 'The URL provided did not return a valid response'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return back()->withErrors([
|
||||||
|
'error' => 'The URL provided did not return a valid response'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$shortlink = new Shortlink();
|
$shortlink = new Shortlink();
|
||||||
$shortlink->create($request->url, auth()->id());
|
$shortlink->create($request->url, auth()->id());
|
||||||
|
@ -27,17 +44,77 @@ class ShortlinkController extends Controller
|
||||||
'error' => $e->getMessage()
|
'error' => $e->getMessage()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function goto(Request $request, $id) {
|
public static function goto(Request $request, $id) {
|
||||||
try {
|
try {
|
||||||
$shortlink = (new Shortlink())->get($id);
|
$shortlink = (new Shortlink())->get($id);
|
||||||
// check if the link is expired or if it has reached the max clicks
|
// check if the link is expired or if it has reached the max clicks
|
||||||
|
if ($shortlink->expires_at != null && strtotime($shortlink->expires_at) < time()) {
|
||||||
|
return response()->json(['error' => 'This link has expired'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($shortlink->max_clicks != null && $shortlink->max_clicks <= (new Link_interaction())->getTotalInteractions($id)) {
|
||||||
|
return response()->json(['error' => 'This link has reached the maximum number of clicks'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
// log the interaction
|
// log the interaction
|
||||||
|
[Link_interactionController::class, 'access']($request);
|
||||||
|
|
||||||
return redirect($shortlink->destination);
|
return redirect($shortlink->destination);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return response()->json(['error' => $e->getMessage()], 404);
|
return response()->json(['error' => $e->getMessage()], 404);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDetails(Request $request, $id) {
|
||||||
|
try {
|
||||||
|
$shortlink = (new Shortlink())->get($id);
|
||||||
|
if ($shortlink->user_id != auth()->id()) {
|
||||||
|
return response()->json(['error' => 'Unauthorized'], 401);
|
||||||
|
}
|
||||||
|
$countrylist = (new Link_interactionController)->getCountryArray($id);
|
||||||
|
return view('details', ['shortlink' => $shortlink, 'countrylist' => $countrylist]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['error' => $e->getMessage()], 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, $id) {
|
||||||
|
try {
|
||||||
|
$shortlink = (new Shortlink())->get($id);
|
||||||
|
if ($shortlink->user_id != auth()->id()) {
|
||||||
|
return response()->json(['error' => 'Unauthorized'], 401);
|
||||||
|
}
|
||||||
|
$request['expiry-toggle'] = $request->input('expiry-toggle') == 'on';
|
||||||
|
$data = $request->validate([
|
||||||
|
'maxclicks' => 'required|integer|min:0',
|
||||||
|
'expiry-toggle' => 'required|boolean'
|
||||||
|
]);
|
||||||
|
if ($request->input('expiry-toggle')) {
|
||||||
|
$request->validate([
|
||||||
|
'expiry-date' => 'date_format:Y-m-d|required',
|
||||||
|
'expiry-hour' => 'date_format:H|min:0|max:23|required',
|
||||||
|
'expiry-minute' => 'date_format:i|min:0|max:59|required'
|
||||||
|
]);
|
||||||
|
$request->expires_at = \DateTime::createFromFormat('Y-m-d H:i', $request['expiry-date'] . ' ' . $request['expiry-hour'] . ':' . $request['expiry-minute']);
|
||||||
|
} else {
|
||||||
|
$request->expires_at = null;
|
||||||
|
}
|
||||||
|
$shortlink->modify($request->maxclicks, $request->expires_at);
|
||||||
|
return back();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['error' => $e->getMessage()], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(Request $request, $id) {
|
||||||
|
try {
|
||||||
|
$shortlink = (new Shortlink())->get($id);
|
||||||
|
$shortlink->delete();
|
||||||
|
return redirect('profile');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['error' => $e->getMessage()], 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
//TODO
|
||||||
|
class Link_interaction extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'link',
|
||||||
|
'ip',
|
||||||
|
'latitude',
|
||||||
|
'longitude',
|
||||||
|
'country',
|
||||||
|
'country_code'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function create(string $link, string $ip): Link_interaction {
|
||||||
|
$this->link = $link;
|
||||||
|
$this->ip = $ip;
|
||||||
|
|
||||||
|
$url = "http://ip-api.com/json/$ip?fields=49347";
|
||||||
|
$curl = curl_init($url);
|
||||||
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
$response = curl_exec($curl);
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
|
||||||
|
if ($data['status'] == 'fail') {
|
||||||
|
$this->latitude = null;
|
||||||
|
$this->longitude = null;
|
||||||
|
$this->country = null;
|
||||||
|
$this->country_code = null;
|
||||||
|
$this->save();
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->latitude = $data['lat'] ?? null;
|
||||||
|
$this->longitude = $data['lon'] ?? null;
|
||||||
|
$this->country = $data['country'] ?? null;
|
||||||
|
$this->country_code = $data['countryCode'] ?? null;
|
||||||
|
|
||||||
|
$this->save();
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRecords(string $link) {
|
||||||
|
$link_interaction = [];
|
||||||
|
$link_interaction = Link_interaction::where('link', $link)->get()->toArray();
|
||||||
|
return $link_interaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCount(string $link) {
|
||||||
|
$link_interaction = [];
|
||||||
|
$link_interaction = Link_interaction::where('link', $link)->count();
|
||||||
|
return $link_interaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCountryArray(string $link) {
|
||||||
|
$link_interaction = [];
|
||||||
|
$link_interaction = Link_interaction::where('link', $link)->select('country', Link_interaction::raw('count(*) as total'), 'country_code')->groupBy('country', 'country_code')->get()->toArray();
|
||||||
|
$country_list = [];
|
||||||
|
foreach ($link_interaction as $country) {
|
||||||
|
if ($country['country_code'] == null) {
|
||||||
|
$country['emoji'] = '❓';
|
||||||
|
} else {
|
||||||
|
$country['emoji'] = preg_replace_callback('/./', static fn (array $letter) => mb_chr(ord($letter[0]) % 32 + 0x1F1E5), $country['country_code']);
|
||||||
|
}
|
||||||
|
array_push($country_list, $country);
|
||||||
|
}
|
||||||
|
return $country_list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotalInteractions(string $link) : int {
|
||||||
|
$link_interaction = Link_interaction::where('link', $link)->count();
|
||||||
|
return $link_interaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTimes(string $link) {
|
||||||
|
$link_interaction = [];
|
||||||
|
$link_interaction = Link_interaction::where('link', $link)->select('created_at')->get()->toArray();
|
||||||
|
return $link_interaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCoordinates(string $link) {
|
||||||
|
$link_interaction = [];
|
||||||
|
$link_interaction = Link_interaction::where('link', $link)->select('latitude', 'longitude')->get()->toArray();
|
||||||
|
return $link_interaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -10,11 +10,13 @@ class Shortlink extends Model
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
|
'id',
|
||||||
'shortid',
|
'shortid',
|
||||||
'destination',
|
'destination',
|
||||||
'user_id',
|
'user_id',
|
||||||
'max_clicks',
|
'max_clicks',
|
||||||
'expires_at',
|
'expires_at',
|
||||||
|
'deleted'
|
||||||
];
|
];
|
||||||
|
|
||||||
public function create(string $url, int $user_id): Shortlink {
|
public function create(string $url, int $user_id): Shortlink {
|
||||||
|
@ -23,6 +25,7 @@ class Shortlink extends Model
|
||||||
$this->user_id = $user_id;
|
$this->user_id = $user_id;
|
||||||
$this->max_clicks = 0;
|
$this->max_clicks = 0;
|
||||||
$this->expires_at = null;
|
$this->expires_at = null;
|
||||||
|
$this->deleted = false;
|
||||||
$this->save();
|
$this->save();
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
|
"guzzlehttp/guzzle": "^7.8",
|
||||||
"laravel/framework": "^11.9",
|
"laravel/framework": "^11.9",
|
||||||
"laravel/tinker": "^2.9"
|
"laravel/tinker": "^2.9"
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "7e8c3c14ff33b199b4a0838993eb8423",
|
"content-hash": "55d18cb1b7b1ab5b14edeb92768c4021",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
|
|
|
@ -12,10 +12,12 @@ return new class extends Migration
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('shortlinks', function (Blueprint $table) {
|
Schema::create('shortlinks', function (Blueprint $table) {
|
||||||
$table->string('shortid')->unique()->primary();
|
$table->id();
|
||||||
|
$table->string('shortid')->unique();
|
||||||
$table->string('destination');
|
$table->string('destination');
|
||||||
$table->foreignId('user_id')->references('id')->on('users');
|
$table->foreignId('user_id')->references('id')->on('users');
|
||||||
$table->integer('max_clicks')->default(0);
|
$table->integer('max_clicks')->default(0);
|
||||||
|
$table->boolean('deleted')->default(false);
|
||||||
$table->timestamp('expires_at')->nullable();
|
$table->timestamp('expires_at')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
@ -25,6 +27,10 @@ return new class extends Migration
|
||||||
$table->string('link');
|
$table->string('link');
|
||||||
$table->foreign('link')->references('shortid')->on('shortlinks');
|
$table->foreign('link')->references('shortid')->on('shortlinks');
|
||||||
$table->string('ip');
|
$table->string('ip');
|
||||||
|
$table->decimal('latitude', 10, 8)->nullable();
|
||||||
|
$table->decimal('longitude', 11, 8)->nullable();
|
||||||
|
$table->string('country')->nullable();
|
||||||
|
$table->string('country_code')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,15 @@ input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 20px 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
background-color: var(--foreground);
|
background-color: var(--foreground);
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
@ -26,6 +35,15 @@ header {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 1.4em;
|
font-size: 1.4em;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
|
box-shadow: 0px 3px 10px 0 #0008;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
header a {
|
header a {
|
||||||
|
@ -57,9 +75,6 @@ header nav a {
|
||||||
.container {
|
.container {
|
||||||
width: 1100px;
|
width: 1100px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-container {
|
.form-container {
|
||||||
|
@ -95,7 +110,7 @@ header nav a {
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-container form button {
|
.form-container button, button[type="submit"] {
|
||||||
margin-block: 10px;
|
margin-block: 10px;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
border: none;
|
border: none;
|
||||||
|
@ -106,11 +121,11 @@ header nav a {
|
||||||
transition: box-shadow 0.3s ease-out, background-color 0.3s ease-out;
|
transition: box-shadow 0.3s ease-out, background-color 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-container button:hover {
|
.form-container button:hover, button[type="submit"]:hover {
|
||||||
box-shadow: 0px 0px 10px 0 #ff005080;
|
box-shadow: 0px 0px 10px 0 #ff005080;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-container button:active {
|
.form-container button:active, button[type="submit"]:active {
|
||||||
background-color: #cc0035;
|
background-color: #cc0035;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +137,7 @@ header nav a {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#shortener-container {
|
#banner-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -132,12 +147,17 @@ header nav a {
|
||||||
gap: 40px;
|
gap: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#shortener-container h1 {
|
#banner-container a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#banner-container h1 {
|
||||||
font-size: 2.5em;
|
font-size: 2.5em;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
#shortener-container input {
|
#banner-container input {
|
||||||
color: black;
|
color: black;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
@ -149,7 +169,7 @@ header nav a {
|
||||||
box-shadow: 2px 2px 5px 0 #0004;
|
box-shadow: 2px 2px 5px 0 #0004;
|
||||||
}
|
}
|
||||||
|
|
||||||
#shortener-container button {
|
#banner-container button {
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
border: none;
|
border: none;
|
||||||
|
@ -159,6 +179,188 @@ header nav a {
|
||||||
background-color: var(--foreground);
|
background-color: var(--foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
#shortener-container input::placeholder {
|
#banner-container input::placeholder {
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
#banner-container.container {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#banner-container.container p, #banner-container.container h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 4fr;
|
||||||
|
font-size: 1.4em;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-container label {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-container p, #info-container a {
|
||||||
|
grid-column: 2 / span 2;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#info-container #maxclicks {
|
||||||
|
width: 80px;
|
||||||
|
height: 24px;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 1em;
|
||||||
|
background-color: #0000;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-container label {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-container #expiry-toggle {
|
||||||
|
display: inline;
|
||||||
|
grid-row: 5;
|
||||||
|
grid-column: 2;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-container #expiry-date {
|
||||||
|
grid-column: 2;
|
||||||
|
width: 150px;
|
||||||
|
grid-row: 5;
|
||||||
|
margin-left: 95px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-container #date-label {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 5;
|
||||||
|
align-self: center;
|
||||||
|
color: #aaa;
|
||||||
|
margin-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-container #time-label {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 5;
|
||||||
|
align-self: center;
|
||||||
|
color: #aaa;
|
||||||
|
margin-left: 270px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-container #expiry-hour, #info-container #expiry-minute {
|
||||||
|
margin-left: 330px;
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 5;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
height: 25px;
|
||||||
|
width: 25px;
|
||||||
|
align-self: center;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#maxclicks-label {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 4;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#maxclicks-info {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
#expiry-label {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#expiry-info {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-container #expiry-minute {
|
||||||
|
margin-left: 390px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-container #time-separator {
|
||||||
|
font-size: 30px;
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 5;
|
||||||
|
color: #aaa;
|
||||||
|
margin-left: 381px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-container #created {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
&::before {
|
||||||
|
content: 'Created: ';
|
||||||
|
font-size: 0.6em;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-container #updated {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
grid-column: 2;
|
||||||
|
margin-left: 360px;
|
||||||
|
grid-row: 1;
|
||||||
|
&::before {
|
||||||
|
content: 'Updated: ';
|
||||||
|
font-size: 0.6em;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-container #timestamp-label {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
align-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-container #expiry-date, #info-container #expiry-hour, #info-container #expiry-minute {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#info-container button {
|
||||||
|
grid-row: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#timestamp-label, #destination-label, #URL-label, #maxclicks-label {
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: flex;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #888;
|
||||||
|
width: 500%;
|
||||||
|
transform: translateY(5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
align-self: center;
|
||||||
|
font-size: 0.8em;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="64px" height="64px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||||
|
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<g id="SVGRepo_iconCarrier"> <path opacity="0.5" d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" fill="#fff"/> <path d="M12 17.75C12.4142 17.75 12.75 17.4142 12.75 17V11C12.75 10.5858 12.4142 10.25 12 10.25C11.5858 10.25 11.25 10.5858 11.25 11V17C11.25 17.4142 11.5858 17.75 12 17.75Z" fill="#fff"/> <path d="M12 7C12.5523 7 13 7.44771 13 8C13 8.55229 12.5523 9 12 9C11.4477 9 11 8.55229 11 8C11 7.44771 11.4477 7 12 7Z" fill="#fff"/> </g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 930 B |
|
@ -0,0 +1,11 @@
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="64px" height="64px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
|
||||||
|
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||||
|
|
||||||
|
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
|
||||||
|
<g id="SVGRepo_iconCarrier"> <circle cx="12" cy="12" r="9" stroke="#fff" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12 11V17" stroke="#fff" stroke-linecap="round" stroke-linejoin="round"/> <path d="M11.75 8V7H12.25V8H11.75Z" stroke="#fff" stroke-linecap="round" stroke-linejoin="round"/> </g>
|
||||||
|
|
||||||
|
</svg>
|
After Width: | Height: | Size: 728 B |
|
@ -0,0 +1,40 @@
|
||||||
|
const expiry_checkbox = document.getElementById('expiry-toggle');
|
||||||
|
|
||||||
|
function toggleExpiryElements() {
|
||||||
|
const expirydate = document.getElementById('expiry-date');
|
||||||
|
const expiryhour = document.getElementById('expiry-hour');
|
||||||
|
const expiryminute = document.getElementById('expiry-minute');
|
||||||
|
|
||||||
|
if (expiry_checkbox.checked) {
|
||||||
|
expirydate.disabled = false;
|
||||||
|
expiryhour.disabled = false;
|
||||||
|
expiryminute.disabled = false;
|
||||||
|
} else {
|
||||||
|
expirydate.disabled = true;
|
||||||
|
expiryhour.disabled = true;
|
||||||
|
expiryminute.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expiry_checkbox.addEventListener('change', toggleExpiryElements);
|
||||||
|
|
||||||
|
toggleExpiryElements();
|
||||||
|
|
||||||
|
const destination_button = document.getElementById('destination_clipboard');
|
||||||
|
const url_button = document.getElementById('url_clipboard');
|
||||||
|
|
||||||
|
function clipboardButton(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
let button = event.target;
|
||||||
|
let parent = button.parentElement;
|
||||||
|
let url = parent.getAttribute('href');
|
||||||
|
navigator.clipboard.writeText(url).then(() => {
|
||||||
|
button.textContent = '✔️';
|
||||||
|
setTimeout(() => {
|
||||||
|
button.textContent = '🔗';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
destination_button.addEventListener('click', event => clipboardButton(event));
|
||||||
|
url_button.addEventListener('click', event => clipboardButton(event));
|
|
@ -9,11 +9,11 @@
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a id="logo" href="home">[slink]</a>
|
<a id="logo" href="/home">[slink]</a>
|
||||||
@if (Auth::check())
|
@if (Auth::check())
|
||||||
<nav>
|
<nav>
|
||||||
<a href="profile"><img src="{{ asset('img/icons/profile.svg') }}"></a>
|
<a href="profile"><img title="Profile" src="{{ asset('img/icons/profile.svg') }}"></a>
|
||||||
<a href="logout"><img src="{{ asset('img/icons/logout.svg') }}"></a>
|
<a href="logout"><img title="Logout" src="{{ asset('img/icons/logout.svg') }}"></a>
|
||||||
</nav>
|
</nav>
|
||||||
@else
|
@else
|
||||||
<nav>
|
<nav>
|
||||||
|
@ -26,5 +26,6 @@
|
||||||
<main>
|
<main>
|
||||||
@yield('content')
|
@yield('content')
|
||||||
</main>
|
</main>
|
||||||
|
@yield('scripts')
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -0,0 +1,90 @@
|
||||||
|
@extends('default')
|
||||||
|
|
||||||
|
@section('title')
|
||||||
|
Details & Logs
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div id="banner-container" class="container">
|
||||||
|
<h1>Details & Logs</h1>
|
||||||
|
<p>View and edit information for your link</p>
|
||||||
|
</div>
|
||||||
|
<div id="details" class="container">
|
||||||
|
<h2>Details</h2>
|
||||||
|
<form id="update-form" method="post" action="{{ url()->current() }}/update">
|
||||||
|
@csrf
|
||||||
|
<div id="info-container">
|
||||||
|
<label id="destination-label" for="destination">Destination</label>
|
||||||
|
<a href="{{ $shortlink->destination }}" target="_blank" id="destination">{{ $shortlink->destination }}<button title="Copy Link" id="destination_clipboard">🔗</button></a>
|
||||||
|
<label id="URL-label" for="url">URL</label>
|
||||||
|
<a href="{{ url()->to($shortlink->shortid) }}" target="_blank" id="url">{{ url()->to($shortlink->shortid) }}<button title="Copy Link" id="url_clipboard">🔗</button></a>
|
||||||
|
<label id="maxclicks-label" for="maxclicks">Max Clicks</label>
|
||||||
|
<img id="maxclicks-info" class="info" src="{{ asset("img/icons/info.svg") }}" title="This link will stop working after the maximum number of clicks has been reached. Set this to 0 to allow an infinite number of uses.">
|
||||||
|
<input id="maxclicks" name="maxclicks" type="number" value="{{ $shortlink->max_clicks }}" required>
|
||||||
|
<label id="expiry-label" for="expiry-toggle">Expiry</label>
|
||||||
|
<img id="expiry-info" class="info" src="{{ asset("img/icons/info.svg") }}" title="This link will stop working after the specified time. This uses UTC time.">
|
||||||
|
<input id="expiry-toggle" name="expiry-toggle" type="checkbox"
|
||||||
|
@if ($shortlink->expires_at != null)
|
||||||
|
checked
|
||||||
|
@endif
|
||||||
|
>
|
||||||
|
<label id="date-label" for="expiry-date">Date</label>
|
||||||
|
<input id="expiry-date" name="expiry-date" type="date"
|
||||||
|
@if ($shortlink->expires_at != null)
|
||||||
|
value="{{ Carbon\Carbon::parse($shortlink->expires_at)->format('Y-m-d') }}"
|
||||||
|
@endif
|
||||||
|
>
|
||||||
|
<label id="time-label" for="expiry-hour">Time</label>
|
||||||
|
<select id="expiry-hour" name="expiry-hour">
|
||||||
|
@for ($i = 0; $i < 24; $i++)
|
||||||
|
{{$j = str_pad($i, 2, '0', STR_PAD_LEFT)}}
|
||||||
|
<option value="{{ $j }}"
|
||||||
|
@if ($shortlink->expires_at != null && Carbon\Carbon::parse($shortlink->expires_at)->hour == $j)
|
||||||
|
selected
|
||||||
|
@endif
|
||||||
|
>{{ $j }}</option>
|
||||||
|
@endfor
|
||||||
|
</select>
|
||||||
|
<label id="time-separator" for="expiry-minute">:</label>
|
||||||
|
<select id="expiry-minute" name=expiry-minute>
|
||||||
|
@for ($i = 0; $i < 60; $i+=5)
|
||||||
|
{{$j = str_pad($i, 2, '0', STR_PAD_LEFT)}}
|
||||||
|
<option value="{{ $j }}"
|
||||||
|
@if ($shortlink->expires_at != null && Carbon\Carbon::parse($shortlink->expires_at)->minute == $j)
|
||||||
|
selected
|
||||||
|
@endif
|
||||||
|
>{{ $j }}</option>
|
||||||
|
@endfor
|
||||||
|
</select>
|
||||||
|
<label id="timestamp-label" for="created">Timestamps</label>
|
||||||
|
<p id="created">{{ Carbon\Carbon::parse($shortlink->created_at)->format('H:i l jS F Y') }}</p>
|
||||||
|
<p id="updated">{{ Carbon\Carbon::parse($shortlink->updated_at)->format('H:i l jS F Y') }}</p>
|
||||||
|
<button type="submit" form="update-form">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="graphs" class="container">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div id="stats" class="container">
|
||||||
|
<h2>Link Clicks</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<th>📍</th>
|
||||||
|
<th>Country</th>
|
||||||
|
<th>Clicks</th>
|
||||||
|
</thead>
|
||||||
|
@foreach ($countrylist as $country)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $country['emoji'] }}</td>
|
||||||
|
<td>{{ $country['country'] ?? 'Unknown' }}</td>
|
||||||
|
<td>{{ $country['total'] }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
<script src="{{ asset('js/details.js') }}"></script>
|
||||||
|
@endsection
|
|
@ -5,7 +5,7 @@
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div id="shortener-container">
|
<div id="banner-container">
|
||||||
@if (auth()->check())
|
@if (auth()->check())
|
||||||
<h1>Shorten your URL</h1>
|
<h1>Shorten your URL</h1>
|
||||||
<form action="shorten" method="post">
|
<form action="shorten" method="post">
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Link_interactionController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use App\Http\Controllers\RegisterController;
|
use App\Http\Controllers\RegisterController;
|
||||||
use App\Http\Controllers\LoginController;
|
use App\Http\Controllers\LoginController;
|
||||||
use App\Http\Controllers\ShortlinkController;
|
use App\Http\Controllers\ShortlinkController;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
return view('welcome');
|
return redirect('/home');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::get('/register', function () {
|
Route::get('/register', function () {
|
||||||
|
@ -29,12 +30,12 @@ Route::post('/login', [LoginController::class, 'login']);
|
||||||
|
|
||||||
Route::get('/logout', function () {
|
Route::get('/logout', function () {
|
||||||
auth()->logout();
|
auth()->logout();
|
||||||
return redirect('/home');
|
return redirect('home');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::get('/profile', function () {
|
Route::get('/profile', function () {
|
||||||
if (!auth()->check()) {
|
if (!auth()->check()) {
|
||||||
return redirect('/home');
|
return redirect('home');
|
||||||
}
|
}
|
||||||
return view('profile');
|
return view('profile');
|
||||||
});
|
});
|
||||||
|
@ -48,6 +49,8 @@ Route::get('/home', function () {
|
||||||
|
|
||||||
Route::post('/shorten', [ShortlinkController::class, 'create']);
|
Route::post('/shorten', [ShortlinkController::class, 'create']);
|
||||||
|
|
||||||
// Route::get('/l/{id}', );
|
Route::get('/l/{id}/details', [Link_interactionController::class, 'getCountryArray']);
|
||||||
|
|
||||||
|
Route::get('/l/{id}', [ShortlinkController::class, 'getDetails']);
|
||||||
|
|
||||||
Route::get('/{id}', [ShortlinkController::class, 'goto']);
|
Route::get('/{id}', [ShortlinkController::class, 'goto']);
|
Loading…
Reference in New Issue