voting added

This commit is contained in:
HolgerHatGarKeineNode
2023-03-10 23:03:19 +01:00
parent 4905134ea6
commit c2b7042eab
50 changed files with 1955 additions and 592 deletions

View File

@@ -1,30 +0,0 @@
created:
- database/factories/BitcoinEventFactory.php
- database/migrations/2022_12_12_171115_create_bitcoin_events_table.php
- app/Models/BitcoinEvent.php
- app/Nova/BitcoinEvent.php
models:
BookCase: { title: string, latitude: 'float:10', longitude: 'float:10', address: 'text nullable', type: string, open: 'string nullable', comment: 'text nullable', contact: 'text nullable', bcz: 'text nullable', digital: boolean, icontype: string, deactivated: boolean, deactreason: string, entrytype: string, homepage: 'text nullable' }
Category: { name: string, slug: string }
City: { country_id: biginteger, name: string, slug: string, longitude: 'float:10', latitude: 'float:10' }
Country: { name: string, code: string, language_codes: 'json default:[]' }
Course: { lecturer_id: biginteger, name: string, description: 'text nullable' }
CourseEvent: { course_id: biginteger, venue_id: biginteger, '"from"': datetime, '"to"': datetime, link: string }
Episode: { guid: string, podcast_id: biginteger, data: json }
Lecturer: { team_id: biginteger, name: string, slug: string, active: 'boolean default:1', description: 'text nullable' }
Library: { name: string, is_public: 'boolean default:1', language_codes: 'json default:[]' }
LibraryItem: { lecturer_id: biginteger, episode_id: 'biginteger nullable', order_column: integer, name: string, type: string, language_code: string, value: 'text nullable' }
LoginKey: { k1: string, user_id: biginteger }
Meetup: { city_id: biginteger, name: string, link: string }
MeetupEvent: { meetup_id: biginteger, start: datetime, location: 'string nullable', description: 'text nullable', link: 'string nullable' }
Membership: { team_id: biginteger, user_id: biginteger, role: 'string nullable' }
OrangePill: { user_id: biginteger, book_case_id: biginteger, date: datetime, amount: integer }
Participant: { first_name: string, last_name: string }
Podcast: { guid: string, locked: 'boolean default:1', title: string, link: string, language_code: string, data: 'json nullable' }
Registration: { course_event_id: biginteger, participant_id: biginteger, active: 'boolean default:1' }
Tag: { name: json, slug: json, type: 'string nullable', order_column: 'integer nullable', icon: 'string default:tag' }
Team: { user_id: biginteger, name: string, personal_team: boolean }
TeamInvitation: { team_id: biginteger, email: string, role: 'string nullable' }
User: { name: string, public_key: 'string nullable', email: 'string nullable', email_verified_at: 'datetime nullable', password: 'string nullable', remember_token: 'string:100 nullable', current_team_id: 'biginteger nullable', profile_photo_path: 'string:2048 nullable', is_lecturer: 'boolean default:', two_factor_secret: 'text nullable', two_factor_recovery_codes: 'text nullable', two_factor_confirmed_at: 'datetime nullable', timezone: 'string default:Europe/Berlin' }
Venue: { city_id: biginteger, name: string, slug: string, street: string }
BitcoinEvent: { venue_id: foreign, from: datetime, to: datetime, title: string, description: text, link: string }

2
.gitignore vendored
View File

@@ -28,3 +28,5 @@ auth.json
yarn-error.log
.yarn/*
!.yarn/releases
.blueprint

View File

@@ -54,6 +54,7 @@ class CreatePermissions extends Command
'OrangePillPolicy',
'ParticipantPolicy',
'PodcastPolicy',
'ProjectProposalPolicy',
'RegistrationPolicy',
'TeamPolicy',
'UserPolicy',

View File

@@ -136,6 +136,7 @@ class Header extends Component
->orderByDesc('date')
->take(2)
->get(),
'projectProposals' => [],
'cities' => City::query()
->select(['latitude', 'longitude'])
->get(),

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Http\Livewire\ProjectProposal\Form;
use App\Models\Country;
use App\Models\ProjectProposal;
use Livewire\Component;
use Livewire\WithFileUploads;
class ProjectProposalForm extends Component
{
use WithFileUploads;
public Country $country;
public ?ProjectProposal $projectProposal = null;
public $image;
public ?string $fromUrl = '';
protected $queryString = [
'fromUrl' => ['except' => ''],
];
public function rules()
{
return [
'image' => [
'nullable', 'mimes:jpeg,png,jpg,gif', 'max:10240'
],
'projectProposal.user_id' => 'required',
'projectProposal.name' => 'required',
'projectProposal.support_in_sats' => 'required|numeric',
'projectProposal.description' => 'required',
];
}
public function mount()
{
if (!$this->projectProposal) {
$this->projectProposal = new ProjectProposal([
'user_id' => auth()->id(),
'description' => '',
]);
}
if (!$this->fromUrl) {
$this->fromUrl = url()->previous();
}
}
public function save()
{
$this->validate();
$this->projectProposal->save();
return redirect($this->fromUrl);
}
public function render()
{
return view('livewire.project-proposal.form.project-proposal-form', [
]);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Http\Livewire\ProjectProposal;
use App\Models\Country;
use Livewire\Component;
class ProjectProposalTable extends Component
{
public Country $country;
public function render()
{
return view('livewire.project-proposal.project-proposal-table');
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace App\Http\Livewire\ProjectProposal;
use App\Models\Country;
use App\Models\ProjectProposal;
use App\Models\User;
use App\Models\Vote;
use Illuminate\Validation\Rule;
use Livewire\Component;
class ProjectProposalVoting extends Component
{
public Country $country;
public ?ProjectProposal $projectProposal = null;
public ?Vote $vote = null;
public ?string $fromUrl = '';
protected $queryString = [
'fromUrl' => ['except' => ''],
];
public function rules()
{
return [
'vote.user_id' => 'required',
'vote.project_proposal_id' => 'required',
'vote.value' => 'required|boolean',
'vote.reason' => [
Rule::requiredIf(!$this->vote->value),
]
];
}
public function mount()
{
$vote = Vote::query()
->where('user_id', auth()->id())
->where('project_proposal_id', $this->projectProposal->id)
->first();
if ($vote) {
$this->vote = $vote;
} else {
$this->vote = new Vote();
$this->vote->user_id = auth()->id();
$this->vote->project_proposal_id = $this->projectProposal->id;
$this->vote->value = false;
}
}
public function yes()
{
$this->vote->value = true;
$this->vote->save();
return to_route('project.voting.projectFunding',
['country' => $this->country, 'projectProposal' => $this->projectProposal, 'fromUrl' => $this->fromUrl]);
}
public function no()
{
$this->validate();
$this->vote->value = false;
$this->vote->save();
return to_route('project.voting.projectFunding',
['country' => $this->country, 'projectProposal' => $this->projectProposal, 'fromUrl' => $this->fromUrl]);
}
public function render()
{
return view('livewire.project-proposal.project-proposal-voting', [
'entitledVoters' => User::query()
->with([
'votes' => fn($query) => $query->where('project_proposal_id',
$this->projectProposal->id)
])
->withCount([
'votes' => fn($query) => $query->where('project_proposal_id',
$this->projectProposal->id)
])
->whereHas('roles', function ($query) {
return $query->where('roles.name', 'entitled-voter');
})
->orderByDesc('votes_count')
->get(),
'otherVoters' => User::query()
->with([
'votes' => fn($query) => $query->where('project_proposal_id',
$this->projectProposal->id)
])
->withCount([
'votes' => fn($query) => $query->where('project_proposal_id',
$this->projectProposal->id)
])
->whereDoesntHave('roles', function ($query) {
return $query->where('roles.name', 'entitled-voter');
})
->orderByDesc('votes_count')
->get(),
]);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Http\Livewire\Tables;
use Illuminate\Database\Eloquent\Builder;
use Rappasoft\LaravelLivewireTables\DataTableComponent;
use Rappasoft\LaravelLivewireTables\Views\Column;
use App\Models\ProjectProposal;
class ProjectProposalTable extends DataTableComponent
{
public ?string $country = null;
public string $tableName = 'project_proposals';
public function configure(): void
{
$this->setPrimaryKey('id')
->setAdditionalSelects(['id', 'created_by'])
->setThAttributes(function (Column $column) {
return [
'class' => 'px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:bg-gray-800 dark:text-gray-400',
'default' => false,
];
})
->setTdAttributes(function (Column $column, $row, $columnIndex, $rowIndex) {
return [
'class' => 'px-6 py-4 text-sm font-medium dark:text-white',
'default' => false,
];
})
->setColumnSelectStatus(false)
->setPerPage(10)
->setConfigurableAreas([
'toolbar-left-end' => [
'columns.project_proposals.areas.toolbar-left-end', [
'country' => $this->country,
],
],
]);
}
public function columns(): array
{
return [
Column::make("Id", "id")
->sortable(),
Column::make("Name", "name")
->sortable(),
Column::make(__('Intended support in sats'), "support_in_sats")
->format(
fn ($value, $row, Column $column) => number_format($value, 0, ',', '.')
)
->sortable(),
Column::make('')
->label(
fn ($row, Column $column) => view('columns.project_proposals.action')->withRow($row)->withCountry($this->country)
),
];
}
public function builder(): Builder
{
return ProjectProposal::query();
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Spatie\Image\Manipulations;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class ProjectProposal extends Model implements HasMedia
{
use InteractsWithMedia;
use HasFactory;
/**
* The attributes that aren't mass assignable.
*
* @var array
*/
protected $guarded = [];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'id' => 'integer',
'user_id' => 'integer',
];
protected static function booted()
{
static::creating(function ($model) {
if (! $model->created_by) {
$model->created_by = auth()->id();
}
});
}
public function registerMediaConversions(Media $media = null): void
{
$this
->addMediaConversion('preview')
->fit(Manipulations::FIT_CROP, 300, 300)
->nonQueued();
$this->addMediaConversion('thumb')
->fit(Manipulations::FIT_CROP, 130, 130)
->width(130)
->height(130);
}
public function registerMediaCollections(): void
{
$this->addMediaCollection('main')
->singleFile()
->useFallbackUrl(asset('img/einundzwanzig.png'));
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@@ -36,7 +36,6 @@ class User extends Authenticatable implements MustVerifyEmail, CanComment, Ciphe
/**
* The attributes that should be hidden for serialization.
*
* @var array
*/
protected $hidden = [
@@ -48,7 +47,6 @@ class User extends Authenticatable implements MustVerifyEmail, CanComment, Ciphe
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
@@ -57,7 +55,6 @@ class User extends Authenticatable implements MustVerifyEmail, CanComment, Ciphe
/**
* The accessors to append to the model's array form.
*
* @var array
*/
protected $appends = [
@@ -93,4 +90,9 @@ class User extends Authenticatable implements MustVerifyEmail, CanComment, Ciphe
{
return $this->morphMany('QCod\Gamify\Reputation', 'subject');
}
public function votes()
{
return $this->hasMany(Vote::class);
}
}

41
app/Models/Vote.php Normal file
View File

@@ -0,0 +1,41 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Vote extends Model
{
use HasFactory;
/**
* The attributes that aren't mass assignable.
*
* @var array
*/
protected $guarded = [];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'id' => 'integer',
'user_id' => 'integer',
'project_proposal_id' => 'integer',
'value' => 'bool',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function projectProposal(): BelongsTo
{
return $this->belongsTo(ProjectProposal::class);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Policies;
use App\Models\ProjectProposal;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class ProjectProposalPolicy extends BasePolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return true;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, ProjectProposal $projectProposal): bool
{
return true;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return true;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, ProjectProposal $projectProposal): bool
{
return $projectProposal->created_by === $user->id || $user->can((new \ReflectionClass($this))->getShortName().'.'.__FUNCTION__);
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, ProjectProposal $projectProposal): bool
{
return $projectProposal->created_by === $user->id || $user->can((new \ReflectionClass($this))->getShortName().'.'.__FUNCTION__);
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, ProjectProposal $projectProposal): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, ProjectProposal $projectProposal): bool
{
return false;
}
}

View File

@@ -61,7 +61,8 @@
"symfony/mailgun-mailer": "^6.2",
"wesselperik/nova-status-field": "^2.1",
"wireui/wireui": "dev-main",
"ziffmedia/nova-select-plus": "^2.0"
"ziffmedia/nova-select-plus": "^2.0",
"jantinnerezo/livewire-range-slider": "dev-main#3b6b28ca22be89bcb65b6b90e32ca7d04c267382"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",
@@ -70,6 +71,7 @@
"laravel-lang/http-statuses": "^3.1",
"laravel-lang/lang": "^12.5",
"laravel-lang/publisher": "^14.6",
"laravel-shift/blueprint": "^2.7",
"laravel/pint": "^1.0",
"laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4",
@@ -138,6 +140,10 @@
"symlink": false
}
},
{
"type": "vcs",
"url": "https://github.com/HolgerHatGarKeineNode/livewire-range-slider"
},
{
"type": "vcs",
"url": "https://github.com/HolgerHatGarKeineNode/blade-country-flags"

862
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
use App\Models\ProjectProposal;
use App\Models\User;
class ProjectProposalFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = ProjectProposal::class;
/**
* Define the model's default state.
*/
public function definition(): array
{
return [
'user_id' => User::factory(),
'name' => $this->faker->name,
'support_in_sats' => $this->faker->randomNumber(),
'description' => $this->faker->text,
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
use App\Models\ProjectProposal;
use App\Models\User;
use App\Models\Vote;
class VoteFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Vote::class;
/**
* Define the model's default state.
*/
public function definition(): array
{
return [
'user_id' => User::factory(),
'project_proposal_id' => ProjectProposal::factory(),
'value' => $this->faker->randomNumber(),
'reason' => $this->faker->text,
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::disableForeignKeyConstraints();
Schema::create('project_proposals', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete()->cascadeOnUpdate();
$table->string('name')->unique();
$table->unsignedInteger('support_in_sats');
$table->text('description');
$table->timestamps();
});
Schema::enableForeignKeyConstraints();
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('project_proposals');
}
};

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::disableForeignKeyConstraints();
Schema::create('votes', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete()->cascadeOnUpdate();
$table->foreignId('project_proposal_id')->constrained()->cascadeOnDelete()->cascadeOnUpdate();
$table->unsignedInteger('value');
$table->text('reason')->nullable();
$table->timestamps();
});
Schema::enableForeignKeyConstraints();
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('votes');
}
};

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('project_proposals', function (Blueprint $table) {
$table->foreignId('created_by')
->nullable()
->constrained('users')
->onDelete('set null');
});
Schema::table('votes', function (Blueprint $table) {
$table->foreignId('created_by')
->nullable()
->constrained('users')
->onDelete('set null');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('project_proposals', function (Blueprint $table) {
//
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('project_proposals', function (Blueprint $table) {
$table->unsignedBigInteger('support_in_sats')
->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('project_proposals', function (Blueprint $table) {
//
});
}
};

View File

@@ -42,6 +42,10 @@ class DatabaseSeeder extends Seeder
'name' => 'super-admin',
'guard_name' => 'web',
]);
Role::create([
'name' => 'entitled-voter',
'guard_name' => 'web',
]);
Permission::create([
'name' => 'translate',
'guard_name' => 'web',
@@ -54,6 +58,23 @@ class DatabaseSeeder extends Seeder
'remember_token' => Str::random(10),
'is_lecturer' => true,
]);
$entitledUser = User::create([
'name' => 'Entitled Voter',
'email' => 'voter1@einundzwanzig.space',
'email_verified_at' => now(),
'password' => bcrypt('1234'),
'remember_token' => Str::random(10),
'is_lecturer' => true,
]);
$entitledUser->assignRole('entitled-voter');
User::create([
'name' => 'Not Entitled Voter',
'email' => 'voter1@einundzwanzig.space',
'email_verified_at' => now(),
'password' => bcrypt('1234'),
'remember_token' => Str::random(10),
'is_lecturer' => true,
]);
$team = Team::create([
'name' => 'Admin Team',
'user_id' => $user->id,

View File

@@ -0,0 +1,11 @@
models:
ProjectProposal:
user_id: foreign
name: string unique
support_in_sats: integer unsigned
description: text
Vote:
user_id: foreign
project_proposal_id: foreign
value: integer unsigned
reason: text nullable

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
{
"/app.js": "/app.js?id=6148211f5802ae601d59d3dfa95d1d35",
"/app.js": "/app.js?id=45904d8bd75c65ee5c136a52a5e8ead6",
"/app-dark.css": "/app-dark.css?id=15c72df05e2b1147fa3e4b0670cfb435",
"/app.css": "/app.css?id=4d6a1a7fe095eedc2cb2a4ce822ea8a5",
"/img/favicon.png": "/img/favicon.png?id=1542bfe8a0010dcbee710da13cce367f",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
{
"/app.js": "/app.js?id=98c5c53ef4ec8700776f999086de129e",
"/app.js": "/app.js?id=04e348094a54ac4cf95cdaa45acb8f58",
"/manifest.js": "/manifest.js?id=d75058ce2144a4049857d3ff9e02de1e",
"/app.css": "/app.css?id=f33fcd06eab1fae1d91d0347f8a3f06c",
"/app.css": "/app.css?id=30653583355cfd55b0fd95f328ff4706",
"/vendor.js": "/vendor.js?id=de86bde8857e857b607852d11627d8e4",
"/fonts/snunitosansv11pe01mimslybiv1o4x1m8cce4g1pty10iurt9w6fk2a.woff2": "/fonts/snunitosansv11pe01mimslybiv1o4x1m8cce4g1pty10iurt9w6fk2a.woff2?id=c8390e146be0a3c8a5498355dec892ae",
"/fonts/snunitosansv11pe01mimslybiv1o4x1m8cce4g1pty14iurt9w6fk2a.woff2": "/fonts/snunitosansv11pe01mimslybiv1o4x1m8cce4g1pty14iurt9w6fk2a.woff2?id=b0735c7dd6126471acbaf9d6e9f5e41a",

View File

@@ -799,5 +799,17 @@
"Language": "Sprache",
"minutes": "Minuten",
"Recurring appointment \/ monthly": "Wiederkehrende Termine \/ monatlich",
"Search and find Bitcoin Podcast episodes.": "Suche und finde Bitcoin Podcast-Episoden."
"Search and find Bitcoin Podcast episodes.": "Suche und finde Bitcoin Podcast-Episoden.",
"Intended support in sats": "Beabsichtigte Unterstützung in sats",
"Vote": "Abstimmen",
"Submit project for funding": "Projekt zur Finanzierung einreichen",
"Project Funding": "Projekt-Unterstützung",
"Project Funding and Voting": "Projekt-Unterstützung und Abstimmung",
"Submitted projects": "Eingereichte Projekte",
"Project Proposal": "Projekt-Vorschlag",
"Project description": "Projekt-Beschreibung",
"Please write a detailed and understandable application text, so that the vote on a possible support can take place.": "Bitte schreibe einen detaillierten und verständlichen Bewerbungstext, damit die Abstimmung über eine mögliche Unterstützung stattfinden kann.",
"Voting": "Abstimmung",
"Reason": "Ablehnungsgrund",
"not voted yet": "bisher nicht abgestimmt"
}

View File

@@ -796,5 +796,17 @@
"Language": "",
"minutes": "",
"Recurring appointment \/ monthly": "",
"Search and find Bitcoin Podcast episodes.": ""
"Search and find Bitcoin Podcast episodes.": "",
"Intended support in sats": "",
"Vote": "",
"Submit project for funding": "",
"Project Funding": "",
"Project Funding and Voting": "",
"Submitted projects": "",
"Project Proposal": "",
"Project description": "",
"Please write a detailed and understandable application text, so that the vote on a possible support can take place.": "",
"Voting": "",
"Reason": "",
"not voted yet": ""
}

View File

@@ -796,5 +796,17 @@
"Language": "",
"minutes": "",
"Recurring appointment \/ monthly": "",
"Search and find Bitcoin Podcast episodes.": ""
"Search and find Bitcoin Podcast episodes.": "",
"Intended support in sats": "",
"Vote": "",
"Submit project for funding": "",
"Project Funding": "",
"Project Funding and Voting": "",
"Submitted projects": "",
"Project Proposal": "",
"Project description": "",
"Please write a detailed and understandable application text, so that the vote on a possible support can take place.": "",
"Voting": "",
"Reason": "",
"not voted yet": ""
}

View File

@@ -797,5 +797,17 @@
"Language": "",
"minutes": "",
"Recurring appointment \/ monthly": "",
"Search and find Bitcoin Podcast episodes.": ""
"Search and find Bitcoin Podcast episodes.": "",
"Intended support in sats": "",
"Vote": "",
"Submit project for funding": "",
"Project Funding": "",
"Project Funding and Voting": "",
"Submitted projects": "",
"Project Proposal": "",
"Project description": "",
"Please write a detailed and understandable application text, so that the vote on a possible support can take place.": "",
"Voting": "",
"Reason": "",
"not voted yet": ""
}

View File

@@ -797,5 +797,17 @@
"Language": "",
"minutes": "",
"Recurring appointment \/ monthly": "",
"Search and find Bitcoin Podcast episodes.": ""
"Search and find Bitcoin Podcast episodes.": "",
"Intended support in sats": "",
"Vote": "",
"Submit project for funding": "",
"Project Funding": "",
"Project Funding and Voting": "",
"Submitted projects": "",
"Project Proposal": "",
"Project description": "",
"Please write a detailed and understandable application text, so that the vote on a possible support can take place.": "",
"Voting": "",
"Reason": "",
"not voted yet": ""
}

View File

@@ -797,5 +797,17 @@
"Language": "",
"minutes": "",
"Recurring appointment \/ monthly": "",
"Search and find Bitcoin Podcast episodes.": ""
"Search and find Bitcoin Podcast episodes.": "",
"Intended support in sats": "",
"Vote": "",
"Submit project for funding": "",
"Project Funding": "",
"Project Funding and Voting": "",
"Submitted projects": "",
"Project Proposal": "",
"Project description": "",
"Please write a detailed and understandable application text, so that the vote on a possible support can take place.": "",
"Voting": "",
"Reason": "",
"not voted yet": ""
}

View File

@@ -797,5 +797,17 @@
"Language": "",
"minutes": "",
"Recurring appointment \/ monthly": "",
"Search and find Bitcoin Podcast episodes.": ""
"Search and find Bitcoin Podcast episodes.": "",
"Intended support in sats": "",
"Vote": "",
"Submit project for funding": "",
"Project Funding": "",
"Project Funding and Voting": "",
"Submitted projects": "",
"Project Proposal": "",
"Project description": "",
"Please write a detailed and understandable application text, so that the vote on a possible support can take place.": "",
"Voting": "",
"Reason": "",
"not voted yet": ""
}

View File

@@ -797,5 +797,17 @@
"Language": "",
"minutes": "",
"Recurring appointment \/ monthly": "",
"Search and find Bitcoin Podcast episodes.": ""
"Search and find Bitcoin Podcast episodes.": "",
"Intended support in sats": "",
"Vote": "",
"Submit project for funding": "",
"Project Funding": "",
"Project Funding and Voting": "",
"Submitted projects": "",
"Project Proposal": "",
"Project description": "",
"Please write a detailed and understandable application text, so that the vote on a possible support can take place.": "",
"Voting": "",
"Reason": "",
"not voted yet": ""
}

View File

@@ -797,5 +797,17 @@
"Language": "",
"minutes": "",
"Recurring appointment \/ monthly": "",
"Search and find Bitcoin Podcast episodes.": ""
"Search and find Bitcoin Podcast episodes.": "",
"Intended support in sats": "",
"Vote": "",
"Submit project for funding": "",
"Project Funding": "",
"Project Funding and Voting": "",
"Submitted projects": "",
"Project Proposal": "",
"Project description": "",
"Please write a detailed and understandable application text, so that the vote on a possible support can take place.": "",
"Voting": "",
"Reason": "",
"not voted yet": ""
}

View File

@@ -759,5 +759,17 @@
"Language": "",
"minutes": "",
"Recurring appointment \/ monthly": "",
"Search and find Bitcoin Podcast episodes.": ""
"Search and find Bitcoin Podcast episodes.": "",
"Intended support in sats": "",
"Vote": "",
"Submit project for funding": "",
"Project Funding": "",
"Project Funding and Voting": "",
"Submitted projects": "",
"Project Proposal": "",
"Project description": "",
"Please write a detailed and understandable application text, so that the vote on a possible support can take place.": "",
"Voting": "",
"Reason": "",
"not voted yet": ""
}

View File

@@ -771,5 +771,17 @@
"Language": "",
"minutes": "",
"Recurring appointment \/ monthly": "",
"Search and find Bitcoin Podcast episodes.": ""
"Search and find Bitcoin Podcast episodes.": "",
"Intended support in sats": "",
"Vote": "",
"Submit project for funding": "",
"Project Funding": "",
"Project Funding and Voting": "",
"Submitted projects": "",
"Project Proposal": "",
"Project description": "",
"Please write a detailed and understandable application text, so that the vote on a possible support can take place.": "",
"Voting": "",
"Reason": "",
"not voted yet": ""
}

View File

@@ -0,0 +1,16 @@
<div class="flex flex-col space-y-1">
@can('update', $row)
<div>
<x-button xs amber :href="route('project.projectProposal.form', ['country' => $country, 'projectProposal' => $row])">
<i class="fa fa-thin fa-edit mr-2"></i>
{{ __('Edit') }}
</x-button>
</div>
@endcan
<div>
<x-button xs black :href="route('project.voting.projectFunding', ['country' => $country, 'projectProposal' => $row])">
<i class="fa fa-thin fa-check-to-slot mr-2"></i>
{{ __('Vote') }} [0]
</x-button>
</div>
</div>

View File

@@ -0,0 +1,6 @@
<div class="w-full mb-4 md:w-auto md:mb-0">
<x-button :href="route('project.projectProposal.form', ['country' => $country, 'projectProposal' => null])">
<i class="fa fa-thin fa-plus"></i>
{{ __('Submit project for funding') }}
</x-button>
</div>

View File

@@ -65,5 +65,6 @@
<!-- ProductLift SDK - Include it only once -->
<script defer src="https://bitcoin.productlift.dev/widgets_sdk"></script>
<x-comments::scripts/>
<x-livewire-range-slider::scripts />
</body>
</html>

View File

@@ -63,5 +63,6 @@
<!-- ProductLift SDK - Include it only once -->
<script defer src="https://bitcoin.productlift.dev/widgets_sdk"></script>
<x-comments::scripts/>
<x-livewire-range-slider::scripts />
</body>
</html>

View File

@@ -31,6 +31,8 @@
@include('livewire.frontend.navigation.bookcases')
@include('livewire.frontend.navigation.association')
@auth
@include('livewire.frontend.navigation.profile')
@endauth

View File

@@ -0,0 +1,91 @@
<div x-data="Components.popover({ open: false, focus: false })" x-init="init()" @keydown.escape="onEscape"
@close-popover-group.window="onClosePopoverGroup">
<button type="button" class="flex items-center gap-x-1 text-sm font-semibold leading-6 text-gray-900"
@click="toggle" @mousedown="if (open) $event.preventDefault()" aria-expanded="true"
:aria-expanded="open.toString()">
{{ __('Project Funding') }}
<svg class="h-5 w-5 flex-none text-gray-400" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path fill-rule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
clip-rule="evenodd"></path>
</svg>
</button>
<div x-show="open" x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-1"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 -translate-y-1"
x-description="'Product' flyout menu, show/hide based on flyout menu state."
class="absolute inset-x-0 top-0 -z-10 bg-white pt-16 shadow-lg ring-1 ring-gray-900/5"
x-ref="panel" @click.away="open = false" x-cloak>
<div class="mx-auto grid max-w-7xl grid-cols-1 gap-y-10 gap-x-8 py-10 px-6 lg:grid-cols-2 lg:px-8">
<div class="grid grid-cols-2 gap-x-6 sm:gap-x-8">
<div>
<h3 class="text-sm font-medium leading-6 text-gray-500">{{ __('Project Funding and Voting') }}</h3>
<div class="mt-6 flow-root">
<div class="-my-2">
<a href="{{ route('project.table.projectFunding', ['country' => $country]) }}"
class="flex gap-x-4 py-2 text-sm font-semibold leading-6 text-gray-900">
<i class="fa-thin fa-search flex-none text-gray-400"></i>
{{ __('Submitted projects') }}
</a>
</div>
</div>
</div>
<div>
<h3 class="text-sm font-medium leading-6 text-gray-500">{{ __('Manage') }}</h3>
<div class="mt-6 flow-root">
<div class="-my-2">
<a href="{{ route('project.projectProposal.form', ['country' => $country]) }}"
class="flex gap-x-4 py-2 text-sm font-semibold leading-6 text-gray-900">
<i class="fa-thin fa-plus flex-none text-gray-400"></i>
{{ __('Submit project for funding') }}
</a>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-10 sm:gap-8 lg:grid-cols-2">
<h3 class="sr-only">Recent posts</h3>
@foreach($projectProposals as $item)
<article
class="relative isolate flex max-w-2xl flex-col gap-x-8 gap-y-6 sm:flex-row sm:items-start lg:flex-col lg:items-stretch">
<div class="relative flex-none">
<img
class="aspect-[2/1] w-full rounded-lg bg-gray-100 object-cover sm:aspect-[16/9] sm:h-32 lg:h-auto"
src="{{ $item->getFirstMediaUrl('main') }}"
alt="">
<div
class="absolute inset-0 rounded-lg ring-1 ring-inset ring-gray-900/10"></div>
</div>
<div>
<div class="flex items-center gap-x-4">
<div
class="relative z-10 rounded-full bg-gray-50 py-1.5 px-3 text-xs font-medium text-gray-600 hover:bg-gray-100">
{{ $item->name }}
</div>
</div>
<h4 class="mt-2 text-sm font-semibold leading-6 text-gray-900">
<a href="{{ route('libraryItem.view', ['libraryItem' => $item]) }}">
<span class="absolute inset-0"></span>
{{ $item->name }}
</a>
</h4>
<p class="mt-2 text-sm leading-6 text-gray-600 truncate">
{{ $item->description }}
</p>
</div>
</article>
@endforeach
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,75 @@
<div>
{{-- HEADER --}}
<livewire:frontend.header :country="null"/>
<div class="container p-4 mx-auto bg-21gray my-2">
<div class="pb-5 flex flex-row justify-between">
<h3 class="text-lg font-medium leading-6 text-gray-200">{{ __('Project Proposal') }}</h3>
<div class="flex flex-row space-x-2 items-center">
<div>
<x-button :href="$fromUrl">
<i class="fa fa-thin fa-arrow-left"></i>
{{ __('Back') }}
</x-button>
</div>
</div>
</div>
<form class="space-y-8 divide-y divide-gray-700 pb-24">
<div class="space-y-8 divide-y divide-gray-700 sm:space-y-5">
<div class="mt-6 sm:mt-5 space-y-6 sm:space-y-5">
<x-input.group :for=" md5('image')" :label="__('Main picture')">
<div class="py-4">
@if ($image)
<div class="text-gray-200">{{ __('Preview') }}:</div>
<img class="h-48 object-contain" src="{{ $image->temporaryUrl() }}">
@endif
@if ($projectProposal->getFirstMediaUrl('main'))
<div class="text-gray-200">{{ __('Current picture') }}:</div>
<img class="h-48 object-contain" src="{{ $projectProposal->getFirstMediaUrl('main') }}">
@endif
</div>
<input class="text-gray-200" type="file" wire:model="image">
@error('image') <span class="text-red-500">{{ $message }}</span> @enderror
</x-input.group>
<x-input.group :for="md5('projectProposal.name')" :label="__('Name')">
<x-input autocomplete="off" wire:model.debounce="projectProposal.name"
:placeholder="__('Name')"/>
</x-input.group>
<x-input.group :for="md5('projectProposal.name')" :label="__('Intended support in sats')">
<x-input type="number" autocomplete="off" wire:model.debounce="projectProposal.support_in_sats"
:placeholder="__('Intended support in sats')"/>
</x-input.group>
<x-input.group :for="md5('projectProposal.description')">
<x-slot name="label">
<div>
{{ __('Project description') }}
</div>
<div
class="text-amber-500 text-xs py-2">{{ __('Please write a detailed and understandable application text, so that the vote on a possible support can take place.') }}</div>
</x-slot>
<div
class="text-amber-500 text-xs py-2">{{ __('For images in Markdown, please use eg. Imgur or another provider.') }}</div>
<x-input.simple-mde wire:model.defer="projectProposal.description"/>
@error('projectProposal.description') <span
class="text-red-500 py-2">{{ $message }}</span> @enderror
</x-input.group>
<x-input.group :for="md5('save')" label="">
<x-button primary wire:click="save">
<i class="fa fa-thin fa-save"></i>
{{ __('Save') }}
</x-button>
</x-input.group>
</div>
</div>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
</div>

View File

@@ -0,0 +1,22 @@
<div class="bg-21gray flex flex-col h-screen justify-between">
{{-- HEADER --}}
<livewire:frontend.header :country="$country"/>
{{-- MAIN --}}
<section class="w-full mb-12">
<div class="max-w-screen-2xl mx-auto px-2 sm:px-10 space-y-4" id="table">
<div class="md:flex md:items-center md:justify-between">
<div class="min-w-0 flex-1">
<h2 class="text-2xl font-bold leading-7 text-white sm:truncate sm:text-3xl sm:tracking-tight">
{{ __('Submitted projects') }}
</h2>
</div>
<div class="mt-4 flex md:mt-0 md:ml-4">
{{----}}
</div>
</div>
<livewire:tables.project-proposal-table :country="$country->code"/>
</div>
</section>
{{-- FOOTER --}}
<livewire:frontend.footer/>
</div>

View File

@@ -0,0 +1,171 @@
<div>
{{-- HEADER --}}
<livewire:frontend.header :country="null"/>
<div class="container p-4 mx-auto bg-21gray my-2">
<div class="pb-5 flex flex-row justify-between">
<h3 class="text-lg font-medium leading-6 text-gray-200">{{ __('Voting') }}
: {{ $projectProposal->name }}</h3>
<div class="flex flex-row space-x-2 items-center">
<div>
<x-button :href="$fromUrl">
<i class="fa fa-thin fa-arrow-left"></i>
{{ __('Back') }}
</x-button>
</div>
</div>
</div>
<form class="space-y-8 divide-y divide-gray-700 pb-24">
<div class="space-y-8 divide-y divide-gray-700 sm:space-y-5">
<div class="mt-6 sm:mt-5 space-y-6 sm:space-y-5">
<div class="w-full flex space-x-4">
<x-button lg primary wire:click="yes">
Yes, support it!
</x-button>
<x-button lg primary wire:click="no">
No, don't support it!
</x-button>
</div>
<div>
<x-input.group :for="md5('vote.reason')" :label="__('Reason')">
<x-textarea autocomplete="off" wire:model.debounce="vote.reason"
:placeholder="__('Reason')"/>
</x-input.group>
</div>
<div wire:ignore>
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<div
x-data="{
yes: [{{ $entitledVoters->pluck('votes')->collapse()->where('value', 1)->count() }},{{ $otherVoters->pluck('votes')->collapse()->where('value', 1)->count() }}],
no: [{{ $entitledVoters->pluck('votes')->collapse()->where('value', 0)->count() }},{{ $otherVoters->pluck('votes')->collapse()->where('value', 0)->count() }}],
labels: ['Entitled voters', 'Other voters',],
init() {
let chart = new ApexCharts(this.$refs.chart, this.options)
chart.render()
this.$watch('valuesEligible', () => {
chart.updateOptions(this.options)
})
this.$watch('valuesOther', () => {
chart.updateOptions(this.options)
})
},
get options() {
return {
theme: { palette: 'palette3' },
chart: { type: 'bar', toolbar: true, height: 350, stacked: true, stackType: '100%'},
xaxis: { categories: this.labels },
plotOptions: { bar: { horizontal: true } },
series: [
{
name: 'Yes',
data: this.yes,
},
{
name: 'No',
data: this.no,
},
],
}
}
}"
class="w-full"
>
<div x-ref="chart" class="rounded-lg bg-white p-8"></div>
</div>
</div>
<div class="w-full grid grid-cols-2">
<div>
<div class="border-b border-gray-200 bg-dark px-4 py-5 sm:px-6">
<h3 class="text-base font-semibold leading-6 text-gray-200">Entitled voters</h3>
</div>
<ul role="list" class="divide-y divide-gray-200">
@foreach($entitledVoters as $voter)
@php
$vote = $voter->votes->first();
if (!$voter->votes->first()) {
$text = __('not voted yet');
} elseif (!$vote->value) {
$text = __('Reason') . ': ' . $voter->votes->first()?->reason;
}
@endphp
<li class="flex py-4">
<img class="h-10 w-10 rounded-full" src="{{ $voter->profile_photo_url }}"
alt="">
<div class="ml-3">
<p class="text-sm font-medium text-gray-200">
{{ $voter->name }}
@if($voter->votes->first()?->value)
<x-badge green>{{ __('Yes') }}</x-badge>
@endif
@if($voter->votes->first() && !$voter->votes->first()?->value)
<x-badge red>{{ __('No') }}</x-badge>
@endif
</p>
<p class="text-sm text-gray-300">
{{ $text ?? '' }}
</p>
</div>
</li>
@endforeach
</ul>
</div>
<div>
<div class="border-b border-gray-200 bg-dark px-4 py-5 sm:px-6">
<h3 class="text-base font-semibold leading-6 text-gray-200">Other voters</h3>
</div>
<ul role="list" class="divide-y divide-gray-200">
@foreach($otherVoters as $voter)
@php
$vote = $voter->votes->first();
if (!$voter->votes->first()) {
$text = __('not voted yet');
} elseif (!$vote->value) {
$text = __('Reason') . ': ' . $voter->votes->first()?->reason;
}
@endphp
<li class="flex py-4">
<img class="h-10 w-10 rounded-full" src="{{ $voter->profile_photo_url }}"
alt="">
<div class="ml-3">
<p class="text-sm font-medium text-gray-200">
{{ $voter->name }}
@if($voter->votes->first()?->value)
<x-badge green>{{ __('Yes') }}</x-badge>
@endif
@if($voter->votes->first() && !$voter->votes->first()?->value)
<x-badge red>{{ __('No') }}</x-badge>
@endif
</p>
<p class="text-sm text-gray-300">
{{ $text ?? '' }}
</p>
</div>
</li>
@endforeach
</ul>
</div>
</div>
</div>
</div>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
</div>

View File

@@ -209,6 +209,25 @@ Route::middleware([])
->name('table.lecturer');
});
/*
* Project Funding
* */
Route::middleware([])
->as('project.')
->prefix('/{country:code}/project-funding')
->group(function () {
Route::get('/project/form/{projectProposal?}', \App\Http\Livewire\ProjectProposal\Form\ProjectProposalForm::class)
->name('projectProposal.form')
->middleware(['auth']);
Route::get('/project/voting/{projectProposal?}', \App\Http\Livewire\ProjectProposal\ProjectProposalVoting::class)
->name('voting.projectFunding')
->middleware(['auth']);
Route::get('/projects', \App\Http\Livewire\ProjectProposal\ProjectProposalTable::class)
->name('table.projectFunding');
});
/*
* Books
* */