1
0
mirror of https://koodu.h-i.works/projects/thebadspace synced 2025-06-30 16:07:37 -05:00

29 Commits

Author SHA1 Message Date
ro
03fbd00db1 Footer tweaks, text adjustments for about, appeals
Just a light touch up for the footer and some additional text changes
for the about and appeals section
2024-02-13 10:24:45 -06:00
ro
847ec4a391 Color Tweaks
Giving the UI a fresh coat of paint for the new design upgrade.
2024-02-12 20:00:57 -06:00
ro
e426eb2735 sources url checking
addded some preliminary url validation to make sure a source url is
still active.

the consolidation process is starting to get top heavy, so it's a stop
gap until a more effecient process can be created.
2024-02-12 14:51:28 -06:00
ro
f1995d2163 Export Update
Updated CSV export methodology to use the Location Repo class. Also
updated export links show location counts for each rating
2024-02-11 12:26:54 -06:00
ro
2bcb887d19 simplified pagination, cleaned up front controller
Adding total actions count to location data has made it possible to
simplify backend data queries, so the custom pagination was optimized
and location repositories have been cleaned up.

the front end controller has been cleaned up as well, which has resulted
in the appropriate template being polished up as well.
2024-02-10 11:48:09 -06:00
ro
fdaf90b89f added total actions count to DB, action count fix
Added a new field to the locations table for total actions which will be
used to set the active state of location. this will result in data being
parsed and sorted easier instead of doing those calculations on the back
end, so DB queries will be simpler

also fixed actions counts for csv imports
2024-02-09 21:05:33 -06:00
ro
fe67927c24 Used laravel validation for appeal form
Switched valdiation from checking for empty strings to using Laravel's
built in form validation. It's pretty.
2024-02-09 17:21:07 -06:00
ro
9e7c7c584e Added more filtering for appeal check
Appeal checks needed an emptry string filter so all requested data is
accounted for and not left emptyAdded more filtering for appeal check
2024-02-09 15:12:04 -06:00
ro
da0ddb3ef0 Plugged in repository class for location data
Seperated data logic for locations and put it into its own repository
class for better organization and readability of controller classes.

Data methods are still simple so no need for an interface class just
yet, but will probably implement at a later date
2024-02-09 14:53:08 -06:00
ro
bce9a430aa Appeal Process upgrade
Changed the appeal process so that each request is tracked in the
database to make reviewing and time limits easier to manage.

An email is still sent but it's just a notifcation to let the admin know
an appeal has been filed.
2024-02-08 14:37:34 -06:00
ro
9be54fa13c Responsive Part 2, environment changes
Hit the major friction points in the responsive UI. Still have some
polishing to do but there shouldn't be any show stoppers at this points.

Also moved some variable to the env so they can be changed easily when
necessary
2024-02-08 13:07:49 -06:00
ro
efe568bf60 Responsive Edits: Part 1
Responsive styles are non-existent so it was time to get that sorted.
This first pass was just getting a feel for what can be done with
list items since that's one of the main components of the site.

Second pass will clean this up as well as the majority of text styling
so it all smoothly adapts.

Ha, or that's the plan anyway.
2024-02-06 15:39:19 -06:00
ro
fe49ca8699 Working Appeals form
Still need to style the email form, but the Appeal Form is working with
a bit of protection from bots to cut down on spam.
2024-02-05 14:02:27 -06:00
ro
572f2434f6 Dependency Update
It's been a little while since the packages were updated, so its time to
update everyting to the latest
2024-02-05 11:10:00 -06:00
ro
c763d749c9 Messaging infrastructure
Put together a quick test to get internal messaging working for appeals
and joining current sources requests. Now that it works, forms that will
leverage messaging will be created.
2024-02-02 12:16:11 -06:00
ro
0785e76df6 Updated Location Page design
Updated the design for pages displaying information about specifiic
locations to clearly display heat rating, silence and suspeneded
statistics.

Also inlcude a descriptor for heat rating.
2024-01-26 14:50:20 -06:00
ro
c13f144e20 Added Bad Space Info to index
Added some preliminary Bad Spade stats to the index to set up a space
for additional information to be shared in the future. Starting with an
    accurate display of locations tracked, Current Sources count and
    last update date
2024-01-26 12:16:38 -06:00
ro
175ea25d7b Updated search and listing links, added new icons
Search methodolgy has been tweaked and the correspending result links
have been updated, as well as links in the Listing page to display both
silence and suspend counts and the overall heat rating.

Also plugged in new silence and suspend icons provideb by
https://rage.love/@puf. Big thanks for that, man.
2024-01-23 12:04:52 -06:00
ro
0a64de2378 Link Updates, Recent Links on Index clean up
Started the front end refresh by starting to plug in the new design on
the index page. The search methodology was still not consistent, so that
process was cleaned up to display what is actually being tracked based
on the two action criteria, then updating the design for the location
links to be clear and display the heat rating.

Added a repo link to the About section.
2024-01-22 13:45:11 -06:00
ro
29c8935bfa Quote clean up CSV export, tweaking action counts
https://tenforward.social/@noracodes reported an issue with the CSV not
rendering properly because of the mix of single and double quotes, so
that's been cleaned up.

Also fixed action sorting which was reversed.
2023-11-12 12:22:22 -06:00
ro
ccb9536204 Quick Test 2
just forgot to edit a minor git config setting. should be good now
2023-10-12 14:44:09 -07:00
ro
b8b697dc7d New Repo Test
Just checking all the plumbings work after the migration
2023-10-12 14:41:24 -07:00
Ro
86652cc112 Extra spaces patch for CSV exports
There are some carriage returns in some of the descriptions which was
throwing off the CSV cell assignment, so added a quick patch that
replaces said returns with a simple space to keep formatting intact
2023-09-28 14:30:36 -07:00
Ro
a15db82697 Added Exports by Heat Rating
Reactivated CSV exports based on Heat Rating, which is the percentage of
Current Sources the have taken action, a suspend or a silence, against
an instanct. The higher the Heat Rating, the more Sources that have
taken actions agaisnt it, so they should be viewed with caution
2023-09-28 14:02:35 -07:00
Ro
0d189a4fc3 Seperated silence and block counts
Actions taken against and instance of have been separted into their
respective buckets so they can be display properly rather than simply
grouped together and mislabled.

This gives a better sense of the severity of response per instance.
2023-09-25 13:24:48 -07:00
Ro
8a513c3f2c Separated silence and suspend counts
The count data is now separated by specific action rather than grouping everything together.

This gives a clearer picture of the severity of a response by current sources
2023-09-25 13:23:58 -07:00
Ro
1c904e5e51 SQL Exploit Patch
Quick fix to patch up a common SQL exploit.
2023-09-21 13:46:14 -07:00
Ro
572f7c5027 Fix for filtering, quick fix for rating term swap
There was a small syntax bug that was causing the sytem to error out
when resetting rating terms.

Also plugged a bug that was returning locations with ony one block
count, which is below the criteria for instances that should be listed
and shown in search results
2023-09-11 17:40:13 -07:00
Ro
0c2b8bae7c Added public search API
Finally moved over the public search API from the old version and
updated the about page to show the new data structure

Also tweaked the location update script to change 'defederate' to
'suspend' for the sake of consistency
2023-09-07 14:31:25 -07:00
32 changed files with 1834 additions and 924 deletions

View File

@ -1,7 +1,9 @@
# The Bad Space # The Bad Space
A searchable catalog of the worst places on the web. A searcable catalog of the worst places on the web.
More features incoming More features incoming
An Hi Project =)

View File

@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers;
use App\Mail\LocationAppeal;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Ramsey\Uuid\Uuid;
use App\Models\Appeal;
class AppealController extends Controller
{
/**
* Send appeal request
*/
public function sendAppeal(Request $request)
{
//$order = Order::findOrFail($request->order_id);
$token = csrf_token();
if ($request->h1 != '' || $request->question != 2) {
return back()->withErrors([
'error' => 'Invalid Request',
]);
} else {
$check = Appeal::where("location", $request->location)->first();
if ($check) {
return back()->withErrors([
'error' => 'Appeal already in process for Location',
]);
} else {
$clean = $request->validate([
'location' => ['required'],
'local_admin' => ['required'],
'local_sponsor' => ['required'],
'appeal_description' => ['required'],
]);
$new = Appeal::create([
'uuid' => Uuid::uuid4(),
'location' => $request->location,
'location_admin' => $request->location_admin,
'sponsor' => $request->sponsor,
'description' => $request->appeal_description,
'approved' => false,
'reviewed' => false,
]);
Mail::to(env('TBS_ADMIN_EMAIL'))->send(new LocationAppeal($request->location, $request->sponsor));
}
//return redirect('/appeals');
return back()->with('message', "Appeal Filed");
};
}
}

View File

@ -2,34 +2,81 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Repositories\LocationRepository;
use App\Models\Source;
class ExportController extends Controller class ExportController extends Controller
{ {
// protected $locationRepository;
public function exportCSV()
{
/*
$columns = [
'id',
'product_name',
'product_url',
'price',
'category'
];
$products = [ public function __construct(LocationRepository $locationRepository)
[1, 'product 1', 'https://example.com/product-1', '9.99', 'category 1'], {
[2, 'product 2', 'https://example.com/product-2', '19.99', 'category 2'], $this->locationRepository = $locationRepository;
[3, 'product 3', 'https://example.com/product-3', '29.99', 'category 3'], }
[4, 'product 4', 'https://example.com/product-4', '39.99', 'category 4'],
]; public function exportIndex()
{
$heatArray = [90, 80, 70, 60, 50, 40, 30, 20];
$sources = Source::where("active", true)->get();
$locations = $this->locationRepository->getActiveLocations();
$list = [];
foreach ($heatArray as $rating) {
$count = 0;
foreach ($locations as $location) {
$rate = $location->actions_count / count($sources);
if ($rate * 100 >= $rating) {
$count++;
}
}
array_push($list, ["heatRating" => $rating, "ratingCount" => $count]);
}
return view('front.exports', [
'title' => "Exports",
'list' => $list
]);
}
//
public function exportCSV($type, $percent)
{
$columns = [];
$list = [];
$locations = $this->locationRepository->getActiveLocations();
$sources = Source::where("active", true)->get();
if ($type == 'mastodon') {
$columns = [
'domain',
'severity',
'public_comment',
'reject_media',
'reject_reports',
'obfuscate',
];
};
foreach ($locations as $location) {
$rate = $location->actions_count / count($sources);
if ($rate * 100 >= $percent) {
if ($type == 'mastodon') {
//comman break teh CSV so just take them out
$comments = str_replace(",", ";", $location->description);
//remove extra white space
$comments = str_replace(["\n\r", "\n", "\r"], " ", $comments);
$comments = str_replace(['"', "'"], "", $comments);
//add to the export list
array_push($list, [$location->url, $location->rating, $comments, "FALSE", "FALSE", "FALSE"]);
}
}
}
header('Content-Type: text/csv'); header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="products.csv"'); header('Content-Disposition: attachment; filename=' . $type . "-" . $percent);
echo implode(',', $columns) . PHP_EOL; echo implode(',', $columns) . PHP_EOL;
foreach ($products as $product) { foreach ($list as $item) {
echo implode(',', $product) . PHP_EOL; echo implode(',', $item) . PHP_EOL;
} }
*/
} }
} }

View File

@ -3,50 +3,39 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Models\Location;
use App\Models\Source; use App\Models\Source;
use App\Repositories\LocationRepository;
class FrontIndexController extends Controller class FrontIndexController extends Controller
{ {
private $limit = 15; protected $locationRepository;
public function __construct(LocationRepository $locationRepository)
{
$this->locationRepository = $locationRepository;
}
public function start() public function start()
{ {
$locations = Location::where("active", true)->get();
$count = count($locations);
$recent = Location::where("active", true)
->where('block_count', '>', 2)
->limit(10)->orderByDesc('updated_at')->get();
//$result = DB::select("SELECT * FROM searchlocations('$terms')");
return view('front.index', [ return view('front.index', [
'count' => $count, 'count' => count($this->locationRepository->getActiveLocations()),
'recent' => $recent, 'sources' => count(Source::where("active", true)->get()),
'title' => "The Bad Space" 'recent' => $this->locationRepository->getRecent(),
'latest_date' => $this->locationRepository->getRecent()[0]->updated_at->format('Y M d'),
'title' => "The Bad Space"
]); ]);
} }
public function indexSearch(Request $request) public function indexSearch(Request $request)
{ {
$terms = $request->index_search;
$rawSearch = $terms;
$terms = str_replace(",", "", $terms);
$terms = str_replace(" ", "|", $terms);
$results = DB::select("SELECT * FROM searchlocations('$terms')");
$locations = Location::where("active", true)->get();
$count = count($locations);
$recent = Location::where("active", true)
->where('block_count', '>', 2)
->limit(10)->orderByDesc('updated_at')->get();
return view('front.index', [ return view('front.index', [
'count' => $count, 'count' => count($this->locationRepository->getActiveLocations()),
'recent' => $recent, 'sources' => count(Source::where("active", true)->get()),
'title' => "The Bad Space", 'recent' => $this->locationRepository->getRecent(),
'results' => $results 'results' => $this->locationRepository->search($request),
'terms' => $request->index_search,
'latest_date' => $this->locationRepository->getRecent()[0]->updated_at->format('Y M d'),
'title' => "Search Results",
]); ]);
} }
@ -59,47 +48,43 @@ class FrontIndexController extends Controller
]); ]);
} }
public function appeals()
{
return view('front.appeals', [
'title' => "LOCATION APPEALS",
]);
}
public function location(string $uuid = "1") public function location(string $uuid = "1")
{ {
$location = Location::where("uuid", $uuid)->first(); $location = $this->locationRepository->getLocation($uuid);
$sources = Source::where("active", true)->get();
$name = "NO LOCATION FOUND"; $name = "NO LOCATION FOUND";
if ($location) { if ($location) {
$name = $location->name; $name = $location->name;
} }
return view('front.location', [ return view('front.location', [
'title' => str_replace(".", " ", $name), 'title' => str_replace(".", " ", $name),
'location' => $location, 'location' => $location,
'images' => json_decode($location->images), 'actions' => $location->block_count + $location->silence_count,
'updated' => $location->updated_at->format('Y M d'), 'sources_count' => count($sources),
'images' => json_decode($location->images),
'updated' => $location->updated_at->format('Y M d'),
]); ]);
} }
public function listings(int $pageNum = 1) public function listings(int $pageNum = 1)
{ {
$range = $pageNum * $this->limit - $this->limit; $listing = $this->locationRepository->getPage($pageNum);
$active = Location::where("active", true)->where('block_count', '>', 2)->get();
$locations = Location::where("active", true)->where('block_count', '>', 2)
->limit($this->limit)->offset($range)->orderByDesc('id')->get();
$pageCount = ceil(count($active) / $this->limit);
$next = $pageNum + 1;
if ($next > $pageCount) {
$next = 1;
}
$prev = $pageNum - 1;
if ($prev <= 0) {
$prev = $pageCount;
}
return view('front.listing', [ return view('front.listing', [
'title' => "Listings", 'title' => "Listings",
"totalPages" => $pageCount, 'sources' => count(Source::where("active", true)->get()),
"prev" => $prev, "totalPages" => $listing[1],
"next" => $next, "prev" => $listing[2],
"next" => $listing[3],
'pageNum' => $pageNum, 'pageNum' => $pageNum,
'locations' => $locations 'locations' => $listing[0]
]); ]);
} }
} }

View File

@ -6,17 +6,10 @@ use Illuminate\Http\Request;
use App\Models\Location; use App\Models\Location;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use League\Csv\Reader;
use App\Models\Source; use App\Models\Source;
class LocationController extends Controller class LocationController extends Controller
{ {
//url to oli's unified tier 3 list
private $three = 'https://codeberg.org/oliphant/blocklists/raw/branch/main/blocklists/_unified_tier3_blocklist.csv';
//url to oli's domain audit containin block counts per domain
private $defed = 'https://codeberg.org/oliphant/blocklists/raw/branch/main/blocklists/other/domain_audit_file.csv';
public function addLocation(Request $request) public function addLocation(Request $request)
{ {
$fields = $request->validate([ $fields = $request->validate([
@ -45,42 +38,59 @@ class LocationController extends Controller
return back()->with('message', 'New Location Added. Take a break!'); return back()->with('message', 'New Location Added. Take a break!');
} else { } else {
return back()->withErrors([ return back()->withErrors([
'error' => 'Uh oh. There was an issue.', 'error' => 'Uh oh. There was an inssue',
]); ]);
} }
} else { } else {
return back()->withErrors([ return back()->withErrors([
'error' => 'All fields are required.', 'error' => 'All fields are required',
]); ]);
} }
} }
public function updateLocations() public function updateLocations()
{ {
//$fresh = file($this->three);
//$deny = Reader::createFromPath($fresh, "r");
//$deny->setHeaderOffset(0);
//$list = $deny->getRecords();
//$recordCount = count($fresh);
$duplicates = 0; $duplicates = 0;
$fresh = 0; $fresh = 0;
// ['url' => "rage.love"], $missing = [];
//['url' => "indiepocalypse.social"],
$unified = []; $unified = [];
//$denycount = array_map('str_getcsv', file($this->defed)); $cleanSources = [];
//$denylist = array_map('str_getcsv', file($this->three)); $sources = Source::where("active", true)->get();
$sources = Source::where("active", true)->get();
//checks source url to make sure they valid
foreach ($sources as $source) { foreach ($sources as $source) {
//parsing for mastodon
if ($source->type == 'mastodon') { if ($source->type == 'mastodon') {
$url = 'https://' . $source->url;
} else {
$url = $source->url;
}
if ($this->urlExists($url)) {
array_push($cleanSources, [
'url' => $source->url,
'token' => $source->token,
'type' => $source->type,
'format' => $source->format]);
} else {
var_dump($url);
array_push($missing, ['source' => $url]);
}
}
//valid source url get compiled for unified
foreach ($cleanSources as $source) {
//check url to make sure it's cool
//parsing for mastodon
if ($source['type'] == 'mastodon') {
$result = []; $result = [];
if ($source->token == null) { if ($source['token'] == null) {
$result = \Mastodon::domain('https://' . $source->url) $result = \Mastodon::domain('https://' . $source['url'])
->get('/instance/domain_blocks'); ->get('/instance/domain_blocks');
} else { } else {
$result = \Mastodon::domain('https://' . $source->url) $result = \Mastodon::domain('https://' . $source['url'])
->token($source->token) ->token($source['token'])
->get('/instance/domain_blocks'); ->get('/instance/domain_blocks');
} }
@ -88,39 +98,96 @@ class LocationController extends Controller
$index = array_search($item['domain'], array_column($unified, 'url')); $index = array_search($item['domain'], array_column($unified, 'url'));
if ($index) { if ($index) {
//if there is a match, update the count //if there is a match, update the count
++$unified[$index]['count']; if ($item['severity'] == "suspend" || $item['severity'] == "defederate") {
++$unified[$index]['block_count'];
} else {
++$unified[$index]['silence_count'];
}
} else { } else {
$silence = 0;
$suspend = 0;
if ($item['severity'] == "suspend" || $item['severity'] == "defederate") {
++$suspend;
} else {
++$silence;
}
array_push($unified, [ array_push($unified, [
'name' => $item['domain'], 'name' => $item['domain'],
'url' => $item['domain'], 'url' => $item['domain'],
'rating' => $item['severity'], 'rating' => $item['severity'],
'comment' => $item['comment'], 'comment' => $item['comment'],
'count' => 1, 'block_count' => $suspend,
'silence_count' => $silence,
]); ]);
} }
} }
} }
//parsing for custom csv //parsing for custom csv
if ($source->type == 'custom' && $source->format == 'csv') { if ($source['type'] == 'custom' && $source['format'] == 'csv') {
$denylist = array_map('str_getcsv', file($source->url)); $denylist = array_map('str_getcsv', file($source['url']));
foreach ($denylist as $item) { foreach ($denylist as $item) {
$index = array_search($item[0], array_column($unified, 'url')); $index = array_search($item[0], array_column($unified, 'url'));
if ($index) { if ($index) {
//if there is a match, update the count //if there is a match, update the count
++$unified[$index]['count']; if ($item[1] == "suspend" || $item['severity'] == "defederate") {
++$unified[$index]['block_count'];
} else {
++$unified[$index]['silence_count'];
}
} else { } else {
$silence = 0;
$suspend = 0;
if ($item[1] == "suspend" || $item[1] == "defederate") {
++$suspend;
} else {
++$silence;
}
array_push($unified, [ array_push($unified, [
'name' => $item[0], 'name' => $item[0],
'url' => $item[0], 'url' => $item[0],
'rating' => $item[1], 'rating' => $item[1],
'comment' => $item[2], 'comment' => $item[2],
'count' => 1, 'block_count' => $suspend,
'silence_count' => $silence,
]); ]);
} }
} }
} }
} }
//TODO: maintenance script to set locations to inactive if they haven't been updated
// over 90 days
//$diff=date_diff($location->updated_at, new DateTime());
//$days = $diff->format("%R%a days")
//$interval = $location->updated_at->diff(new DateTime());
//$days = $interval->format("%a");
//get all locations and sort which are present in unified or not
/*
$sorted = [];
$listed = 0;
$notlisted = 0;
foreach (Location::all() as $location) {
if (array_search($location->url, array_column($unified, 'url'))) {
++$listed;
// locations present in unfied, so updated
array_push($sorted, [
'location' => $location,
'listed' => true
]);
} else {
++$notlisted;
//locations not present
array_push($sorted, [
'location' => $location,
'listed' => false
]);
}
};
*/
//once the unified list is created, update current entries or create fresh ones //once the unified list is created, update current entries or create fresh ones
foreach ($unified as $item) { foreach ($unified as $item) {
@ -129,7 +196,14 @@ class LocationController extends Controller
++$duplicates; ++$duplicates;
//update block count for existing item //update block count for existing item
$location->block_count = $item['count']; $location->block_count = $item['block_count'];
$location->silence_count = $item['silence_count'];
$location->actions_count = $item['block_count'] + $item['silence_count'];
if (($item['block_count'] + $item['silence_count']) < 2) {
$location->active = false;
}
//replace null with empty array //replace null with empty array
if ($location->images == null) { if ($location->images == null) {
@ -140,41 +214,50 @@ class LocationController extends Controller
// make new entries for instances not present // make new entries for instances not present
++$fresh; ++$fresh;
$images = []; $images = [];
$rating = ($item['rating'] == 'defederate') ? 'suspend' : $item['rating'];
$status = true;
if (($item['block_count'] + $item['silence_count']) < 2) {
$status = false;
}
$new = Location::create([ $new = Location::create([
'uuid' => Uuid::uuid4(), 'uuid' => Uuid::uuid4(),
'name' => $item['url'], 'name' => $item['url'],
'url' => $item['url'], 'url' => $item['url'],
'description' => ($item['comment'] != null) ? $item['comment'] : "No description provided.", 'description' => ($item['comment'] != null) ? $item['comment'] : "no description",
'active' => true, 'active' => $status,
'rating' => $item['rating'], 'rating' => $rating,
'added_by' => 1, 'added_by' => 1,
'tags' => 'poor moderation, hate speech', 'tags' => 'poor moderation, hate speech',
'images' => json_encode($images), 'images' => json_encode($images),
'block_count' => $item['count'], 'block_count' => $item['block_count'],
'silence_count' => $item['silence_count'],
'actions_cont' => $item['block_count'] + $item['silence_count']
]); ]);
} }
} }
//TODO: Send update post to TBS social account
//$lookfor = '0sint.social'; return back()->with('message', $duplicates . ' UPDATED - ' . $fresh . ' CREATED - ' . count($missing) . ' SOURCE(S) NOT CHECKED');
//$index = array_search($lookfor, array_column($unified, 'url')); }
//return back()->with('message', 'TOTAL: ' . count($unified) . " - " . $unified[$index]['count'] . " COUNT");
return back()->with('message', $duplicates . ' UPDATED - ' . $fresh . ' CREATED');
//$domain = $csv[1000][0]; public function urlExists($url)
//$record = null; {
// Remove all illegal characters from a url
/* $url = filter_var($url, FILTER_SANITIZE_URL);
foreach ($blockcount as $line) { // Validate URI
if ($line[0] == $domain) { if (
$record = $line; filter_var($url, FILTER_VALIDATE_URL) === false || // check only for http/https schemes.
} !in_array(
} strtolower(parse_url($url, PHP_URL_SCHEME)),
if ($record != null) { ["http", "https"],
return back()->with('message', $domain . ' has ' . $record[1] . ' blocks.'); true
} else { )
return back()->with('message', 'NO MATCHES'); ) {
} return false;
*/ } // Check that URL exists
$file_headers = @get_headers($url);
return !(!$file_headers || $file_headers[0] === "HTTP/1.1 404 Not Found");
} }
} }

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class LocationCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
//return parent::toArray($request);
return [
'listingCount' => count($this->collection),
'locations' => LocationResource::collection($this->collection),
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class LocationResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'url' => $this->url,
'name' => $this->name,
'description' => $this->description,
'rating' => $this->rating,
'count' => $this->block_count,
'link' => "/location/" . $this->uuid,
];
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Queue\SerializesModels;
class LocationAppeal extends Mailable
{
use Queueable;
use SerializesModels;
/**
* Create a new message instance.
*/
public function __construct(public $location, public $sponsor)
{
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
from: new Address(env('TBS_FROM_ADDRESS'), 'TBS Appeal Request'),
subject: 'Location Appeal',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'email.appeal',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

34
app/Models/Appeal.php Normal file
View File

@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Appeal extends Model
{
use HasFactory;
use SoftDeletes;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = "appeal";
protected $primaryKey = 'id';
public $incrementing = true;
protected $fillable = [
"uuid",
"location",
"location_admin",
"sponsor",
"description",
"approved",
"reviewed",
"created_at",
"updated_at"
];
}

View File

@ -4,10 +4,12 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Location extends Model class Location extends Model
{ {
use HasFactory; use HasFactory;
use SoftDeletes;
/** /**
* The table associated with the model. * The table associated with the model.
@ -29,7 +31,9 @@ class Location extends Model
"added_by", "added_by",
"tags", "tags",
"block_count", "block_count",
"silence_count",
"created_at", "created_at",
"updated_at" "updated_at",
"actions_count"
]; ];
} }

View File

@ -3,6 +3,8 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use App\Repositories\LocationRepository;
use App\Models\Location;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
@ -11,7 +13,9 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function register(): void public function register(): void
{ {
// $this->app->bind(LocationRepository::class, function ($app) {
return new LocationRepository(new Location());
});
} }
/** /**

View File

@ -0,0 +1,73 @@
<?php
namespace App\Repositories;
use App\Models\Location;
use Illuminate\Support\Facades\DB;
class LocationRepository
{
protected $model;
private $limit = 15;
public function __construct(Location $model)
{
$this->model = $model;
}
public function search($request)
{
// this grabs the search results from the db
$terms = $request->index_search;
$rawSearch = $terms;
$terms = str_replace(",", "", $terms);
$terms = str_replace(" ", "|", $terms);
$raw = DB::select("SELECT * FROM searchlocations(?)", [$terms]);
$results = [];
foreach ($raw as $item) {
if (($item->block_count + $item->silence_count) >= 2) {
array_push($results, $item);
}
}
return $results;
}
public function getLocation($uuid)
{
return $this->model::where("uuid", $uuid)->first();
}
public function getActiveLocations()
{
return $this->model::where("active", true)->where('actions_count', '>=', 2)->get();
}
public function getRecent()
{
return $locations = $this->model::where("active", true)->where('actions_count', '>=', 2)
->orderByDesc('updated_at')->limit(10)->get();
}
public function getPage($pageNum)
{
$range = $pageNum * $this->limit - $this->limit;
$active = $this->model::where("active", true)->where('actions_count', '>=', 2)->get();
$locations = $this->model::where("active", true)->where('actions_count', '>=', 2)
->limit($this->limit)->offset($range)->orderBy('id', 'asc')->get();
$pageCount = ceil(count($active) / $this->limit);
$next = $pageNum + 1;
if ($next > $pageCount) {
$next = 1;
}
$prev = $pageNum - 1;
if ($prev <= 0) {
$prev = $pageCount;
}
return $result = [$locations, $pageCount, $prev, $next];
}
}

1416
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -32,6 +32,18 @@ form.index-search-form > button {
right: 0; right: 0;
} }
form.index-search-form > button > img#search-icon {
float: none;
}
form.index-search-form > button > label {
font-weight: 500;
top: 15px;
position: relative;
font-size: 1.5em;
display: none;
}
::placeholder { ::placeholder {
/* Chrome, Firefox, Opera, Safari 10.1+ */ /* Chrome, Firefox, Opera, Safari 10.1+ */
color: var(--highlight); color: var(--highlight);
@ -41,6 +53,23 @@ section.index-meta article {
margin-top: 20px; margin-top: 20px;
} }
div.index-meta {
display: grid;
grid-template-columns: 50% 50%;
gap: 10px;
width: 98%;
font-weight: 500;
color: var(--secondary);
}
div.index-meta > label:nth-child(2),
div.index-meta > label:nth-child(4),
div.index-meta > label:nth-child(6) {
color: var(--white);
width: 100%;
text-align: right;
}
@media only screen and (max-width: 800px) { @media only screen and (max-width: 800px) {
form.index-search-form > input[type="text"] { form.index-search-form > input[type="text"] {
width: 85%; width: 85%;
@ -48,3 +77,32 @@ section.index-meta article {
font: 34px var(--base-type); font: 34px var(--base-type);
} }
} }
@media only screen and (max-width: 650px) {
form.index-search-form > input[type="text"] {
width: 80%;
height: 50px;
font: 34px var(--base-type);
}
}
@media only screen and (max-width: 480px) {
form.index-search-form > input[type="text"] {
width: 99%;
height: 50px;
font: 27px var(--base-type);
}
form.index-search-form > button {
width: 99%;
top: 15px;
}
form.index-search-form > button > label {
display: inline;
}
form.index-search-form > button > img#search-icon {
float: right;
}
}

View File

@ -37,7 +37,119 @@ section[role="listings"] div[role="paginate"] span {
a.list-link { a.list-link {
display: grid; display: grid;
grid-template-columns: 30px 50px 300px; grid-template-columns: 70px 1fr 80px 80px;
width: 80%; gap: 10px;
height: 45px; height: auto;
padding-bottom: 20px;
cursor: pointer;
}
a.list-link > .item-rating {
background: var(--secondary);
border-radius: 3px;
color: var(--white);
font-weight: 500;
padding: 10px;
position: relative;
}
a.list-link > .item-name {
background: var(--white);
border-radius: 3px;
color: var(--black);
font-weight: 400;
padding: 9px 5px;
cursor: pointer;
position: relative;
}
a.list-link > .item-silence {
background: var(--silence);
border-radius: 3px;
color: var(--black);
font-weight: 500;
padding: 10px;
position: relative;
}
a.list-link > .item-block {
background: var(--suspend);
border-radius: 3px;
color: var(--black);
font-weight: 500;
padding: 10px;
position: relative;
}
a.list-link > .item-silence > .item-icon,
a.list-link > .item-block > .item-icon {
width: 30px;
vertical-align: top;
}
/*
responsive
*/
@media only screen and (max-width: 800px) {
a.list-link {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
grid-template-rows: 100% 100px 30px 30px;
gap: 5px;
height: auto;
padding-bottom: 20px;
cursor: pointer;
}
a.list-link > .item-rating {
grid-row: 3;
grid-column: 1/4;
height: 100%;
}
a.list-link > .item-name {
grid-row: 2/3;
grid-column: 1/6;
}
a.list-link > .item-silence {
grid-row: 3/4;
height: 100%;
}
a.list-link > .item-block {
grid-row: 3/4;
height: 100%;
}
}
@media only screen and (max-width: 480px) {
a.list-link {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
grid-template-rows: 100% auto 30px 30px;
gap: 5px;
height: auto;
padding-bottom: 20px;
cursor: pointer;
}
a.list-link > .item-rating {
grid-row: 3;
grid-column: 1/2;
height: 100%;
}
a.list-link > .item-silence {
grid-row: 3/4;
grid-column: 2/4;
height: 100%;
}
a.list-link > .item-block {
grid-row: 3/4;
height: 100%;
grid-column: 4/6;
}
} }

View File

@ -14,8 +14,71 @@ section[role="location"] img {
vertical-align: top; vertical-align: top;
} }
div.location-rating {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
width: 98%;
font-weight: 500;
font-size: 0.8em;
color: var(--secondary);
}
div.location-rating > div {
border-radius: 3px;
color: var(--primary);
padding: 5px 5px 0;
}
div.location-rating > div:nth-child(1) {
background: var(--secondary);
}
div.location-rating > div:nth-child(2) {
background: var(--silence);
}
div.location-rating > div:nth-child(3) {
background: var(--suspend);
}
div.location-rating > div > span {
position: relative;
top: 6px;
float: right;
width: 80%;
text-align: center;
}
.rating-icon { .rating-icon {
width: 45px; width: 35px;
top: 10px;
position: relative; position: relative;
} }
@media only screen and (max-width: 800px) {
div.location-rating {
font-size: 0.65em;
}
div.location-rating > div > span {
width: 75%;
}
}
@media only screen and (max-width: 670px) {
div.location-rating {
grid-template-columns: 1fr 1fr;
font-size: 0.7em;
}
}
@media only screen and (max-width: 480px) {
div.location-rating {
grid-template-columns: 1fr;
font-size: 0.8em;
}
div.location-rating > div > span {
width: 85%;
}
}

View File

@ -1,13 +1,13 @@
:root { :root {
/* BASE COLORS */ /* BASE COLORS */
--primary: #140c08; --primary: #0c1119;
--secondary: #d66365; --secondary: #c3639e;
--highlight: #69b04f; --highlight: #4d7555;
--white: #efebe3; --white: #efded5;
--grey: #abb7b7; --grey: #abb7b7;
--black: #32302f; --black: #32302f;
--error: #b62520; --error: #b62520;
--silence: #ea6010; --silence: #d2896b;
--suspend: #fb263a; --suspend: #d94959;
--primary-rgb: 20 13 13; --primary-rgb: 20 13 13;
} }

View File

@ -97,6 +97,11 @@ main > section > article {
min-height: 400px; min-height: 400px;
} }
textarea[name="appeal_description"] {
width: 300px;
height: 200px;
}
/* NAV */ /* NAV */
#main-nav { #main-nav {
@ -181,32 +186,32 @@ sup {
} }
footer { footer {
width: 100%;
color: var(--primary); color: var(--primary);
background: var(--secondary); background: var(--secondary);
height: 200px;
}
footer > div:nth-child(1) {
display: grid; display: grid;
grid-template-columns: 50% 50%; grid-template-columns: 50% 48%;
padding: 10px; padding: 10px;
gap: 10px; gap: 10px;
height: 200px; height: auto;
width: 80%; width: 80%;
margin: 0 auto; margin: 20px auto;
max-width: 1000px; max-width: 1000px;
position: relative; position: relative;
} }
footer a {
color: var(--white);
}
footer > div:nth-child(2) {
text-align: right;
}
/* /*
responsive responsive
*/ */
@media only screen and (max-width: 960px) { @media only screen and (max-width: 960px) {
header > div:nth-child(1) {
}
header > div nav { header > div nav {
bottom: 17px; bottom: 17px;
} }
@ -223,5 +228,29 @@ footer > div:nth-child(1) {
} }
} }
@media only screen and (max-width: 800px) { @media only screen and (max-width: 650px) {
header > div:nth-child(1) {
grid-template-columns: 150px 65% 1fr;
grid-template-rows: 75% 1fr;
height: auto;
gap: 15px;
}
header > div > div.header-left {
grid-row: 1/2;
position: relative;
width: 100px;
}
header > div > div.header-center {
grid-row: 2/3;
grid-column: 1/4;
position: relative;
}
}
@media only screen and (max-width: 440px) {
header > div:nth-child(1) {
grid-template-columns: 150px 40% 1fr;
}
} }

View File

@ -63,6 +63,26 @@ h2 {
} }
h3 { h3 {
font-size: 1.5em; font-size: 1em;
font-weight: 300; font-weight: 500;
}
@media only screen and (max-width: 800px) {
h1 {
font-size: 2em;
}
}
@media only screen and (max-width: 650px) {
h1 {
font-size: 1.5em;
letter-spacing: -3px;
width: 100%;
position: relative;
}
}
@media only screen and (max-width: 480px) {
font-size: 1.5em;
letter-spacing: -3px;
} }

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.4.3 -->
<svg width="500" height="500" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
<path id="Ellipse" fill="#efebe3" stroke="none" d="M 250 500 C 111.928947 500 0 388.071045 0 250 C 0 111.928955 111.928947 0 250 0 C 388.071045 0 500 111.928955 500 250 C 500 388.071045 388.071045 500 250 500 Z M 249.5 425.849609 C 334.610199 425.849609 403 351.977203 403 264.537109 C 403 244.390259 393.74881 215.814362 379.837891 187.855469 C 365.721375 159.485382 345.643799 129.607941 321.933594 106.994141 C 316.519958 101.854614 308.159698 101.854797 302.746094 106.925781 C 293.494965 115.697205 284.928558 126.45636 277.390625 136.324219 C 260.807159 115.62915 243.057907 96.510132 224.144531 78.761719 C 218.799438 73.759277 210.509155 73.690887 205.164063 78.693359 C 173.230606 108.296906 146.230286 141.942657 127.111328 174.21875 C 108.197952 206.015137 96 238.15448 96 264.46875 C 96 351.908844 163.43045 425.849609 249.5 425.849609 Z M 250.664063 360.0625 C 207.423721 360.0625 172.750168 332.241577 172.681641 285.300781 C 172.681641 264.674225 184.33017 246.309418 207.492188 217.185547 C 211.877914 211.7034 220.102753 211.771759 224.419922 217.253906 C 235.932419 231.850098 256.14621 257.546875 267.453125 271.9375 C 271.701782 277.351105 279.858093 277.489319 284.380859 272.28125 L 301.648438 252.203125 C 306.102661 246.995087 313.98465 247.404846 317.068359 253.572266 C 334.405609 285.231598 326.660309 325.525024 297.810547 345.671875 C 283.35141 355.265625 268.001312 360.0625 250.664063 360.0625 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.3.8 --> <!-- Generated by Pixelmator Pro 3.4.3 -->
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path id="Path" fill="none" stroke="none" d="M 0 0 L 24 0 L 24 24 L 0 24 Z"/> <path id="path2292" fill="#efebe3" stroke="none" d="M 11.931641 1.986328 C 6.851659 2.0299 2.1895 6.012724 2.005859 11.675781 L 2 12 L 2.00586 12.324219 C 2.161932 17.140404 5.551549 20.72867 9.675782 21.730469 C 9.588913 21.443272 9.558197 21.13777 9.597657 20.835938 C 9.47312 18.918024 9.545824 16.97234 10.00586 15.101562 C 8.788254 14.549991 8.926215 13.552958 10.486329 13.134766 C 10.688758 12.435676 11.011918 11.808259 11.775392 11.5625 C 12.475244 11.321042 13.30658 11.50242 13.822267 12.039062 C 14.182369 12.380361 14.347692 12.851138 14.367189 13.318359 C 15.557706 13.858945 15.447286 14.835346 13.984376 15.267578 C 13.876838 15.708144 13.794972 16.040434 13.751954 16.230469 C 14.035457 16.215448 14.343313 16.25919 14.525392 16.310549 C 15.273297 15.605935 16.559305 15.683684 17.234377 16.451174 C 18.030642 17.245865 17.777361 18.485273 17.160156 19.302736 C 16.834358 19.841322 16.59597 20.440948 16.324219 21.01758 C 16.59798 20.884926 16.870295 20.73892 17.138672 20.578127 C 23.68045 16.658728 23.604391 7.153141 17 3.339844 C 15.364385 2.395456 13.624967 1.971804 11.931641 1.986328 Z M 7.978516 7.474609 C 8.504453 7.484797 9.026345 7.633665 9.589844 7.914063 C 9.807306 8.022272 9.962496 8.065799 10.025391 8.089844 C 10.30621 8.197204 10.600216 8.074305 10.783203 8.117188 C 11.423864 8.267323 11.413255 9.201465 10.703123 9.310547 C 10.368721 9.361912 9.987375 9.327701 9.675781 9.240234 C 10.263738 9.753045 10.07576 10.87933 9.126953 10.992188 L 9 11 L 8.882813 10.992188 C 7.706968 10.852325 7.706968 9.147676 8.882813 9.007813 L 9.060547 9 C 8.389929 8.673583 7.954104 8.424998 6.910156 8.925781 C 6.097967 9.296431 5.682192 8.17984 6.337891 7.869141 C 6.921612 7.592548 7.452579 7.464422 7.978516 7.474609 Z M 16.025391 7.476559 C 16.551327 7.466373 17.082294 7.596451 17.666016 7.873043 C 18.321714 8.183743 17.905939 9.298381 17.09375 8.927732 C 16.049803 8.426949 15.612026 8.677486 14.941406 9.003903 L 15.119141 9.009764 C 16.294985 9.149627 16.294985 10.856229 15.119141 10.996092 L 15.003906 11.003892 L 14.876953 10.996092 C 13.928146 10.883233 13.738215 9.756949 14.326172 9.244139 C 14.014578 9.331604 13.635186 9.365818 13.300781 9.31445 C 12.590652 9.205368 12.578091 8.269274 13.218751 8.119138 C 13.401738 8.076256 13.697696 8.199155 13.978517 8.091794 C 14.041407 8.067749 14.196601 8.024221 14.414063 7.916012 C 14.977562 7.635616 15.499455 7.486746 16.025393 7.476559 Z"/>
<path id="path1" fill="#ea6010" stroke="none" d="M 17 3.34 C 21.1674 5.746208 23.030024 10.779379 21.433001 15.318825 C 19.835976 19.858273 15.232252 22.616514 10.476249 21.883377 C 5.720245 21.15024 2.160861 17.133654 2.005 12.324 L 2 12 L 2.005 11.676 C 2.118919 8.162982 4.068822 4.967705 7.140892 3.259882 C 10.212963 1.552061 13.95609 1.582479 17 3.34 Z M 15 14 L 9 14 L 8.883 14.007 C 8.37995 14.066836 8.001114 14.493402 8.001114 15 C 8.001114 15.506598 8.37995 15.933164 8.883 15.993 L 9 16 L 15 16 L 15.117 15.993 C 15.62005 15.933164 15.998886 15.506598 15.998886 15 C 15.998886 14.493402 15.62005 14.066836 15.117 14.007 L 15 14 Z M 9.01 9 L 8.883 9.007 C 8.37995 9.066836 8.001114 9.493402 8.001114 10 C 8.001114 10.506598 8.37995 10.933164 8.883 10.993 L 9 11 L 9.127 10.993 C 9.630051 10.933164 10.008885 10.506598 10.008885 10 C 10.008885 9.493402 9.630051 9.066836 9.127 9.007 L 9.01 9 Z M 15.01 9 L 14.883 9.007 C 14.37995 9.066836 14.001114 9.493402 14.001114 10 C 14.001114 10.506598 14.37995 10.933164 14.883 10.993 L 15 11 L 15.127 10.993 C 15.630051 10.933164 16.008886 10.506598 16.008886 10 C 16.008886 9.493402 15.630051 9.066836 15.127 9.007 L 15.01 9 Z"/> <path id="path1819" fill="#efebe3" stroke="none" d="M 11.711456 13.193301 L 10.960155 16.263454 L 10.723999 18.598185 L 10.817269 21.317106 C 10.888359 21.702995 11.090498 21.968763 11.535805 22.011623 C 12.458734 22.051924 13.518139 21.943733 14.323691 21.738602 C 14.589794 21.658991 14.723038 21.499723 14.821309 21.311239 L 16.050283 18.831099 L 16.454288 18.104069 C 16.90646 17.320887 15.688282 16.454172 15.087541 17.494686 L 14.908501 17.80386 C 13.974688 16.986099 12.358489 17.746168 12.358489 17.746168 L 12.426389 16.614141 L 13.161877 13.597538 C 13.372696 12.60058 11.976026 12.279035 11.711465 13.193299 Z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.3.8 --> <!-- Generated by Pixelmator Pro 3.4.3 -->
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path id="Path" fill="none" stroke="none" d="M 0 0 L 24 0 L 24 24 L 0 24 Z"/> <path id="path1" fill="#efebe3" stroke="none" d="M 14.865711 21.562855 C 13.484051 21.973886 11.977767 22.11401 10.475826 21.882484 C 5.720059 21.149376 2.160853 17.13295 2.005 12.323484 L 2 11.999497 L 2.005 11.67551 C 2.118913 8.162631 4.068719 4.96748 7.140635 3.259726 C 10.212553 1.551971 13.955494 1.582388 16.999252 3.33984 C 20.471048 5.344435 22.335369 9.211392 21.941496 13.063743 C 21.638853 12.282332 20.494862 12.311415 20.494862 12.311415 C 20.33622 10.935238 17.531656 10.99734 17.395786 12.263875 C 17.394386 12.277055 17.356598 12.320015 17.356598 12.320015 C 16.094175 12.350565 15.655445 13.278266 15.569292 13.501472 L 15.569296 13.50147 C 13.25033 13.340255 10.939906 13.917868 8.969625 15.151405 C 8.50131 15.444105 8.358945 16.061031 8.651641 16.529352 C 8.944338 16.997669 9.561259 17.140038 10.029573 16.847338 C 11.734911 15.86276 13.531968 15.365489 15.429303 15.497391 C 15.421703 15.655235 15.402763 15.934267 15.402763 15.934267 C 13.722934 15.730796 12.71482 17.372704 13.250748 18.700893 Z M 10.408088 8.316023 L 7.034182 7.900524 C 6.303076 7.827415 6.131269 8.98021 6.91 9.092553 C 7.125181 9.123596 8.310906 9.275673 8.310906 9.275673 C 8.11896 9.458725 8.000814 9.716671 8.000814 9.999577 C 8.000814 10.506154 8.379631 10.932703 8.882656 10.992537 L 8.99965 10.999537 L 9.126644 10.992537 C 9.62967 10.932697 10.008486 10.506154 10.008486 9.999577 C 10.008486 9.805412 9.997353 9.657336 9.855869 9.468498 C 9.855869 9.468498 9.999368 9.481016 10.294614 9.510522 C 10.994176 9.502202 11.148743 8.475587 10.408088 8.316023 Z M 13.556226 8.327742 C 12.888803 8.452742 12.975172 9.494161 13.694857 9.506617 C 13.804587 9.508516 14.176233 9.453409 14.176233 9.453409 C 14.079473 9.607802 14.000515 9.805637 14.000515 9.999578 C 14.000515 10.506154 14.379333 10.932703 14.882357 10.992537 L 14.999352 10.999537 L 15.126345 10.992537 C 15.629371 10.932697 16.008186 10.506154 16.008186 9.999578 C 16.008186 9.720305 15.873525 9.445825 15.685923 9.263232 C 15.685923 9.263232 16.734871 9.135027 17.114626 9.084742 C 17.836151 8.928703 17.728067 7.867409 16.94478 7.904829 Z"/>
<path id="path1" fill="#fb263a" stroke="none" d="M 17 3.34 C 21.1674 5.746208 23.030024 10.779379 21.433001 15.318825 C 19.835976 19.858273 15.232252 22.616514 10.476249 21.883377 C 5.720245 21.15024 2.160861 17.133654 2.005 12.324 L 2 12 L 2.005 11.676 C 2.118919 8.162982 4.068822 4.967705 7.140892 3.259882 C 10.212963 1.552061 13.95609 1.582479 17 3.34 Z M 15.57 13.502 C 13.250918 13.340779 10.940379 13.918414 8.97 15.152 C 8.501662 15.444711 8.359289 16.061663 8.652 16.530001 C 8.944711 16.998337 9.561663 17.140711 10.03 16.848 C 11.642129 15.838702 13.53257 15.366092 15.43 15.498 C 15.98118 15.53666 16.459339 15.121181 16.497999 14.57 C 16.536659 14.01882 16.12118 13.54066 15.57 13.502 Z M 9.01 9 L 8.883 9.007 C 8.37995 9.066836 8.001114 9.493402 8.001114 10 C 8.001114 10.506598 8.37995 10.933164 8.883 10.993 L 9 11 L 9.127 10.993 C 9.630051 10.933164 10.008885 10.506598 10.008885 10 C 10.008885 9.493402 9.630051 9.066836 9.127 9.007 L 9.01 9 Z M 15.01 9 L 14.883 9.007 C 14.37995 9.066836 14.001114 9.493402 14.001114 10 C 14.001114 10.506598 14.37995 10.933164 14.883 10.993 L 15 11 L 15.127 10.993 C 15.630051 10.933164 16.008886 10.506598 16.008886 10 C 16.008886 9.493402 15.630051 9.066836 15.127 9.007 L 15.01 9 Z"/> <path id="path1034" fill="#efebe3" stroke="none" d="M 21.293535 15.272347 C 21.293535 14.230235 22.856705 14.230235 22.856705 15.272347 L 22.856705 19.701324 C 22.856705 21.427954 21.456995 22.827662 19.730368 22.827662 L 18.688253 22.827662 L 18.796635 22.827662 C 17.7449 22.827839 16.763556 22.299183 16.185101 21.420811 C 16.150921 21.36879 16.11688 21.316687 16.082973 21.264494 C 15.920404 21.014908 15.349847 20.02021 14.370783 18.279884 C 14.163248 17.911001 14.287053 17.443947 14.650069 17.226309 C 15.03267 16.99674 15.522393 17.056881 15.838077 17.372204 L 16.616611 18.138157 L 16.604033 14.230235 C 16.597332 13.188145 18.1672 13.188123 18.1672 14.230235 L 18.1672 15.923668 L 18.1672 13.188123 C 18.1672 12.146011 19.73037 12.146011 19.73037 13.188123 L 19.73037 14.222953 C 19.73037 13.188123 21.293537 13.188123 21.293537 14.230253 L 21.293537 15.27504"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,3 @@
<div>
Appeal filed for: {{$location}}<br />
</div>

View File

@ -42,6 +42,12 @@
<a href="/listings/1" title="instance listing" class="nav-links"> <a href="/listings/1" title="instance listing" class="nav-links">
Listings Listings
</a><br /> </a><br />
<a href="/exports" title="list exports" class="nav-links">
Exports
</a><br />
<a href="/appeals" title="location appeals" class="nav-links">
Appeals
</a><br />
@if(Auth::check()) @if(Auth::check())
<a href="/den" title="den-start" class="nav-links"> <a href="/den" title="den-start" class="nav-links">
Den Den
@ -77,7 +83,11 @@
</main> </main>
<footer> <footer>
<div> <div>
<p>The Bad Space © 2023</p> The Bad Space © 2024<br />
an <a href="https://h-i.works">h.i.</a> project
</div>
<div>
a0.5
</div> </div>
</footer> </footer>
</body> </body>

View File

@ -4,71 +4,83 @@
@parent @parent
<section> <section>
<article> <article>
<h2 id="what">What Is The Bad Space?</h2> <h2 id="what">What is The Bad Space?</h2>
<p>The Bad Space project was born from a need to effectively identify instances that house bad actors and are poorly moderated, which puts marginalized communities at risk. <p>The Bad Space arose from a need to identify instances that house bad actors, are poorly moderated, and/or contain inappropriate/offensive content (CSAM, hate speech, fascist ideology, etc.) that puts marginalized communities at risk.
</p> </p>
<p> <p>
It is an extension of the It is an extension of the
<strong>#fediblock</strong> <strong>#fediblock</strong>
hashtag created by hashtag - orginally created by
<a href="https://www.artistmarciax.com/" target="_new">Artist Marcia X</a>, <a href="https://www.artistmarciax.com/">Artist Marcia X</a>
with additional support from with additional support from
<a href="https://digital.rooting.garden" target="_new">Ginger</a>, <a href="https://digital.rooting.garden">Ginger</a>
to provide a catalog of instances that seek to cause harm and reduce the quality of experience in the fediverse. to provide a catalog of instances that seek to cause harm and reduce the quality of experience in the fediverse.
</p> </p>
<p> <p>
Technical support provided by The searchable online catalog is built and maintained by
<a href="https://roiskinda.cool/profile.html" target="_new">Ro</a>. <a href="https://roiskinda.cool/profile.html">Ro</a>. The repo can be found <a href="https://koodu.h-i.works/projects/thebadspace">here</a>.
</p> </p>
<h2 id="how">How Does It Work?</h2> <p>Custom silence and suspend icons graciously provided by <a href="https://rage.love/@puf">puf</a>.</p>
<p>The Bad Space is a collaboration of instances committed to actively moderating against racism, sexism, heterosexism, transphobia, ableism, casteism, and religion.</p> <h2 id="how">How does it work?</h2>
<p>The Bad Space is a collaboration of communities, referred to as Current Sources, committed to actively moderating against racism, sexism, heterosexism, transphobia, ableism, casteism, or religion.</p>
<p>These instances have permitted The Bad Space to read their respective blocklists to create a composite directory of sites tagged for the behavior above that can be searched and, through a public API, can be integrated into external services.</p> <p>These communities have permitted The Bad Space to ingest their respective blocklists detailing their silences and suspension to create a composite directory of sites that engage in the behavior(s) listed in the section above. For each behavior, the directory of locations can be searched and, through The Bad Space's public API, integrated into external services.</p>
<h2>Adding Locations</h2>
<p>
Current Sources continually review the #fediblock hashtag and update their silences and suspensions when warranted. If an instance meets the criteria of a Current Source to be suspended or silenced, The Bad Space will automatically be updated according to said Current Sources' curated data.
For an instance to be listed on The Bad Space, at least two (2) Current Sources must have that location silenced and/or suspended. Instances will not display in the directory until two (2) Current Sources have taken moderation action against them.
</p>
<h2>Removing Locations</h2>
Locations that are displayed in The Bad Space may petition to be removed from the catalog by sending an appeal request to The Bad Space. The appeal process is outlined <a href="/appeals">here</a>.
<p><strong>Current Sources:</strong></p> <p><strong>Current Sources:</strong></p>
Mastodon:<br /> Maston:<br />
@foreach($sources as $source) @foreach($sources as $source)
@if($source->format == 'json') @if($source->format == 'json')
<a href="https://{{$source->url}}">{{$source->url}}</a><br /> <a href="https://{{$source->url}}">{{$source->url}}</a><br />
@endif @endif
@endforeach<br /> @endforeach
Custom CSV:<br /> Custom CSV<br />
@foreach($sources as $source) @foreach($sources as $source)
@if($source->format == 'csv') @if($source->format == 'csv')
<a href="{{$source->url}}">{{$source->url}}</a><br /> <a href="{{$source->url}}">{{$source->url}}</a><br />
@endif @endif
@endforeach @endforeach
<h2>How Do I Use It?</h2> <h2>How do I use it?</h2>
<p> <p>
The Bad Space is meant to be a resource for anyone looking to improve the quality of their online experience by creating a tool that catalogs sources for harassment and abuse. There are several options for how it can be used. The Bad Space is meant to be a resource for anyone looking to improve the quality of their online experience by creating a tool that catalogs sources for harassment and abuse. There are several options for how it can be used.
<h3>Search</h3> <h3>Search</h3>
To see if an instance is listed in the database, use the To see if a site is listed in the database, use the
<a href="/">search feature</a> <a href="/">search feature</a>
to search for its URL. If it is in the database, information for that instance will be returned and include associated instances, if applicable. to search for that URL. If it is in the database, information for that instance will be returned and associated instances if applicable.
<h3>CSV Exports</h3> <h3>CSV Exports</h3>
For a list of the current instances being tracked, click on one of the links below to download a dynamically generated CSV file that can be consumed as a blocklist. More formats will be added over time. For a list of the current locations being tracked, click on one of the links below to download a dynamically generated CSV file that can be consumed as a blocklist. More formats will be added over time.
<br /><br /> <br />
<a href="/exports/mastodon">For Mastodon</a> <a href="/exports/mastodon">For Mastodon</a>
<h3>API</h3> <h3>API</h3>
The Bad Space has a public API that can be used to search the database programatically and return results in the JSON format. The API can be accessed at<br /> The Bad Space has a public api that can be used to search the database programatically and return results in the JSON format. The API can be accsess at<br />
<code>https://thebad.space/api/v1/search</code> <code>https://thebad.space/api/v1/search</code>
by posting a JSON object with the following format: by posting a JSON object with the following format:
<code>{"url":"search.url"}</code><br /><br /> <code>{"url":"search.url"}</code><br />
Data from API requests will be returned in the following format:<br /> Data from API request will be returned in the follow format:<br />
<pre> <pre>
<code>{ <code>{
"listingCount":1, data:{
"locations": "listingCount":1,
[ "locations":
{ [
"url":"search.url", {
"name":"Instance Name", "url":"search.url",
"description":"instance description", "name":"Instance Name",
"link":"bad-space-instance-link" "description":"instance description",
"link":"bad-space-instance-link"
}
]
} }
]
}</code> }</code>
</pre> </pre>
</p> </p>

View File

@ -0,0 +1,62 @@
@extends('frame')
@section('title', 'The Bad Space|Appeals')
@section('main-content')
@parent
<section>
<article>
<h2>Appeals</h2>
Locations listed in The Bad Space have the right to appeal their inclusion if they feel they have been added unfairly.
<h3>Starting the Appeals Process</h3>
<p>This process can be initiated by sending an Official Appeal to The Bad Space using the Appeal Form (found below) stating why they should not be included. All appeal requests must be sponsored by at least one Current Source, and their name(s) must be included in the submission.</p>
<p>The Bad Space will validate the appeal request by contacting a moderator or administrator of the respective instance. If no moderator or administrator is detailed in the appeal request, it will be immediately disqualified.</p>
<p>If the Appeal contains any threats, hate speech, microaggressions, taunts, or slurs, the Appeal will be automatically disqualified.</p>
An Appeal can only be made once every three (3) months.
<h3>Process Description</h3>
<p>After an appeal request is validated, it will be reviewed by all Current Sources. Each Current Source will vote on whether to approve the requesting instance's appeal request.</p>
<p>80% of Current Sources must vote in favor of the request to proceed to the next step. Appeals that do not meet this threshold will be rejected, and the administrator or moderator who petitioned for the appeal will be notified.
</p>
<p>If an appeal is approved to proceed, a chat room will be created on the h.i. cloud community for the petitioner to be interviewed by Current Sources. Here, they will explain why they feel their instance should be removed from the database.</p>
The petitioner must include the following:
<ul>
<li>Verification of problematic content and members have been removed.</li>
<li>A public explanation of steps to recognize and remove problematic content and members.</li>
<li>A working plan to keep said location as free as possible from problematic members and content finding a home there.</li>
</ul>
<p>The petitioner also must be available to answer any questions from a member of Current Sources concerning the information provided.</p>
<p>Failure to provide any of this information or refusing to be interviewed by Current Sources will result in an automatic disqualification.</p>
<p>Upon completing the interview process, each Current Source will choose to remove or maintain their current block or silence action. The result of each choice will be reflected in the database itself, which can be publicly reviewed at any time.</p>
<h2>Appeals Form</h2>
<form action="/appeal" method="post" enctype="multipart/form-data">
@csrf
<label>Appeal Location</label><br />
<input type="text" name="location" value="" /><br />
<label>Appeal Location Admin</label><br />
<input type="text" name="location_admin" value="" /><br />
<label>Appeal Sponsor</label><br />
<input type="text" name="sponsor" value="" /><br />
<label>What is 1+1?</label><br />
<input type="text" name="question" value="" /><br />
<label>Appeal Summary</label><br />
<textarea name="appeal_description">Appeal Summary</textarea>
<input type="hidden" name="h1" value="" /><br />
<input type="submit" value="File Appeal" name="submit_button">
</form>
</article>
</section>
@endsection

View File

@ -0,0 +1,23 @@
@extends('frame')
@section('title', 'The Bad Space|Exports')
@section('main-content')
@parent
<section>
<article>
<h2>CSV Exports</h2>
Heat Rating is the percentage of Current Sources that have taken action against an instance. The higher the number of Sources that have silenced and/or suspended an instance, the higher the Heat Rating.*
<h3>For Mastodon</h3>
@foreach($list as $item)
<a href="/exports/mastodon/{{$item['heatRating']}}">Heat Rating: {{$item['heatRating']}}% - Location Count: {{$item['ratingCount']}}</a><br />
@endforeach
<br />
<i>* Heating Ratings are still a work in progress so please review list before using.</i>
<br /><br />
</article>
</section>
@endsection

View File

@ -6,7 +6,8 @@
<form class="index-search-form" action="/search" method="post" enctype="multipart/form-data"> <form class="index-search-form" action="/search" method="post" enctype="multipart/form-data">
<input type="text" name="index_search" value="" placeholder="Hi! This is where you search." /> <input type="text" name="index_search" value="" placeholder="Hi! This is where you search." />
<button aria-label="search-button"> <button aria-label="search-button">
<img class="button-icon" src="assets/images/global/icon-search.svg" /> <label id="search-label">LOOK FOR IT</label>
<img id="search-icon" class="button-icon" src="assets/images/global/icon-search.svg" />
</button> </button>
@csrf @csrf
</form> </form>
@ -14,32 +15,50 @@
@isset($results) @isset($results)
<section> <section>
<article> <article>
<strong> <h2>Found {{count($results)}} results for {{$terms}}</h2>
Found {{count($results)}} results: @foreach($results as $item)
</strong><br> <a class="list-link" role="listitem" href="/location/{{$item->uuid}}">
@foreach($results as $location) <span class="item-rating">{{($item->actions_count / $sources)*100}}%</span>
<a role="listitem" href="/location/{{$location->uuid}}">{{$location->name}}</a></a><br /> <label class="item-name">{{$item->name}}</label>
<div class="item-silence">
<img class="item-icon" src="/assets/images/global/status-silence.svg" title="silenced" />
{{$item->silence_count}}
</div>
<div class="item-block">
<img class="item-icon" src="/assets/images/global/status-suspend.svg" title="suspended" />
{{$item->block_count}}
</div>
</a>
@endforeach @endforeach
</article> </article>
</section> </section>
@endisset @endisset
<section class="index-meta"> <section class="index-meta">
<article> <article>
<h2>Bad Space Stats</h2>
<strong>{{$count}}</strong><br />
Instances being tracked.
<h2>Recent Updates</h2> <h2>Recent Updates</h2>
@foreach($recent as $location) @foreach($recent as $item)
<a class="list-link" role="listitem" href="/location/{{$location->uuid}}"> <a class="list-link" role="listitem" href="/location/{{$item->uuid}}">
<span>{{$location->block_count}}</span> <span class="item-rating">{{($item->actions_count / $sources)*100}}%</span>
@if($location->rating == 'silence') <label class="item-name">{{$item->name}}</label>
<img class="menu-icon" src="/assets/images/global/status-silence.svg" title="silenced" /> <div class="item-silence">
@else <img class="item-icon" src="/assets/images/global/status-silence.svg" title="silenced" />
<img class="menu-icon" src="/assets/images/global/status-suspend.svg" title="suspended" /> {{$item->silence_count}}
@endif </div>
<label>{{$location->name}}</label> <div class="item-block">
<img class="item-icon" src="/assets/images/global/status-suspend.svg" title="suspended" />
{{$item->block_count}}
</div>
</a> </a>
@endforeach @endforeach
<h2>Info</h2>
<div class="index-meta">
<label>Locations Tracked</label>
<label>{{$count}}</label>
<label>Total Sources</label>
<label>{{$sources}}</label>
<label>Latest Update</label>
<label>{{$latest_date}}</label>
</div>
</article> </article>
</section> </section>
@endsection @endsection

View File

@ -9,15 +9,21 @@
{{$pageNum}} of {{$totalPages}} {{$pageNum}} of {{$totalPages}}
<a href="/listings/{{$next}}">NEXT</a><br /><br /> <a href="/listings/{{$next}}">NEXT</a><br /><br />
@foreach($locations as $location) @foreach($locations as $location)
@php
$action = $location->block_count + $location->silence_count;
$rating = ($action / $sources)*100;
@endphp
<a class="list-link" role="listitem" href="/location/{{$location->uuid}}"> <a class="list-link" role="listitem" href="/location/{{$location->uuid}}">
@if($location->rating == 'silence') <span class="item-rating">{{$rating}}%</span>
<span>{{$location->block_count}}</span> <label class="item-name">{{$location->name}}</label>
<img class="menu-icon" src="/assets/images/global/status-silence.svg" title="silenced" /> <div class="item-silence">
@else <img class="item-icon" src="/assets/images/global/status-silence.svg" title="silenced" />
<span>{{$location->block_count}}</span> {{$location->silence_count}}
<img class="menu-icon" src="/assets/images/global/status-suspend.svg" title="suspended" /> </div>
@endif <div class="item-block">
<label>{{$location->name}}</label> <img class="item-icon" src="/assets/images/global/status-suspend.svg" title="suspended" />
{{$location->block_count}}
</div>
</a> </a>
@endforeach @endforeach
<br /> <br />

View File

@ -6,29 +6,42 @@
@parent @parent
<section> <section>
<article> <article>
<h1 class="location-title">{{$title}}</h1>
<h2>Description</h2> <h2>Description</h2>
{{$location->description}}<br /> {{$location->description}}<br />
<h2>Screens</h2> <h2>References</h2>
<h3>Images</h3>
@if($images != null) @if($images != null)
@foreach($images as $image) @foreach($images as $image)
<a href="/{{$image->path}}" class="location-image" style="background: url(/{{$image->path}}) no-repeat center center / cover #fc6399" /> <a href="/{{$image->path}}" class="location-image" style="background: url(/{{$image->path}}) no-repeat center center / cover #fc6399" />
</a> </a>
@endforeach @endforeach
@endif @endif
@php
$action = $location->block_count + $location->silence_count;
$rating = ($action / $sources_count)*100;
@endphp
<h3>Links</h3>
<div class="location-rating">
<div>
<img class="rating-icon" src="/assets/images/global/heat.svg" title="heat-rating" />
<span>RATING: {{$rating}}%</span>
</div>
<div>
<img class="rating-icon" src="/assets/images/global/status-silence.svg" title="silenced" />
<span>SILENCED: {{$location->silence_count}}</span>
</div>
<div>
<img class="rating-icon" src="/assets/images/global/status-suspend.svg" title="suspended" />
<span>SUSPENDED: {{$location->block_count}}</span>
</div>
</div>
<br /> <br />
@if($location->rating == 'silence') Heat Rating is the percentage of <a href="/about#how">Current Sources</a> that have taken action against an instance. The higher the number of Sources that have silenced and/or suspended an instance, the higher the Heat Rating.
<img class="rating-icon" src="/assets/images/global/status-silence.svg" title="silenced" />
<strong>Silenced Count: {{$location->block_count}}</strong>
@else
<img class="rating-icon" src="/assets/images/global/status-suspend.svg" title="suspended" />
<strong>Suspended Count: {{$location->block_count}}</strong>
@endif <br />UPDATED : {{$updated}}
<br /><br />
This count reflects the number of times this instance has been suspended or silenced by two or more <a href="/about#how">Current Sources</a>.
<br /><br />UPDATED: {{$updated}}
</article> </article>
</section> </section>
@endsection @endsection

View File

@ -2,6 +2,8 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\DB;
use App\Http\Resources\LocationCollection;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -17,3 +19,13 @@ use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->get('/user', function (Request $request) { Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user(); return $request->user();
}); });
// public search API
Route::post("/v1/search", function (Request $request) {
$data = json_decode($request->getContent());
$search = $data->url;
$search = str_replace(",", "", $search);
$search = str_replace(" ", "|", $search);
$results = DB::select("SELECT * FROM searchlocations('$search')");
return new LocationCollection($results);
});

View File

@ -6,6 +6,7 @@ use App\Http\Controllers\AuthController;
use App\Http\Controllers\DenController; use App\Http\Controllers\DenController;
use App\Http\Controllers\LocationController; use App\Http\Controllers\LocationController;
use App\Http\Controllers\ExportController; use App\Http\Controllers\ExportController;
use App\Http\Controllers\AppealController;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -23,10 +24,13 @@ Route::get("/", [FrontIndexController::class, 'start']);
Route::get("/listings/{pageNum}", [FrontIndexController::class, 'listings']); Route::get("/listings/{pageNum}", [FrontIndexController::class, 'listings']);
Route::get("/about", [FrontIndexController::class, 'about']); Route::get("/about", [FrontIndexController::class, 'about']);
Route::get("/location/{uuid}", [FrontIndexController::class, 'location']); Route::get("/location/{uuid}", [FrontIndexController::class, 'location']);
Route::get("/appeals", [FrontIndexController::class, 'appeals']);
Route::post("/search", [FrontIndexController::class, 'indexSearch']); Route::post("/search", [FrontIndexController::class, 'indexSearch']);
Route::post("/appeal", [AppealController::class, 'sendAppeal']);
//exports //exports
Route::get("/exports/test", [ExportController::class, 'exportCSV']); Route::get("/exports", [ExportController::class, 'exportIndex']);
Route::get("/exports/{type}/{rate}", [ExportController::class, 'exportCSV']);
//auth //auth
Route::get("/login", [AuthController::class, 'showLogin']); Route::get("/login", [AuthController::class, 'showLogin']);