first commit still needs bug testing or something
This commit is contained in:
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
36
README.md
36
README.md
@@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
|
|||||||
22
components.json
Normal file
22
components.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
517
dp2.txt
Normal file
517
dp2.txt
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
DISCIPLINARY PRINCIPLES II
|
||||||
|
Moderation for Minecraft
|
||||||
|
|
||||||
|
OCTOBER 31, 2025
|
||||||
|
BY MR. R, MS. W, MS. L, AND MR. T
|
||||||
|
Revision 11
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
TABLE OF CONTENTS
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
Concepts of DP II ............................................................ 3
|
||||||
|
Major Revisions .............................................................. 4
|
||||||
|
Introduction and Policies .................................................... 4
|
||||||
|
What counts as inappropriate language .................................... 4
|
||||||
|
The Point System ......................................................... 5
|
||||||
|
Discord and Minecraft are separate ....................................... 5
|
||||||
|
Permanent isn’t REALLY permanent ......................................... 5
|
||||||
|
Point Decay and the new requirement for Trusted .......................... 5
|
||||||
|
Rules for Trespassing .................................................... 5
|
||||||
|
Requirements for Trusted ................................................. 6
|
||||||
|
Awards and Illegal Items ................................................. 6
|
||||||
|
How to Report ............................................................ 7
|
||||||
|
The Process .............................................................. 7
|
||||||
|
Calculating Item Points .................................................. 7
|
||||||
|
Anti-Bullying Frameworks ..................................................... 8
|
||||||
|
The signs of bullying .................................................... 8
|
||||||
|
Starting an anti-bullying case ........................................... 9
|
||||||
|
Monitoring an anti-bullying case ......................................... 9
|
||||||
|
Specially Protected Persons .............................................. 9
|
||||||
|
Non-communication agreements ............................................ 10
|
||||||
|
What counts as Slander? .................................................. 10
|
||||||
|
How Bullying Cases End ................................................... 10
|
||||||
|
Bullying punishments are added on top .................................... 11
|
||||||
|
Crimes and Punishments ....................................................... 11
|
||||||
|
Item Offenses ............................................................ 11
|
||||||
|
Block Offences ........................................................... 14
|
||||||
|
Hacking Offences ......................................................... 18
|
||||||
|
Communication Offences ................................................... 22
|
||||||
|
Examples of Each Crime ....................................................... 27
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
CONCEPTS OF DP II
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
The primary goal of this revision to Disciplinary Principles is to strictly codify all
|
||||||
|
possible offenses. Some players, such as naniclan and ender_sam_ have complained that
|
||||||
|
either the justice system is too lenient, different moderators hand out different
|
||||||
|
punishments for the same thing, and we keep favourites. The use of this system will not
|
||||||
|
only convert discipline into an objective measure, it will ensure that everybody gets what is
|
||||||
|
fair to them.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
MAJOR REVISIONS
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
Revision 11: Adds anti bullying frameworks, mandated by the Landry Strike Action Deal of October 2025.
|
||||||
|
Revision 10: Adds new trespassing policy, mandated by the Landry Strike Action Deal of October 2025.
|
||||||
|
Revision 9: Cleans up the formatting of the document.
|
||||||
|
Revision 8: Adds a new crime: Conspiring to Act Corruptly.
|
||||||
|
Revision 7: Updates the requirements for trusted to meet R6 standards for DP II.
|
||||||
|
Revision 6: Merges the multiple point categories into one single point category.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
INTRODUCTION AND POLICIES
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
WHAT COUNTS AS INAPPROPRIATE LANGUAGE
|
||||||
|
Throughout this guide, you will see numerous references to inappropriate language.
|
||||||
|
Inappropriate language is any text that:
|
||||||
|
* Uses highly sexual or suggestive language
|
||||||
|
* Contains racial or homophobic slurs
|
||||||
|
* Contains traditional curse words aimed at an individual.
|
||||||
|
- "What the f*ck" – OK
|
||||||
|
- "F*ck you, [Player Name]" – Not OK
|
||||||
|
* Note: hell and damn do not count as inappropriate language under any circumstance.
|
||||||
|
"Ass" is permitted as long as it is not used in a sexual manner.
|
||||||
|
* Expresses undue hatred towards a specific group or player.
|
||||||
|
|
||||||
|
THE POINT SYSTEM
|
||||||
|
There is now only one point count. This count is incremented for every crime.
|
||||||
|
|
||||||
|
DISCORD AND MINECRAFT ARE SEPARATE
|
||||||
|
If a player commits a crime on Minecraft, their discord account will remain unaffected,
|
||||||
|
except in these circumstances:
|
||||||
|
* The player has committed a massive grief
|
||||||
|
* The player has been convicted of “Grand Manipulation” or “Corruption”
|
||||||
|
|
||||||
|
If a player commits any of these crimes, their discord account will be banned and will be
|
||||||
|
unbanned along with them if that ever happens.
|
||||||
|
|
||||||
|
Likewise, if a discord account does something bad, the Minecraft account will
|
||||||
|
remain unaffected. This is partially to protect people from answering for the crimes of
|
||||||
|
others impersonating them on Discord.
|
||||||
|
|
||||||
|
An exception to this is when discord has been used to clearly plot corrupt acts
|
||||||
|
against the server. Then, the players may be charged both on Discord and Minecraft with
|
||||||
|
“Conspiring to Act Corruptly.”
|
||||||
|
|
||||||
|
PERMANENT ISN'T REALLY PERMANENT
|
||||||
|
In many cases, permanently banned players will be unbanned after many months if
|
||||||
|
staff believe the player is genuinely reformed.
|
||||||
|
NOTE – Exception to this rule: If this player has a history of manipulation or Grand
|
||||||
|
Manipulations, they will not be believed, so unbans in this case will be greatly
|
||||||
|
delayed if ever.
|
||||||
|
|
||||||
|
POINT DECAY AND THE NEW REQUIREMENT FOR TRUSTED
|
||||||
|
Points decay at one point for every week. When reporting a crime, make sure to
|
||||||
|
check when the user’s last offence was, and subtract points accordingly. To be
|
||||||
|
trusted, a user must now have 0 active points. This means that players with criminal
|
||||||
|
records need to wait for their records to naturally decay.
|
||||||
|
|
||||||
|
RULES FOR TRESPASSING
|
||||||
|
Prior to November 1, 2025, trespassing, snooping, and looking in chests was not a
|
||||||
|
crime at all. After several complaints, rigid trespassing rules were implemented.
|
||||||
|
* For the most part, you will not get in trouble just for walking on somebody’s land.
|
||||||
|
* Trespassing works on a complaint-based system. If somebody tells you to leave, you
|
||||||
|
must leave.
|
||||||
|
* If you fail to leave, you will be teleported out by staff and issued a punishment.
|
||||||
|
* Snooping (looking in private chests) is a semi-enforced crime. You will be
|
||||||
|
questioned if caught, but not punished retroactively.
|
||||||
|
* Punishments are double if the player trespasses in a staff area (e.g., the Nani
|
||||||
|
Base).
|
||||||
|
|
||||||
|
REQUIREMENTS FOR TRUSTED
|
||||||
|
To become trusted, one of the following criteria must be met:
|
||||||
|
* Total play time > 10 hours AND Joined at least one week ago AND Existing
|
||||||
|
trusted player vouches for them AND zero active disciplinary points.
|
||||||
|
* Total play time > 15 hours AND joined at least two weeks ago AND zero
|
||||||
|
active disciplinary points.
|
||||||
|
|
||||||
|
Once a user becomes trusted, they require three active disciplinary points to be
|
||||||
|
demoted.
|
||||||
|
|
||||||
|
AWARDS AND ILLEGAL ITEMS
|
||||||
|
The following are the ONLY legal unobtainable items:
|
||||||
|
* Award for Service, 1st class – Enchanted netherite ingot
|
||||||
|
* Award for Service, 2nd class – Enchanted diamond
|
||||||
|
* Award for Service, 3rd class – Enchanted iron ingot
|
||||||
|
* Award for Bugfinding – Enchanted gold ingot
|
||||||
|
* Award for 5 exploits – 50% scale stick (“small stick”)
|
||||||
|
* Award for Playtime – Enchanted bedrock
|
||||||
|
* Halloween 2025 winner – enchanted jack o lantern
|
||||||
|
* 1 year anniversary award – enchanted cake
|
||||||
|
* Ban Hammer – mace with lore #banhammer (staff only)
|
||||||
|
|
||||||
|
Any other illegal item must be treated as a violation.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
HOW TO REPORT
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
1. Run /history on the perpetrator.
|
||||||
|
2. Find the date of their last offence and the [note].
|
||||||
|
3. Subtract two points for each month that has passed. If less than two weeks has
|
||||||
|
passed, do not subtract any points.
|
||||||
|
4. If points decreased due to decay, run a new /note <user> (new point total).
|
||||||
|
5. Execute punishment commands (punishments are calculated AFTER adding points
|
||||||
|
from the current offense).
|
||||||
|
6. Add a new /note <user> with the updated total.
|
||||||
|
|
||||||
|
THE PROCESS
|
||||||
|
1. Receive Report: From players or staff discovery.
|
||||||
|
2. Investigate: Use CoreProtect inspection.
|
||||||
|
3. Discover Scope: For griefs/thefts, determine the block/item count.
|
||||||
|
4. Reverse Damage: Use CoreProtect rollback (check for #tnt, #water, etc.).
|
||||||
|
5. Issue Punishment: Apply based on the specific crime section.
|
||||||
|
|
||||||
|
CALCULATING ITEM POINTS (FOR THEFT)
|
||||||
|
* 20 points: Elytra, Nether Star, Beacon, Heavy Core, Mace, Netherite Block,
|
||||||
|
Special Awards.
|
||||||
|
* 10 points: Netherite derivatives, Diamond Block, Gold Block, Emerald Block,
|
||||||
|
Shulker Box.
|
||||||
|
* 5 points: Diamonds and derivatives, Iron Block, Potions.
|
||||||
|
* 2 points: Any tool/armour piece, any ore/ingot, Firework Rockets.
|
||||||
|
* 1 point: All other blocks.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
ANTI-BULLYING FRAMEWORKS
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
Created October 30, 2025, to stop chronic trolling that causes serious distress.
|
||||||
|
|
||||||
|
THE SIGNS OF BULLYING
|
||||||
|
Defined as chronic harassment or intimidation. Includes:
|
||||||
|
* Chronically insulting or slandering (false statements).
|
||||||
|
* Harassment via signs, posters, voice calls, or spying.
|
||||||
|
* Repeated killing or snooping.
|
||||||
|
* Seemingly innocuous actions like "shipping" or jokes if the victim is upset.
|
||||||
|
|
||||||
|
STARTING A CASE
|
||||||
|
Staff may start a case if they believe a player is being bullied. They must confirm
|
||||||
|
with the victim if they feel harassed. Isolated incidents are treated as standard
|
||||||
|
offences (Inciting Verbal Conflict or Abusive Chat).
|
||||||
|
|
||||||
|
MONITORING A CASE
|
||||||
|
1. Notify the bullies. If they stop, no further action is taken.
|
||||||
|
2. If they continue, implement further protections.
|
||||||
|
|
||||||
|
SPECIALLY PROTECTED PERSONS (SPP)
|
||||||
|
If bullying is consistent, a player is declared an SPP.
|
||||||
|
* Their name becomes forbidden to say on the server.
|
||||||
|
* Crimes against them have stricter punishments (e.g., Trespassing becomes a
|
||||||
|
1-week ban).
|
||||||
|
* Slander is introduced as a harsher offence.
|
||||||
|
|
||||||
|
NON-COMMUNICATION AGREEMENTS (NCA)
|
||||||
|
Both parties agree not to talk. Any breach results in punishments.
|
||||||
|
|
||||||
|
WHAT COUNTS AS SLANDER?
|
||||||
|
Slander is a factual, untrue statement designed to damage reputation.
|
||||||
|
* "I do not like [Player]" – NOT SLANDER (Opinion).
|
||||||
|
* "[Player] has an STD" – SLANDER (Untrue fact statement).
|
||||||
|
* "[Player] is an idiot" – SLANDER (Harmful to reputation).
|
||||||
|
* "F*** [Player]" – NOT SLANDER (Abusive language).
|
||||||
|
|
||||||
|
HOW BULLYING CASES END
|
||||||
|
1. Not enough evidence.
|
||||||
|
2. Victim agrees the bullying has stopped. (SPP cases continue to be monitored).
|
||||||
|
|
||||||
|
BULLYING PUNISHMENTS
|
||||||
|
Added on top of standard crime punishments. They do NOT decay, ever.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
CRIMES AND PUNISHMENTS
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
ITEM OFFENSES
|
||||||
|
|
||||||
|
Inappropriate Item Names
|
||||||
|
Criteria: Named weapon in inventory or publicly featured item with inappropriate
|
||||||
|
language.
|
||||||
|
Value: 1 point per offence.
|
||||||
|
Punishment:
|
||||||
|
- 1-5 points: Warning
|
||||||
|
- 6-8 points: Tempban 1 week
|
||||||
|
- 9+ points: Tempban 4 weeks
|
||||||
|
|
||||||
|
Inappropriate Book Contents
|
||||||
|
Criteria: Book with inappropriate language without a preceding warning page.
|
||||||
|
Value: 1 point.
|
||||||
|
Action: Confiscate book.
|
||||||
|
Punishment: Warning.
|
||||||
|
|
||||||
|
Minor Theft
|
||||||
|
Criteria: Theft totalling < 50 item points.
|
||||||
|
Value: 1 point.
|
||||||
|
Punishment:
|
||||||
|
- 0-3 points: Warning
|
||||||
|
- 4-5 points: 2 day tempban
|
||||||
|
- 6-10 points: 1 week tempban
|
||||||
|
- 11-15 points: 2 week tempban
|
||||||
|
- 16+ points: 4 week tempban
|
||||||
|
|
||||||
|
Moderate Theft
|
||||||
|
Criteria: Theft 50 to 500 item points.
|
||||||
|
Value: 2 points.
|
||||||
|
Punishment:
|
||||||
|
- 0-5 points: Warning
|
||||||
|
- 6-10 points: 5 day tempban
|
||||||
|
- 11-15 points: 2 week tempban
|
||||||
|
- 16+ points: Permanent ban
|
||||||
|
|
||||||
|
Severe Theft
|
||||||
|
Criteria: Theft > 500 item points.
|
||||||
|
Value: 3 points.
|
||||||
|
Punishment:
|
||||||
|
- 0-5 points: Warning
|
||||||
|
- 6-10 points: 1 week tempban
|
||||||
|
- 11-15 points: 2 week tempban
|
||||||
|
- 16+ points: Permanent ban
|
||||||
|
|
||||||
|
Unconsensual Killing
|
||||||
|
Criteria: Repeatedly (2+ times) killing a player without permission.
|
||||||
|
Value: 2 points.
|
||||||
|
Punishment:
|
||||||
|
- 0-5 points: Warning
|
||||||
|
- 6-10 points: 3 day tempban
|
||||||
|
- 11-15 points: 1 week tempban
|
||||||
|
- 16+ points: 2 week tempban
|
||||||
|
|
||||||
|
Using an illegal item without reporting it
|
||||||
|
Criteria: Deliberately used an illegal item.
|
||||||
|
Value: 1 point.
|
||||||
|
Punishment: Warning.
|
||||||
|
|
||||||
|
BLOCK OFFENSES
|
||||||
|
|
||||||
|
Vandalism
|
||||||
|
Criteria: < 10 blocks destroyed (excluding Theft-Grief) or < 5 entities killed.
|
||||||
|
Value: 1 point.
|
||||||
|
Punishment: Warning.
|
||||||
|
|
||||||
|
Theft-Grief
|
||||||
|
Criteria: < 100 blocks destroyed, primarily valuables (Beacons, Ore blocks, etc.).
|
||||||
|
Value: Rollback count divided by 20 (min. 2 points).
|
||||||
|
Punishment:
|
||||||
|
- 0-5 points (or 1st offence): Warning
|
||||||
|
- 6-8 points: 3 day tempban
|
||||||
|
- 9-10 points: 5 day tempban
|
||||||
|
- 11-15 points: 2 week ban
|
||||||
|
- 16-20 points: 1 month ban
|
||||||
|
- 21+ points: Permanent Ban
|
||||||
|
|
||||||
|
Minor Grief
|
||||||
|
Criteria: < 100 blocks without permission or 5-20 valuable entities.
|
||||||
|
Value: 3 points.
|
||||||
|
Punishment: Same as Theft-Grief.
|
||||||
|
|
||||||
|
Vandalism of Public Infrastructure
|
||||||
|
Criteria: Damage rendering Iron Farms, Railways, etc., inoperable.
|
||||||
|
Value: +3 points.
|
||||||
|
Punishment: Same as Theft-Grief.
|
||||||
|
|
||||||
|
Moderate Grief
|
||||||
|
Criteria: 100 to 1,000 blocks.
|
||||||
|
Value: 5 points.
|
||||||
|
Punishment: Same as Theft-Grief (Last level with clemency/warning).
|
||||||
|
|
||||||
|
Large Grief
|
||||||
|
Criteria: 1,000 to 100,000 blocks.
|
||||||
|
Value: 8 points.
|
||||||
|
Punishment: Same as Theft-Grief (Immediate temp/perm ban).
|
||||||
|
|
||||||
|
Massive Grief
|
||||||
|
Criteria: > 100,000 blocks (TNT carpet bombing, lavacasts).
|
||||||
|
Value: 25 points.
|
||||||
|
Punishment: Immediate permanent ban.
|
||||||
|
|
||||||
|
Trespassing
|
||||||
|
Criteria: Remaining on property after being asked to leave or snooping.
|
||||||
|
Value: 1 point.
|
||||||
|
Punishment:
|
||||||
|
- 0-5 points: Warning
|
||||||
|
- 6-10 points: 1 day ban
|
||||||
|
- 11+ points: 5 day ban
|
||||||
|
|
||||||
|
Trespassing on Staff/SPP Land
|
||||||
|
Criteria: Snooping or remaining on Staff/SPP property after being asked to leave.
|
||||||
|
Value: 3 points.
|
||||||
|
Punishment:
|
||||||
|
- Same as Trespassing, but if perpetrator has bullied the SPP: immediate 1 week ban
|
||||||
|
following warning.
|
||||||
|
|
||||||
|
HACKING OFFENSES
|
||||||
|
|
||||||
|
X-Raying
|
||||||
|
Criteria: Using X-ray or Baritone #mine to find items.
|
||||||
|
Value: 1 point.
|
||||||
|
Punishment:
|
||||||
|
- 0-9 points: Warning
|
||||||
|
- 10+ points: Wipe of inventory and ender chest.
|
||||||
|
|
||||||
|
Use of Hacking Client
|
||||||
|
Criteria: Flying, killaura, etc.
|
||||||
|
Value: 5 points.
|
||||||
|
Punishment:
|
||||||
|
- 0-5 points: Kick with warning
|
||||||
|
- 6-15 points: 3 day temp ban
|
||||||
|
- 16+ points: Permanent ban
|
||||||
|
|
||||||
|
Deliberately Lagging the Server
|
||||||
|
Criteria: Lag machines, entity hoarding, or refusing to leave laggy areas.
|
||||||
|
Value: 5 points.
|
||||||
|
Punishment:
|
||||||
|
- 0-5 points: Warning
|
||||||
|
- 6-15 points: 5 day temp ban
|
||||||
|
- 16+ points: Permanent ban
|
||||||
|
|
||||||
|
Misuse of Worldedit
|
||||||
|
Criteria: Crashing or causing excessive lag.
|
||||||
|
Value: 5 points.
|
||||||
|
Punishment:
|
||||||
|
- 1st offence: Warning
|
||||||
|
- 2nd offence: Demotion from trusted (1 month).
|
||||||
|
|
||||||
|
Finding an exploit
|
||||||
|
Value: 0 points.
|
||||||
|
Action: Thank player, issue Gold Ingot. (If found 5 exploits, give "small stick").
|
||||||
|
Punishment: If they have a history of cheating, ban until exploit is patched.
|
||||||
|
|
||||||
|
Abuse of Exploits
|
||||||
|
Criteria: Repeatedly using exploit for unfair advantage.
|
||||||
|
Value: 10 points.
|
||||||
|
Punishment:
|
||||||
|
- 0-10 points: 5 day ban
|
||||||
|
- 11-15 points: 1 week ban
|
||||||
|
- 16-25 points: 2 week ban
|
||||||
|
- 26+ points: permanent ban
|
||||||
|
|
||||||
|
ADMIN OFFENSES
|
||||||
|
|
||||||
|
Light Admin Abuse
|
||||||
|
Criteria: Unnecessary teleporting, time/weather setting.
|
||||||
|
Value: N/A.
|
||||||
|
Punishment:
|
||||||
|
- 1st: Warning
|
||||||
|
- 2nd: Demotion to trusted.
|
||||||
|
|
||||||
|
Moderate Admin Abuse
|
||||||
|
Criteria: Changing gamemodes, issuing illegal items, unauthorized demotions, or
|
||||||
|
harassment.
|
||||||
|
Value: 5 points.
|
||||||
|
Punishment: Immediate demotion to standard player.
|
||||||
|
|
||||||
|
Severe Admin Abuse
|
||||||
|
Criteria: Deleting server files, root access theft, hardware damage, or
|
||||||
|
distributing passwords.
|
||||||
|
Punishment: Immediate permanent ban and demotion.
|
||||||
|
|
||||||
|
Neglect of Duty
|
||||||
|
Criteria: Ignoring players, showing bias, or behaving disrespectfully.
|
||||||
|
Punishment:
|
||||||
|
- 1st: Warning
|
||||||
|
- 2nd: Warning
|
||||||
|
- 3rd: Demotion or suspension (30+ days).
|
||||||
|
|
||||||
|
COMMUNICATION OFFENSES
|
||||||
|
|
||||||
|
Abusive Chat
|
||||||
|
Criteria: Inappropriate language.
|
||||||
|
Value: 1 point.
|
||||||
|
Punishment:
|
||||||
|
- 0-3 points: Warning
|
||||||
|
- 4-5 points: 15 minute mute
|
||||||
|
- 6-10 points: 1 hour mute
|
||||||
|
- 11+ points: 1 day mute
|
||||||
|
- If victim is SPP: Escalates to 1 day mute + permanent loss of trusted.
|
||||||
|
|
||||||
|
Inciting Verbal Conflict
|
||||||
|
Criteria: Chat aiming to incite violence or negative reactions.
|
||||||
|
Value: 2 points.
|
||||||
|
Punishment:
|
||||||
|
- 0-3 points: Warning
|
||||||
|
- 4-5 points: 30 minute mute
|
||||||
|
- 6-10 points: 2 hour mute
|
||||||
|
- 11+ points: 1 day mute
|
||||||
|
- If victim is SPP: Escalates to permanent loss of trusted.
|
||||||
|
|
||||||
|
Abusive VC Language
|
||||||
|
Criteria: Inappropriate language or inciting conflict in voice chat.
|
||||||
|
Value: 1 point.
|
||||||
|
Punishment:
|
||||||
|
- 0-3 points: Warning
|
||||||
|
- 4-5 points: 1 hour VC ban
|
||||||
|
- 6-10 points: 1 day VC ban
|
||||||
|
- 11+ points: Permanent VC ban
|
||||||
|
|
||||||
|
Lying to staff member
|
||||||
|
Criteria: Untrue statements regarding theft, griefing, etc.
|
||||||
|
Value: 1 point.
|
||||||
|
Punishment:
|
||||||
|
- 0-5 points: Warning
|
||||||
|
- 6-10 points: 1 day ban
|
||||||
|
- 11+ points: 3 day ban
|
||||||
|
|
||||||
|
Manipulation
|
||||||
|
Criteria: Repeatedly lying to conceal illicit activities or framing others.
|
||||||
|
Value: 5 points.
|
||||||
|
Punishment:
|
||||||
|
- 0-5 points: 3 day ban
|
||||||
|
- 6-10 points: 1 week ban
|
||||||
|
- 11+ points: 2 week ban
|
||||||
|
|
||||||
|
Grand Manipulation
|
||||||
|
Criteria: Lies involving large-scale cleanup or out-of-game malicious actions
|
||||||
|
(e.g., newsletters).
|
||||||
|
Value: 20 points.
|
||||||
|
Punishment: Permanent Ban.
|
||||||
|
|
||||||
|
Corruption
|
||||||
|
Criteria: Working against the server, inviting griefers, or distributing secrets.
|
||||||
|
Punishment: Warning, tempban, or permanent ban (Staff discretion).
|
||||||
|
|
||||||
|
Conspiring to Act Corruptly
|
||||||
|
Criteria: Planning/working against the server.
|
||||||
|
Value: 3 points.
|
||||||
|
Punishment: Warning and loss of trusted.
|
||||||
|
|
||||||
|
Misuse of Public Utilities
|
||||||
|
Criteria: Excessive taking from public chests or reselling output.
|
||||||
|
Value: 2 points.
|
||||||
|
Punishment: Return items; > 5 points: Loss of business access.
|
||||||
|
|
||||||
|
Slander (Against SPP Only)
|
||||||
|
Value: 3 points.
|
||||||
|
Punishment: Mute for (offence number) days. 3rd offence: Victim's choice of
|
||||||
|
punishment up to permanent ban.
|
||||||
|
|
||||||
|
Violation of NCA
|
||||||
|
Criteria: Breaking a non-communication agreement.
|
||||||
|
Value: 2 points.
|
||||||
|
Punishment:
|
||||||
|
- 1st: 12-hour mute
|
||||||
|
- 2nd: 1 day mute
|
||||||
|
- 3rd+: (offence number) days mute.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
EXAMPLES OF EACH CRIME (HISTORICAL)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
* Item Names: Lapizi101 naming a sword "Enderbyte09's [Redacted]"
|
||||||
|
* Minor Theft: Shellbuck69 stealing materials from naniclan.
|
||||||
|
* Severe Theft: Shellbuck69 cleaning out Ender Sam’s shop and Khai1705’s base.
|
||||||
|
* Vandalism: Shellbuck69 stealing the public ender chest.
|
||||||
|
* Vandalism (Infrastructure): joyousssss removing the public iron farm wall.
|
||||||
|
* Massive Grief: RokoPlays lavacasting spawn (2 million blocks).
|
||||||
|
* Use of Hacking Client: Blackadder71 flying on May 16.
|
||||||
|
* Finding Exploit: herodoge finding over 10 exploits.
|
||||||
|
* Abuse of Exploit: Koops and gang creating illegal items in survival.
|
||||||
|
* Moderate Admin Abuse: Skaterva creating numerous illegal items.
|
||||||
|
* Severe Admin Abuse: Former admin Skaterva sending OS console password to chat.
|
||||||
|
* Neglect of Duty: jc_gamestar9032 being biased and insulting players.
|
||||||
|
* Abusive Chat: Ad6nmalt cursing 200+ times in one day.
|
||||||
|
* Grand Manipulation: herodoge pretending to be a 16-year-old girl for months.
|
||||||
|
* Corruption: Koops spamming pornography on Discord while impersonating others.
|
||||||
|
* Trespassing: shellbuck69 repeatedly snooping in naniclan’s old base.
|
||||||
18
eslint.config.mjs
Normal file
18
eslint.config.mjs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||||
|
import nextTs from "eslint-config-next/typescript";
|
||||||
|
|
||||||
|
const eslintConfig = defineConfig([
|
||||||
|
...nextVitals,
|
||||||
|
...nextTs,
|
||||||
|
// Override default ignores of eslint-config-next.
|
||||||
|
globalIgnores([
|
||||||
|
// Default ignores of eslint-config-next:
|
||||||
|
".next/**",
|
||||||
|
"out/**",
|
||||||
|
"build/**",
|
||||||
|
"next-env.d.ts",
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
7
next.config.ts
Normal file
7
next.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
8334
package-lock.json
generated
Normal file
8334
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
package.json
Normal file
41
package.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "dp2-moderation-assistant",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
"@radix-ui/react-radio-group": "^1.3.8",
|
||||||
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"lucide-react": "^0.562.0",
|
||||||
|
"next": "16.1.4",
|
||||||
|
"react": "19.2.3",
|
||||||
|
"react-dom": "19.2.3",
|
||||||
|
"react-hook-form": "^7.71.1",
|
||||||
|
"zod": "^4.3.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@shadcn/ui": "^0.0.4",
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "16.1.4",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
postcss.config.mjs
Normal file
7
postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
1
public/file.svg
Normal file
1
public/file.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
1
public/globe.svg
Normal file
1
public/globe.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
1
public/next.svg
Normal file
1
public/next.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
public/vercel.svg
Normal file
1
public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 128 B |
1
public/window.svg
Normal file
1
public/window.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
BIN
src/app/favicon.ico
Normal file
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
195
src/app/globals.css
Normal file
195
src/app/globals.css
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--font-sans: var(--font-geist-sans);
|
||||||
|
--font-mono: var(--font-geist-mono);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--radius-2xl: calc(var(--radius) + 8px);
|
||||||
|
--radius-3xl: calc(var(--radius) + 12px);
|
||||||
|
--radius-4xl: calc(var(--radius) + 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.205 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.205 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.922 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.556 0 0);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.556 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make the UI bigger and more usable */
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Increase font sizes for better readability */
|
||||||
|
.text-sm {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-lg {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make cards more prominent */
|
||||||
|
.card {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-muted-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Increase spacing for better usability */
|
||||||
|
.space-y-6 > * + * {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-y-4 > * + * {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-y-2 > * + * {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make buttons more prominent */
|
||||||
|
.btn {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make inputs larger */
|
||||||
|
.input {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make labels more prominent */
|
||||||
|
.label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/app/layout.tsx
Normal file
34
src/app/layout.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "DP2 Moderation Assistant",
|
||||||
|
description: "Calculate punishments based on DP2 guidelines",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<body
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} antialiased dark`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/app/page.tsx
Normal file
11
src/app/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { DP2Form } from '@/components/DP2Form';
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-background">
|
||||||
|
<div className="container mx-auto py-8">
|
||||||
|
<DP2Form />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
540
src/components/DP2Form.tsx
Normal file
540
src/components/DP2Form.tsx
Normal file
@@ -0,0 +1,540 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { CRIMES, Crime, ITEM_POINTS } from '@/lib/dp2-rules';
|
||||||
|
import { useDP2Calculator } from '@/hooks/useDP2Calculator';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
|
||||||
|
import { Copy, RefreshCw, Moon, Sun, X } from 'lucide-react';
|
||||||
|
|
||||||
|
const FormSchema = z.object({
|
||||||
|
playerName: z.string().min(2, "Player name must be at least 2 characters"),
|
||||||
|
currentPoints: z.number().min(0, "Current points must be 0 or higher"),
|
||||||
|
lastOffenseDate: z.date().optional(),
|
||||||
|
crimeId: z.string(),
|
||||||
|
itemDetails: z.array(z.object({
|
||||||
|
type: z.string(),
|
||||||
|
quantity: z.number().min(1, "Quantity must be at least 1"),
|
||||||
|
})).optional(),
|
||||||
|
blockCount: z.number().min(0, "Block count must be 0 or higher").optional(),
|
||||||
|
entityCount: z.number().min(0, "Entity count must be 0 or higher").optional(),
|
||||||
|
isSPP: z.boolean().optional(),
|
||||||
|
specialItems: z.object({
|
||||||
|
elytra: z.number().min(0),
|
||||||
|
netherStar: z.number().min(0),
|
||||||
|
beacon: z.number().min(0),
|
||||||
|
netheriteBlock: z.number().min(0),
|
||||||
|
diamondBlock: z.number().min(0),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof FormSchema>;
|
||||||
|
|
||||||
|
export function DP2Form() {
|
||||||
|
const [activeCategory, setActiveCategory] = useState<Crime['category'] | null>(null);
|
||||||
|
const { result, isLoading, calculatePunishment, copyToClipboard } = useDP2Calculator();
|
||||||
|
|
||||||
|
const form = useForm<FormData>({
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
playerName: '',
|
||||||
|
currentPoints: 0,
|
||||||
|
crimeId: '',
|
||||||
|
itemDetails: [],
|
||||||
|
blockCount: 0,
|
||||||
|
entityCount: 0,
|
||||||
|
isSPP: false,
|
||||||
|
specialItems: {
|
||||||
|
elytra: 0,
|
||||||
|
netherStar: 0,
|
||||||
|
beacon: 0,
|
||||||
|
netheriteBlock: 0,
|
||||||
|
diamondBlock: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (data: FormData) => {
|
||||||
|
console.log('Form submitted with data:', data);
|
||||||
|
console.log('CrimeId value:', data.crimeId);
|
||||||
|
|
||||||
|
if (!data.crimeId || data.crimeId.trim() === '') {
|
||||||
|
console.error('CrimeId is empty!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculatePunishment(
|
||||||
|
{
|
||||||
|
playerName: data.playerName,
|
||||||
|
currentPoints: data.currentPoints,
|
||||||
|
lastOffenseDate: data.lastOffenseDate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
crimeId: data.crimeId,
|
||||||
|
itemDetails: data.itemDetails,
|
||||||
|
blockCount: data.blockCount,
|
||||||
|
entityCount: data.entityCount,
|
||||||
|
isSPP: data.isSPP,
|
||||||
|
specialItems: data.specialItems,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedCrime = CRIMES.find(c => c.id === form.watch('crimeId'));
|
||||||
|
|
||||||
|
const filteredCrimes = CRIMES.filter(crime =>
|
||||||
|
activeCategory ? crime.category === activeCategory : true
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<CardTitle>dp2 moderation assistant</CardTitle>
|
||||||
|
<CardDescription>calculate punishments and generate commands based on dp2 guidelines</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="icon" className="ml-auto">
|
||||||
|
<Moon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
|
<Sun className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
|
<span className="sr-only">Toggle theme</span>
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={form.handleSubmit((data) => onSubmit(data as unknown as FormData))} className="space-y-6">
|
||||||
|
{/* Player Information */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="playerName">player name</Label>
|
||||||
|
<Input
|
||||||
|
id="playerName"
|
||||||
|
placeholder="enter player name"
|
||||||
|
className="h-12 text-lg"
|
||||||
|
{...form.register('playerName', { required: true })}
|
||||||
|
/>
|
||||||
|
{form.formState.errors.playerName && (
|
||||||
|
<p className="text-sm text-red-500">{form.formState.errors.playerName.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="currentPoints">current points (from /history)</Label>
|
||||||
|
<Input
|
||||||
|
id="currentPoints"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
placeholder="0"
|
||||||
|
className="h-12 text-lg"
|
||||||
|
{...form.register('currentPoints', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
{form.formState.errors.currentPoints && (
|
||||||
|
<p className="text-sm text-red-500">{form.formState.errors.currentPoints.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Crime Category Selection */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>crime category</Label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
variant={activeCategory === null ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveCategory(null);
|
||||||
|
form.setValue('crimeId', '');
|
||||||
|
}}
|
||||||
|
className="text-sm"
|
||||||
|
>
|
||||||
|
all
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={activeCategory === "item" ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveCategory("item");
|
||||||
|
form.setValue('crimeId', '');
|
||||||
|
}}
|
||||||
|
className="text-sm"
|
||||||
|
>
|
||||||
|
item offenses
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={activeCategory === "block" ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveCategory("block");
|
||||||
|
form.setValue('crimeId', '');
|
||||||
|
}}
|
||||||
|
className="text-sm"
|
||||||
|
>
|
||||||
|
block offenses
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={activeCategory === "hacking" ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveCategory("hacking");
|
||||||
|
form.setValue('crimeId', '');
|
||||||
|
}}
|
||||||
|
className="text-sm"
|
||||||
|
>
|
||||||
|
hacking offenses
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={activeCategory === "communication" ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveCategory("communication");
|
||||||
|
form.setValue('crimeId', '');
|
||||||
|
}}
|
||||||
|
className="text-sm"
|
||||||
|
>
|
||||||
|
communication offenses
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Crime Selection */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="crimeId">specific offense</Label>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => form.setValue('crimeId', value)}
|
||||||
|
value={form.watch('crimeId')}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="select an offense" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{filteredCrimes.map((crime) => (
|
||||||
|
<SelectItem key={crime.id} value={crime.id}>
|
||||||
|
{crime.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{form.formState.errors.crimeId && (
|
||||||
|
<p className="text-sm text-red-500">{form.formState.errors.crimeId.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Crime Details */}
|
||||||
|
{selectedCrime && (
|
||||||
|
<div className="border-t pt-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">offense details</h3>
|
||||||
|
|
||||||
|
{/* Classification Guidelines */}
|
||||||
|
{selectedCrime.id === 'theft' && (
|
||||||
|
<div className="bg-muted rounded-lg p-4 mb-4">
|
||||||
|
<p className="text-sm font-medium text-muted-foreground mb-2">Theft Classification:</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{`Minor: < 50 item points | Moderate: 50-500 item points | Severe: > 500 item points`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedCrime.id === 'grief' && (
|
||||||
|
<div className="bg-muted rounded-lg p-4 mb-4">
|
||||||
|
<p className="text-sm font-medium text-muted-foreground mb-2">Grief Classification:</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{`Minor: < 100 blocks | Moderate: 100-1,000 blocks | Large: 1,000-100,000 blocks | Massive: > 100,000 blocks`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Special Items - Only for Theft */}
|
||||||
|
{selectedCrime.id === 'theft' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>special items stolen</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Use these dedicated inputs for special items. Do not manually enter them below.
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="grid grid-cols-1 gap-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm">elytra (20 points)</span>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
placeholder="0"
|
||||||
|
className="w-20 h-8"
|
||||||
|
{...form.register('specialItems.elytra', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm">nether star (20 points)</span>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
placeholder="0"
|
||||||
|
className="w-20 h-8"
|
||||||
|
{...form.register('specialItems.netherStar', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm">beacon (20 points)</span>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
placeholder="0"
|
||||||
|
className="w-20 h-8"
|
||||||
|
{...form.register('specialItems.beacon', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm">netherite block (10 points)</span>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
placeholder="0"
|
||||||
|
className="w-20 h-8"
|
||||||
|
{...form.register('specialItems.netheriteBlock', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm">diamond block (10 points)</span>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
placeholder="0"
|
||||||
|
className="w-20 h-8"
|
||||||
|
{...form.register('specialItems.diamondBlock', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Item Details */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>additional items stolen</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Add any other stolen items not listed above.
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{form.watch('itemDetails')?.map((_, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-2">
|
||||||
|
<div className="grid grid-cols-2 gap-2 flex-1">
|
||||||
|
<Input
|
||||||
|
placeholder="item type"
|
||||||
|
{...form.register(`itemDetails.${index}.type`)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="quantity"
|
||||||
|
min="1"
|
||||||
|
{...form.register(`itemDetails.${index}.quantity`, { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-red-500 hover:text-red-700 hover:bg-red-50 p-1 h-8 w-8"
|
||||||
|
onClick={() => {
|
||||||
|
const currentItems = form.getValues('itemDetails') || [];
|
||||||
|
const newItems = currentItems.filter((_, i) => i !== index);
|
||||||
|
form.setValue('itemDetails', newItems);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const currentItems = form.getValues('itemDetails') || [];
|
||||||
|
form.setValue('itemDetails', [...currentItems, { type: '', quantity: 1 }]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
add item
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Item Details for non-theft crimes */}
|
||||||
|
{selectedCrime.requiresItemDetails && selectedCrime.id !== 'theft' && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>item details</Label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{form.watch('itemDetails')?.map((_, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-2">
|
||||||
|
<div className="grid grid-cols-2 gap-2 flex-1">
|
||||||
|
<Input
|
||||||
|
placeholder="item type"
|
||||||
|
{...form.register(`itemDetails.${index}.type`)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="quantity"
|
||||||
|
min="1"
|
||||||
|
{...form.register(`itemDetails.${index}.quantity`, { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-red-500 hover:text-red-700 hover:bg-red-50 p-1 h-8 w-8"
|
||||||
|
onClick={() => {
|
||||||
|
const currentItems = form.getValues('itemDetails') || [];
|
||||||
|
const newItems = currentItems.filter((_, i) => i !== index);
|
||||||
|
form.setValue('itemDetails', newItems);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const currentItems = form.getValues('itemDetails') || [];
|
||||||
|
form.setValue('itemDetails', [...currentItems, { type: '', quantity: 1 }]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
add item
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Block Details */}
|
||||||
|
{selectedCrime.requiresBlockDetails && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="blockCount">block count</Label>
|
||||||
|
<Input
|
||||||
|
id="blockCount"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
placeholder="0"
|
||||||
|
{...form.register('blockCount', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Entity Details */}
|
||||||
|
{selectedCrime.requiresEntityDetails && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="entityCount">entity count</Label>
|
||||||
|
<Input
|
||||||
|
id="entityCount"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
placeholder="0"
|
||||||
|
{...form.register('entityCount', { valueAsNumber: true })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* SPP Status */}
|
||||||
|
{selectedCrime.requiresSPP && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="isSPP"
|
||||||
|
{...form.register('isSPP')}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="isSPP">victim is SPP (specially protected person)</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button type="submit" disabled={isLoading}>
|
||||||
|
{isLoading ? 'calculating...' : 'calculate punishment'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Results */}
|
||||||
|
{result && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>results</CardTitle>
|
||||||
|
<CardDescription>generated commands and explanation</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>commands</Label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{result.commands.map((command, index) => (
|
||||||
|
<div key={index} className="flex items-center justify-between p-3 bg-muted rounded-md">
|
||||||
|
<code className="text-sm font-mono">{command}</code>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => copyToClipboard(command)}
|
||||||
|
className="ml-2"
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>summary</Label>
|
||||||
|
<div className="p-4 bg-muted rounded-md">
|
||||||
|
<div className="text-sm space-y-1">
|
||||||
|
<div><strong>crime:</strong> {result.crime.name}</div>
|
||||||
|
<div><strong>base points:</strong> {result.basePoints}</div>
|
||||||
|
<div><strong>total points:</strong> {result.totalPoints}</div>
|
||||||
|
<div><strong>punishment:</strong> {result.punishmentLevel.replace('_', ' ')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>detailed explanation</Label>
|
||||||
|
<Textarea
|
||||||
|
value={result.explanation}
|
||||||
|
readOnly
|
||||||
|
className="min-h-[120px] font-mono text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center pt-4 border-t">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
note: always verify the generated commands before executing
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => form.reset()}
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
<span>reset form</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/components/ui/accordion.tsx
Normal file
66
src/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||||
|
import { ChevronDownIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Accordion({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||||
|
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
data-slot="accordion-item"
|
||||||
|
className={cn("border-b last:border-b-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
data-slot="accordion-trigger"
|
||||||
|
className={cn(
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
data-slot="accordion-content"
|
||||||
|
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||||
62
src/components/ui/button.tsx
Normal file
62
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
92
src/components/ui/card.tsx
Normal file
92
src/components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
||||||
32
src/components/ui/checkbox.tsx
Normal file
32
src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||||
|
import { CheckIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Checkbox({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
data-slot="checkbox"
|
||||||
|
className={cn(
|
||||||
|
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
data-slot="checkbox-indicator"
|
||||||
|
className="grid place-content-center text-current transition-none"
|
||||||
|
>
|
||||||
|
<CheckIcon className="size-3.5" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Checkbox }
|
||||||
167
src/components/ui/form.tsx
Normal file
167
src/components/ui/form.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import type * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
FormProvider,
|
||||||
|
useFormContext,
|
||||||
|
useFormState,
|
||||||
|
type ControllerProps,
|
||||||
|
type FieldPath,
|
||||||
|
type FieldValues,
|
||||||
|
} from "react-hook-form"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
|
||||||
|
const Form = FormProvider
|
||||||
|
|
||||||
|
type FormFieldContextValue<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = {
|
||||||
|
name: TName
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
|
{} as FormFieldContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
const FormField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>({
|
||||||
|
...props
|
||||||
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
|
return (
|
||||||
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
|
<Controller {...props} />
|
||||||
|
</FormFieldContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useFormField = () => {
|
||||||
|
const fieldContext = React.useContext(FormFieldContext)
|
||||||
|
const itemContext = React.useContext(FormItemContext)
|
||||||
|
const { getFieldState } = useFormContext()
|
||||||
|
const formState = useFormState({ name: fieldContext.name })
|
||||||
|
const fieldState = getFieldState(fieldContext.name, formState)
|
||||||
|
|
||||||
|
if (!fieldContext) {
|
||||||
|
throw new Error("useFormField should be used within <FormField>")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = itemContext
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: fieldContext.name,
|
||||||
|
formItemId: `${id}-form-item`,
|
||||||
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
|
formMessageId: `${id}-form-item-message`,
|
||||||
|
...fieldState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormItemContextValue = {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
|
{} as FormItemContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
const id = React.useId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItemContext.Provider value={{ id }}>
|
||||||
|
<div
|
||||||
|
data-slot="form-item"
|
||||||
|
className={cn("grid gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</FormItemContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
const { error, formItemId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
data-slot="form-label"
|
||||||
|
data-error={!!error}
|
||||||
|
className={cn("data-[error=true]:text-destructive", className)}
|
||||||
|
htmlFor={formItemId}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slot
|
||||||
|
data-slot="form-control"
|
||||||
|
id={formItemId}
|
||||||
|
aria-describedby={
|
||||||
|
!error
|
||||||
|
? `${formDescriptionId}`
|
||||||
|
: `${formDescriptionId} ${formMessageId}`
|
||||||
|
}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
const { formDescriptionId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="form-description"
|
||||||
|
id={formDescriptionId}
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
const { error, formMessageId } = useFormField()
|
||||||
|
const body = error ? String(error?.message ?? "") : props.children
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="form-message"
|
||||||
|
id={formMessageId}
|
||||||
|
className={cn("text-destructive text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
useFormField,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormMessage,
|
||||||
|
FormField,
|
||||||
|
}
|
||||||
21
src/components/ui/input.tsx
Normal file
21
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input }
|
||||||
24
src/components/ui/label.tsx
Normal file
24
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label }
|
||||||
45
src/components/ui/radio-group.tsx
Normal file
45
src/components/ui/radio-group.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||||
|
import { CircleIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function RadioGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
data-slot="radio-group"
|
||||||
|
className={cn("grid gap-3", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function RadioGroupItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
data-slot="radio-group-item"
|
||||||
|
className={cn(
|
||||||
|
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator
|
||||||
|
data-slot="radio-group-indicator"
|
||||||
|
className="relative flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem }
|
||||||
190
src/components/ui/select.tsx
Normal file
190
src/components/ui/select.tsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Select({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
|
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectValue({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectTrigger({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
|
size?: "sm" | "default"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
data-slot="select-trigger"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
position = "item-aligned",
|
||||||
|
align = "center",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
data-slot="select-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
align={align}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
data-slot="select-label"
|
||||||
|
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
data-slot="select-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
data-slot="select-item-indicator"
|
||||||
|
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||||
|
>
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
data-slot="select-separator"
|
||||||
|
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollUpButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollDownButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
}
|
||||||
18
src/components/ui/textarea.tsx
Normal file
18
src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
118
src/hooks/useDP2Calculator.ts
Normal file
118
src/hooks/useDP2Calculator.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { CRIMES, calculateTotalPoints, getPunishmentLevel, generateCommands, generateExplanation, Crime, PunishmentLevel } from '@/lib/dp2-rules';
|
||||||
|
|
||||||
|
export interface PlayerFormData {
|
||||||
|
playerName: string;
|
||||||
|
currentPoints: number;
|
||||||
|
lastOffenseDate?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OffenseFormData {
|
||||||
|
crimeId: string;
|
||||||
|
itemDetails?: Array<{ type: string; quantity: number }>;
|
||||||
|
blockCount?: number;
|
||||||
|
entityCount?: number;
|
||||||
|
isSPP?: boolean;
|
||||||
|
specialItems?: {
|
||||||
|
elytra: number;
|
||||||
|
netherStar: number;
|
||||||
|
beacon: number;
|
||||||
|
netheriteBlock: number;
|
||||||
|
diamondBlock: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalculationResult {
|
||||||
|
crime: Crime;
|
||||||
|
basePoints: number;
|
||||||
|
totalPoints: number;
|
||||||
|
punishmentLevel: PunishmentLevel;
|
||||||
|
commands: string[];
|
||||||
|
explanation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDP2Calculator() {
|
||||||
|
const [result, setResult] = useState<CalculationResult | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const calculatePunishment = useCallback((playerData: PlayerFormData, offenseData: OffenseFormData) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Calculating punishment for:', { playerData, offenseData });
|
||||||
|
const crime = CRIMES.find(c => c.id === offenseData.crimeId);
|
||||||
|
if (!crime) {
|
||||||
|
console.error('No crime found for crimeId:', offenseData.crimeId);
|
||||||
|
console.error('Available crimes:', CRIMES.map(c => c.id));
|
||||||
|
throw new Error('Invalid crime selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate item points if applicable
|
||||||
|
let itemPoints = 0;
|
||||||
|
if (offenseData.itemDetails) {
|
||||||
|
itemPoints = offenseData.itemDetails.reduce((total, item) => {
|
||||||
|
return total + (item.quantity || 0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate special item points if applicable
|
||||||
|
let specialItemPoints = 0;
|
||||||
|
if (offenseData.specialItems) {
|
||||||
|
specialItemPoints =
|
||||||
|
(offenseData.specialItems.elytra * 20) +
|
||||||
|
(offenseData.specialItems.netherStar * 20) +
|
||||||
|
(offenseData.specialItems.beacon * 20) +
|
||||||
|
(offenseData.specialItems.netheriteBlock * 10) +
|
||||||
|
(offenseData.specialItems.diamondBlock * 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate base points (including item points and special item points)
|
||||||
|
const basePoints = crime.basePoints + itemPoints + specialItemPoints;
|
||||||
|
|
||||||
|
// Calculate total points with decay
|
||||||
|
const totalPoints = calculateTotalPoints(basePoints, playerData.currentPoints, playerData.lastOffenseDate);
|
||||||
|
|
||||||
|
// Determine punishment level
|
||||||
|
const punishmentLevel = getPunishmentLevel(totalPoints, crime.category, offenseData.isSPP);
|
||||||
|
|
||||||
|
// Generate commands
|
||||||
|
const commands = generateCommands(playerData.playerName, totalPoints, punishmentLevel);
|
||||||
|
|
||||||
|
// Generate explanation
|
||||||
|
const explanation = generateExplanation(crime, basePoints, totalPoints, punishmentLevel);
|
||||||
|
|
||||||
|
const calculationResult: CalculationResult = {
|
||||||
|
crime,
|
||||||
|
basePoints,
|
||||||
|
totalPoints,
|
||||||
|
punishmentLevel,
|
||||||
|
commands,
|
||||||
|
explanation,
|
||||||
|
};
|
||||||
|
|
||||||
|
setResult(calculationResult);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Calculation error:', error);
|
||||||
|
setResult(null);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const copyToClipboard = useCallback(async (text: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to copy to clipboard:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
isLoading,
|
||||||
|
calculatePunishment,
|
||||||
|
copyToClipboard,
|
||||||
|
};
|
||||||
|
}
|
||||||
423
src/lib/dp2-rules.ts
Normal file
423
src/lib/dp2-rules.ts
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// Types for DP2 system
|
||||||
|
export type CrimeCategory = 'item' | 'block' | 'hacking' | 'communication';
|
||||||
|
|
||||||
|
export interface Crime {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
category: CrimeCategory;
|
||||||
|
description: string;
|
||||||
|
basePoints: number;
|
||||||
|
requiresItemDetails?: boolean;
|
||||||
|
requiresBlockDetails?: boolean;
|
||||||
|
requiresEntityDetails?: boolean;
|
||||||
|
requiresSPP?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerHistory {
|
||||||
|
name: string;
|
||||||
|
currentPoints: number;
|
||||||
|
lastOffenseDate?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PunishmentResult {
|
||||||
|
totalPoints: number;
|
||||||
|
punishmentLevel: PunishmentLevel;
|
||||||
|
commands: string[];
|
||||||
|
explanation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PunishmentLevel =
|
||||||
|
| 'warning'
|
||||||
|
| '15min_mute'
|
||||||
|
| '30min_mute'
|
||||||
|
| '1hour_mute'
|
||||||
|
| '1day_mute'
|
||||||
|
| '12hour_vc_ban'
|
||||||
|
| '1day_vc_ban'
|
||||||
|
| 'permanent_vc_ban'
|
||||||
|
| 'kick'
|
||||||
|
| '1day_ban'
|
||||||
|
| '2day_ban'
|
||||||
|
| '3day_ban'
|
||||||
|
| '5day_ban'
|
||||||
|
| '1week_ban'
|
||||||
|
| '2week_ban'
|
||||||
|
| '4week_ban'
|
||||||
|
| '1month_ban'
|
||||||
|
| 'permanent_ban'
|
||||||
|
| 'wipe_inventory'
|
||||||
|
| 'demote_trusted'
|
||||||
|
| 'demote_standard';
|
||||||
|
|
||||||
|
// Crime definitions based on DP2 guidelines
|
||||||
|
export const CRIMES: Crime[] = [
|
||||||
|
// Item Offenses
|
||||||
|
{
|
||||||
|
id: 'inappropriate_item_names',
|
||||||
|
name: 'Inappropriate Item Names',
|
||||||
|
category: 'item',
|
||||||
|
description: 'Named weapon in inventory or publicly featured item with inappropriate language',
|
||||||
|
basePoints: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'inappropriate_book_contents',
|
||||||
|
name: 'Inappropriate Book Contents',
|
||||||
|
category: 'item',
|
||||||
|
description: 'Book with inappropriate language without a preceding warning page',
|
||||||
|
basePoints: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'theft',
|
||||||
|
name: 'Theft',
|
||||||
|
category: 'item',
|
||||||
|
description: 'Theft of items - classification based on total item points: Minor (< 50), Moderate (50-500), Severe (> 500)',
|
||||||
|
basePoints: 1,
|
||||||
|
requiresItemDetails: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'unconsensual_killing',
|
||||||
|
name: 'Unconsensual Killing',
|
||||||
|
category: 'item',
|
||||||
|
description: 'Repeatedly (2+ times) killing a player without permission',
|
||||||
|
basePoints: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'illegal_item_use',
|
||||||
|
name: 'Using Illegal Item',
|
||||||
|
category: 'item',
|
||||||
|
description: 'Deliberately used an illegal item',
|
||||||
|
basePoints: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Block Offenses
|
||||||
|
{
|
||||||
|
id: 'vandalism',
|
||||||
|
name: 'Vandalism',
|
||||||
|
category: 'block',
|
||||||
|
description: '< 10 blocks destroyed or < 5 entities killed',
|
||||||
|
basePoints: 1,
|
||||||
|
requiresBlockDetails: true,
|
||||||
|
requiresEntityDetails: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'grief',
|
||||||
|
name: 'Grief',
|
||||||
|
category: 'block',
|
||||||
|
description: 'Griefing - classification based on block count: Minor (< 100), Moderate (100-1,000), Large (1,000-100,000), Massive (> 100,000)',
|
||||||
|
basePoints: 1,
|
||||||
|
requiresBlockDetails: true,
|
||||||
|
requiresEntityDetails: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'theft_grief',
|
||||||
|
name: 'Theft-Grief',
|
||||||
|
category: 'block',
|
||||||
|
description: '< 100 blocks destroyed, primarily valuables',
|
||||||
|
basePoints: 2,
|
||||||
|
requiresBlockDetails: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vandalism_infrastructure',
|
||||||
|
name: 'Vandalism of Public Infrastructure',
|
||||||
|
category: 'block',
|
||||||
|
description: 'Damage rendering Iron Farms, Railways, etc., inoperable',
|
||||||
|
basePoints: 3,
|
||||||
|
requiresBlockDetails: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'trespassing',
|
||||||
|
name: 'Trespassing',
|
||||||
|
category: 'block',
|
||||||
|
description: 'Remaining on property after being asked to leave or snooping',
|
||||||
|
basePoints: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'trespassing_staff',
|
||||||
|
name: 'Trespassing on Staff/SPP Land',
|
||||||
|
category: 'block',
|
||||||
|
description: 'Snooping or remaining on Staff/SPP property after being asked to leave',
|
||||||
|
basePoints: 3,
|
||||||
|
requiresSPP: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Hacking Offenses
|
||||||
|
{
|
||||||
|
id: 'x_raying',
|
||||||
|
name: 'X-Raying',
|
||||||
|
category: 'hacking',
|
||||||
|
description: 'Using X-ray or Baritone #mine to find items',
|
||||||
|
basePoints: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hacking_client',
|
||||||
|
name: 'Use of Hacking Client',
|
||||||
|
category: 'hacking',
|
||||||
|
description: 'Flying, killaura, etc.',
|
||||||
|
basePoints: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lagging_server',
|
||||||
|
name: 'Deliberately Lagging Server',
|
||||||
|
category: 'hacking',
|
||||||
|
description: 'Lag machines, entity hoarding, or refusing to leave laggy areas',
|
||||||
|
basePoints: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'worldedit_misuse',
|
||||||
|
name: 'Misuse of Worldedit',
|
||||||
|
category: 'hacking',
|
||||||
|
description: 'Crashing or causing excessive lag',
|
||||||
|
basePoints: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'exploit_abuse',
|
||||||
|
name: 'Abuse of Exploits',
|
||||||
|
category: 'hacking',
|
||||||
|
description: 'Repeatedly using exploit for unfair advantage',
|
||||||
|
basePoints: 10,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Communication Offenses
|
||||||
|
{
|
||||||
|
id: 'abusive_chat',
|
||||||
|
name: 'Abusive Chat',
|
||||||
|
category: 'communication',
|
||||||
|
description: 'Inappropriate language',
|
||||||
|
basePoints: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'inciting_verbal_conflict',
|
||||||
|
name: 'Inciting Verbal Conflict',
|
||||||
|
category: 'communication',
|
||||||
|
description: 'Chat aiming to incite violence or negative reactions',
|
||||||
|
basePoints: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'abusive_vc',
|
||||||
|
name: 'Abusive VC Language',
|
||||||
|
category: 'communication',
|
||||||
|
description: 'Inappropriate language or inciting conflict in voice chat',
|
||||||
|
basePoints: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lying_to_staff',
|
||||||
|
name: 'Lying to Staff Member',
|
||||||
|
category: 'communication',
|
||||||
|
description: 'Untrue statements regarding theft, griefing, etc.',
|
||||||
|
basePoints: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'manipulation',
|
||||||
|
name: 'Manipulation',
|
||||||
|
category: 'communication',
|
||||||
|
description: 'Repeatedly lying to conceal illicit activities or framing others',
|
||||||
|
basePoints: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'grand_manipulation',
|
||||||
|
name: 'Grand Manipulation',
|
||||||
|
category: 'communication',
|
||||||
|
description: 'Lies involving large-scale cleanup or out-of-game malicious actions',
|
||||||
|
basePoints: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'slander',
|
||||||
|
name: 'Slander (Against SPP Only)',
|
||||||
|
category: 'communication',
|
||||||
|
description: 'False statements designed to damage reputation',
|
||||||
|
basePoints: 3,
|
||||||
|
requiresSPP: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'violation_nca',
|
||||||
|
name: 'Violation of NCA',
|
||||||
|
category: 'communication',
|
||||||
|
description: 'Breaking a non-communication agreement',
|
||||||
|
basePoints: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Item point values for theft calculations
|
||||||
|
export const ITEM_POINTS = {
|
||||||
|
'elytra': 20,
|
||||||
|
'nether_star': 20,
|
||||||
|
'beacon': 20,
|
||||||
|
'heavy_core': 20,
|
||||||
|
'mace': 20,
|
||||||
|
'netherite_block': 20,
|
||||||
|
'award_1st_class': 20,
|
||||||
|
'award_2nd_class': 10,
|
||||||
|
'award_3rd_class': 5,
|
||||||
|
'award_bugfinding': 5,
|
||||||
|
'award_5_exploits': 5,
|
||||||
|
'award_playtime': 20,
|
||||||
|
'netherite_ingot': 10,
|
||||||
|
'netherite_sword': 10,
|
||||||
|
'netherite_pickaxe': 10,
|
||||||
|
'netherite_axe': 10,
|
||||||
|
'netherite_shovel': 10,
|
||||||
|
'netherite_hoe': 10,
|
||||||
|
'netherite_helmet': 10,
|
||||||
|
'netherite_chestplate': 10,
|
||||||
|
'netherite_leggings': 10,
|
||||||
|
'netherite_boots': 10,
|
||||||
|
'diamond_block': 10,
|
||||||
|
'gold_block': 10,
|
||||||
|
'emerald_block': 10,
|
||||||
|
'shulker_box': 10,
|
||||||
|
'diamond': 5,
|
||||||
|
'diamond_sword': 5,
|
||||||
|
'diamond_pickaxe': 5,
|
||||||
|
'diamond_axe': 5,
|
||||||
|
'diamond_shovel': 5,
|
||||||
|
'diamond_hoe': 5,
|
||||||
|
'diamond_helmet': 5,
|
||||||
|
'diamond_chestplate': 5,
|
||||||
|
'diamond_leggings': 5,
|
||||||
|
'diamond_boots': 5,
|
||||||
|
'iron_block': 5,
|
||||||
|
'potion': 5,
|
||||||
|
'tool': 2,
|
||||||
|
'armor': 2,
|
||||||
|
'ore': 2,
|
||||||
|
'ingot': 2,
|
||||||
|
'firework_rocket': 2,
|
||||||
|
'other': 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Punishment calculation functions
|
||||||
|
export function calculatePointDecay(currentPoints: number, lastOffenseDate?: Date): number {
|
||||||
|
if (!lastOffenseDate) return currentPoints;
|
||||||
|
|
||||||
|
const weeksSinceOffense = Math.floor((Date.now() - lastOffenseDate.getTime()) / (1000 * 60 * 60 * 24 * 7));
|
||||||
|
const decayPoints = Math.min(weeksSinceOffense, currentPoints);
|
||||||
|
return Math.max(0, currentPoints - decayPoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateTotalPoints(basePoints: number, currentPoints: number, lastOffenseDate?: Date): number {
|
||||||
|
const decayedPoints = calculatePointDecay(currentPoints, lastOffenseDate);
|
||||||
|
return decayedPoints + basePoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPunishmentLevel(points: number, crimeCategory: CrimeCategory, isSPP?: boolean): PunishmentLevel {
|
||||||
|
// Special cases for specific crimes
|
||||||
|
if (crimeCategory === 'hacking') {
|
||||||
|
if (points <= 5) return 'kick';
|
||||||
|
if (points <= 15) return '3day_ban';
|
||||||
|
return 'permanent_ban';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (crimeCategory === 'communication') {
|
||||||
|
if (isSPP) {
|
||||||
|
if (points <= 3) return '1day_mute';
|
||||||
|
return 'permanent_ban';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points <= 3) return 'warning';
|
||||||
|
if (points <= 5) return '15min_mute';
|
||||||
|
if (points <= 10) return '1hour_mute';
|
||||||
|
return '1day_mute';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (crimeCategory === 'block') {
|
||||||
|
if (points <= 5) return 'warning';
|
||||||
|
if (points <= 8) return '3day_ban';
|
||||||
|
if (points <= 10) return '5day_ban';
|
||||||
|
if (points <= 15) return '2week_ban';
|
||||||
|
if (points <= 20) return '1month_ban';
|
||||||
|
return 'permanent_ban';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item offenses
|
||||||
|
if (points <= 5) return 'warning';
|
||||||
|
if (points <= 8) return '1week_ban';
|
||||||
|
if (points <= 15) return '2week_ban';
|
||||||
|
return '4week_ban';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateCommands(playerName: string, totalPoints: number, punishmentLevel: PunishmentLevel): string[] {
|
||||||
|
const commands: string[] = [];
|
||||||
|
|
||||||
|
// Always add note command
|
||||||
|
commands.push(`/note ${playerName} ${totalPoints}`);
|
||||||
|
|
||||||
|
// Add punishment command based on level
|
||||||
|
switch (punishmentLevel) {
|
||||||
|
case 'warning':
|
||||||
|
commands.push(`/warn ${playerName} DP2 violation`);
|
||||||
|
break;
|
||||||
|
case '15min_mute':
|
||||||
|
case '30min_mute':
|
||||||
|
case '1hour_mute':
|
||||||
|
case '1day_mute':
|
||||||
|
commands.push(`/mute ${playerName} ${punishmentLevel.replace('_', ' ')}`);
|
||||||
|
break;
|
||||||
|
case '12hour_vc_ban':
|
||||||
|
case '1day_vc_ban':
|
||||||
|
case 'permanent_vc_ban':
|
||||||
|
commands.push(`/vcban ${playerName} ${punishmentLevel.replace('_', ' ')}`);
|
||||||
|
break;
|
||||||
|
case 'kick':
|
||||||
|
commands.push(`/kick ${playerName} DP2 violation`);
|
||||||
|
break;
|
||||||
|
case '1day_ban':
|
||||||
|
case '2day_ban':
|
||||||
|
case '3day_ban':
|
||||||
|
case '5day_ban':
|
||||||
|
case '1week_ban':
|
||||||
|
case '2week_ban':
|
||||||
|
case '4week_ban':
|
||||||
|
case '1month_ban':
|
||||||
|
commands.push(`/ban ${playerName} ${punishmentLevel.replace('_ban', '')}`);
|
||||||
|
break;
|
||||||
|
case 'permanent_ban':
|
||||||
|
commands.push(`/ban ${playerName} permanent`);
|
||||||
|
break;
|
||||||
|
case 'wipe_inventory':
|
||||||
|
commands.push(`/wipe ${playerName} inventory`);
|
||||||
|
break;
|
||||||
|
case 'demote_trusted':
|
||||||
|
commands.push(`/demote ${playerName} trusted`);
|
||||||
|
break;
|
||||||
|
case 'demote_standard':
|
||||||
|
commands.push(`/demote ${playerName} standard`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateExplanation(crime: Crime, basePoints: number, totalPoints: number, punishmentLevel: PunishmentLevel): string {
|
||||||
|
const punishmentText = punishmentLevel.replace('_', ' ');
|
||||||
|
|
||||||
|
return `Crime: ${crime.name} (${basePoints} points)
|
||||||
|
Total points after decay: ${totalPoints}
|
||||||
|
Punishment: ${punishmentText}
|
||||||
|
|
||||||
|
${crime.description}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form validation schemas
|
||||||
|
export const playerSchema = z.object({
|
||||||
|
playerName: z.string().min(2, "Player name must be at least 2 characters"),
|
||||||
|
currentPoints: z.number().min(0, "Current points must be 0 or higher"),
|
||||||
|
lastOffenseDate: z.date().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const offenseSchema = z.object({
|
||||||
|
crimeId: z.string(),
|
||||||
|
itemDetails: z.array(z.object({
|
||||||
|
type: z.string(),
|
||||||
|
quantity: z.number().min(1, "Quantity must be at least 1"),
|
||||||
|
})).optional(),
|
||||||
|
blockCount: z.number().min(0, "Block count must be 0 or higher").optional(),
|
||||||
|
entityCount: z.number().min(0, "Entity count must be 0 or higher").optional(),
|
||||||
|
isSPP: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const formSchema = z.object({
|
||||||
|
player: playerSchema,
|
||||||
|
offense: offenseSchema,
|
||||||
|
});
|
||||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
34
tsconfig.json
Normal file
34
tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts",
|
||||||
|
"**/*.mts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user