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